pax_global_header00006660000000000000000000000064150126244500014511gustar00rootroot0000000000000052 comment=998885377d79bd3b159fec580f8894579a522a29 sparse-0.17.0/000077500000000000000000000000001501262445000130735ustar00rootroot00000000000000sparse-0.17.0/.codecov.yml000066400000000000000000000004741501262445000153230ustar00rootroot00000000000000comment: false coverage: status: project: default: # Total project must be 95% target: '100%' threshold: '5%' patch: default: # Patch coverage must be 92% target: '100%' threshold: '8%' precision: 2 round: down range: 80...98 sparse-0.17.0/.coveragerc000066400000000000000000000002561501262445000152170ustar00rootroot00000000000000[run] source= sparse/ omit= sparse/_version.py **/tests/* [report] exclude_lines = pragma: no cover return NotImplemented raise NotImplementedError sparse-0.17.0/.gitattributes000066400000000000000000000001661501262445000157710ustar00rootroot00000000000000sparse/_version.py export-subst # GitHub syntax highlighting pixi.lock linguist-language=YAML linguist-generated=true sparse-0.17.0/.github/000077500000000000000000000000001501262445000144335ustar00rootroot00000000000000sparse-0.17.0/.github/CODE_OF_CONDUCT.md000066400000000000000000000001031501262445000172240ustar00rootroot00000000000000# Code of Conduct Please see [`docs/conduct.md`](docs/conduct.md) sparse-0.17.0/.github/FUNDING.yml000066400000000000000000000012251501262445000162500ustar00rootroot00000000000000# These are supported funding model platforms github: [Quansight, Quansight-Labs] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] sparse-0.17.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001501262445000166165ustar00rootroot00000000000000sparse-0.17.0/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000037231501262445000215160ustar00rootroot00000000000000name: Bug report description: Report to help us reproduce the bug title: "Bug: " labels: ["bug", "needs triage"] body: - type: markdown attributes: value: > ## Thanks for taking the time to fill out this report - type: checkboxes id: checks attributes: label: sparse version checks options: - label: > I checked that this issue has not been reported before [list of issues](https://github.com/pydata/sparse/issues). required: true - label: > I have confirmed this bug exists on the latest version of sparse. required: true - label: > I have confirmed this bug exists on the main branch of sparse. - type: textarea attributes: label: Describe the bug description: > A clear and concise description of what the bug is. validations: required: true - type: textarea attributes: label: Steps or code to reproduce the bug description: | Please add a minimal code example to reproduce the bug. validations: required: true - type: textarea attributes: label: Expected results description: > Please paste or describe the expected results. placeholder: > Example: No error is thrown. validations: required: true - type: textarea attributes: label: Actual results description: | Please paste or describe the results you observe instead of the expected results. validations: required: true - type: textarea attributes: label: Please describe your system. value: | 1. OS and version: [e.g. Windows 10] 2. sparse version (sparse.__version__) 3. NumPy version (np.__version__) 4. Numba version (numba.__version__) validations: required: true - type: textarea attributes: label: Relevant log output description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: shell sparse-0.17.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000001361501262445000206060ustar00rootroot00000000000000blank_issues_enabled: true name: Blank issue url: https://github.com/pydata/sparse/issues/new sparse-0.17.0/.github/ISSUE_TEMPLATE/doc_issue.yml000066400000000000000000000016511501262445000213210ustar00rootroot00000000000000name: Documentation improvement description: Report to improve the docs. You could also directly open a PR with your suggestions. title: "Doc: " labels: ["docs", "needs triage"] body: - type: markdown attributes: value: > ## Thanks for taking the time to fill out this form - type: dropdown id: TYPE attributes: label: What type of report is this? options: - 'Correction' - 'Improvement' validations: required: true - type: textarea attributes: label: Please describe the issue. description: > Tell us if something is unclear or incorrect, and where. validations: required: true - type: textarea attributes: label: If you have a suggestion on how it should be, add it below. description: > How can we improve it - type: markdown attributes: value: > ### If you are interested in opening a pull request to fix this, please let us know. sparse-0.17.0/.github/ISSUE_TEMPLATE/feature_request.yml000066400000000000000000000024261501262445000225500ustar00rootroot00000000000000name: Feature request description: Form to request a new feature title: "Enh: " labels: ["enhancement", "needs triage"] body: - type: markdown attributes: value: > ## Thanks for helping us improve sparse! - type: markdown attributes: value: > ### Before submitting a request, please check if it has already been discused in the [list of issues](https://github.com/pydata/sparse/issues). - type: textarea attributes: label: Please describe the purpose of the new feature or describe the problem to solve. description: > A clear description of the objective. validations: required: true - type: textarea attributes: label: Suggest a solution if possible. description: > Please suggest a solution if you can. validations: required: false - type: textarea attributes: label: If you have tried alternatives, please describe them below. description: > What you have tried if applicable. - type: textarea attributes: label: Additional information that may help us understand your needs. description: > Context, screenshots, or any useful information. - type: markdown attributes: value: > ### If you are interested in opening a pull request to fix this, please let us know. sparse-0.17.0/.github/ISSUE_TEMPLATE/question-support.yml000066400000000000000000000010341501262445000227200ustar00rootroot00000000000000name: Question/Support description: A question about how to use this library. title: "Usage: " labels: "usage" body: - type: markdown attributes: value: > ## Thank you for your interest in sparse - type: textarea attributes: label: Please provide a description of what you'd like to do. validations: required: true - type: textarea attributes: label: Example Code description: > Syntactically valid Python code that shows what you want to do, possibly with placeholder functions or methods. sparse-0.17.0/.github/dependabot.yml000066400000000000000000000007151501262445000172660ustar00rootroot00000000000000# Set update schedule for GitHub Actions # This opens a PR when actions in workflows need an update version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every week interval: "weekly" commit-message: prefix: "skip changelog" # So this PR will not be added to release-drafter include: "scope" # List of the updated dependencies in the commit will be added sparse-0.17.0/.github/pull_request_template.md000066400000000000000000000013651501262445000214010ustar00rootroot00000000000000 ## What type of PR is this? (check all applicable) - [ ] ๐Ÿ’พ Refactor - [ ] ๐Ÿช„ Feature - [ ] ๐Ÿž Bug Fix - [ ] ๐Ÿ”ง Optimization - [ ] ๐Ÿ“š Documentation - [ ] ๐Ÿงช Test - [ ] ๐Ÿ› ๏ธ Other ## Related issues - Related issue # - Closes # ## Checklist - [ ] Code follows style guide - [ ] Tests added - [ ] Documented the changes *** ## Please explain your changes below. sparse-0.17.0/.github/release-drafter.yml000066400000000000000000000027331501262445000202300ustar00rootroot00000000000000exclude-labels: # When PR will not be classified if it has these labels - skip changelog - release name-template: 'Sparse v$RESOLVED_VERSION' change-template: '- $TITLE (#$NUMBER)' autolabeler: - label: breaking title: # Example: feat!: ... - '/^(build|chore|ci|depr|docs|feat|fix|perf|refactor|release|test)(\(.*\))?\!\: /' - label: build title: - '/^(build)/' - label: internal title: - '/^(chore|ci|refactor|test)/' - label: deprecation title: - '/^depr/' - label: documentation title: - '/^(docs|docstring)/' - label: enhancement title: - '/^feat/' - label: fix title: - '/^fix/' - label: performance title: - '/^perf/' - label: release title: - '/^release/' - label: 'skip changelog' title: - '/^\[pre-commit.ci\]/' categories: - title: ๐Ÿ“ฃ Highlights labels: highlight - title: ๐Ÿงจ Breaking changes labels: - breaking - breaking python - title: ๐Ÿšง Deprecations labels: deprecation - title: ๐Ÿช„ Performance improvements labels: performance - title: ๐ŸŽŠ Enhancements labels: enhancement - title: ๐Ÿž Bug fixes labels: fix - title: ๐Ÿ“š Documentation labels: documentation - title: ๐Ÿงฐ Build system labels: build - title: ๐Ÿ”ง Other improvements labels: internal template: | ## Changes $CHANGES Thank you to all our contributors for making this release possible! $CONTRIBUTORS sparse-0.17.0/.github/workflows/000077500000000000000000000000001501262445000164705ustar00rootroot00000000000000sparse-0.17.0/.github/workflows/ci.yml000066400000000000000000000070771501262445000176210ustar00rootroot00000000000000defaults: run: shell: bash -leo pipefail {0} concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: test: strategy: matrix: os: ['ubuntu-latest'] python: ['3.10', '3.11', '3.12'] pip_opts: [''] numba_boundscheck: [0] include: - os: macos-latest python: '3.10' - os: windows-latest python: '3.10' - os: ubuntu-latest python: '3.10' numba_boundscheck: 1 - os: ubuntu-latest python: '3.10' pip_opts: 'numpy<2' fail-fast: false runs-on: ${{ matrix.os }} env: PYTHON_VERSION: ${{ matrix.python }} NUMBA_BOUNDSCHECK: ${{ matrix.numba_boundscheck }} steps: - name: Checkout Repo uses: actions/checkout@v4 - uses: mamba-org/setup-micromamba@v2 with: environment-file: ci/environment.yml init-shell: >- bash cache-environment: true cache-downloads: true post-cleanup: 'all' create-args: >- python=${{ matrix.python }} ${{ matrix.pip_opts }} - name: Install package run: | pip install -e '.[tests]' - name: Run tests run: ci/test_backends.sh - uses: codecov/codecov-action@v5 if: always() with: token: ${{ secrets.CODECOV_TOKEN }} files: ./**/coverage*.xml examples: runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' cache: 'pip' - name: Build and install Sparse run: | pip install -U setuptools wheel pip install '.[finch]' scipy dask networkx graphblas-algorithms - name: Run examples run: ci/test_examples.sh notebooks: runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' cache: 'pip' - name: Build and install Sparse run: | pip install -U setuptools wheel pip install '.[notebooks]' - name: Run notebooks run: ci/test_notebooks.sh array_api_tests: strategy: matrix: backend: ['Numba', 'Finch'] fail-fast: false env: ARRAY_API_TESTS_DIR: ${{ github.workspace }}/array-api-tests runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v4 - name: Checkout array-api-tests run: ci/clone_array_api_tests.sh - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' cache: 'pip' - name: Install build and test dependencies from PyPI run: | pip install pytest-xdist -r "$ARRAY_API_TESTS_DIR/requirements.txt" - name: Build and install Sparse run: | pip install -U setuptools wheel pip install '.[finch]' - name: Run the test suite env: SPARSE_BACKEND: ${{ matrix.backend }} run: ci/test_array_api.sh on: # Trigger the workflow on push or pull request, # but only for the main branch push: branches: - main - vnext pull_request: branches: - main - vnext # Also trigger on page_build, as well as release created events page_build: release: types: # This configuration does not affect the page_build event above - created sparse-0.17.0/.github/workflows/codspeed.yml000066400000000000000000000011441501262445000210010ustar00rootroot00000000000000name: codspeed-benchmarks on: push: branches: - "main" # or "master" pull_request: # `workflow_dispatch` allows CodSpeed to trigger backtest # performance analysis in order to generate initial data. workflow_dispatch: jobs: benchmarks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install dependencies run: pip install ".[all]" - name: Run benchmarks uses: CodSpeedHQ/action@v3 with: run: pytest benchmarks/ --codspeed sparse-0.17.0/.github/workflows/release-drafter.yml000066400000000000000000000025361501262445000222660ustar00rootroot00000000000000name: Release Drafter on: push: # branches to consider in the event; optional, defaults to all branches: - main # pull_request event is required only for autolabeler pull_request: # Only following types are handled by the action, but one can default to all as well types: [opened, reopened, synchronize, edited] # pull_request_target event is required for autolabeler to support PRs from forks pull_request_target: types: [opened, reopened, synchronize, edited] permissions: contents: read jobs: update_release_draft: permissions: # write permission is required to create a github release contents: write # write permission is required for autolabeler # otherwise, read permission is required at least pull-requests: write runs-on: ubuntu-latest steps: # (Optional) GitHub Enterprise requires GHE_HOST variable set #- name: Set GHE_HOST # run: | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV # Drafts your next Release notes as Pull Requests are merged into "main" - uses: release-drafter/release-drafter@v6 # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml # with: # config-name: my-config.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} sparse-0.17.0/.gitignore000066400000000000000000000020101501262445000150540ustar00rootroot00000000000000#####=== Python ===##### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .pytest_cache/ test_results/ junit/ .hypothesis/ coverage_*.xml # Translations *.mo *.pot # Django stuff: *.log # mkdocs documentation site/ # PyBuilder target/ # IDE .idea/ .vscode/ default.profraw # Sandbox sandbox.py # macOS **/.DS_Store # Version file sparse/_version.py # Benchmark Results results/ # Notebooks converted to scripts. docs/examples_ipynb/ # Envs .pixi/ pixi.lock .venv/ sparse-0.17.0/.pre-commit-config.yaml000066400000000000000000000014641501262445000173610ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - id: fix-byte-order-marker - id: destroyed-symlinks - id: fix-encoding-pragma args: ["--remove"] - id: mixed-line-ending - id: name-tests-test args: ["--pytest-test-first"] - id: no-commit-to-branch - id: pretty-format-json args: ["--autofix", "--no-ensure-ascii"] exclude: ".ipynb" - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.10 hooks: - id: ruff-check args: ["--fix"] types_or: [ python, pyi, jupyter ] - id: ruff-format types_or: [ python, pyi, jupyter ] - repo: https://github.com/kynan/nbstripout rev: 0.8.1 hooks: - id: nbstripout sparse-0.17.0/.readthedocs.yml000066400000000000000000000007501501262445000161630ustar00rootroot00000000000000# Read the Docs configuration file for MkDocs projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.12" mkdocs: configuration: mkdocs.yml fail_on_warning: false # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . extra_requirements: - docs sparse-0.17.0/LICENSE000066400000000000000000000027551501262445000141110ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2018, Sparse developers All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. sparse-0.17.0/README.md000066400000000000000000000016041501262445000143530ustar00rootroot00000000000000![Sparse](docs/assets/images/logo_with_text.svg) # Sparse Multidimensional Arrays [![Build Status](https://github.com/pydata/sparse/actions/workflows/ci.yml/badge.svg)]( https://github.com/pydata/sparse/actions/workflows/ci.yml) [![Docs Status](https://readthedocs.org/projects/sparse-nd/badge/?version=latest)]( http://sparse.pydata.org/en/latest/?badge=latest) [![Coverage](https://codecov.io/gh/pydata/sparse/branch/main/graph/badge.svg)]( https://codecov.io/gh/pydata/sparse) ## This library provides multi-dimensional sparse arrays. - ๐Ÿ“š [Documentation](http://sparse.pydata.org) - ๐Ÿ™Œ [Contributing](https://github.com/pydata/sparse/blob/main/docs/contributing.md) - ๐Ÿชฒ [Bug Reports/Feature Requests](https://github.com/pydata/sparse/issues) - ๐Ÿ’ฌ [Discord Server](https://discord.gg/vur45CbwMz) [Channel](https://discord.com/channels/786703927705862175/1301155724646289420) sparse-0.17.0/benchmarks/000077500000000000000000000000001501262445000152105ustar00rootroot00000000000000sparse-0.17.0/benchmarks/__init__.py000066400000000000000000000000001501262445000173070ustar00rootroot00000000000000sparse-0.17.0/benchmarks/conftest.py000066400000000000000000000002131501262445000174030ustar00rootroot00000000000000import pytest @pytest.fixture def seed(scope="session"): return 42 @pytest.fixture def max_size(scope="session"): return 2**26 sparse-0.17.0/benchmarks/test_benchmark_coo.py000066400000000000000000000105351501262445000214170ustar00rootroot00000000000000import itertools import operator import sparse import pytest import numpy as np DENSITY = 0.01 def format_id(format): return f"{format=}" @pytest.mark.parametrize("format", ["coo", "gcxs"]) def test_matmul(benchmark, sides, format, seed, max_size, ids=format_id): m, n, p = sides if m * n >= max_size or n * p >= max_size: pytest.skip() rng = np.random.default_rng(seed=seed) x = sparse.random((m, n), density=DENSITY, format=format, random_state=rng) y = sparse.random((n, p), density=DENSITY, format=format, random_state=rng) x @ y # Numba compilation @benchmark def bench(): x @ y def get_test_id(params): side, rank, format = params return f"{side=}-{rank=}-{format=}" @pytest.fixture(params=itertools.product([100, 500, 1000], [1, 2, 3, 4], ["coo", "gcxs"]), ids=get_test_id) def elemwise_args(request, seed, max_size): side, rank, format = request.param if side**rank >= max_size: pytest.skip() rng = np.random.default_rng(seed=seed) shape = (side,) * rank x = sparse.random(shape, density=DENSITY, format=format, random_state=rng) y = sparse.random(shape, density=DENSITY, format=format, random_state=rng) return x, y @pytest.mark.parametrize("f", [operator.add, operator.mul]) def test_elemwise(benchmark, f, elemwise_args): x, y = elemwise_args f(x, y) @benchmark def bench(): f(x, y) def get_elemwise_ids(params): side, format = params return f"{side=}-{format=}" @pytest.fixture(params=itertools.product([100, 500, 1000], ["coo", "gcxs"]), ids=get_elemwise_ids) def elemwise_broadcast_args(request, seed, max_size): side, format = request.param if side**2 >= max_size: pytest.skip() rng = np.random.default_rng(seed=seed) x = sparse.random((side, 1, side), density=DENSITY, format=format, random_state=rng) y = sparse.random((side, side), density=DENSITY, format=format, random_state=rng) return x, y @pytest.mark.parametrize("f", [operator.add, operator.mul]) def test_elemwise_broadcast(benchmark, f, elemwise_broadcast_args): x, y = elemwise_broadcast_args f(x, y) @benchmark def bench(): f(x, y) @pytest.fixture(params=itertools.product([100, 500, 1000], [1, 2, 3], ["coo", "gcxs"]), ids=get_test_id) def indexing_args(request, seed, max_size): side, rank, format = request.param if side**rank >= max_size: pytest.skip() rng = np.random.default_rng(seed=seed) shape = (side,) * rank return sparse.random(shape, density=DENSITY, format=format, random_state=rng) def test_index_scalar(benchmark, indexing_args): x = indexing_args side = x.shape[0] rank = x.ndim x[(side // 2,) * rank] # Numba compilation @benchmark def bench(): x[(side // 2,) * rank] def test_index_slice(benchmark, indexing_args): x = indexing_args side = x.shape[0] rank = x.ndim x[(slice(side // 2),) * rank] # Numba compilation @benchmark def bench(): x[(slice(side // 2),) * rank] def test_index_fancy(benchmark, indexing_args, seed): x = indexing_args side = x.shape[0] rng = np.random.default_rng(seed=seed) index = rng.integers(0, side, size=(side // 2,)) x[index] # Numba compilation @benchmark def bench(): x[index] def get_sides_ids(param): m, n, p = param return f"{m=}-{n=}-{p=}" @pytest.fixture(params=itertools.product([200, 500, 1000], [200, 500, 1000], [200, 500, 1000]), ids=get_sides_ids) def sides(request): m, n, p = request.param return m, n, p @pytest.fixture(params=([(0, "coo"), (0, "gcxs"), (1, "gcxs")]), ids=["coo", "gcxs-0-axis", "gcxs-1-axis"]) def densemul_args(request, sides, seed, max_size): compressed_axis, format = request.param m, n, p = sides if m * n >= max_size or n * p >= max_size: pytest.skip() rng = np.random.default_rng(seed=seed) if format == "coo": x = sparse.random((m, n), density=DENSITY / 10, format=format, random_state=rng) else: x = sparse.random((m, n), density=DENSITY / 10, format=format, random_state=rng).change_compressed_axes( (compressed_axis,) ) t = rng.random((n, p)) return x, t def test_gcxs_dot_ndarray(benchmark, densemul_args): x, t = densemul_args # Numba compilation x @ t @benchmark def bench(): x @ t sparse-0.17.0/benchmarks/test_elemwise.py000066400000000000000000000032261501262445000204360ustar00rootroot00000000000000import importlib import itertools import operator import os import sparse import pytest import numpy as np import scipy.sparse as sps DENSITY = 0.001 def get_test_id(side): return f"{side=}" @pytest.fixture(params=[100, 500, 1000], ids=get_test_id) def elemwise_args(request, seed, max_size): side = request.param if side**2 >= max_size: pytest.skip() rng = np.random.default_rng(seed=seed) s1_sps = sps.random(side, side, format="csr", density=DENSITY, random_state=rng) * 10 s1_sps.sum_duplicates() s2_sps = sps.random(side, side, format="csr", density=DENSITY, random_state=rng) * 10 s2_sps.sum_duplicates() return s1_sps, s2_sps def get_elemwise_id(param): f, backend = param return f"{f=}-{backend=}" @pytest.fixture( params=itertools.product([operator.add, operator.mul, operator.gt], ["SciPy", "Numba", "Finch"]), scope="function", ids=get_elemwise_id, ) def backend(request): f, backend = request.param os.environ[sparse._ENV_VAR_NAME] = backend importlib.reload(sparse) yield f, sparse, backend del os.environ[sparse._ENV_VAR_NAME] importlib.reload(sparse) def test_elemwise(benchmark, backend, elemwise_args): s1_sps, s2_sps = elemwise_args f, sparse, backend = backend if backend == "SciPy": s1 = s1_sps s2 = s2_sps elif backend == "Numba": s1 = sparse.asarray(s1_sps) s2 = sparse.asarray(s2_sps) elif backend == "Finch": s1 = sparse.asarray(s1_sps.asformat("csc"), format="csc") s2 = sparse.asarray(s2_sps.asformat("csc"), format="csc") f(s1, s2) @benchmark def bench(): f(s1, s2) sparse-0.17.0/benchmarks/test_tensordot.py000066400000000000000000000041321501262445000206420ustar00rootroot00000000000000import itertools import sparse import pytest import numpy as np DENSITY = 0.01 def get_sides_ids(param): m, n, p, q = param return f"{m=}-{n=}-{p=}-{q=}" @pytest.fixture( params=itertools.product([10, 50], [10, 20], [20, 50], [10, 50]), ids=get_sides_ids, scope="function", ) def sides(request): m, n, p, q = request.param return m, n, p, q def get_tensor_ids(param): left_index, right_index, left_format, right_format = param return f"{left_index=}-{right_index=}-{left_format=}-{right_format=}" @pytest.fixture( params=([(1, 2, "dense", "coo"), (1, 2, "coo", "coo"), (1, 1, "coo", "dense")]), ids=get_tensor_ids, scope="function", ) def tensordot_args(request, sides, seed, max_size): m, n, p, q = sides if m * n * p * q >= max_size: pytest.skip() left_index, right_index, left_format, right_format = request.param rng = np.random.default_rng(seed=seed) t = rng.random((m, n)) if left_format == "dense" and right_format == "coo": left_tensor = t right_tensor = sparse.random((m, p, n, q), density=DENSITY, format=right_format, random_state=rng) if left_format == "coo" and right_format == "coo": left_tensor = sparse.random((m, p), density=DENSITY, format=left_format, random_state=rng) right_tensor = sparse.random((m, n, p, q), density=DENSITY, format=right_format, random_state=rng) if left_format == "coo" and right_format == "dense": left_tensor = sparse.random((m, n, p, q), density=DENSITY, format=left_format, random_state=rng) right_tensor = t return left_index, right_index, left_tensor, right_tensor @pytest.mark.parametrize("return_type", [np.ndarray, sparse.COO]) def test_tensordot(benchmark, return_type, tensordot_args): left_index, right_index, left_tensor, right_tensor = tensordot_args sparse.tensordot(left_tensor, right_tensor, axes=([0, left_index], [0, right_index]), return_type=return_type) @benchmark def bench(): sparse.tensordot(left_tensor, right_tensor, axes=([0, left_index], [0, right_index]), return_type=return_type) sparse-0.17.0/benchmarks/utils.py000066400000000000000000000001021501262445000167130ustar00rootroot00000000000000import os CI_MODE = bool(int(os.getenv("CI_MODE", default="0"))) sparse-0.17.0/benchmarks_original/000077500000000000000000000000001501262445000170745ustar00rootroot00000000000000sparse-0.17.0/benchmarks_original/__init__.py000066400000000000000000000000001501262445000211730ustar00rootroot00000000000000sparse-0.17.0/benchmarks_original/elemwise_example.py000066400000000000000000000040051501262445000227720ustar00rootroot00000000000000import importlib import operator import os import sparse from utils import benchmark import numpy as np import scipy.sparse as sps LEN = 10000 DENSITY = 0.001 ITERS = 3 rng = np.random.default_rng(0) if __name__ == "__main__": print("Elementwise Example:\n") for func_name in ["multiply", "add", "greater_equal"]: print(f"{func_name} benchmark:\n") s1_sps = sps.random(LEN, LEN, format="csr", density=DENSITY, random_state=rng) * 10 s1_sps.sum_duplicates() s2_sps = sps.random(LEN, LEN, format="csr", density=DENSITY, random_state=rng) * 10 s2_sps.sum_duplicates() # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) s1 = sparse.asarray(s1_sps.asformat("csc"), format="csc") s2 = sparse.asarray(s2_sps.asformat("csc"), format="csc") func = getattr(sparse, func_name) # Compile & Benchmark result_finch = benchmark(func, args=[s1, s2], info="Finch", iters=ITERS) # ======= Numba ======= os.environ[sparse._ENV_VAR_NAME] = "Numba" importlib.reload(sparse) s1 = sparse.asarray(s1_sps) s2 = sparse.asarray(s2_sps) func = getattr(sparse, func_name) # Compile & Benchmark result_numba = benchmark(func, args=[s1, s2], info="Numba", iters=ITERS) # ======= SciPy ======= s1 = s1_sps s2 = s2_sps if func_name == "multiply": func, args = s1.multiply, [s2] elif func_name == "add": func, args = operator.add, [s1, s2] elif func_name == "greater_equal": func, args = operator.ge, [s1, s2] # Compile & Benchmark result_scipy = benchmark(func, args=args, info="SciPy", iters=ITERS) np.testing.assert_allclose(result_numba.todense(), result_scipy.toarray()) np.testing.assert_allclose(result_finch.todense(), result_numba.todense()) np.testing.assert_allclose(result_finch.todense(), result_scipy.toarray()) sparse-0.17.0/benchmarks_original/matmul_example.py000066400000000000000000000031241501262445000224600ustar00rootroot00000000000000import importlib import os import sparse from utils import benchmark import numpy as np import scipy.sparse as sps LEN = 100000 DENSITY = 0.00001 ITERS = 3 rng = np.random.default_rng(0) if __name__ == "__main__": print("Matmul Example:\n") a_sps = sps.random(LEN, LEN - 10, format="csr", density=DENSITY, random_state=rng) * 10 a_sps.sum_duplicates() b_sps = sps.random(LEN - 10, LEN, format="csr", density=DENSITY, random_state=rng) * 10 b_sps.sum_duplicates() # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) a = sparse.asarray(a_sps) b = sparse.asarray(b_sps) @sparse.compiled() def sddmm_finch(a, b): return a @ b # Compile & Benchmark result_finch = benchmark(sddmm_finch, args=[a, b], info="Finch", iters=ITERS) # ======= Numba ======= os.environ[sparse._ENV_VAR_NAME] = "Numba" importlib.reload(sparse) a = sparse.asarray(a_sps) b = sparse.asarray(b_sps) def sddmm_numba(a, b): return a @ b # Compile & Benchmark result_numba = benchmark(sddmm_numba, args=[a, b], info="Numba", iters=ITERS) # ======= SciPy ======= def sddmm_scipy(a, b): return a @ b a = a_sps b = b_sps # Compile & Benchmark result_scipy = benchmark(sddmm_scipy, args=[a, b], info="SciPy", iters=ITERS) # np.testing.assert_allclose(result_numba.todense(), result_scipy.toarray()) # np.testing.assert_allclose(result_finch.todense(), result_numba.todense()) # np.testing.assert_allclose(result_finch.todense(), result_scipy.toarray()) sparse-0.17.0/benchmarks_original/mttkrp_example.py000066400000000000000000000026401501262445000225040ustar00rootroot00000000000000import importlib import os import sparse from utils import benchmark import numpy as np I_ = 1000 J_ = 25 K_ = 1000 L_ = 100 DENSITY = 0.0001 ITERS = 3 rng = np.random.default_rng(0) if __name__ == "__main__": print("MTTKRP Example:\n") B_sps = sparse.random((I_, K_, L_), density=DENSITY, random_state=rng) * 10 D_sps = rng.random((L_, J_)) * 10 C_sps = rng.random((K_, J_)) * 10 # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) B = sparse.asarray(B_sps.todense(), format="csf") D = sparse.asarray(np.array(D_sps, order="F")) C = sparse.asarray(np.array(C_sps, order="F")) @sparse.compiled() def mttkrp_finch(B, D, C): return sparse.sum(B[:, :, :, None] * D[None, None, :, :] * C[None, :, None, :], axis=(1, 2)) # Compile & Benchmark result_finch = benchmark(mttkrp_finch, args=[B, D, C], info="Finch", iters=ITERS) # ======= Numba ======= os.environ[sparse._ENV_VAR_NAME] = "Numba" importlib.reload(sparse) B = sparse.asarray(B_sps, format="gcxs") D = D_sps C = C_sps def mttkrp_numba(B, D, C): return sparse.sum(B[:, :, :, None] * D[None, None, :, :] * C[None, :, None, :], axis=(1, 2)) # Compile & Benchmark result_numba = benchmark(mttkrp_numba, args=[B, D, C], info="Numba", iters=ITERS) np.testing.assert_allclose(result_finch.todense(), result_numba.todense()) sparse-0.17.0/benchmarks_original/sddmm_example.py000066400000000000000000000034611501262445000222710ustar00rootroot00000000000000import importlib import os import sparse from utils import benchmark import numpy as np import scipy.sparse as sps LEN = 10000 DENSITY = 0.00001 ITERS = 3 rng = np.random.default_rng(0) if __name__ == "__main__": print("SDDMM Example:\n") a_sps = rng.random((LEN, LEN - 10)) * 10 b_sps = rng.random((LEN - 10, LEN)) * 10 s_sps = sps.random(LEN, LEN, format="coo", density=DENSITY, random_state=rng) * 10 s_sps.sum_duplicates() # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) s = sparse.asarray(s_sps) a = sparse.asarray(np.array(a_sps, order="F")) b = sparse.asarray(np.array(b_sps, order="C")) @sparse.compiled() def sddmm_finch(s, a, b): return sparse.sum( s[:, :, None] * (a[:, None, :] * sparse.permute_dims(b, (1, 0))[None, :, :]), axis=-1, ) # Compile & Benchmark result_finch = benchmark(sddmm_finch, args=[s, a, b], info="Finch", iters=ITERS) # ======= Numba ======= os.environ[sparse._ENV_VAR_NAME] = "Numba" importlib.reload(sparse) s = sparse.asarray(s_sps) a = a_sps b = b_sps def sddmm_numba(s, a, b): return s * (a @ b) # Compile & Benchmark result_numba = benchmark(sddmm_numba, args=[s, a, b], info="Numba", iters=ITERS) # ======= SciPy ======= def sddmm_scipy(s, a, b): return s.multiply(a @ b) s = s_sps.asformat("csr") a = a_sps b = b_sps # Compile & Benchmark result_scipy = benchmark(sddmm_scipy, args=[s, a, b], info="SciPy", iters=ITERS) np.testing.assert_allclose(result_numba.todense(), result_scipy.toarray()) np.testing.assert_allclose(result_finch.todense(), result_numba.todense()) np.testing.assert_allclose(result_finch.todense(), result_scipy.toarray()) sparse-0.17.0/benchmarks_original/spmv_add_example.py000066400000000000000000000032541501262445000227620ustar00rootroot00000000000000import importlib import os import sparse from utils import benchmark import numpy as np import scipy.sparse as sps LEN = 100000 DENSITY = 0.000001 ITERS = 3 rng = np.random.default_rng(0) if __name__ == "__main__": print("SpMv_add Example:\n") A_sps = sps.random(LEN - 10, LEN, format="csc", density=DENSITY, random_state=rng) * 10 x_sps = rng.random((LEN, 1)) * 10 y_sps = rng.random((LEN - 10, 1)) * 10 # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) A = sparse.asarray(A_sps) x = sparse.asarray(np.array(x_sps, order="C")) y = sparse.asarray(np.array(y_sps, order="C")) @sparse.compiled() def spmv_finch(A, x, y): return sparse.sum(A[:, None, :] * sparse.permute_dims(x, (1, 0))[None, :, :], axis=-1) + y # Compile & Benchmark result_finch = benchmark(spmv_finch, args=[A, x, y], info="Finch", iters=ITERS) # ======= Numba ======= os.environ[sparse._ENV_VAR_NAME] = "Numba" importlib.reload(sparse) A = sparse.asarray(A_sps, format="csc") x = x_sps y = y_sps def spmv_numba(A, x, y): return A @ x + y # Compile & Benchmark result_numba = benchmark(spmv_numba, args=[A, x, y], info="Numba", iters=ITERS) # ======= SciPy ======= def spmv_scipy(A, x, y): return A @ x + y A = A_sps x = x_sps y = y_sps # Compile & Benchmark result_scipy = benchmark(spmv_scipy, args=[A, x, y], info="SciPy", iters=ITERS) np.testing.assert_allclose(result_numba, result_scipy) np.testing.assert_allclose(result_finch.todense(), result_numba) np.testing.assert_allclose(result_finch.todense(), result_scipy) sparse-0.17.0/benchmarks_original/utils.py000066400000000000000000000011011501262445000205770ustar00rootroot00000000000000import os import time from collections.abc import Callable, Iterable from typing import Any CI_MODE = bool(int(os.getenv("CI_MODE", default="0"))) def benchmark( func: Callable, args: Iterable[Any], info: str, iters: int, ) -> object: # Compile result = func(*args) if CI_MODE: print("CI mode - skipping benchmark") return result # Benchmark print(info) start = time.time() for _ in range(iters): func(*args) elapsed = time.time() - start print(f"Took {elapsed / iters} s.\n") return result sparse-0.17.0/ci/000077500000000000000000000000001501262445000134665ustar00rootroot00000000000000sparse-0.17.0/ci/Finch-array-api-skips.txt000066400000000000000000000006361501262445000202750ustar00rootroot00000000000000# `test_nonzero` name conflict array_api_tests/test_searching_functions.py::test_nonzero_zerodim_error # flaky test array_api_tests/test_special_cases.py::test_unary[sign((x_i is -0 or x_i == +0)) -> 0] # `broadcast_to` is not defined in Finch, hangs as xfail array_api_tests/test_searching_functions.py::test_where # `test_solve` is not defined in Finch, hangs as xfail array_api_tests/test_linalg.py::test_solve sparse-0.17.0/ci/Finch-array-api-xfails.txt000066400000000000000000000426051501262445000204340ustar00rootroot00000000000000# test_signatures # not implemented # stats functions array_api_tests/test_signatures.py::test_func_signature[mean] array_api_tests/test_signatures.py::test_func_signature[std] array_api_tests/test_signatures.py::test_func_signature[var] # set functions array_api_tests/test_signatures.py::test_func_signature[unique_all] array_api_tests/test_signatures.py::test_func_signature[unique_counts] array_api_tests/test_signatures.py::test_func_signature[unique_inverse] array_api_tests/test_signatures.py::test_func_signature[unique_values] # creation functions array_api_tests/test_signatures.py::test_func_signature[meshgrid] array_api_tests/test_signatures.py::test_func_signature[tril] array_api_tests/test_signatures.py::test_func_signature[triu] # inspection functions array_api_tests/test_signatures.py::test_func_signature[isdtype] array_api_tests/test_signatures.py::test_func_signature[result_type] # other functions array_api_tests/test_signatures.py::test_func_signature[concat] array_api_tests/test_signatures.py::test_func_signature[argsort] array_api_tests/test_signatures.py::test_func_signature[sort] array_api_tests/test_signatures.py::test_func_signature[broadcast_arrays] array_api_tests/test_signatures.py::test_func_signature[broadcast_to] array_api_tests/test_signatures.py::test_func_signature[expand_dims] array_api_tests/test_signatures.py::test_func_signature[flip] array_api_tests/test_signatures.py::test_func_signature[roll] array_api_tests/test_signatures.py::test_func_signature[squeeze] array_api_tests/test_signatures.py::test_func_signature[stack] array_api_tests/test_signatures.py::test_func_signature[matrix_transpose] array_api_tests/test_signatures.py::test_func_signature[vecdot] array_api_tests/test_signatures.py::test_func_signature[take] array_api_tests/test_signatures.py::test_func_signature[argmax] array_api_tests/test_signatures.py::test_func_signature[argmin] array_api_tests/test_signatures.py::test_func_signature[from_dlpack] array_api_tests/test_signatures.py::test_func_signature[cumulative_sum] array_api_tests/test_signatures.py::test_func_signature[searchsorted] array_api_tests/test_signatures.py::test_func_signature[repeat] array_api_tests/test_signatures.py::test_func_signature[tile] array_api_tests/test_signatures.py::test_func_signature[unstack] array_api_tests/test_signatures.py::test_func_signature[clip] array_api_tests/test_signatures.py::test_func_signature[copysign] array_api_tests/test_signatures.py::test_func_signature[hypot] array_api_tests/test_signatures.py::test_func_signature[logical_not] array_api_tests/test_signatures.py::test_func_signature[maximum] array_api_tests/test_signatures.py::test_func_signature[minimum] array_api_tests/test_signatures.py::test_func_signature[signbit] # linalg namespace array_api_tests/test_signatures.py::test_extension_func_signature[linalg.cross] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.matmul] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.cholesky] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.matrix_norm] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.matrix_rank] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.matrix_transpose] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.outer] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.pinv] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.svdvals] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.tensordot] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.vecdot] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.det] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.diagonal] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.eigh] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.eigvalsh] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.inv] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.matrix_power] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.qr] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.slogdet] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.solve] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.svd] array_api_tests/test_signatures.py::test_extension_func_signature[linalg.trace] # Array object namespace array_api_tests/test_signatures.py::test_array_method_signature[__dlpack__] array_api_tests/test_signatures.py::test_array_method_signature[__dlpack_device__] array_api_tests/test_signatures.py::test_array_method_signature[__setitem__] # not implemented array_api_tests/test_creation_functions.py::test_meshgrid # test_array_object array_api_tests/test_array_object.py::test_getitem array_api_tests/test_array_object.py::test_setitem array_api_tests/test_array_object.py::test_getitem_masking array_api_tests/test_array_object.py::test_setitem_masking # test_operators_and_elementwise_functions # throws for x < 1 instead of NaN array_api_tests/test_operators_and_elementwise_functions.py::test_acosh # not implemented array_api_tests/test_operators_and_elementwise_functions.py::test_logical_not # test_data_type_functions # not implemented array_api_tests/test_data_type_functions.py::test_broadcast_arrays array_api_tests/test_data_type_functions.py::test_broadcast_to array_api_tests/test_data_type_functions.py::test_isdtype array_api_tests/test_data_type_functions.py::test_result_type array_api_tests/test_data_type_functions.py::test_finfo[Float32] # test_has_names array_api_tests/test_has_names.py::test_has_names[linalg-cholesky] array_api_tests/test_has_names.py::test_has_names[linalg-cross] array_api_tests/test_has_names.py::test_has_names[linalg-det] array_api_tests/test_has_names.py::test_has_names[linalg-diagonal] array_api_tests/test_has_names.py::test_has_names[linalg-eigh] array_api_tests/test_has_names.py::test_has_names[linalg-eigvalsh] array_api_tests/test_has_names.py::test_has_names[linalg-inv] array_api_tests/test_has_names.py::test_has_names[linalg-matmul] array_api_tests/test_has_names.py::test_has_names[linalg-matrix_norm] array_api_tests/test_has_names.py::test_has_names[linalg-matrix_power] array_api_tests/test_has_names.py::test_has_names[linalg-matrix_rank] array_api_tests/test_has_names.py::test_has_names[linalg-matrix_transpose] array_api_tests/test_has_names.py::test_has_names[linalg-outer] array_api_tests/test_has_names.py::test_has_names[linalg-pinv] array_api_tests/test_has_names.py::test_has_names[linalg-qr] array_api_tests/test_has_names.py::test_has_names[linalg-slogdet] array_api_tests/test_has_names.py::test_has_names[linalg-solve] array_api_tests/test_has_names.py::test_has_names[linalg-svd] array_api_tests/test_has_names.py::test_has_names[linalg-svdvals] array_api_tests/test_has_names.py::test_has_names[linalg-tensordot] array_api_tests/test_has_names.py::test_has_names[linalg-trace] array_api_tests/test_has_names.py::test_has_names[linalg-vecdot] array_api_tests/test_has_names.py::test_has_names[statistical-cumulative_sum] array_api_tests/test_has_names.py::test_has_names[statistical-mean] array_api_tests/test_has_names.py::test_has_names[statistical-std] array_api_tests/test_has_names.py::test_has_names[statistical-var] array_api_tests/test_has_names.py::test_has_names[set-unique_all] array_api_tests/test_has_names.py::test_has_names[set-unique_counts] array_api_tests/test_has_names.py::test_has_names[set-unique_inverse] array_api_tests/test_has_names.py::test_has_names[set-unique_values] array_api_tests/test_has_names.py::test_has_names[searching-argmax] array_api_tests/test_has_names.py::test_has_names[searching-argmin] array_api_tests/test_has_names.py::test_has_names[searching-searchsorted] array_api_tests/test_has_names.py::test_has_names[creation-from_dlpack] array_api_tests/test_has_names.py::test_has_names[creation-meshgrid] array_api_tests/test_has_names.py::test_has_names[creation-tril] array_api_tests/test_has_names.py::test_has_names[creation-triu] array_api_tests/test_has_names.py::test_has_names[manipulation-broadcast_arrays] array_api_tests/test_has_names.py::test_has_names[manipulation-broadcast_to] array_api_tests/test_has_names.py::test_has_names[manipulation-concat] array_api_tests/test_has_names.py::test_has_names[manipulation-expand_dims] array_api_tests/test_has_names.py::test_has_names[manipulation-flip] array_api_tests/test_has_names.py::test_has_names[manipulation-repeat] array_api_tests/test_has_names.py::test_has_names[manipulation-roll] array_api_tests/test_has_names.py::test_has_names[manipulation-squeeze] array_api_tests/test_has_names.py::test_has_names[manipulation-stack] array_api_tests/test_has_names.py::test_has_names[manipulation-tile] array_api_tests/test_has_names.py::test_has_names[manipulation-unstack] array_api_tests/test_has_names.py::test_has_names[sorting-argsort] array_api_tests/test_has_names.py::test_has_names[sorting-sort] array_api_tests/test_has_names.py::test_has_names[data_type-isdtype] array_api_tests/test_has_names.py::test_has_names[data_type-result_type] array_api_tests/test_has_names.py::test_has_names[elementwise-clip] array_api_tests/test_has_names.py::test_has_names[elementwise-copysign] array_api_tests/test_has_names.py::test_has_names[elementwise-hypot] array_api_tests/test_has_names.py::test_has_names[elementwise-logical_not] array_api_tests/test_has_names.py::test_has_names[elementwise-maximum] array_api_tests/test_has_names.py::test_has_names[elementwise-minimum] array_api_tests/test_has_names.py::test_has_names[elementwise-signbit] array_api_tests/test_has_names.py::test_has_names[linear_algebra-matrix_transpose] array_api_tests/test_has_names.py::test_has_names[linear_algebra-vecdot] array_api_tests/test_has_names.py::test_has_names[indexing-take] array_api_tests/test_has_names.py::test_has_names[array_method-__dlpack__] array_api_tests/test_has_names.py::test_has_names[array_method-__dlpack_device__] array_api_tests/test_has_names.py::test_has_names[array_method-__setitem__] array_api_tests/test_has_names.py::test_has_names[array_attribute-T] # test_indexing_functions # not implemented array_api_tests/test_indexing_functions.py::test_take # test_linalg # not implemented array_api_tests/test_linalg.py::test_matrix_transpose array_api_tests/test_linalg.py::test_vecdot array_api_tests/test_linalg.py::test_eigh array_api_tests/test_linalg.py::test_eigvalsh array_api_tests/test_linalg.py::test_inv array_api_tests/test_linalg.py::test_linalg_matmul array_api_tests/test_linalg.py::test_matrix_norm array_api_tests/test_linalg.py::test_matrix_power array_api_tests/test_linalg.py::test_matrix_rank array_api_tests/test_linalg.py::test_linalg_matrix_transpose array_api_tests/test_linalg.py::test_outer array_api_tests/test_linalg.py::test_pinv array_api_tests/test_linalg.py::test_qr array_api_tests/test_linalg.py::test_slogdet array_api_tests/test_linalg.py::test_cholesky array_api_tests/test_linalg.py::test_det array_api_tests/test_linalg.py::test_diagonal array_api_tests/test_linalg.py::test_vector_norm array_api_tests/test_linalg.py::test_svdvals array_api_tests/test_linalg.py::test_svd array_api_tests/test_linalg.py::test_trace array_api_tests/test_linalg.py::test_linalg_vecdot array_api_tests/test_linalg.py::test_linalg_tensordot # test_manipulation_functions # not implemented array_api_tests/test_manipulation_functions.py::test_concat array_api_tests/test_manipulation_functions.py::test_expand_dims array_api_tests/test_manipulation_functions.py::test_squeeze array_api_tests/test_manipulation_functions.py::test_flip array_api_tests/test_manipulation_functions.py::test_roll array_api_tests/test_manipulation_functions.py::test_stack # test_searching_functions # not implemented array_api_tests/test_searching_functions.py::test_argmax array_api_tests/test_searching_functions.py::test_argmin # 0D issue array_api_tests/test_searching_functions.py::test_nonzero # test_set_functions # not implemented array_api_tests/test_set_functions.py::test_unique_all array_api_tests/test_set_functions.py::test_unique_counts array_api_tests/test_set_functions.py::test_unique_inverse array_api_tests/test_set_functions.py::test_unique_values # test_sorting_functions # not implemented array_api_tests/test_sorting_functions.py::test_argsort array_api_tests/test_sorting_functions.py::test_sort # test_special_cases array_api_tests/test_special_cases.py::test_unary[acos(x_i > 1) -> NaN] array_api_tests/test_special_cases.py::test_unary[acos(x_i < -1) -> NaN] array_api_tests/test_special_cases.py::test_unary[acosh(x_i is NaN) -> NaN] array_api_tests/test_special_cases.py::test_unary[acosh(x_i < 1) -> NaN] array_api_tests/test_special_cases.py::test_unary[acosh(x_i is 1) -> +0] array_api_tests/test_special_cases.py::test_unary[acosh(x_i is +infinity) -> +infinity] array_api_tests/test_special_cases.py::test_unary[asin(x_i > 1) -> NaN] array_api_tests/test_special_cases.py::test_unary[asin(x_i < -1) -> NaN] array_api_tests/test_special_cases.py::test_unary[atanh(x_i < -1) -> NaN] array_api_tests/test_special_cases.py::test_unary[atanh(x_i > 1) -> NaN] array_api_tests/test_special_cases.py::test_unary[cos(x_i is +infinity) -> NaN] array_api_tests/test_special_cases.py::test_unary[cos(x_i is -infinity) -> NaN] array_api_tests/test_special_cases.py::test_unary[log(x_i < 0) -> NaN] array_api_tests/test_special_cases.py::test_unary[log1p(x_i < -1) -> NaN] array_api_tests/test_special_cases.py::test_unary[log2(x_i < 0) -> NaN] array_api_tests/test_special_cases.py::test_unary[log10(x_i < 0) -> NaN] array_api_tests/test_special_cases.py::test_unary[signbit(x_i is +0) -> False] array_api_tests/test_special_cases.py::test_unary[signbit(x_i is -0) -> True] array_api_tests/test_special_cases.py::test_unary[signbit(x_i is +infinity) -> False] array_api_tests/test_special_cases.py::test_unary[signbit(x_i is -infinity) -> True] array_api_tests/test_special_cases.py::test_unary[signbit(isfinite(x_i) and x_i > 0) -> False] array_api_tests/test_special_cases.py::test_unary[signbit(isfinite(x_i) and x_i < 0) -> True] array_api_tests/test_special_cases.py::test_unary[signbit(x_i is NaN) -> False] array_api_tests/test_special_cases.py::test_unary[signbit(x_i is NaN) -> True] array_api_tests/test_special_cases.py::test_unary[sin((x_i is +infinity or x_i == -infinity)) -> NaN] array_api_tests/test_special_cases.py::test_unary[sqrt(x_i < 0) -> NaN] array_api_tests/test_special_cases.py::test_unary[tan((x_i is +infinity or x_i == -infinity)) -> NaN] array_api_tests/test_special_cases.py::test_binary[copysign(x2_i < 0) -> NaN] array_api_tests/test_special_cases.py::test_binary[copysign(x2_i is -0) -> NaN] array_api_tests/test_special_cases.py::test_binary[copysign(x2_i is +0) -> NaN] array_api_tests/test_special_cases.py::test_binary[copysign(x2_i > 0) -> NaN] array_api_tests/test_special_cases.py::test_binary[maximum(x1_i is NaN or x2_i is NaN) -> NaN] array_api_tests/test_special_cases.py::test_binary[minimum(x1_i is NaN or x2_i is NaN) -> NaN] array_api_tests/test_special_cases.py::test_binary[pow(x1_i is -infinity and x2_i > 0 and not (x2_i.is_integer() and x2_i % 2 == 1)) -> +infinity] array_api_tests/test_special_cases.py::test_binary[pow(x1_i is -infinity and x2_i < 0 and x2_i.is_integer() and x2_i % 2 == 1) -> -0] array_api_tests/test_special_cases.py::test_binary[pow(x1_i is -0 and x2_i > 0 and x2_i.is_integer() and x2_i % 2 == 1) -> -0] array_api_tests/test_special_cases.py::test_binary[pow(x1_i < 0 and isfinite(x1_i) and isfinite(x2_i) and not x2_i.is_integer()) -> NaN] array_api_tests/test_special_cases.py::test_binary[__pow__(x1_i < 0 and isfinite(x1_i) and isfinite(x2_i) and not x2_i.is_integer()) -> NaN] array_api_tests/test_special_cases.py::test_binary[__pow__(x1_i is -infinity and x2_i > 0 and not (x2_i.is_integer() and x2_i % 2 == 1)) -> +infinity] array_api_tests/test_special_cases.py::test_binary[__pow__(x1_i is -infinity and x2_i < 0 and x2_i.is_integer() and x2_i % 2 == 1) -> -0] array_api_tests/test_special_cases.py::test_binary[__pow__(x1_i is -0 and x2_i > 0 and x2_i.is_integer() and x2_i % 2 == 1) -> -0] array_api_tests/test_special_cases.py::test_iop[__ipow__(x1_i is -infinity and x2_i > 0 and not (x2_i.is_integer() and x2_i % 2 == 1)) -> +infinity] array_api_tests/test_special_cases.py::test_iop[__ipow__(x1_i is -infinity and x2_i < 0 and x2_i.is_integer() and x2_i % 2 == 1) -> -0] array_api_tests/test_special_cases.py::test_iop[__ipow__(x1_i is -0 and x2_i > 0 and x2_i.is_integer() and x2_i % 2 == 1) -> -0] array_api_tests/test_special_cases.py::test_iop[__ipow__(x1_i < 0 and isfinite(x1_i) and isfinite(x2_i) and not x2_i.is_integer()) -> NaN] array_api_tests/test_special_cases.py::test_empty_arrays[mean] array_api_tests/test_special_cases.py::test_empty_arrays[std] array_api_tests/test_special_cases.py::test_empty_arrays[var] array_api_tests/test_special_cases.py::test_nan_propagation[cumulative_sum] array_api_tests/test_special_cases.py::test_nan_propagation[max] array_api_tests/test_special_cases.py::test_nan_propagation[mean] array_api_tests/test_special_cases.py::test_nan_propagation[min] array_api_tests/test_special_cases.py::test_nan_propagation[prod] array_api_tests/test_special_cases.py::test_nan_propagation[std] array_api_tests/test_special_cases.py::test_nan_propagation[sum] array_api_tests/test_special_cases.py::test_nan_propagation[var] # test_statistical_functions # not implemented array_api_tests/test_statistical_functions.py::test_mean sparse-0.17.0/ci/Numba-array-api-skips.txt000066400000000000000000000000001501262445000202710ustar00rootroot00000000000000sparse-0.17.0/ci/Numba-array-api-xfails.txt000066400000000000000000000212671501262445000204500ustar00rootroot00000000000000array_api_tests/test_array_object.py::test_setitem array_api_tests/test_array_object.py::test_getitem_masking array_api_tests/test_array_object.py::test_setitem_masking array_api_tests/test_creation_functions.py::test_arange array_api_tests/test_creation_functions.py::test_linspace array_api_tests/test_creation_functions.py::test_meshgrid array_api_tests/test_data_type_functions.py::test_finfo[float32] array_api_tests/test_has_names.py::test_has_names[linalg-cholesky] array_api_tests/test_has_names.py::test_has_names[linalg-cross] array_api_tests/test_has_names.py::test_has_names[linalg-det] array_api_tests/test_has_names.py::test_has_names[linalg-diagonal] array_api_tests/test_has_names.py::test_has_names[linalg-eigh] array_api_tests/test_has_names.py::test_has_names[linalg-eigvalsh] array_api_tests/test_has_names.py::test_has_names[linalg-inv] array_api_tests/test_has_names.py::test_has_names[linalg-matmul] array_api_tests/test_has_names.py::test_has_names[linalg-matrix_norm] array_api_tests/test_has_names.py::test_has_names[linalg-matrix_power] array_api_tests/test_has_names.py::test_has_names[linalg-matrix_rank] array_api_tests/test_has_names.py::test_has_names[linalg-matrix_transpose] array_api_tests/test_has_names.py::test_has_names[linalg-outer] array_api_tests/test_has_names.py::test_has_names[linalg-pinv] array_api_tests/test_has_names.py::test_has_names[linalg-qr] array_api_tests/test_has_names.py::test_has_names[linalg-slogdet] array_api_tests/test_has_names.py::test_has_names[linalg-solve] array_api_tests/test_has_names.py::test_has_names[linalg-svd] array_api_tests/test_has_names.py::test_has_names[linalg-svdvals] array_api_tests/test_has_names.py::test_has_names[linalg-tensordot] array_api_tests/test_has_names.py::test_has_names[linalg-trace] array_api_tests/test_has_names.py::test_has_names[linalg-vecdot] array_api_tests/test_has_names.py::test_has_names[linalg-vector_norm] array_api_tests/test_has_names.py::test_has_names[set-unique_all] array_api_tests/test_has_names.py::test_has_names[set-unique_inverse] array_api_tests/test_has_names.py::test_has_names[creation-arange] array_api_tests/test_has_names.py::test_has_names[creation-from_dlpack] array_api_tests/test_has_names.py::test_has_names[creation-linspace] array_api_tests/test_has_names.py::test_has_names[creation-meshgrid] array_api_tests/test_has_names.py::test_has_names[sorting-argsort] array_api_tests/test_has_names.py::test_has_names[array_method-__dlpack__] array_api_tests/test_has_names.py::test_has_names[array_method-__dlpack_device__] array_api_tests/test_has_names.py::test_has_names[array_method-__setitem__] array_api_tests/test_indexing_functions.py::test_take array_api_tests/test_set_functions.py::test_unique_all array_api_tests/test_set_functions.py::test_unique_inverse array_api_tests/test_signatures.py::test_func_signature[unique_all] array_api_tests/test_signatures.py::test_func_signature[unique_inverse] array_api_tests/test_signatures.py::test_func_signature[arange] array_api_tests/test_signatures.py::test_func_signature[from_dlpack] array_api_tests/test_signatures.py::test_func_signature[linspace] array_api_tests/test_signatures.py::test_func_signature[meshgrid] array_api_tests/test_signatures.py::test_func_signature[argsort] array_api_tests/test_signatures.py::test_array_method_signature[__dlpack__] array_api_tests/test_signatures.py::test_array_method_signature[__dlpack_device__] array_api_tests/test_signatures.py::test_array_method_signature[__setitem__] array_api_tests/test_sorting_functions.py::test_argsort array_api_tests/test_has_names.py::test_has_names[fft-hfft] array_api_tests/test_has_names.py::test_has_names[fft-ihfft] array_api_tests/test_has_names.py::test_has_names[fft-fftfreq] array_api_tests/test_has_names.py::test_has_names[fft-rfftfreq] array_api_tests/test_has_names.py::test_has_names[fft-fftshift] array_api_tests/test_has_names.py::test_has_names[fft-ifftshift] array_api_tests/test_has_names.py::test_has_names[fft-fft] array_api_tests/test_has_names.py::test_has_names[fft-ifft] array_api_tests/test_has_names.py::test_has_names[fft-fftn] array_api_tests/test_has_names.py::test_has_names[fft-ifftn] array_api_tests/test_has_names.py::test_has_names[fft-rfft] array_api_tests/test_has_names.py::test_has_names[fft-irfft] array_api_tests/test_has_names.py::test_has_names[fft-rfftn] array_api_tests/test_has_names.py::test_has_names[fft-irfftn] array_api_tests/test_creation_functions.py::test_empty_like array_api_tests/test_data_type_functions.py::test_finfo[complex64] array_api_tests/test_manipulation_functions.py::test_squeeze array_api_tests/test_has_names.py::test_has_names[utility-diff] array_api_tests/test_has_names.py::test_has_names[manipulation-repeat] array_api_tests/test_has_names.py::test_has_names[manipulation-tile] array_api_tests/test_has_names.py::test_has_names[manipulation-unstack] array_api_tests/test_has_names.py::test_has_names[statistical-cumulative_sum] array_api_tests/test_has_names.py::test_has_names[statistical-cumulative_prod] array_api_tests/test_has_names.py::test_has_names[indexing-take_along_axis] array_api_tests/test_has_names.py::test_has_names[searching-count_nonzero] array_api_tests/test_has_names.py::test_has_names[searching-searchsorted] array_api_tests/test_signatures.py::test_func_signature[diff] array_api_tests/test_signatures.py::test_func_signature[repeat] array_api_tests/test_signatures.py::test_func_signature[tile] array_api_tests/test_signatures.py::test_func_signature[unstack] array_api_tests/test_signatures.py::test_func_signature[take_along_axis] array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity] array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity] array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity] array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity] array_api_tests/test_special_cases.py::test_binary[floor_divide(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0] array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity] array_api_tests/test_special_cases.py::test_binary[floor_divide(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0] array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity] array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity] array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity] array_api_tests/test_special_cases.py::test_binary[__floordiv__(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0] array_api_tests/test_special_cases.py::test_binary[__floordiv__(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0] array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity] array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity] array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity] array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0] array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity] array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0] array_api_tests/test_array_object.py::test_getitem_arrays_and_ints_1[1] array_api_tests/test_statistical_functions.py::test_cumulative_prod array_api_tests/test_statistical_functions.py::test_cumulative_sum array_api_tests/test_array_object.py::test_getitem_arrays_and_ints_1[None] array_api_tests/test_array_object.py::test_getitem_arrays_and_ints_2[1] array_api_tests/test_array_object.py::test_getitem_arrays_and_ints_2[None] array_api_tests/test_manipulation_functions.py::test_repeat array_api_tests/test_searching_functions.py::test_count_nonzero array_api_tests/test_searching_functions.py::test_searchsorted array_api_tests/test_manipulation_functions.py::test_tile array_api_tests/test_signatures.py::test_func_signature[cumulative_sum] array_api_tests/test_signatures.py::test_func_signature[cumulative_prod] array_api_tests/test_manipulation_functions.py::test_unstack array_api_tests/test_signatures.py::test_func_signature[count_nonzero] array_api_tests/test_signatures.py::test_func_signature[searchsorted] sparse-0.17.0/ci/array-api-tests-rev.txt000066400000000000000000000000511501262445000200420ustar00rootroot000000000000002db6c7b807a609a1539e312e01af093a45d34764 sparse-0.17.0/ci/clone_array_api_tests.sh000077500000000000000000000010421501262445000203730ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail ARRAY_API_TESTS_DIR="${ARRAY_API_TESTS_DIR:-"../array-api-tests"}" if [ ! -d "$ARRAY_API_TESTS_DIR" ]; then git clone --recursive https://github.com/data-apis/array-api-tests.git "$ARRAY_API_TESTS_DIR" fi git --git-dir="$ARRAY_API_TESTS_DIR/.git" --work-tree "$ARRAY_API_TESTS_DIR" clean -xddf git --git-dir="$ARRAY_API_TESTS_DIR/.git" --work-tree "$ARRAY_API_TESTS_DIR" fetch git --git-dir="$ARRAY_API_TESTS_DIR/.git" --work-tree "$ARRAY_API_TESTS_DIR" reset --hard $(cat "ci/array-api-tests-rev.txt") sparse-0.17.0/ci/environment.yml000066400000000000000000000004151501262445000165550ustar00rootroot00000000000000name: sparse-dev channels: - conda-forge - nodefaults dependencies: - python - pip - pip: - finch-tensor>=0.2.12 - finch-mlir>=0.0.2 - pytest-codspeed - numpy - numba - scipy - dask - pytest - pytest-cov - pytest-xdist sparse-0.17.0/ci/setup_env.sh000077500000000000000000000004051501262445000160340ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail if [ ! -d ".venv" ]; then python -m venv .venv source .venv/bin/activate pip install -e .[all] source ci/clone_array_api_tests.sh pip install -r ../array-api-tests/requirements.txt pip uninstall -y matrepr fi sparse-0.17.0/ci/test_Finch.sh000077500000000000000000000003221501262445000161100ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail python -c 'import finch' PYTHONFAULTHANDLER="${HOME}/faulthandler.log" SPARSE_BACKEND=Finch pytest --pyargs sparse/tests --cov-report=xml:coverage_Finch.xml -n auto -vvv sparse-0.17.0/ci/test_MLIR.sh000077500000000000000000000002201501262445000156210ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail SPARSE_BACKEND=MLIR pytest --pyargs sparse/mlir_backend --cov-report=xml:coverage_MLIR.xml -n auto -vvv sparse-0.17.0/ci/test_Numba.sh000077500000000000000000000005131501262445000161250ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail if [ $(python -c 'import numpy as np; print(np.lib.NumpyVersion(np.__version__) >= "2.0.0a1")') = 'True' ]; then pytest --pyargs sparse --doctest-modules --cov-report=xml:coverage_Numba.xml -n auto -vvv else pytest --pyargs sparse --cov-report=xml:coverage_Numba.xml -n auto -vvv fi sparse-0.17.0/ci/test_all.sh000077500000000000000000000005671501262445000156440ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail ACTIVATE_VENV="${ACTIVATE_VENV:-0}" if [ $ACTIVATE_VENV = "1" ]; then source .venv/bin/activate fi source ci/test_backends.sh source ci/test_examples.sh source ci/test_notebooks.sh SPARSE_BACKEND="Numba" source ci/test_array_api.sh SPARSE_BACKEND="Finch" PYTHONFAULTHANDLER="${HOME}/faulthandler.log" source ci/test_array_api.sh sparse-0.17.0/ci/test_array_api.sh000077500000000000000000000007671501262445000170450ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail source ci/clone_array_api_tests.sh if [ "${SPARSE_BACKEND}" = "Finch" ]; then python -c 'import finch' fi ARRAY_API_TESTS_MODULE="sparse" pytest "$ARRAY_API_TESTS_DIR/array_api_tests/" -v -c "$ARRAY_API_TESTS_DIR/pytest.ini" --ci --max-examples=2 --derandomize --disable-deadline --disable-warnings -o xfail_strict=True -n auto --xfails-file ../sparse/ci/${SPARSE_BACKEND}-array-api-xfails.txt --skips-file ../sparse/ci/${SPARSE_BACKEND}-array-api-skips.txt sparse-0.17.0/ci/test_backends.sh000077500000000000000000000001571501262445000166410ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail source ci/test_Numba.sh source ci/test_Finch.sh source ci/test_MLIR.sh sparse-0.17.0/ci/test_examples.sh000077500000000000000000000001761501262445000167060ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail for example in $(find ./examples/ -iname '*.py'); do CI_MODE=1 python $example done sparse-0.17.0/ci/test_notebooks.sh000077500000000000000000000001571501262445000170720ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail CI_MODE=1 pytest -n 4 --nbmake --nbmake-timeout=600 ./examples/*.ipynb sparse-0.17.0/conftest.py000066400000000000000000000013241501262445000152720ustar00rootroot00000000000000import pathlib import sparse import pytest @pytest.fixture(scope="session", autouse=True) def add_doctest_modules(doctest_namespace): import sparse import numpy as np doctest_namespace["np"] = np doctest_namespace["sparse"] = sparse def pytest_ignore_collect(collection_path: pathlib.Path, config: pytest.Config) -> bool | None: if "numba_backend" in collection_path.parts and sparse._BackendType.Numba != sparse._BACKEND: return True if "mlir_backend" in collection_path.parts and sparse._BackendType.MLIR != sparse._BACKEND: return True if "finch_backend" in collection_path.parts and sparse._BackendType.Finch != sparse._BACKEND: return True return None sparse-0.17.0/docs/000077500000000000000000000000001501262445000140235ustar00rootroot00000000000000sparse-0.17.0/docs/api.md000066400000000000000000000000001501262445000151040ustar00rootroot00000000000000sparse-0.17.0/docs/assets/000077500000000000000000000000001501262445000153255ustar00rootroot00000000000000sparse-0.17.0/docs/assets/images/000077500000000000000000000000001501262445000165725ustar00rootroot00000000000000sparse-0.17.0/docs/assets/images/check-list-icon.png000066400000000000000000000074521501262445000222640ustar00rootroot00000000000000‰PNG  IHDR๊z„฿qขPLTE %:5tRNSฑ %+37น#5ฉIMjR“คฉAjคฉAjR“šิค&5H Rƒิ 5H Rƒิค&5ฉIMjR“คฉAjคฉIMjR“šิค&5H Rƒิ 5H Rƒิค&5ฉIMjคฉAjคฉIMjR“šิŒิ 5H Rƒิ 5HMjR“šิค&5ฉAjคฉAjค&5ฉIMjRƒิ 5H Rƒิ 5HMjR“šิคฉAjzฅv8tช9๒ูN]๊“ว งป™3ศSะj๊p ฏŒim5๕ตพ๑าะd๊ฆฎiาะb๊๊Jื,b"ฺK}ถะ ฮ‰h.u<ัM๚Dด–:7ะฆฒฐฺน{œ/~ฆ๙dะ.q๊Žn–ศ๎โ๖\บษEtคฉ™n–สฮ~ฯี“สQฆ.ง๊0“…ื๊‰U๋Sๆ_๖[„}๕G็่RGCujษnฎี'อcK}ซNiAv๒[ฝ’–Ž+uKบฒ“xก~ๅŽ)u}ฅNฑ์ไR}๓๛ˆR—๊””d'แB}34•บt9๏ปตP6ศ?ชSV—ิิ;ูฉิ•ก>™7ืc‡๗๊ึ–uี?็VR‡W™3,์ง'ปzP\I๋้+ำyๅ\F‘์jช Œค๎่ใW*ฉ:อOdg ๕ฯะF๊rถ๕‹๒้–4iYv7W๊Ÿ+ฉ›[ฯu^ช[ส>ฺ๊Ÿบ‰ิทบIV•๛•ํ{>ิญคIcฉ?}‹H)QงeQ๖4V฿๔ฤD๊{๗YƒยLV5ูWC=“•lค๎้fY5|๘ž๋๕น‘๚ๅVlคnซร์VnไЉ๚d–7’บค{F๒ฟ3๕GR#ฉeค;›ไs.V”ฎ‰™ิuUR’ฯช/ีำ’ุI-}M๖[>๏คŸ้ฯท๊ฦb)uaก;น”/Qบ]๊ฯ6๏žZ{f๋—๎ขŸ“/Vš›Ÿ้ๆ๊ข”3๘x^O?nฬs๓>งฮฯ๕ฃง๓9ติ3˜U…^~ง–[˜ ry•z%šZง–Zฆ๏ป็ eRหพkส{MคŽg๚ŽคH+ ฉ฿_ยณ:ฉLค~ oSสL๊ํKxPvRK5SงQD(CฉๅZ]ๆ't2•:ž:ฮ"“ญิRษt“Œ:lฅv?5E${ฉฃG]p>ิ`jฉฌ๔ญ 7#˜L-}cyF"KฉฏzMซ2šZย}aฮื,ฉ๊}ฒบแบฅ้ิ’ซŒๆหiะ*P‡MAjค&5ฉIMjRƒิ 5H Rƒิ 5HMjR“šิคฉAjคฉAjšิค&5ฉIMjRƒิ 5H Rƒิ 5HMjR“šิคฉAjค~–ห‘๎Rวอ`že‹‡6ปYNถ—๚duอ๛f ง. ๕…i‰‚VS็'๚ส’]Qฆކ๚ฦ”ฝ=lฆพั5]ZLิu)oˆ7˜บถา ZD4—๚lษ9ปใHOtฃ9ฅ,%ขฑิW๊ัV๊FฆD4•บœชKŸˆ–RŸฬีฉ!๛สล๙Ÿ)>ิัH‘์ฃะ์OW๚Sญ&ท๘(S๗ิญ){(ฎ๔งKบ…ใKVทปœ์,ผLีหs“ฉรH\๊™:M๓ฒณ๘N}qK7๏ชษจS” Љ:%EูYฎี?MฉบI ษ๕ฟKศPsc"uo‘ศฅบก์eฆน3‘๚^7JNEไwฆN“X๖ณP Mคtณ๛œ”uZžษžๆ๊Ÿ{ฉoิกY˜ฉำช*๛ซz&R7ี!}ฯ!i_si"๕ivเ#าฆ๚งf"ต tgก์๏,S฿,#ฉ‹+ัฌ Ÿ๑ พนฉฅฃปIJ๒)5๕ฬ๊ฬJ๊hฌปศ~ษ' ิ/Wb%ต”V‡==YจOฦ‘ิrฉืฯษงUR๕วฬิฝe#จq,_ –๘Sบ(–RK)ีYœส—(=ชNฤVji๋‡ค๙"Q'ัŸoั ล@๊}–๐s๙:๙๖8ำŸl๕pY|fซ˜๊๛บ๒ต๒ต‹V๛gjWbซObถ๔]wกภ@๊pจ๏xไอฑžงv/แผKิ@๊=–๐URfR็๔ o4žZฮuบฅ“ฅิาT—aD'KฉK๘œwK-ง‰n’ฒiƒฉิ๎'lณ‘Lฅv2YL}บิท์f2๕๚sZcva1šZZฏ[Oู ฯlji$๚ฯ_ณ ง–ำTxl๐ฺDjท|ฃ;z—eโฐฉ1H Rƒิค&5ฉIMjคฉAjคฉIMjR“šิ 5H Rƒิ 5H R“šิค&5ฉIMjคฉAjคฉIMjR“šิ 5H RƒิฯN+•"ปู้O]น]จช&ƒ_ผƒิt๊“>ฑญ‡แิๅ…พฒฑ‡ูิฅฅพ’"กอิ…™พ‘œัะb๊hจk4ด˜๚Fืelwl0uS•ธŽ"umฅ› ‰h-๕ูR7ZัX๊xข›ฅDด•: ิaID[ฉฏิeLDSฉฯ3uน!ขฅิๅTjD4”๚dฎNฎdJิ)ใCm)uOบ$๔+uฎXญ•"ูฌญn๗vไU๊Joฉชบzhl Wฯิišง Gฉ }}๖X•ทJ‰:%E๙„จXฅ<ฉJs}aี”ื๒Suส๊ฒท๒๕$ำ๏ฑ่7"Rฏ+.฿ไป—ยukหพj#V‹vD๊7ขฑพ‘–ไ…kuป•=ล=veRฟึา5“Hž]จ(’œN๔V RฟฮทVR]ฉำD๖s2ำƒศค~กฆฌส๒วูBา๒งdU!๕?Uu.แ๑๘;>2Wz0ณ˜ิฯ๚[ฮvๆ๊ึ‘=Wz8R?ปืฒŠˆtิmสžzz@Ižิบูc$L&ฑ์)Ÿ๊!5I๎ืๆn)Uงๅ™์๋\๊ิ5ิ![จำช*{ปีƒJCR?‰ำฏŠ#=ฌ"ฉ+๘๛nไfzX5RUX๊ŽB๙„ฅVิฯบ›YA>cฮง๚ฉบ‹ค$"ฏ๖3uaก|๑๛0zzPซˆิ{.แ—๒IzPCพWฟาื๊็ไ“ +=คฉ๗[ยG‘|Z_(-๚ต_๚!‹S๙ผRฆ‡ำๅสึ^KiEพยฬ"O๊ท๒s}฿น|‰๘Q$ซqสบ๚รณ…F“{ห๖บโtสW)ฮ๔ฒ&wŒ๎ต„?ๆๅ๋๎๕-j๎Pฯt‹eQพTcช฿*ํๆyบcฏ%|U“/ึ{‹o๋ะ*๐ฬึ๑์ะ'N*๕__ฏVŒx๓ตLn–RปOn #2YJํ^ย็*YK-ีLืฅlลb*ต๛ฆยŒXLฆŽงผุxj๗;nผkะhjฉฝn}วมทูิR๋ณU‡ะN-แลS์ไ–ฝvŒคv;บlVYปู้คฉAjR“šิค&5H Rƒิ 5H Rƒิค&5ฉIMjคฉAjคฉIMjR“šิค&5H Rƒิ 5H Rƒิค&5ฉIMjคฉAjคฉIMjR“šิ 5H Rƒิ 5H R“šิค&5ฉIMjคฉAjคฉIMjR“šิ 5H Rƒิ 5H R“šิค&5ฉAjคฉAj|]๊n1b|>้ƒ ฏ[ค1๓๓Gœ๊เFท้2@tu››เRทษZLะญLทน ฮuปQ๋W?ฏึHท;สŠฃPโS8ซ81c8c ไš1ƒk คฮŽA]‰ๆ`_I าc๖๕D‘*ƒฐฏ๚_j™0 ๋&๒'๕9ฃฐ๎)u8eถMรงิา`ถ5ไoj2 ห†๒/u1evฅลฉฅล@์jษหิน€‰Xไ^ฅ–ฮbฝสะ$ษzฏลZ-ƒคซซช{ไซEMk$ฌEึHุ0ŠึHุ$ŠผS €@ €@ €@ €ะ.#aณ(๊Z#acgs๗E k$ฌซzๆHืธซฮ\ŠOืmฌwodตcณๆิ&™jEนบท`ž†ฑฅ๚Cงืส๏๛ุKปงึyP}Qjฆ]kบ(uEฟ๐ฺตบ@ €@ €@ €@ €@ €@ €@ €@€@ €@ €@ €@€่เ่dูq%ครฺษฒฺ!ปัy{4[|ฒw/สฉ*i†Pใ“๕8ุํ+>'€F๛Šรใฐฦh_แ!€@ €@ €@ €@ €@ €@ €@ €@ €@ €@w `…ัพโ8|`ดฏุ๐๏Jๆ' ใQโPวั* Šฌ‚ฃU@-ŠsดชRtKฎย9’€!Žืc—ฐ|ม+‹DLqภ fIBสธV[,A“ฤผเ˜ŠMขฦ8h๒A„˜ฃS ฏgนB๐mŠง<ลj0‚yb+ฟ-ฐศ3oeื(!ฃZO฿œอฎžพšI้ม>แd6 @ €@ €@@-ฯปเแ๐ึ3๛๕นfj๔—fิฌ—แtyF€๋ š:]๓ิ›๎@ฑุ,ยิ–`ผ๛iฯ๗ๅNAี๑ ‡O–'พงีซNQตๆ‡้b0๓ภโ™„ิฆ>(NK‹„Ug(FปลR_ €เู:ล๕r@y`๙D ๏ศs—7Jจ}D๙ `ื ฤฬ9ศkŽA2 ว›$i@ธผ4O{ทNIT^#€|เ6Hชาโ๑ะ* ซีnYr8ึI2๓ะญมt} ๘_ŸลคWฯQnH~ภำpแ๑5‡YฯศGงฅ@_<@ๆšC๘๓Žฆ~‹RalŠภำิใ[ ลบ”’๒>—ˆ฿w๊J0คิิOล @ท=Žj฿V8€JQ;(j-‹˜ชฐ)Qtfฝัจ—5Šฮ.hƒ ‹ู7ิ เ\ฃH4k่l/9j้อผˆhSๆu• เ•"ะฺณs™๗Jy(^๚œc๚ 0ฃŒแพ๊<ฉาmญ€น‹๑( รRฒฯ+!qั˜qL~GตฺtK๏ศก.ฃ o‹ภc;ืี `F7T>๙ฆ3`E ภ๒9พญฎRง2…๋œ8‚`คQธ๗ ๏8‰‰J๔)ˆ#ZšสhRaŒ8ฟกN-พjูีิ™0ืI€มษ๔(Kใš“ตไ฿mZ฿; y‰“yก ฬYž1ลRšส1)ฎห5#:'ำUเษ`!งละุ๓-๎3ลฃy,ื–hœLGมืสOถ}ๆลRgษกฦ9™ถz›%ไoœ์๘/ค๖ยฒYžƒ€3หด!Qึ…ฃ ^(†ห&ขก$ฐสRy$จ|ไจผบŒ5nฆˆTีฟะbนฬื k„ญYถE†ป-ว”บหU#!ํดฏŒบ,>รˆCฉ๋ณ\๕4฿MrิIษ|ฯeภHgm,ื‰hฅ}NดXพF†' f๎NๅTงh+๑Ÿ'฿k†O:”ถKh$@๗XLP‰ฟ ฬ~[˜ถใฤZ =$_เs๊ทว–ožๅEจ5ฅฬ`ฉ)_†~'1;–๏้EV‹Rv`™ฦ$b–r`T 8eŠฤ๐Y‚ ฅฬนใอ EฺO [œ†v–ำซตb[‚รfสo(u…น๗›ฺ๙x๕ึKดL๛กฝ3 ๙๘O{wข,ฒธX5A\——EQ”ญษ๎ํฬฤŒ‚]Eœๆ{๛†ฎญ‘‚ว๚ว@ฬGqจ€3 …XๅLด[T’5จ35!ั@mฮb4ˆ™ ล ™h‰‚‚q@๏œ‰ศP[ข(๒๎๙=อ1“$"ํ=Jร]3ฉj‚ŠGœyD™มwภฮT 7C!2›พั'š A‘•œ?Lำฝ™ข+(jM™"—ฏิb Vอมฏ€-RI4ถ h๚0ƒ Cมf:IVR‰P€ฮรŒRp$ขRฅศ€ฮš๋+๐ฌ>สฅPƒขิŒ4.ๆ#ฌมดฑ~<ฬ8๕7ž!ีc‚RชWณTYl$[ไN)๐“ หApLš)0“6้า"$ี‡๋ i๔(ทวTšมๅ{ฦHkอbซภๅ9ใžebคคแ†ณแ:=ฎจ9”ป‹rง@aาๆฬ>RsษWC๘Xs๛Œ4ๅดAr6๙}\๏ภ้“yPผC™้"=ฎS‚ป*‰ด2ฺ๕5ๅค>ุฤ฿ขsเๅ๒&ฌ<ย ‡\ฺำhา^6–“ต Œ]lD๑‘…Mzœท5k๋วyธ”Q๖ศn,#ณxPKธF;"“6a<๘Q๎ึpกœ>Yt๔นxt๑เ“†œ zJ4…จ#›.YVj๒(w์n ,่ะA>[ช- Pก 3ฮ|ี˜ๆีุสQŸh B%,,*ีก,=ยโา&\แ"'—&ืพS {ฮ๋Bg%RF†œ๖4'TฤกœงŸฐจคY`=‡Iฎ} HžU๎0งจ‘|๑bd‘…พ๓Beชxคฺy-ขด“๑dท;nฏ๛Nง}มฏˆฺ฿ณํ–ี4ฒ[ @UNผwlฐ๏ฆคS…!ถ1TๅIDmแ0ๅ.฿•โau#™c ช2bพีB ชบ โํสีSช์Z๕%กษ๖ฤugc ”๗o#ม<JSLo›ผ&sKฦ’nOญ~f ฃ@ic‚ย!๓ฃvีU\N "๊หqTŽ“™ฬ€ฯ๙ ภ?0nฅั๏:้<๎ %xปณ•xŽฑด3”ค่ƒฮีv Ÿv๙i>!z%w’๏•ูgi^4เอO]แ}ƒ4+ผเžŠฃƒ8ส‹HL`งญ๐^แ+แGฺไ.}iƒ8ƒ_n๙(ภฺ‚šY_ฦq#O๚R X)“…H&Q?Bา|ฏส๛VF ฯ Š`Pฌ‚P๏x]ซสใีpั†> Mu(ฆอ>+ง3Fž=–ฐjwกpัฬ„"๚์;ภšq@้c)ฎฤC็Œ4’ๅณเc@ป‚๊๗ค ๗ณXR8:Oฃๅ)eซžะXเŒใ /๊ภิiŠๅy= ะ†ศ`7ึลu‰Ds๐fษ๎กŽxŸ`jPึฤ=!›xiรUญ„ตRQล7๓žfG ๘Y „ฺ_‘๖ฺ6`œฝ2๚<ลbย!”aฝy s๖#[…ปไพ๗๙$Nพg๘Œƒ๓ี๑ ‹๖ก˜ๆไ+๕—c[Rด๑+t˜Ypูำo•๖เ>ฺ[ˆๅ๘C n๔|๕V/‹Sž บ>6ษŸ—ห(๐•+0๗พ26„VS๓)พRปA๙ _ผย๒เdจา ง7 ŠภพŽ๐™‘๕aวˆUPยR”ภŸ๙I9% (u8%]‚งแqj ๛E9e4R<ŒลฃF์๐i<’„์ฉาKM@S’sDฤšO|คiภ" งC3*ํณ้ˆwhฆgv jซOฒรL@L๓ๆe"@ˆ D€"@ˆ D€"@ฐp0ะศ๕ๆ ่ขV&›&ภ+P/ษ† Q3O&`530}ิL%6Kภ‘๋ฏ@5SnZDอd&>™}๘ €ห;|‹ฅ"@ˆ Dภ๛๖\ะH€xๅบ๊บวu˜ธ. Š€ฅ๋0q]ภฤu˜ธ. t]Fฎ ป.`๊บŒ]0v]ภฤu5๑šฤํ`ยC@€k“ต0โ! วตฉ xb! @ dญLทg•ุS+ี๖ฌšด€}B๑ึD@ูN@ฦAภjเฑ€>j i'`ฬAค:V†ต๐ศB€wƒk๒3," •€' D€"@ˆ D€"@ˆ D€"@ˆ Dภv1B/€–*๔๗ ฉ7X@ˆฟS4E/ ฐOีZd` ภงูชƒฟจจจR“7H/ งปqฒd! ฅฝs”^ภ.ล^%|&c! ฤg"ขI/ภ'Xก›ช"@,@eมสพ๓’ƒ๕฿3Š็้จฒฒ3เกๅ๛็Sตœž…Hํ๎ู์โ c6:๘Bmลภ!พs .จฯภ8'jOฝ€๙=b็{`‹'c$ภวY๊ฟแ=>ฃF*`$๚ซl๘๊ NFฌ๘+l๛๕ ฝ@- ๙,๕ช้O๔๏,Oษx9ฮา$:˜3,ข6ๆOา €nณ€ะ่ญC9pฺ( GฬUYไ NPQผyk~ผ้๓๙ŒC%๐!†ท„ฤ'฿U4rํPํ ๆูก ุ*s%โŸฝ5i์ว้จt7}ทJMม 9sกแSฎ|ไ'`๒(qfุแ% x๛]{ฆฯทIy ภ7|4๛๖-,>šๆ‘‡€ฮ‚&เh›จ/-jผย ช ˆa†g dจจh;pdจ๖w ๋€ฝEf?0a$`B)CDEL5๐pOS %ว฿ิA`ษGภˆ"p ดจb‘"0a# ! €ˆY.hƒ u<7UไD €…?†็h…h๑T@ใซ&€$~00! ŒU @‰'๎Jชศ€œžj์ภ(Q‘ƒ ๐W@–˜ข๊˜2D+dŽภ=ธ‚p l9ฝ[\BuMฺผ‘่ ถŸx๔GO฿eอSสธAืuอƒ๓N€+คฎ h๐/…ธ‚_ะNำำs]€—ำNOH €Jธ.Rื๘ฤkƒ้้`>10ท่เ] ม1Bฆ๖/ƒ๋s‚R๘8ษ๑™๊ Œใ๓pใoฆฑๅ‰,ฮRฌbปฅpิ|ถพPกหJ@ฺpๆฉ)rF>S,ึ ๙6๖‰fตJสE@฿ฅŽ์M T@H }๋าลลล1,งฯB€—๋]ต์)๐™ogซฯ‘ื<nLฺ|}TFะL€P๋ย๕รbถ–&ซF4ใ_œaํ—Eูz8นโ-•ผ„U}vR›˜O?พ๔uธ\€3QvืG๋–ำiMKMณgฒ๋%_๕๐๖2๏W มธ้จ-szธWkf“มเข6\B7L ิxซJะE qฟWดุ กิ•‰ะฤd l๊‹ฺg“ๆเ.๘Nะ์บา@ใ‘:๛ผหค!;˜kิSG8)4ผน8ะธ™ห+ฺ-ิ>ซdš@[์ุ\เว(ต์(ช5 าk ˆดศ้คซู–๖๘:ฯต8jŸKฺ—ี‡ศฤ ฐ{—ใ๊ฤฺ้Ul%ฑ–t2ึ๗|ชศ;›7a ›ยUีๆJต•ธา๎ถ\Q•Š+•ฒอฉjน๒ฝฟวP:yxืRภzlฯŒํ™ื ฐ $บใลั!^hM่MLUiยYhปเ}DŠxytˆิš๓•ภ%RV“.zเ‚ {ืcZ!!\q&Cี= Z)ญเ•!e์•(ดps๕ัTๆ/ E" Zx“Cฉƒาก๓sRŠ!๔ˆKX•3๛@ปYฅค`๛ง>ถจฮ]ฦชๅ=”ง‚ฏVLP้ฉsฒU#&5aƒ๔ฉXฐ{0V4ทฉ0?`~00`dƒ๔Y vศ—j8HOt๛มณฦเqฑ˜=฿“/จฌ๓RŽธ่ด่jCฯH…Jฮหเ u1จ X๘€Nู]%"ฮภhธแo c?ธH๕,ำมSA+’ใ<เ2U เ+€H„พุqไทC“€k5(Žp…(๔ภัccณh=หt ‘วฤดšeI"ๅ G๔ดšd็๏‰ศโ6=๓ภศ„ิ=7G€@๗lPจ=ะซค €†๎Y#ฐ8 €ส03%Fอญำ๑ฝR€ิS‹ำ˜œง&€Wƒจj*ฅqธgภไ|kิะ@S52; @ว‰สI{ัkฌภ›€z ๏ศˆฐ‡Cฬ๘ซผB๒Pw ๅใU^F๏…ฉ‘ซจ…ภTฮูY_๘ฌฎฃภp#ทAro„ง‰R#Gฒƒ™“ฒzxข๒,Rplฮ‰•j”“ฉฃภHeีh„’หlภ@ๅ[vh~@'ะQทƒม โk bf๎้๔5พp,วัžอฒP8กJ<^s)j;sˆˆlmFภค:่ธB๔…0ชค”สC|หั^ ฆ+นd$pDโJ.+t ฤˆKแญเD“ืVrKDzj=Q}ล‰™็Y%ฉXฎD่a'hIVžๆ`ง๎V„oo?€วU็+ƒฐต—[‡๋้ตฎญ‹Sซ`4˜ปเPmไศ๒V hฏฐ7คƒล‹ุ˜ ฐl<ญิ9๓Fะ-`ะ“มฦดVํ๎;๙ ผC๕mฯ๊€K7ถเนPพ๒>ˆฉbŽ+Rเ h๛~ร>–oฮแฝ๛๗KxN)ํ๎ow_์พใ่ŸSฬA/ชgš๕มzพูf>}&๔ 8 K-x<ฝฌ๗๘ฯห้้}s5B0ฺ|๗๘ทผซij[‡ขJึํ+/[Cก†ยKถn ๕6L ูาะŽท$๚๛) b๋Jบ=UšA+Y>บ๗œs- ):x[ธม_ุ#/ q35ƒ’]b|BoJ ŽG7ฎeป`*Sg|Tnศ(uถ#|ั p๘จ< ๘ุธนw1‘@๐V hWŒšpม‰ฆ"†๕‰j.ะฆุtอเ่kY"'เ a๛0๐pC'เ —OE•1ฦ แ@Dภ๑œ๔T๕้โ`†Yใ9เsฟ|๙eห–<—ถwN๊—มฤ์าŽ5Q€:“าถ™‰—m๐6(ศ œ`๘[ๆy.qŠt„t›ฝQ๛ร•Pพึ‘-)๘ ITฏๅษ์Žด‘”ำข€ฐ NˆIงฺ็Œฺ*๎O~๚o]oIqซไoเ€๊„ šฝEฌ*สๆqก’์”ฝ@ค๋t'๊ฆ”Kฦํ˜ทิมูๅออ๕ล)ปimกNƒZ฿ต4ษJv"tาU฿Wขฉฯl–ฟYา*น๕Ž:้5ฟใญjซ? ฬ^๛นลฺ7c’ฤ#ํฆเœC`ะ๛ี?/žรฉขZnN ่ฏwzAEย ž๊vฺธฯ[j\oฯ?฿\ž%LฅฐˆmาŸZ%^43ๆZi;\๕อjiนค>•]4บzs+ใCฬgI25๙ๆฃt‹Xt‡ี'ฒ3i0UŒt๘&ฆ.๛ฉxศ๐3OM|‚ีŸŽณ~ุ?ดŠ“ศ.Jก>4๙ {Xhๅ_iไ ฮพฎCณ.ฬะ'kิ๘g๓Y0ฑูฅ•}yณ2\๋อ๚~ึŒE[จฺta่ƒฅ –พ|๚;่ษใ†HŽZ>ฃ) ‰Œืใยru^w=Zf„5ฑ P7ดRtฤ๙วว็ki}l/ํฦ{i> ‰‚ฎfข๊+ูœๆ๚โใัฃ๗็_%G?†ฦ#Ÿ1พŽึ}ฃรŽ™E Eอ›[l๛\+๊จ4งฒภ25[XใG<ทา(จUoฏฯvc6z}||#ฌ‚žโกc Œœ@ ึจE6ถœ\พ ฝ๔…†M7ด˜–ก3@žd๚‚}|ห$ฬภœUฉ‚๏aบ%ฬŽQ j1u็€(xWยฺk4iอ™Œยถ-ม |(…,.f7”—Žœ@D‚ฦ'ฏƒ42—˜o๏Wˆ้up %ึAปงเ}๎€m#0F๋VF ไHบลุ@เ'ฆศ‰‰\Pvฒ7ฃฐง’91Cิ[ูKf`* œpU€ฮ์่+฿ชว#‘ฃ"ว•ถ.Zไํ=ผ~Œ*ฅฏสžอึh/Žš*ฆฏ+ู๋ภ ๘ŒqUะ–๕”อa!“้ะฺ 'mt{ ำ€‘š-j่€[กฆฮ Enธว(8ผ*ฝม>€ฏ†J€ฉZ๗ีPๅ €5|5T+นฉ‡ 5ลUi#ทD5โH๗ €[t5ิˆ้Dhb# —Fฏ€ ผ๐๒ฝ „{aoนWะสภ+ oi๐Šฝ ๕ะVะk๗ :+๖Zศะ๎%W„lษ6OIENDฎB`‚sparse-0.17.0/docs/assets/images/install-software-download-icon.png000066400000000000000000000104361501262445000253350ustar00rootroot00000000000000‰PNG  IHDRภ dK฿จPLTE,฿H†7tRNS %*.37;@DHLQV\bflotx}†‹—œกงญณบฟฤศอาืแใ็๋๏๓๘๛ฯu๎IDATxฺ์้r8…แฃ}ณ"๏vlล–ckณdวZฯ_ืT%๓g–๊QOษ€๑<7ะ๐ I‘œ–๎ภป$bo๔ฎฅฟe *@T€จQขDˆ  *@T€จQขDˆ  *@T€จQขDˆ  *@T€จQขDˆ  *@T€จQขDˆ  *@T€จQขDˆ  *@T€จQขDˆ  *@T€จQขDˆ  *@T€จQขDˆ  *@T€จQขDจ T€ P*@จ T€ P*@จ T€ P*@จ T€ P*@จ T€ P*@จ T€ P*@จ T€ P*@จ T€ P*@จ T€ P*@จ T€ P*@จ T€ P*@จ T€ P*@จ ๔ทฌDˆ ็๚,้ำฃ}คฃตส๊ลีฆ,‘i ล=ฏฒx‡+f62•6giฬ๋0x |ฝz`fOฐh,X‹:,Fฬไ๊wWC˜4?X M˜<3“g8๒ษฬaำ๚d |ถ`๓ฤLๆpฃม์พรจ๓ล“๗ี†ัfฒซย‰ fwซ๎š'n…ีู๔แฤูภ์lร“ถ9ƒู#ณy€fw ป–'l๘๋l 'Vฬ๎ œ๏xฒv็pเžู|ม…sธ€ —{žจ%\ธcFm8pลpโzข\ร‰We<1‡>ธๅIบƒ7ฬhฆฬกGพ๓=ภ‘kfด€ๆะ+CžœpๅŠํ๋0๋06œyๆ‰y†3—!oฬn˜G๎ผ๐คผภ f๕ณ๓จรส+Oศ[๎œ3ซ ฬๆฬฃ‡ชcžŒI}cVkXUwฬ‡47v0fึxoc;ธU›k&์๏œ1ณ[1 ซ/x ธีcf?a๔“yฌเZsฉ™ฐC]fถ„ั‚y|ยนึg™gย์i๖ ˜ิ๖๙j‡{ํ_LฺWฮตรฝฎฦ\๐ ณ*๑L˜ /hแCoS™0๛๐๎&ฏฬe†Cฅž~ƒ๕< V`๑ม\ฆ๐cฐKv&ฬ‹Zฐ7๖ ๆ3†'I&ฐฟ‚ี`c)็ฬ็ ‡ส;7ถฟ/{f๗jžK7วส;7vovกาŒ™ฯ นgbเฯ6ิะฦ๓มฃวrฮ„ูG๗ฎPX‹9=มง'&dŸVฦำ.™ำ^ฝ0?แีs˜[)็๔ฟ^ห4fคƒ}๙”9}‡_•q gย์O๊๚(jอœ๎เYuยฬj๐lๆf‡yภทฺŒั{ฏรทE˜uืฬ๋ี฿นeฝ3ฏpซvฎเ_c™L˜œA–rอ˜ืŒาไฏ6˜97Wถฬk›๔็ฦV„0 ๒xชห๚0InlCๆฒ๖6ฎ‡0z๋ค?e7f.๛Zจgฐคษนผน˜a=0O๊™'8๋{ๆถA`?LT๘น%r0ฟB1 ฏ•ุำ฿ืƒœa?OF`\E`ร/ํ˜฿‚ซผ๑่ฆU„๖`x“๙-^u’โLX๘ฯ.O‘S“ฬqตY[†!l_D~มf8†๚<ต™0ป;๏m‡,`‚ฃh,๛L”ญuฌ0ฦq4?าš ณปanฏศgลp$ญฯคfย์ฎฝPk^,—ๆVฅ›ŽไŠ๙5 e๕‚ฃ้ฎš ณปd~—6๘แxฮ6้ฬ„ู]xy5ณพ ๏6™ญํพŸึT6,bˆc:฿ลฝu`ภ-&์‹ศปพฟœ“ุ-Žฉฯ๚†Ÿ›†Uสัฯ%๙G=๓šˆBล๏าถ๏,จH4qฯ„ูํ้sŒ๏5‹ๆGิ[ฺํXฤฺฐTภ๙€๔็ฦึ]Dc๋๓:`QUฤฃ๑ึvkŸฯํฯYะ1้ฎโ:ะnๅ๓y๖m•&ฺญํ~ฑ%ฒณ 5โา฿&๘™(๛ฝš}๙ ๚BdปHทด๛๐๘๊ฆลข>›ห=ธCd–G[ฎXิ ขช๔ฑYฐ˜ฉื=ˆฯm[†bยพˆ|สขๆˆะwšŸ9ฝฝฐจb4คัODhๆ๏Žฆรย&ˆาณy๋ภMYะซปฅ‡ฦˆำ‹q๋ภMฎXุโTy5}&*Jcz{;w{มL|nl^GœHOีญ1๚ด็ฦ– D๊ี{,๎ัชอผฮ„ฅ๔-ี™ว'(OˆW}a˜ ;ฉปmี฿}๓k, Ÿ‰:ฅxfxlxzžุบ‹ˆ|-|ฏํใg%Vฅ›3ฤ์ูื๖> ๎ทฮส0™'๖ห฿{”Dฎท1lx:o;Z†Ÿ™†ลษIอํฏนG_ว้ƒ—ˆ`g˜ ‹ษƒงท๕= ฮฟ‹]๊ทด๖ห๕ปaฉ€aejBscˆ฿=‹ี Wรฃ†dๆฦž€;OTวด่HŸฯ(แŽํ๎~ัขƒ4<fhbqC๚ๅiาคD<fย"qMƒŸKš4‘ŠรLXฎœญะคŽdผfยขpI/Oo'4ฉ"•1y่=†ฯ|uM‹=Rfย"0 ลฏM“-RR›™gยbbยพˆš&k$ฅ~ฐu`B๚49๗ฒธๆ ii,fยาั๓ฒ?ยŒ&Ÿ08๖'็ึ=$ฅK“)NeK็ทฉฬm๛HK‡&›Š‡ฌธภกDๆฦvƒไโฅMืววๆHOw}0–†m๎|lอ0E‚ฮ6ไษi๚๘(ย;m&Hั`ห[คงA›ชํh3F’.๎‘ :š8pFฃWH05]โภ=~๊ธ„Sกัผา่Eว% =mf8ฐคัณK@;ฺlซึฅ‡žtXฺt;ู= ีP‡% 5๎๏ู ระŠFo๘oดบืa ฟล„รEไŸดบำa ่“V-E“f7:,}ะ๊qAณk–€–t๋}HณK–€ดzว_Lhvฎร~‹ w‹ศW4๛ฆระ๑kำ๎L‡% ฉณ“ช€์่ฐคฅ็บ€฿๚; ่ฬฮ– ตuX;;bUจ่PยsZ€:t๊* „4[@* ผ็g?T@ย็€ŽNe, ๑Ÿ;ม๊™ (๏9   [Uๅฝจ๊Fฐดิ*  ๙Ÿซ๔Z ”ดU@ญซs@9 ๘๗U SQๅ>ด ซ@r~ป<4T@ษฏM่*œ?:จG๗u ฎJชึ๘Pบ ”๔€ฆฎษ๚ํฒ€šฮe=4TQU%ฟ ิกส}จฉ€vX@บLฯ h๊*P๖s@U”ป€JCฟส]ูฝ6%r„Q>s/€!* บ"—๙$V™T,SตัŠป๊ HชืUWdF้่>ฯg๒๖ฉทoรŒ˜01ky’(‹™-P๑I[€ “-U_ภ˜0X}ู์์L{ ฑ 9หะเฎXc &@s€&ห ฑ4ฯ4gs%จ9‡=€=€ ะšล0L€ึlฎ™๖๖า˜รhฮฐ™อYL@Zใ,ภฐzณ€/ษu–g4ท tY•MพCo 9“ `ˆ=€4f0์L@œH[pภs1ตุธ`˜าภฝภ@์ฅ๋ฯgง'ฟ~~9@ถธ'ŽPฏšใ‡da฿—ซสaˆLซ`ใ%wซ'5พ@tŽ๓&^฿๎๛าt2‡"ี&๑พ, ไฯDฟ!TำWL/‡7ุ;าP๔gโBฅT›<ผญไหั๒L€|ํ$ฆู๐ฅ86˜้:)LW๔eh๋L€lี๙9ฉ9ไ™`ฮ.ฤ้ˆwo๒cกา}๙ŒN๎™Oวๆ๚œ Pั็+๕็€ Pฯ๘  bิsyเฮ๏!ุˆ{ูFƒ 6dlx2/‚ ˜ษฎFใŸ1ฤ1’]!”แ{€bพ"œ๑๋€1# ำอœ%เฮ›€LฃยนƒPฃ! ื! !ิ70r‡ก๎˜อ aƒ‡‚2ูษbP‹m BฒHfฦoJ Bฐศๆ†{กพฑศๆ] ”Dบ0ฒฅJ< ‘n“ ห‹!ŒU0Š12# กฎม• t„\†P_ูไKฅ\Bo˜€ไ˜›Pื#ภ๎8(Ry9U6DOL@J1“]ƒXทณL€U* u8[‚HT ิใะว๙Jูสภ:๐E๓๊h60•น็ ็๘‰๔s˜ยฺ๕…kใมGDพไิ}๑๊x็xD`ฯมd^ำ— ‚‡# ญ &ฐ~้๙2d๑ภ๊q8"ฑท„W˜๋M_Žพซs0"RหXx.Vh๚’4๑จฤกˆฬ๑Nฮ5๐?หฌ๕}iถ.ๆC๗ฐถ[ญ7๛พTY<2ฺ๔lฐอz่ง†'VYไ๐„qฤ‚h= ๐S3ชโทฯ’hfฯีXฝ4๐Bš5ั …CE'm /ญณ*:ูฤOŒหขc?หฐ.๚(เ>.จ›ฆ…ื,๑L@<ม๛!ิ0รKb-t]L’eutPฤdU–G}๛&sZ,๊: ผe™๛ีe๑ถKคถ รฅ€ฎ๊&ฆฑ๖Y&u5Lใ‘ฒฺ.‚HpC จฮ‚qy6จค๎2‚J2 ๊,#8—r:ยˆ๓ฑAลด’วๆใใJ๙GX&Fคช…wX็{ั/โ}O,ž šหช๐ชp๑m๘{‚ืZรว˜EฎXฏlแรโ[œ UีลL$ซฬภ"ชy˜™ไ_;บ`๚ปf*^ๆmั้T\ฬœ™ฎฒ,„~-kA ;[ํฐภ๓ญป—s ’แm๎ณฮ๓ฉS+ญ˜!‘ฉ79'ฬ~๛ Zฮธ$ณฉฅๅŠิฒ—r ฟๆะๅ ‰.็‚IENDฎB`‚sparse-0.17.0/docs/assets/images/logo.png000066400000000000000000006724451501262445000202620ustar00rootroot00000000000000‰PNG  IHDRคค]#าะ pHYs  า~tEXtSoftwareCelsys Studio Toolมงแ| IDATxฺ์y|wa๗๑ฯof๖”dห๗}฿ฑใ8N์ุ‰ฯฤนI8 ”ถด ”ปจZ่Cฎ–(NG ”คO -<Bศ}XพวN๑}HŽ๏SฒคฝๆzXIถ›\๖Xใ|฿Jซ™อj~๛yอึ„aˆˆˆˆˆˆˆˆˆHTŒ‚”ˆˆˆˆˆˆˆˆDIAJDDDDDDDD"ฅ %""""""""‘R‘H)H‰ˆˆˆˆˆˆˆHคคDDDDDDDD$R R""""""""))‰”‚”ˆˆˆˆˆˆˆˆDJAJDDDDDDDD"ฅ %""""""""‘R‘H)H‰ˆˆˆˆˆˆˆHคคDDDDDDDD$R R""""""""))‰”‚”ˆˆˆHDŽ๗๊ฦะx -ฌ…ี5uzFDDDไํJAJDDD$"ว๏บบ<๑ h0ฦซญฎYrฏžy;R‰HGjง0%"""oS R""""yUjะ`Œ[S]ณ๔ทz–DDDไํ@AJDDD$"g RํSชญฎYVงgKDDD.d R""""yอ ีNaJDDD.p R""""yAช]ภbc‚š๊šบตz๖DDDไBข %"""‘7คฺ}ฦ˜ฺ๊šบz=‹"""r!P‰ศ›RํฆDDDไก %"""‘ทคฺ•รTMuM]ฃžU‰#)‘ˆœต ะoก…ตPaJDDDโFAJDDD$"g5HตS˜‘R‰ศ9 RํšŒ๑jซk–,ิ3-"""]‚”ˆˆˆHDฮijะะฆ๎ี3."""]•‚”ˆˆˆHD" RํฆDDDค S‰HคAช]@ƒ1ฅชk–ีiDDDคซP‰ศy RํSชU˜‘ฎ@AJDDD$"็5HตS˜‘.@AJDDD$"]"Hต XlLp[uM]ฝFFDDDขฆ %"""‘.คฺ}ฦ˜Z…)‰’‚”ˆˆˆHDบdjง0%"""R‰H—Rํฎถ0ีจ‘sEAJDDD$"ฑRMก๑ZX ฆDDDไ\P‰Hl‚T;…)9GคDDDD"ป ี. ษฏฆบfษฝE9คDDDD" `ูT\๓น†ไ๘ตX๖ฝMy+คDDDD"็ eœูŸ#9fXvƒปkEmbฤ•๗jTEDDไMอ-คDDDDข๋ •ฬ’]๐y’ฃgƒฑpท/%ท่{/†ญ๛jชk–ีitEDDไ อ-คDDDDข๋ •ฎขโฺฟ#1b&‹า–งษ/๙A๎,6ฆTซ0%"""ฏ{nก %"""8)+ƒ์ตObุๅๅ ต๑Qrห0ส—๏)L‰ˆˆศ๋ค %"""‘Xฉส>dฏ;Cฆ‚1ื?H~ลO '^}ใภภSS]SWฏQ‘ำQ‰HฌƒTทT\๛yœA—€โฺ!ฬ}„ล–3Qเ฿gŒฉU˜‘WR‰Hœƒ”c0ู‡3`"Rx—VOXสฝ๖+L‰ˆˆศ+(H‰ˆˆˆD$ึAชืฒ >‡ำo<„…็๎งฐๆ—„nu฿GzwZX ซk๊u4ˆˆˆผฝ)H‰ˆˆˆD$ึAช๏*ฎ๙vŸั๘ไŸ๙ลตC่฿ุ4…ฦ[จ0%""๒๖ฆ %"""‘8)gภEdฏ์^# ๙?กด๗o พ๛ึ๎8 มฏถบfษฝ:BDDD>คDDDD"็ •6ฬOcW"t๓ไ฿Ciห“เ{g็ฆDDDVคDDDD"๋ 5๒*ฒs>…ีญ?aฑ…\]”ถ-ภ;ปค0%""๒ถ  %"""‘8ฉไุ๙df} ซฒ/aก‰ึงฟ‹ปsนyภ€ลฦ”jซk–ี้ศน๐(H‰ˆˆˆD$ึA๊ขศฬ0VE/‚1rO}ท~„มน}`…)‘ ’‚”ˆˆˆHDโคR“฿I๚Š?วสTดก๕‰ƒท็ˆj.ฉ0%""rAQ‰HœƒTz๊๛H_ง˜T%A๓!Zgผ}๋ฃ฿ภฟฯS[]SWฏ#JDD$พคDDDD"฿ eHO3าS฿‡Id๑›๖‘{์xทœฟMR˜‰5)‘ˆฤ6H‹ฬŒH]๒nL"|7ญ~ศฮ๓ฟmๅ0US]Sืจ#LDD$Fำ )‘hฤ6HY™ซ>B๊โ[0vh=ญ|๘ห]c๛šBใ-ดฐ*L‰ˆˆฤƒ‚”ˆˆˆHDbค์ููŸ$9๑FŒeใA๋รwœ8ะตถSaJDD$6คDDDD"ื eœ™นท“špƒw`s9Hตํšฆzึ,ฉีQ'""าE็ R""""ัˆmJdศ^]Crฬ|ผhyจ–ฐpขkox@ƒ1^muอ’{u๔‰ˆˆtฑ๙…‚”ˆˆˆH4bค’Yฒ >OrิU†ธ{_ค๕กZยR.; 0%""า๕ๆ R""""ัˆmJUQq฿“1ƒ0๐๐vฏก๕ก; Rผv$ มทฆบf้ou4Šˆˆœ็๙…‚”ˆˆˆH4bค2ฉธ‹$†]N่ปxปžฅๅกZ ฆ๓ศ€ลNŸ!ตUผฏNGฅˆˆศyš_(H‰ˆˆˆD#ฎAสส๖ โฦ3x ก_ยฑŒึGพ๋ฑp๚ฃโ_[lU๖ช๊ttŠˆˆDKAJDDD$"ฑ Rฝจธ้qN"๔Šธำ๚๘?วy Œ3h•7‰ษVด]\|—5™ูŸ\ซฃTDD$ขณฑ‚”ˆˆˆH4bค*๛Pq๓8วบJ[ž"๗ิwb<6$†Lฅโฦ/cาU-‡)ฌล~}Ÿ1ฆถบฆฎ^Gซˆˆศ9>+H‰ˆˆˆDใ๘]ื„q\wษ๊ึŸสw|ปฯ่rฺ๘(นบปc<6$†ฯ โ๚/bRอ)ฌบŸโ†‡สฟ|…)‘s}:V‰ฦษ+ค q SV๗T๒5์^ร <ล๕ฟ'ฟ๔‡1ž[$F]Eลu_ภ$2M๛ษ?s/ฅอOtพ]เ฿ีฆu๔Šˆˆœๅำฑ‚”ˆˆˆH4š๛ฏCศNBฏุ>#aส๎1˜Š[พc0a)Gqไ—8พaู$วฬ#ปเs'…|…•Ni[ซoะoก…ตPaJDDไ์Q‰ˆtW่n_Bi๛Rฃป cึตร”s•ท~ซ๛ยb+ลตฟ&ฬฝ๑ห!9~WlX๙ๅ?ฦนโฬฃ0%""rV)H‰ˆˆˆD'$๐๐์คดๅ)J;W4๎ๅdŒ๊šaส๎=ฒคช๚[(NŸQ`':~[๐๖ญงด้ =/ๆ›บฬv;/ฆ๒ึฏaRU„นF๒ซƒโ‹ฟ๏8‘&5๕df€ทoนล๗เฺ๚ๆ๏4 ม˜าmี5ห๊tค‹ˆˆผŽ๓ฑ‚”ˆˆˆH4Ž฿uuเ๔ฟgุ4’ฃฎย๎5,ปใ6aก๗ๅ5”^z๏ภFยbหy฿ngศT*oพ“ช ศฃฐ๒g7<฿ p2K๚ฒžAผ=kษีดญ฿yภbcJต S"""ฏq>V‰ฦ๑ปฎ O]สt ‰‘W’>ปzpง0ไŽแีฏขธ๑1C[ y๎ฤ๐+จธ๑c’Y‚ึฃไ—„าฆวโ;NUž็คงพw๗r‹๎"hs๖DaJDDไŸคDDDDขั~…ิ+-OŒ˜Ab๘ C/รช๊ถำ๑ป ๙๎Že7?Apดžะ+Fพ‰QณจธแK'Eะ|ˆ๒Sฺ๒T|'ภ้*23n#uษปpw=Cฎ๎n‚ฮƒฦ˜š๊šบzฝDDDN9+H‰ˆˆˆDใd๊˜Šัฆ,›ไจู$†Oว<ซฒฯษ+ฆย iฅmu”6?‰฿ด|7ฒํNŽฝš์ตŸo Rษ/ฅm‹ใ;ฮT“น๒รค&ฝwวฒrj9r๎4๐๏3ฦิ*L‰ˆˆดคDDDDข๑๊ ี1%ฃ=L™d–ฤจY$Fฬฤ0ซขSพYเใ7๎กด๙ J[4‚ภ?็œp=ื-ุAำ>rK„ปcYlวมส๖$3๋ฃ$'\@ib๒uwไŽŸ๛W˜)ฯyคDDDDขqๆ ušIZฆ;ษ1sI ฟป฿8ฌlสแ Bฟ„dฅMใn_B;็pN—št3ู๙w€ๅ4๎!ทไธปž‰ํ8X•}ศฬ8ษฑWPฺ$นล๗NDถ a่ia-ฌฎฉkิ+CDDŽคDDDD"๒F‚T;ซขษqืเ ›†ำ{&ำ Œ@Xlล?ผโฆวqw,o๛Fพณ?ทK]๒.ฒsocแk ฟ๘๛ธปWวvฌnศฮ๙$‰Qณ(m|”’๏[ฃ€ฆะx ฆDDไํHAJDDD$"o&Hตณบ$5แZœ!Sฑ{วค*สa* ‹อxทP๐{]+ม๗ฮ๊vงงพฬฌO€1๘Gv’[|žตฑซzูนท“~ล ‘_๒ƒ๓๗M† S""๒6ค %"""‘7ค:#Ÿg4ษqW“|)V๕`L2[^c*๐ '๐๖ญงธ๎ผ—_8kž๖งdฎ+ภเFฎ๎ผ}๋c;vฯadๆNb่e_-๙ฅ?"๔K็wรšŒ๑jชk–ซW‹ˆˆ\่คDDDD"๒Vฎze˜r]Lr‚๒ย็๚—ร€๏ไŽแ๎~žโ๚฿แ๒g‹†๔๔ฟ 3ใ/๐n!_w7Mฑป๗Hฒ๓>ƒ3h2…็E~๙!๐บฦ4ใี*L‰ˆศ…LAJDDD$"o-HuL฿85L%†^Nrยต8'`*zaœ4ฝม‰xปืP\ ฑ†7๗p–Mfฦmคงไ๊๎ฦ?ด5ถใ`๗Cv8. ๐ฯษฏ้9]MQ˜‘ ˜‚”ˆˆˆHDฮN๊˜ฦัฆŒ!9z‰1๓p๚รd{`์Dy})ท@ะดท~ล 4|๕฿!ถCๆสž๚~ผฝ๋สA๊ศฮุŽƒำูyŸม๎7)<๛ไŸนฏ๋npภ‹ฦ”jชk–ี้U$"" )‘ˆœ ี1ฃ=,;Ib์|’c็c๗…•้– _Fพฦ=ธ;WP๐{ยB๓ซด๗๎คศ\๕RS๏ๅ็ษี}๏อ_qี8'‘v๏Q†ไW”ยs?๏๚ฐุ˜Rญย”ˆˆ\คDDDD"rn‚TวดŽŽ0•ช 9แ’cๆ`๗†IU–ฟ‘/๐๒M๘G๋qท-ฆธ้ัSพ‘๏๔aส$2df}œิไ[p๋W‘_r๑=ฑg๐”r๊9 ย€ฒฃ๐/ใณ S""rP‰ศน Rำ;ฺร’Uู›ไฤ›IŽบซภถoไ| w๏เfํK)m}๚”๕“:‡)“ช 3๋ค& €ปs9น%? hฺqH ฝŒฬผฯ`๗Onษ๗)พ๘๘ํHเ฿—ž๒žฺฬ;๊๕๊‘ธQ‰H4A๊ี์žรHNผ‰ฤ๐้ๅoไs’B่—[ใํ฿Hi๛RหO&!&UEv๎งHNธ€าถลไ— ๙Plว!1bูนทcuพGฎ๎.ŠŠๅพค/}/™ซ>zvขจืซLDDโBAJDDD$"็+Hตณ๛Ž!u๑-$†\†Uูlh[๘ผ๙พ ”ถ>๗๒ ''‹™jฒsšไธk(m~’ฒ%h=qHŒšEvฮงฐบ๕'๔KไžฅอOฤr_า— ™ม?ถ๛พงถ๒๏ฉืซMDDบ:)‘ˆœ๏ ี~ี“3`"ฉK3d Vฆบผพ–rM{๑๖ฌฃดu'E๖บ/3€โK_๑cย\clว!9f.™9Ÿยช์C่ษ=MJ๊bน/้้NๆŠ?หม?ZO๎๑o5นื/ดฐVืิ5๊U'""]•‚”ˆˆˆHDฮ๊˜‚e“v9ฉ‹oม0 “ฎ,ส฿ศwผwวrC[INzวษ ตA๒+~JX8qHŽ_@fึวฑ*zบyZฦ+>ฎ™™&}๙สA๊๐vZŸ6กญะOaJDDบ,)‘ˆt ี>ด0NŠฤจซHMผ ปฯLชขํ—!aฎ ๘ห˜l์ƒ(<+ ซƒฐุqH^t™ซ>Š•ํAXสั๚P-๎๎ีฑ—ฬฌ‘พ๔}`ู๘‡ถ–ƒิแํ'o 0%""]”‚”ˆˆˆHDบ\jgู˜TษัณIŽฟปฯhL"]]เ—ฟฯv(ฎr‹๏‰๕8ค&ฝƒฬ•…ษt',4ำ๒๛/ใํ}1–๛’™๓)าSฦย;ฐ‘“฿ม?บ๋ี7 h0ฦซญฎYrฏ^‰""า(H‰ˆˆˆDคหฉv–ƒUั‹ไ˜9ๅ0ีk8XNง›๘วwS\๗;J›Ÿ ,4วrR—ผ‹ฬŒaาU„๙ฦrฺท!–๛’๗R“o-ฉ}๋ษ=๕/๘วฮ S""าE(H‰ˆˆˆDคหฉvvป[cๆ’šx#Vท~ดลณwืJ]ฯ–rฑ‡๔ฅ๏%}ล_`R•ญGi—๑lŠแL"{u ฉ‰7•ƒิžตไžใ{^๛oŒqkชk–VฏL9/ง1)‘hฤ&H"5๑&าWVU฿ฮฟ‚ๆCธปืเ6ฌยน/๛”พ์คง“ฬ4ขๅ๗_./7ถCลีŸ%9แz0w๗jrO/$hฺ๗๚๏#`ฑ1ฅฺ๊šeuz…ŠˆH”คDDDD"ว ๅ œDvงฑ๛Œ฿%๔]L"ฆ|>ม‰ธ๕ฯโึฏยmxฎห๏Sz๚I_๖r:ฑŸ–ฟŒdG&๒NŠ์5Ÿ%9nA9HํZIฎ๎{'ผ๑;S˜‘จฯc R""""ัˆcJ ฝŒฬ์O`๗E่)ฝ๔&U3d*VEฏ“7 ใ/ใ๎X†[ฟ o฿๚.ปO™™&u้{1‰4๑=ด>๔ง_ผซOไ“Yฒื-ษ1๓สAjว2r‹๏!h>๔ๆ๏4`ฑ1AMuMZฝbEDไœžวคDDDDขห 5b™ซ>V^เ๗ศ-๚.~ใ^ฌส$Fฯ!1ไRLช๊ไ„‘”ถ-มฝ๏เ–.ทO™Y'uษป0N ่.Zบ๘๎๘MไSUT,๘‰QณภJ[‘_๒‚ึฃoฮ>cLmuM]ฝ^น""rNฮc R""""ัˆe5‹ฬUล๎1|—ึงพƒxVe/0Ve#fโ ผ“ช8๙‡พ‹wxฅอOแํ]ืฅ>—{;ษI7—ƒิแmดx๖ฉโบฟ'1v~9Hํ]G๋c฿xk๋.'VU฿r:0ื=@~ๅฯ 'ฮอ4…ฦ[ha-T˜‘ทJAJDDD$"q RฉI7“žAฌช~„ลVZ๛:๋ิ)%aชะ –ƒ3pษฑ๓ฐ{;ัqหฐ”รmxŽาๆ'๐n9;๋ฝฑ้/7|‰ไู่`'pwฏ!๗ฤ7 ZŽฤ๎xฒบ bม็q_ ฯŠยช๛ ‹อ็๖ฆDDไlœ‘คDDDDขห uษปI_ฌส>„…fZZ[Œ:ฎœฆŠญ'…3d*‰‘WbW้ธeX8ปke๙Šฉ#;su กโ๚/‘yุ๎ฎgศ=๙ํณป๎RD์Cศ^๓9œA“ „ยšขฐ๚็„ลึh6 -L๕ฌYRซWทˆˆผQ R""""‰cJO}?ฉฉ๏รช่E˜kค๕ัฏท…ฅ?ด+ง„)ท€IfH ›Ž3๔2์๎ม:ฆ‚–รธปžกดๅI#๕็๊“ศPqร?6ญคถ/ฅ๕้๏ž›u—ฮ1ปื๐r0ยยชKแ๙_–rัnH@ƒ1^muอ’{๕*‘ื}NV‰F,ƒิด?#5ๅ=XูญGi}์Ÿ0ง\้๔SM:ย”๏a’Yรง“zVUฟN๛ š๖Sฺฑ w"ฃ „nL~ำUT๐%Cฆ‚ฑ)m]Dฎ๎๎sท๎า9d๗C๖šฯโ๔ O~ๅฯ(พ๘Bทp~6HaJDDศ9YAJDDD$q R™ท‘š|+&SMะ|ˆึ'พ‰9eMจื9ๅค#LVถ‰ก—แ นซข7SพYโ7๎ฅด๙ ห ๗zลณบ?Vถ'7| gะล`,J›'ทไ„ล–ุON dฏ์>ฃภ๗ศ/1ล๕ž๕็์ h0ฆt[uอฒ:ฝ๊EDไŒณ)‘hฤ2H]๕QR“nฦคซš{๊)ƒSONจŸฉ่Mrฤ ์ae{ด<ฃ๕ๅ0ตk%‰ƒเปgeฌชพT\ฅ๒ว€โ†‡ศ/q๔s; œA“ษฮฟปืpBฏD~้(ฝ๔ก_๊ฐุ˜Rญย”ˆˆœvV  %"""Xฉ9Ÿ"uั๕˜d%~ใrOใ$ฯส}‡^‰ะอใ๔KbไUุ}ว`eบw|”/๔Š๘‡ทSฺ๘(nร๊๒7๒[zLซz0ืง๏ุ๒บK๋ ฐ๒฿ฯ฿ว‚ฤะหศฬ๛4v๕Bฏ@~๑๗)n~โฌลปณFaJDDNCAJDDD$"q Rู๙ww &Yฌพค’ูณ๚aก,ง#f`๗Iw๋๘(_XสแุD้ฅ‡๑๖ฎ#ศ7Bเฟฉวฒ{ งโบ/`๗Ex_๘V?ๆ๖&$Fฬ$;็SX–rไ๊๎ฆดm๘^ืเ€ลฦทUืิี๋ฟˆˆ(H‰ˆˆˆD$–Ajม็HŽ™‡Iค๑์ ท่.Lช๒œcจธ๎๏;>ๆVXs kป๋]U๔:$วฬ%3๋cXU ‹-ไž.ฅห๒Ud็\เ฿gŒฉU˜y{S‰HƒTล๕‹ฤจY;‰wh+๙บป1™๎็๔1รB3&S3๘ƒง`U>yUVดร๓ล ใVFพื9งuL$ปเsุ=†บ ฯGแล฿tซŠ€ไ๘dฎVeย|ญO ๎ฎ•o๚๊ฑศ)L‰ˆผญ)H‰ˆˆˆD$–A๊ฆ/“>c;x๛7’[๚ร๒:O อX๚ใ ž‚3๐bฌชพ˜d0เ{อpw?Oqร๏ ŽฟL่•8uั๔ำqOกโšฟล๊>€ฐุJ~ลO(พ๔p|"ฮ)Ro"=๓CXูžนcไžnร๊7|ีุy๘w%Nฌญใ4๊ฟ„ˆศ‡‚”ˆˆˆHDโค*o๙*ฮะห1ฦยปŽสŸbฅปEบ aกปืœA“ฑ๛_„Uูใคภ@่Nภญ–โ‹ผๆย็‰แWฝบฆ|UQแนฅ?ขด๙‰ื}…UW’บไ]คง+SMะz”ึวฟ‰ท็…X๎Kๆส6%'^ฟะุ‰…&Uฅ0%"๒6  %"""‘Xฉw}g๐%xปŸงฐ๊bาU็e[Bฏ„ำ{ฮ )ุฦbฒ=0vยฐ”รo‹ป} ล—!ฬ7qบซฅฃg“{;VE/‚|#๙บ๏Qฺถ„ืบฒช+JO}?้ห?€IUดข๕ฑฦท!–๛RyหืH ›˜&w๗๊…ญ~ia๕gžR˜น€)H‰ˆˆˆD$vAสXTฝ็8&†-Vd IDAT~๙*ค5ฟฤค+ฯ๛ฆ9}วbšŒkVบ;ุaฑh=ฅ-OSฺ๒$ก[8u‡ส๋.อVฆ;Aหr‹๎*ฏปC้้$}้๛0ษ,ม‰ด>๖ ผ›cน/U๏๙6ฮภ‹pwฏกๅท_j2ฦญฉฎYrฏsˆˆ\˜คDDDD"ท e์$•๔œ~ใ}w็ Š๋~{r๑๓อNb๗Cb๐ฅุีƒห฿ศgูxนใ๘ทPฺฑ”าๆ'vศšx™+?‚IWœ8H๎้มฝ&†ณxCfฦmค.yO๙๗ะ๚ศW๑์Œ฿พX6U๏๙N‹สแsื3ด>|g๙w ฦxต S"")‘ˆฤ.H%ณTฝ็;ุ}Fzล๒วแ6>‚Idบฬ6†^ ‚€ฤศ™$†\Šี}@๖BฟDุzoFJ—เ๎\Aj๒;ษ\๙aLฒฟqนงพ…ทw}&ห&sๅGHMพc'๐ํฆ๕แฏเ฿ฟ7$‰4•๏v[๘tqw,ฅ๕ฑ๊|#…)‘ Ž‚”ˆˆˆHDbคฒีTฝ๛ุฝ†บJ[žขดuฦIvนm อ˜T%‰QณpMฦช๊W^๘< w’„nŽะ+•ืœ2ศNZ๛ฑ†๘Mโ™นMrยucแB๋ร_!h9ป}ฑ*z•ณ^ร KyJ[ž$ท่ฎ?G‹)ี*L‰ˆฤ—‚”ˆˆˆHDbคชS๕ฮยชHXhก๘าCxปื”ืi๊โยRป็pฃga๗•๊ถ aฦ@AหarOทู๖ฉ1q๙†:“H“๗’ใเุHหรต„น๘}1ีญ?•๏๚g์๊!ๅ๐น๑qrKพ๚XaJD$ถคDDDD"ฏ eฐ{ ง๒ึฏcu๋G˜?Aqƒx๛6”ปML„พ‹ำo‰aำฑ{j๛†ภถ๐]ผ›)m[„ปsA๓กŽ}๏๊aส$ณdฏ,ษ1s o฿:Zช%,ถฤํํv!ๅใฌz aก™โ†‡ศ/๑ป›ภภSS]SWฏ4""19(H‰ˆˆˆD#vAช๏*o๙*Veo‚\#ลƒhk Ÿy‹0๐pN"=ๅ˜TUวoส Ÿร?ผwื3”ถ/%,6w<]5L™T%ูŸ#9j>๎ห/ะ๚p-ก[ˆ์#ฉผ๕kXU} ๓MืŽ3๗พนป ๛Œ1ต S""18(H‰ˆˆˆD#nAส8‘Š›๏ฤส๖ h=Jqํเู็?,ถ’บ๔ฝ$วฬm๛Aว๗h S-G๐๖oฤ๕ ๎Že„~ฉใน่jaสคซจธ๎ $†ฯ \ผ†ีดโฒVงใlภ$*nGฌlฯr๘|Wžๅู๓ะปำยZX]Sืจ@""]์  %"""ฦ๏฿†^ฑ}ึตใฑH ›Fล _ยค* šQX๓฿M{ใ;v’ิE7’9|w๗ ซAjโ8รฆae{žผbสอ4๎ลณw็ ผ=kO}rฮ๛ุYูžT๘8ƒงzEหi}๔k1|7bH ™Jล ะ>SX Š/๖์=F@Shผ… S""]์  %"""ยณ–v,ว?Vพ>ฃK†)c‘y%ื}“ฬœุOน_ถŠ๏ฤ7ไ๘kI ›พKiว2Z:&™ล๎;–ิไ[I šŒษTwMXlลoƒท{5๎ฮๅx6Ÿz็m์ฌŠ^T๔8'•ƒิถ:Zf ล"1|:ืฑ-|คฐ๊?)n๘ู,…)‘ฎu P‰ˆ๏…๑Jใ๎\|7๘ํktฑ0eู$Gฯ!ปเ๓˜Dš q/…ี?'h=฿‰o"Cj๒;หW๙%m‹i}์Ÿ:žซข'v ค&Œ3h2&‘้๘ฐุŠd'nรณธปž}ลZZัUี—Š›jq๚'๔Š”6?I๎ฉ๏ฤoP,ป>ฏ๛r๘lฺOู๛(mz=f@ƒ1^muอ’{๕OIDไ<ž—คDDDD"Sžx๙ฑ]”ถ<ปsA๓!N~”ฏซ„‡ไธซษ^๓YŒฤ?พ›ยs?'ฬว๘ยc“ž๖ง8&๚ฅrฤy๒ฏธมช์‹3h2ฉI7แ๔Ÿถsr‹-๘‡ถRฺนฏa๑=็gxบ๕ง๒wb๗C่(m|”\๑ห!9ฆ-|:)‚ฦ=ไWŒาึE็ฑฆDDฮ๏iYAJDDD$a)šd๖ไ|่ฎ๒S;–4๎:aสvHMธž์ผ;ภv๐ึSx๎~ยbK|Ÿภ#{ๅGฑ๛+_Uด๑Qr‹๎:ร,ูย๊>€ฤเ)$'ฝงฯจŽ…ฯย\#มMธ;–ใ๎^Cะ|0า}ฑชQ๙Žฏ`๗QR"ทไ๛๑!9๖ฒืMG๘ฬ/ ๎Žeัmƒย”ˆศyก %"""‘โ{ยไ่9ุ}วt๚8XG˜ฺZGiว2ยๆC„~้<‡‚ฉI7“{; ศ ซ๎'t๓ฑ}C฿#;็SุฝGzEŠ๋$ฟไฏ1[ถฐ{ '1lษ๑ืb๗–~ญว๐๖mภพoฯฺ๒7๗E1<=†Pqหืฐ{ !,ๅ(ฎ{€๒วoPN>๓หทู่ท%`ฑ1ฅฺ๊šeu๚o%"r๎)H‰ˆˆˆDไ๘]W‡vŸ18ƒ'“5ป฿8Œ“:yƒถ0U๎Žๅๅ๕š:?x’h'IMพ•ฬœOะV๒ซ๎‡๓สŠภ'3๗v์žรล๓๚#Žภ้=’ฤศ+IŽU=ธ๓]ทลฝšาถ%x๛6›ฯ้ฎุ=‡Qyหืฐช•ƒิฺ_“_๙ณ๘ฝฑ“$'ิ)|ๆ—w๗š๓xœ(L‰ˆDrP‰ฦ๑ปฎ๎˜xู}F“6ฤˆุ}ฦ`้“7๔]ผร)mzwืJย\cไWL'Ejส{ศ\๕Qผ›(ฌ๚Oฝ๘@วษˆ๓ยฏศ?s฿{^i์>cHŒšErฬ\ฌชพข๙๎ฎgpw,ล;ฐ‰ฐ”kKฮๆย็v๏‘T๚uฌช~„ลV ฯ7…U๗ว๏อˆ“*‡ฯูŸ =|ๆ–|o๏๚.pผ(L‰ˆœำs€‚”ˆˆˆH4สAชs˜p๚ร6ฤ๐+ฐ{ ็ิ5ฆBฏˆxฅ-Oโ๎XFoŠ์Š)“ศš๚>23nภทยsI—๚&ภ7( B*-VU_ยRŽย๊_ดํำ›x~’YœHŒžCbฤLฌสง๓‹๏ม;ฐฉ๋ldเ฿gŒฉญฎฉซื1‘ณxP‰ฦฉWHฝ*L๕ฟgุ4รงa๗ŠIUv.t๓'ริ๖ฅลf๐ฯํ•J&™%}ูHO`9์YKa๕ฯมXฑ}ร0คโฺฟรช่UŽ8ซ๎ง๐/฿ฺ๓”ฎย8‰ไ˜y8C/วส๖8๙หภวoƒปcฅmK๐ํ:eZ˜ฒ๛+ฉlOยbsพ*~oFN>su฿ร?ผญ๋mฌย”ˆศู=(H‰ˆˆˆDฃs๊˜Žั)L ธˆฤ๐+p†^ŽsX็+ฆ<แํ”6>Jiว๒๒วม‚sฆLช’๔ด?#}ูเ๎^Ca๕/0ถ฿กโบ/`ฒี„…f๒ฯGqํœ•ปถฒ=q]Lbฬ<C/๋ |c ”ถ>ป}๑—O๓7ฆœQy๋ื1™๎g}_"}3rš๐™ซปh}ืh…)‘ณsP‰ฦ้ƒTวดŒW^1•= gะ์žC;‡ฉRะVŠล๕ aฑยเ์NำUdfF๊’wเึ?Kaอ/1N"ฦ3_Cลu_ฤคซ๓MไW;ล๕žี‡ฐ*๛เ ™Jrฬ\œม—t6ลภร;ดาฆวp๋Ÿ%8qเŒใZœSyหืฺ๖ฅ‘3๗R\๗ป๘ Iช‚๔ดv Ÿ๙บป๐๏ฺ้ะoก…ตฐบฆฎQDDฤ9@AJDDD$8HuLฯ่ฆ^Lr๔œAcu„IUด&$,ดเฺzr๑๓b+gk'“ฉ&3๓Cค.พฅ v.งธ๖7ใ+คŒ• {0ฉ ย\#๙?ก๘ารg๋;=๗V๗$†_Ar๔์~ใ;-Z๚%›(n| ฏแน๒ท)žแ~ฮฤr)•7ๅไพฌ)ล ลoLาUdฎ๘KRSS>ฮ๊Ÿ%ท่n‚๛ใฑ S""o  %"""ืค:ฆit Sƒง5ซฆบ ,_1e „!aพฉฆ6?Žปc๙) hฟyVถ™ซ>J๒ข(m[Liƒ๑RNš์ต‡If Z’_cJ›?าi์^รI ›Nb๔์#;‡ฉRo†rP€์œฟ&1๒JŠ/=oโ;w4ฝจ˜7`;M๛ษ-นง|%Y4ฮซ‚โศซp†^†}@ง0ดลน‚าฦว๐์8ํG0“ใฏํืwวฒุษซย็ๆ'สAชุ|aผ่Œ)V]ณฌNED^qfT‰ฦู Rำ8:Ž3IŽปง฿xLEOŒS^ซ(๔Š-G๐lคด๙ k^ืฝ=“™s;‰แำ(ฎท™7uตUWau๋Ov๎ํ`9๘ว๗_|nรชจง฿'วอ#ฏ"1b‰ม—bU๖9นFWœ8@i๛J›Ÿภ?พ“มLMผ‰์š“qm๑=ธปVฦpL๛)#ฏ*g/=B~ษ๗เีaq†Z[JDไ4gD)‘hœ ี1ใd˜2$F]ีฆฦaฒ=1v Aหแ๒S›ว๛โ๎ฃฬ๎9Œ์ผOใ ™Z/ฆํ*ซ žO~`๕Fv๖'มX๘วศี}๏ๅ็ฯื4ผใ97v’ฤู่$†_3h2VE/ฐ์ถํ๖๑›๖Rฺ๒4ฅ-O4๎BR฿Bvงมr๗ซ๛nรsฑซz0ูนMb๘ๅใlƒไ—ะ-\0ฏ{)‘3œ คDDDDขqn‚ิ้ๅ;$G]u๒ฃ|™`ๅoไ+ๅ šโํ‰า–'๑๖ฎ?ujH{$ฑ{$;๏38ƒ&Px—ธ๕ฯal;žQภ๗p๚#sๅGภ#;สAj๏บ๓=๏xฮMช’ไ˜น$†_Vถg๙›|๘nŠฅดๅ)’cฏ&;๛mW{ํ&ท่๎๓ื<ป็02so'1๔2 >๓K•ะ/]0ฏ{)‘3œคDDDDขYjŸ่9)ฃg“ฟปฯฌtUZE!a1Gpb?พ๕”ถ.ยทกs(่;†์ผ;p\@แนŸใึฏย$ำ๑Œ^ gะd230๘‡ถ–#ฮ]n[ญlOใๆ—รT๏QX™๊Ž0–rx6_ย<“ศเU—NWฝลC9|~gะ%ๅใ์๙_‘_๑ใNOŒ;)‘3ฬSคDDDDขu๊˜๐%ณ$Fอ"5แ:์>ฃ1ฉJ0VSญ{q๗ฎรถ๏ภfœ~ใศฮฏม๎7)<๛ๅ •ฎŠgp $†^Nz๚๐l"Ww7ม-]v›ญชพ$'\Gb่ๅุ=‡bาสใ๘„‡ฑๅE๎้…x๛ึวn\์>cศฮ ฮ€‰V‚ส๕Zeฏ:๖คDDN??Q‰ฦ๙ RฟT%ษัณINธป๗HLฒฒ|ๅM[๑๗เํ}‘โบ฿aeบ“™๗œ€ŠŸโึฏยช่ฯ(เHŒผ’๔ิ๗เํ]_RGvt๙mท{#9แ:œม—`๗‚IdOฎ1Eyแ๚าQ\๗@์ฦล้?žฬผ;p๚ ๐์ๆ>โmŽฏ:๖คDDN?/Q‰ฦ๙RภLu9L_€{&™m็LXlม;ด•0w ป฿x์C ๐ษ/7†็ฐช๚ฦ๓ษw $Fฯ!5ๅ=x/ฟPRวบ๒TSรŒำ<ษฑืเ นด<.vขใwA๎๎5ื=€ทcl†ล0‘์ผฯ`๗„ไWŒยช๛/จืฝ‚”ˆศฮr R""""ั่*Aช< 4XฝHŒšu2L9ฉถwะa)ถำ๑ณ“฿ยทซ๛@N๗ญ|]> ธ’ใฎ!u๑-ธ๕ซศ/น๘ž8Lู;=฿‰‘3ษฬ+์#;๏ฃW$h>Œท{5ล๕โีๅ๗ฬt ูyŸ.๏K’_c k๋‚z+H‰ˆœแ์ฆ %""".คฺYvG˜JฟถNน๒ฆป})๙ีฟภsส:R๑ Sก[ 9แ:Ro*๏ฯฮๅไ–€ iœฆ๎ฯwfฮงH_๚๒พyล๒oํdพๆ š๖โ6ฌฆธ๗'๖wู๑J ฝŒฬฑ{ƒ0 ทไ‡ื๚‚z+H‰ˆœแฌฆ %""".คฺู ฌlฃf“บ่:์ž#ภvNพฉv๓๘ทเ6พOPS‰F,‚T›ไ„๋ษฮ๚8&[ _Žฦ:๙&ปุ‚h๋ษ0]:L…n๔”?"1z6ล ‘_๙Sย\c&๐ษ,ูนท“ผ่+(ผ๐kŒ“ฏ„=h2ษ3ฑช—ฌ7| w ๐J๊(m~โิ{UIvgHŽ_”ฏ*ฎฆmํฏะ+บy’cๆ’1ซช&‘ ก_"l9Šw`#๎Že”ถ->๕ž#ฏไุ๙df}ซช/กW$๗๘?ฟb›โOAJDไ ็3)‘hฤ)Hฅ&Lzๆ‡ฑฒ= '(พ๔Vท'‰ีc(v๗ๅ+oฺ฿t[;"‡xะต?}๔eLbุ4 ฯ’ยช๛ ‹-๑›ภgบ—ƒT๛วทBฏ„ำ{8‰ัsฐ๛Œฦ$2ํฃEXhฦ;ดฏ~ฅํK šž›ใl๊๛HO๛ณ๒•xนFš๗ฟ๐nน ^๗ R""g8)H‰ˆˆˆD#VA๊Šฟ$=๕}˜d–เฤ~ร;Kญฏcvicา0ษ4ฆข7vU_ฐNฎm[๑๖ญรพ” ุY˜2v‚๔Œa๗ a@n๑=7<พปใศ๎1”์ี5ึร๒๖พุ้สดือฒ! ฑz#9fv๏ปฃ•!Aฎภ&๚g)ํXJ˜k<ปวูๅB๚๒t\‰ื๒ภลฟp(H‰ˆœแฌ %"""8ฉฬ•E๊’wc’Yฦ}GwพŽ uสZC–ƒIU•รTฆGyฉNaชo฿๚r˜*4แซฏฮฦคืI“ž๙!์žร ๐ศ-บ‹โฆวภ๗bwูฝG’wGวzX๙g๎ร?ธ™ทดฮS๐ูป๓๘ชฎ๛๎๗Ÿ=žIณ„„˜gฬŒcใyสิดiฆฆอmฺไ6M“fhฺคฉดฝiโไIœฆMำด้“ฆ7O๛คNว‰ ƒ ๐Œ™ัŒ„@๓pฆ=ญ๛วBƒ ^ฟ๗๖ห>G็œฝืึพึ๚ญ ;„Uท {ฅำวซ’=xํฏโ6๏รmฺNฟผขk?@tๅ{FGโ ๔ฯ๐ปฏซ๓^)!„˜เ,”B!DnไU ต๑ฃD–>ˆfล๐๛Oโ๗4รฅŒ ป˜Œ ฆ0L๐\Œฉ๓/ ฆœษ—qŽ๏De‡A๙Wงำkฝ้Caะโป$Ÿ:ฮัง!๐๓๎82*็ฟ๕˜ี‹Ez๗๗ร็ ์;ๅf@)ฌkฑ็†^\=าžก`่ ^KธMฯใถ์ ฿VŽณ›~›ศŠw‡#๑:้๐{[ฎซ๓^)!„˜เ,”B!DnไS ฿๔q์ล๗กYQ‚พ6ผžfpS—ีd4˜า๔p*Ÿ ‹ŸT\Lน-๛qO์@นู+Liั"b76zq สห’ฺ๒Uœใ;@yw™ี‹ˆo๚รฐ–RคŸ๛nX| ๎3ๅป ๖์ Xณ7 Vž{1๐ ;ร๖jูผ๗M๏วุ๚นแํแHผพ6’?๛โU/คžkH !ฤ๗f ค„B!r#ฏฉอŸย^xš!่m )/fปœœ1e วJมŽฃลK0`X็3Cธm/แฟB‘/#vใั‹ชQNŠไ“_ฦmฺ yุ6k—ฟๅcSๆBเ“z๖;จกำaˆt…)฿Gณ"XณึcีฏA/จ8๗bเใดใ6์ยm}ฏํๅหุฦY๚@8ฏง‰แŸผ6๏ IDAT9ม@วuuK %„›%B!„ศ| คw~kmhfฟท%œฒ็eโงŽŸสงวKมŠกลKะๅcŠiC๊รk ๗ุ6”๗ Ÿ>zษ4ขซ฿‡^X‰ส“ล_แถ๎ฯห@สช[ElใวยํพGj็ทQร=จซX‡K)ะฃa05}%Zฌxฬ‹~Oฮ๑xญ/เuพไฯ฿๚ ์E๗ YQฎใ ?ƒืีy/”BLะ+@J!„"7๒*บ็ Xs6ข6~O3~o3xWjEผ‹Sv-V‚ž(Lฉt?n๋K8GŸF๙.šnpนลป•็`Tฬ&บ๊=่จ๔รO<Œw๒UR!๐kฤšนŽุ†฿ร(ซGyYา;Ž ู A ดkzผkึzฬiKั"็^ |ผ3วqoวm{ๅ’Vห‹฿i์w ™ผฮ#$Ÿxˆ`่ฬuuK %„R$B!„ศ ค4๗} kึบ0๊n M๛ฮžqมTขอŽกลŠะโc‚)ฅาx-๛pŽ?ƒ๒=4ไRร$ๅf0ซY๑k่๑2‚T/ษŸ๙eไ™Lฌ9‰ŒาZ”›!ตใoQ้h7l๔xึœ ˜U‹ะ์๘นํe๑Oล9ฒ๏ิมptw ึ[ร@ชใษ'Aฒ็บ:๏%Bˆ zH !„BไFR†Eม}Yฟอฐ๐ป๑๛ZภฟZSยฮ ฆ ฆ„มTด-VŠfFFž์‚แnึp?ƒ 4รโ‚)•Mbึญ"บ์hฑ‚แฎp5ทฎyyู๓7[๗แฐ@ป“"ฝ[ูกซถBแ„๛ีsPnณj๖[1*็ขYัsฏ;)ผฮร8‡Ÿฤ๋VŒถ•rRh‘ฌ้ซ0gฌม(ชLษ†]8'v[๛Aฬ๊ล ้8 ฯ‘z๚kจl๒บ:๏%Bˆ ๎H !„BไFR…SI๗็˜•๓P* ่n GH]“80>˜Š W…#ฆฮ†S† Jกœมเ)–ธ ฯA3m‚มำDVผ{mhVฟง‰ก๓)Tv8/ฃ่ช๗Y๙๋่๑R‚แž0 <&วЁc‚)฿Eaึญยช]Ž^T๚ู`J๔w€E• ็ศึp๚แ[อqr@J!&ธcH %„B‘yH•ิRp๏1*fกฟง™ ฏT0y~คล(ช‚HAX=R€ฆ[ ”“ย๏kรk๏ไ+ƒงˆฎ๙ึœแjnง1_ŸFน้ผ<Žขk?@tูปยzXรgH=๓(ŠษH>fŒฅ@aีฏฦฌ]Ž^Pš>๒>พW)œ†gIm๛*=p]๗H !ฤw ค„B!r#_)ฃ|&‰{พ€QV‡๒‚พ6Vฎํ”ฐ X1Œข*ดH!DโhV M7!Pูaพ6ฦ]˜ตห1k—ฃ&ฉร ไsy;'ถ๎รDnxZค`๐ฉํ฿mฬ(ฒษ๕ธq๎wi:zมฬ้+1k– วหF~wHe‡ศ์wœ†็PC](฿น.ฮ{ ค„b‚;„RB!„น‘7Tๅ<w)FI-สอเ๗ท๔ถ29Vฃธ:œฦgวั" จภEฅะ ;(ถ—๏ฯ‡Cw๒Plร๏Yršวh'ฬทA7&w๛Œ ฆt ฝh*๖œ[ย:ec Ÿป๎FœCฟฤmูึ๙๒ฝผ>๏%Bˆ ๎ H !„BไFพRf๕bw zq *›$่ภ๏mar#[ป•ย(Ÿ‰^\ฎบงแtCM~_+ร๕‚dO^G๑Mว^xwXซท…ิŽoฃ™‘ผhŸณŒŠ™ฤozขltๆศ?PูaผำGqฏํe‚ฬ ~^ถ—RB1ม=[)!„Bˆศ›@ชv9‰;?‡^X‰ส โœ kHๅSเนhv ฃb6zมดHœัฐรอเตพ€ฒ็๘Tf0ฏถ-พ๙Sุ๓oG3m๎FR;อŠๆี6ƒงA$๎8œ๊Œdณใแ ‰#SUzฏูCฟภ๋8ˆr’“ซ–ูฅ‹H !ฤEI %„B‘#๙HiX๕kˆ฿๑i๔D9AบŸ`เAษ|์๊‚C•bTฮE Pn•๊ร;sฏe?ฮ‰จl2/ถ)qื็ฐๆl ๋a9Fz็?Œn๙#๎Fณใ$๎๚ยŒ YQ์waV-ผpฤT_nห~ฦ๘gŽ_ี`J/จ เ`”ีฃฒIœ#Oแ6๎]%0šลฤจ˜M์ฦ‚fเ9Fj๋#๘o3 {ึอุ‹๏ งhF‹รขTfฟทท๕Eœ#[;ฏZO)!„˜ ฯ!”B!Dn ๓{”9ํฌk1งSš>๚z๊ ‹gŽื๑:AบTฮaดุอฟKd้ ภ๏mม๏m/“ง{]G/šŠQ9อŠ๔Ÿ$ฝ๛๛จห ุ”็€—A/™Žฝ๐"ม”๏๔ถเ6๏รmืu+?•O/ฌค๐]_C/ฉEe‡ษ|ฏ๕EะดอN„๛ว๗ 2๘ a;5‚sl;nร.”—พโ#ฆ๔ยJโท}kๆM็ฉฎ9ฏeuๅ6ศ@/ชมฌZš†ืy„ฬuลฟFe†F uฯ ฆๆ^ธ*_O๓h0ๅtŒuv้ก‡Q9‚ท=^ŠJ๕“y้?๐{๓ฏ]T€ฝเN์w‚ p›๗’๒UTzเ๊>X1๔ฒ:"Kฤšน=^2ฒG‚ฉŽpๅห†]x'_๛—ผู`J)!„˜เš,”B!Dnผa 5ๆแืžณs๚ ฬiหะ‹ฆŽ 7*3Œ฿u็่Vœฯข๔ซ1ฅN%~วgฐ๊VRm##p๒ดฯจ›่EU˜ี‹๐N$ณ๗_ฏZ๐0˜ฒ0ฆฬยž{+ฦิ๙Nๅ๋i ๋ƒ5ํฦ่ผฌัgfอ +ดhAชฬ Nะื–ํ๘ุKยัJจทaษญ_Ee“นyŠbL™Mไ†ทcี.G‹ŸkC'ฎœุ๚nำผS‡ฦ›—{.H %„\‹%B!„ศKคฮฑfญว™สงWL ŠUvฟซ็ศœฦจฬเ˜ฉFoŽ^\MโŽฯbึ.Hอ฿ฎ›่Eี˜ี‹๐NพBfฟƒฎ_ีฏ=L™U๓ฑfm‹ชŸLy a}ฐฆฝจdฯ%Mร4งฏ เ‡ั"ษ2๛xๅ ็’๏YฌY๋A8วwฺ๚สอํjŽzผc๊|"K฿†Yณ-’8ื†ู$~o๓่ส‰~wรุว(.5˜’@J!.N)!„Bˆy3ิYVZฌู๋1ซกUฌฆ ย็๎œcqžEฅ๚฿๔o4Jk‰฿๑9ฬš%แhžvำG๒wง๋ึศฉ0r[_$๓ย‡6ฆFืีค2C`F0ซbอบฃbึธฉ|๘.^ื œ#[รขฺฉ_LY๕kI๗%4;N0Mf๏†ป๒ฎY”๏]๙X3ึ†ิัm$ท>rอŠ็๋…•˜ีKˆ,นฃf šaŸ๛ญูpDขธทy~_ุ๋ว)(˜’@J!.N)!„Bˆysิ๘^ซn5ึœa0U8อއ+๓ฉ•ฤ๏nยix็ุ3c–ฒฟtFY=๑ป>9u~X{ #ฟ)รฉช…ธอ๛ศผ๘#4ำฮEW{ดํTfอŽcL]€5sF๙ŒฐํF(/Žv;๚tX<ัpฦšตžฤ=_@ณขCgH๏๙Tบ/๏šE๙.ฑ5๏วœพ็ศ’[ฟvmk•izQ5ึ๔ุ‹๎ญ;6๚›GpO<‡ฒŸ`่๔ฅmซRBq๑หฎRB!„น๑VFH]LญยšฝณfqLYฑpZเ‡5ฆzšpžร9พ}ฬˆฉ7อa”ฯ$q๗ŸbL™Rํ๙=eฯˆ WaN]€ธ‹์ห?†œRถ]L%0ช`อผ ฃ|ๆ๘โ็gƒฉรOโ6๎&ศ Œ+\oฯDฎฯฃ™‚มSคw•สปfQพG์ฦ฿ยœv>ูƒOฺ๖I๒„คc”Lรฌ[Edษๅ3ระ๗์oO๕ใuภixฏํฅ7\P)!„˜เr+”B!Dnผต@jด๛ฦุPษœถ {m#มTUXcJำ!๐ฒรแสnปqŽ>=fฤิฤม”1eNH•ฯ ฉพ6ฎใ๙ปำอFq5Fๅ|œ;ษพ๒šน]oฮฆฬš%˜๕ซ1สgิ )/‹๚ูCฟภm~• Wฆ›;‰;?†E0ะAzื๗ยข๖๙ฦ๗ˆฎ๛p8•2๐ศxœิ3฿šdOJ:ฦ”ูX3nฤž;FiธSAฒ๏ไ+8วwเตฟึpป ค„b‚หฌRB!„นqeฉ‹3k–bฯ฿ŒYณtd*_, ฆ|— 3Hะ‚ำด็๐Sa]ฃฐ+ศ๙ม”1u>‰ป ฃt:สห๔ตWฬ9ฯ˜Q๔โฬสน8Gท‘=๐ำqแฯ5่‚sA05}ๆ๔•ฅำวOๅsR๘gŽ‘=S๖์ฤo๛#0,พ“คw#\B1๔I'๐‰ปS็ƒ๏‘y๕1าฯ~gr>06Fๅ\ฌY7cฯฝฝธzฆ w……ฯ๏ภ๋<„rRใ^—@J!&ธพJ %„B‘W3:หฌZ„ฝ๐ฎpUพยส‘S๘Azฟทฏe?ฮ‘ญฉณต‡ฮ$fี"๗zqMH๕ถเ๗4ๅ๏Nทโแฉ)sp?I๖เ/ฎq ล๛]e†ะbล˜ตหฑj—ฃOC‹ฤGพ๎:ˆrำX๕kะ"๘ฝ-คŸ.uตทษD>๑ลจ˜R/'้]฿›NV ฃj๖์XณืฃL๗z0ุ‰ธ็ฤNำGQ^6Vๅ?\๖ษษP!ฮปฎJ %„B‘นคฮ2ฆฬ%ฒ๔ฬฺe่‰Š๑มTfฟปฏ๕%œใฯ ;ณf)‰{พ€^XN๋i"่mษ฿n'0Šk0*f}็8GถL’@jดKฮธ`*^Š9mYL•L;ทขbเฃ|7,ศฎ้#ิw๒ฒY”Rฤ7~ ฃ|๘.™๔๓?ศจHณf)ึ์X3ึข'สวn~_;๎‰ธ ฯโu7ขผดŒBˆ‹]O%B!„ศ\RgSๆY2LU.gฏ•ฤ๏mมk{ ็ุv‚มNฬช$๘K๔D9สMใw7๔ทๅogืN —ิ†ม}๕'8วถ„<“๎ืr~0eีญฦœถฝธf$Dณ๊››!๓โ๐Z๖ฃE ๓ฎmbท|ฃดๅ;d๖ฟd๖0ฟŽญhVํrฌ9ท`ีญD‹•œ{1๐๑๛ZqŽ๏ภ9พแโเ!น !ฤyืQ ค„B!rใZRgณย`ชnU8•ฯ)๊ํ{™žfใฯ]>๔โ๊ฐ~QOAษํ์F ัKฆa”ีŠ์ห…sb็$ คF5cƒ)ฝ ณ~ f๕Œ’i`Xฃ๏T™Aผ๖ืpN~wใรFๅ‡ไ („็ $B!„ศkH 9Œาุ้Kภžu3Zแ”ัSจ€`ธ •ซH!*3Rƒช•๙&ug7R„^Z‹Q:Pd^๘n๓๓hV>7็˜*ช"ถ๊=•๓ฦผG…กbชฏ๓0nรshั‚Iพi๑>^8ๅeI?๗]ฒฏ$฿Žฎq็„^8ซ~ ึœ˜5KฦcษP!ฮปŠJ %„B‘ื6๓ญieuุ‹๎มžu3zQ่f๘ฒ Fฆƒ p›๗ก2>„O๚ฮnดฃt:zษ4P™?ฤm{9ฌร”?[(‚ฌyทป้CaSyYPA*j:สwPษ‘`๊ฤ‚แ๎ษ;•O7‰฿๖I๔‚ ”›!ฝ๓๏ษพxพ>R;'๔’Z์Y๋ฐfmภจœ‹fE%Bˆ‹]=%B!„ศษHyˆ6 Œา:์๙ทcอบ9œv6˜แw7เถพ„fวั sย‡๐Iู•`”ึกWCเ“๛ผS‡ฮ– าXsn!บ๚ฝa๔4 œB/ฎF/ฎ>Wฬห ๗เwฤ9๑,*ี7&˜šํฆ้ฑ?…/C9)Rฯ-ฮแ'๓ัjพ5*faอ\‡5๋ๆ‡อช…ษP!ฮปjJ %„B‘“+ำ!4l๔’์น›ฐŽ˜ฅP™ažfผ๎tำ˜๐!|าm[ผฃฌฝp*้]„฿p6ไๅคฑๆl$บโธMฯ“๓}๔X1‘Žฦ‰•ŒิS('M0t๏ิAฦจtไ ฆฬ‰อŸB‹• ฒIRฯ<Šsd๋๕๒ˆ5n฿S็=\๔›๐\…โผซฅRB!„น1Yฉฑขซ฿KlGFK1nUท‘"็~ื‰p4ŽฎO๘>i:ป๑rŒฒ:๔ยJ๐=Rฯ~‡ ฟ}์fๅ ภšตŽศฒwเถ์#ฝใ๘}'ม0ฑgoฤ^pFๅ<๔h&(…r3ƒง๐ฺเ6?ึฃบึม”%q๛ฃE‹PูaRพsl๛๕๖จ(”๒.๛ไฮ‡ไ („็]%%B!„ศ|ค์E๗ฟํะฬ*r3h๑าsซ๒ึ.๒ป๑NEทข i<„Ozขฝฌ>ฌUไ;คŸ๙[‚แฎ<€ ซ~-‘ฅเ6๎!ต๓๏:ฮํ}3‚5g#๖‚;1ฆฬA†ฃม‚ๅฆ :๐:เ6ํE9ษkLiv‚๘ๆKe2 IDATOกE P™A’O๗ฤณืๅyฏ”pู'w<$W@!„8๏^ ”B!DnไC นแmฤ6~ อŒ t๗oD—ฟฃ|ๆธSสwz[๐NYQl๒’า ฆ„TขๅeIm•๊อฯŽปวฌ_Cd๑ฝ8'v’๙‚กำพืŽcอน…ศ‚;1ฆฬF‹ŒชWู$@^๛ซธM{มหไ<˜า์โท1šGฅ๚I>5ฦืๅy/#ค„b‚{RB!„น‘S๖Vผ;œฒgX'I>๙7x‡0Jk1๋oฤž;ฦ”Yฃดa$˜๊;‰ืyอฐC`Rl^85 คโฅ(7C๊้ฏฃฒC๙y™Q์Y7c/ผ ็ุ6าฯ~๗WŽ๘าb%ุs6†ํV1-gt*Yfฟฟ ฏ5ๆน ฆhฑbโ›?‰fลRฝคถ<‚ผ๗บ<๏%Bˆ ๎QH !„BไFิบ้wภ0๑๛ZIm^ว๋แ‹†‰^8ซn๖;ย"ฺcฆ๒กพถpฤTด,(šn^X…^>=VŒrR$ท>n:?;๎บ‰5o3๖อ8‡Ÿ"?ผแˆ/ =Q62•๏Œ๒™##ฺ*;Œ฿ำ‚ื๖n๋KธW7˜ |๔ข*b›>Žฤ๎"ตๅซธญ/^—็ฝRB1มI)!„Bˆศ‹@๊ฦ฿"ถ๖ ›๘=Ma ีy๘ผคŽ^4ซvyXซจjแ๘` ๐๛;ยฉq^– wMถG/ฎม(ซ?W<{หWQพ“ง=w {แ=ุs7=๘้]„J๗_โฮ0ะc%a0ต๐nŒ๒็ฺM ฆšq^ฤk{๏ชSส๗0ส๊ˆm๘0:M๒ฉฏเ|ๅบ<๏%Bˆ nkH !„BไF>Rฑu&บ๚ฝ ๘]'Hn™ccปŒ &๔โjฌฺX๓nลฌZ„fวว}^0E0ิnš =›ำํั‹ง…มหH๑์ิ–GP9 WLนแmXณ7}ํงค๗3*s™Su=Q†=็์๙ทฃ_L แ๗4แถผ€ืฺฆ”็`Vฮ%บ๎รฃตส’[พ‚ื~เบ<๏%Bˆ‹“@J!„"G๒"ฺ๐๛DW:h:้ฃคถ~ ฟปแbH.Lอ€YฝxL€ ’=CgภMค!Gฃ”๔’้แฉH‚ ีG๊้ฏAเ็ๅ๑ฃ|่ส_วšq#ูW#ฝ็_Pูแ7๗#S0ํY๋รSๅ3มSด>3ˆ฿ˆฒฏUP๊ŠSสMcV/!zำ‡ะ ฟฏิ–ฏเ:t]ž๗H !ฤลI %„B‘#๙Hล7‘e๏ ฉSI=užๆ_ีd\0UT9m)๖์ ˜5Kะb%ใค๚Pร]จ์๐H0•ฝชฃ—ึ…”'๎&ตํ‚ ๒๓ |"ซƒUท€ฬKAf๏ฟขœิ[๛\รย(šŠ5{ค๘yY=่ฦ่ห*;Œw๚(^ห~ผŽpำ[ ฆ”“ยœพŠุš๗‡ตสz[H=๕7xง^—็ฝRB1AB)!„Bˆ˜๔”ฆฟ๕D–พ 4 ๏ไ+คถ}ฟฏ๕R˜qมTa%ๆดeXณnฦœถ=^6!=3D0ิ๛*SzษtŒŠYhV”`่4ฉm฿ไชญwต)Et๕{1k—ู๏d๖u…Šดkfฝฐk๖†ฐ๘yู ะดs_?LนM{๐G๊Šฝ™`Je†ฑfDtีo†TwษงพŠ฿uบ<๏%Bˆ ๎;H !„Bไฦคคt“๘mDd๑} iธญ/’ฺM‚๖ห้^rA0UณkๆM˜ำ–กTŒXw’ƒจT?Avผ+L้%ตSๆŒึ*J=๓ญ|๎บ]>ฬiKศ์W2/ๅfฎ์ท˜‘p5ลน›ฐ็ŠQ:๔1S๙ฒCa0ีฐ ฬQ@ปฌ`Jeฑๆl"บโืยโ๙ง’๚~wใuyK %„o$B!„ศษHif„๘ๆOa/ธ3 คš๗’ฺ-‚มSoๆำL%ส1k–`อ\‡Yป ฝ` h๚น‡v'I0ิ…J๕dมsธ#™ฦR~_+ฉ‡6fิO^ัuขซ?€Yฝ€๔ž๏“}้?Q^๖ชzษ4์9ฑๆlย(ซ฿fู!ผฮ#ธป๐NEณbhฆ}ั๖+H๕Yx'‘บ‰ืyˆิ–G๐{[ฎห๓^)!„˜เ>#”B!DnL๚@สŽ฿วุ๓o4†็Hํ๘vXŒอ*cƒ -V‚Uป kฦ˜ำn@/จ_H;›$H๕ ’ฝแช|^–ทL้%ำ1*็„ลณปI?๗ใB•ผbุDืผs๊|าปพG๖ๅฃฎrxอŠข—Lวž}3ึ[1J๋ฦOๅsRxฏใ6ํ!H๕ž7สํย`*H๖Y๒‘ฅ„ตสฺ_ k•๕ต]—็ฝRB1มE)!„Bˆ˜๔Tค๘ํŒ=wฮฑํคŸมp๗•ฎhๆดฐfˆUป ฝp*ึน‡x7C์B ๗ค๛มsห/Fn”ึaL™†…ๆ8้]8ฎ`w^1"ฤึ~ฃr. Hํ{ฒฏ|77ว‡A/ซรžฝk๎-%ำ/ ฆฺ_ลmฺK๎›p๚e0Cdู;ˆ,พ7 คฺ^"นํ—954H %„W$B!„ศIHลKHฌY7เ~Š๔sŽzนฒ฿ฤูQ3šวฌ]5cm8•ฏจ อฐฯ=ฬ{‚แTฒ‡ ีพsYซไ้ฅu˜#”ืyˆฬž๏ซ‡”Ww+Ftํ1*f Hํ๘6ูื฿หํ๏0#3ฑfoฤšฝฃxฺ…ซ๒uผŽธ› 3pA0${‰ฎx7๖‚;@ำq[๖‘ฺ๖่›œ:๙I %„O$B!„ศษH้ฤo4ึŒศพ8้฿Gฅ๛ฏVW”ั้\บŽ–šฑณf zQ5š=๗P๏fย๚Rษ^‚dฯHฉ7ฆ๔า:ฬสนaญข๖ืH๏๛Wด| ค"Dื|ฃ|ฉํ’=๔$ต๙=vฃlึœX3oย(ฉ=/˜ย๋8S้ั•Uฒ—ศ๊฿ฤž{kH5๎&๕ฬทโิะษK)!„˜เ>"”B!DnL๚@ชp*๑;>ƒUท €์ซ‘~จฬเU๎‘j ฦSkรU๙ช†ม”# ฏยโ็*=H์ ง๚ฟj*ŸNูซœบ‰๚"ู—๓์Gๅ_ว=Zސ*พKr7pŽlภฟถฟหŽc”ฯฤšฝkึzŒ’i็ฌOแ|ทแ9Tv˜่š๗Rฮ๑คw|; ฏCH !ฤ๗ ค„B!rcาR%ำHiฬฺๅd^฿d๖•ฮUื”s#ฆ ฌ๚5a๑๓๊ลแT>;JกTLฅ๚†NS็ํ^M™ฒ7; คšž'๛๚ฯ!p๓๒๘ัใeDืผฝคๅeIm}็ุ3—5…๑jถ™fF0ฆ.ภšu3ึฬuลีใGL9)ฆ็q›vc/บk๚ส0:ฒ…๔ณ฿ฝ SC' ค„b‚;ˆRB!„น1ู)ฃฌž๘ํŸฦฌY(2๛ฬ‹ๅคrEelธdีŒ˜ชY<2b๊l0 œ4*;Sƒงฦ๘ึอp„Tล,ะ ฯโ†rำ๙ูqOT[๓~๔โj”›!๕ิ฿เ4<{ntูตuŒ ฆชcฯ€Yท ฃจzJŠ้~@C‹‚ฆ“}็คŸ๛nƒฯ’@J!&ธsH %„B‘“>š2—๘ๆObV-ฅH๏๙>ูW๋8็Skฐfฎ GLWกู‰0˜ |”“Be†Rฝแˆฉภรย(ŽQRฮัm8'vLธ๚dงV]๓^๔ยฉ('E๒—$ ค.l3อŽcV-ยšฝs๚Š‘`สบเ/ฦ=คw~OำuyK %„1$B!„ศษH™U‹ˆ฿๖ Œสy๘คw}์kบๆฮ๘`สœพ2}Sณฝpd*Ÿ>Le† †Nใ๗ตขY1๔ยฉSf‡Sร็๘3y{่%ำˆฎ|za%*;D๒‰ฟฤm}‘ ฆ*NŽวŒั฿ฅE˜SŽSซ0Šชฦ˜"๐p๗=๐3๎†p5ล๋ˆRB1มB)!„Bˆ˜๔ิดˆ฿๚‡แ7฿'๕์฿ใล$ค&๘ฝำWbฯูŽ˜*œŠfลFƒฉ =€฿‚Q\ƒ^4ะศ๘)ฮฑํใV๏ห'zi=ั•ฟŽ^PJ0๓‡๐ฺ_crRฃฃฟOณb˜ำn๋Gอธ๑‚vP™Aๆ}d>฿ำŒJL๒mป4H !ฤw ค„B!rcฒRV*bท฿ๅ3PžCzว฿แ~ ๅ;“zฟšำW`ฯZ9ํ†ฐ๘นMC๙.šf€aูW#{เg่…•yyๅณˆฌ๘5๔D9Aช—ไใ_ย;u(_;8.™ี‹‰฿๙YŒาบ‘้†jช|Aชฏe?ูƒ?ว๏iAe†ศ็`J)!„˜เฮ ”B!DnL๚@jๆ:b?ŠQZอ-œcฦ Ÿฤฬšฅุ๓n ƒฉยฉhvl|ะ1ุI๖๕'๐ปŽ“‡Q9่ฒwกลK’= ๔ฯ๐ฯฯฟํ(ŸIŽฯ`N]€๒‚พV4+ŽVPfFFฅ†ฮเตฝL๖ภใ๘}m#Eฯ๓ฏ$Bˆ‹“@J!„"G๚ฝ]Mๆj{ฮ-ฤ6^ธŠ›“&๕๔ฤiุ พ—W๛ูฌZˆฝ๐nฌ๚5แh(_|‚T/~O~วAผ๎†ฐ๘y>P ณf ‘ฅoC‹ a๘ฟ4/ ›ี‹‰o$Fล,”“&๓โ†บฐ๊VbV-LAฒท๕Eฒฏ” $สI“Oม”RBqqH !„BไHz็฿+ทีI;ชล^p'ฑuฟƒ^4•&น๕ฆ็๓'ด9Oไ†ทป้Chฑโ‘d€pๆ˜๏ค๛๑ป›๐;แ>AึŸšฌs๚J"K๎G‹ œb่'Bะ2๏ฺลฌ]NึO`”ีฃ|‡๔3฿"{๐h‘BฌY๋ฐfฌลœบญ อฐร?๒]‚d7nำ^ฒAะืŠ๒๒!˜’@J!.N)!„Bˆ\=˜บiๅwภ9ถฏ~wรค๚}‘%๗ฝ๑ทยขูฉ’[&\ล-๐๓r›ำW„มGi]8า&;Œfวัฬ0ไPพ‹J๕ใ๗4ใ|ทใu4 E '฿ฑใปX3ืYt7š]€฿฿ฦ๐cB0t:๏ฺลช_Klำ`”ิข4ฉ฿ฦ9๚๔่ิP-Z„=็ฌ๚5•sะโeฃ#ฆ”—%๊ยkK๖ะ/๑๛ZGF๐Mg ค„โโ$B!„ศแณ)€r3๘gŽแู‚ื๑z๘P= ๚d‘e๏ บๆ่๑ฒฐh๖“_ฦ;๙ จ /wถ5c-๑MG/ž†r†ษพ๒Zผ๔\๑๓ณำย| ‡฿ำ„ื๖2^๛ซ€6ฉ‚)ๅ9ุs7aฯฟอŽใ๗63_Ÿ%H๕ๆ_ปฬ@|ใGั‹ชQN’ิ๖oโ฿92๏\๑s=^Š5wVŒŠ™h๑า0LT ผ ม@n๋‹8ฏ?฿rางH !ฤลI %„B‘ณP!ซฮmG{๘งแ†ื@ว5]๙DVฝ=VL0M๒—w๊๕I–ฝึ์ ฤo๙za*3ˆsl{2ฉ-V‚^:#Q†Aเค๐ป๐NพŒืZุažม”rำุ ๎ยžwšล๏n`่วŸAe‡๒ฎ]์y›‰m๘zA%*3Hj7pwMx์๋…•Xณ7`ีฏล(Ÿ+ง๒ฉๅฆ ๚รS~J0tf๒๗H !ฤEI %„B‘#™ฝ?Pึฬu่e๕Œ ฆ4ฉC8GŸฦ๋8@0p๊šŒ๖ˆฎ ัๅ๏B‹ v’ล_โ>F>ฎl`ฯ฿Ll๏กTคpwซIคแ๔ฝH!zแ๔x9ๆHƒ#มิqผ–๐ฮ฿นฆม”rRD–>ˆ5{šaใ9ฮ๐?…๒ฒ๙ื.‹๎ k•%*าคถ>‚ผ๏ yฝฐ{๎ญ˜๕k0สg G‹@7!๐Qn ฟ$nำ^œCฟ$๎š4+”B\œRB!„9า๗ญ•9mV*ฌ๚ต่euใƒฉ์0^็!œ#[๑:^G{ไ0˜Šญ๛0‘ŽIเ๗ท“|โa๎&๒6Ztฑ›/๔x)Aฒฏํๅ‘m9ป=่&šE‹กTŒ1ฅTzฏปฏy/~w#สM_“`Je†‰ฌ|7ึŒ›ะ ๏ิa†๛tญ€Y๚`Xซ,^F์!ตๅ+ธm/]๒H<ฝจ{มํXuซัKงฃG ร0ั๗ฒƒ}mธอ๛pŽlม”RBqqH !„BไH฿ฃ›รŽ—ฆaึฎภช_ƒUท ฝคอŠŽ †๐Nฤ9ฉƒษžœSฑ%ฒ๘pJXo ษ'ย๏;™ท๛;ฒ์ฤึZฌˆ`่ ้ฃเeนhภfXhf-Zˆ/ร(œ:fฤ”Be๐ป›p›๗โwศy0ฅ2ƒDืผณn5šfเuผฦะcŸหห๚^ั๏&บ๚}aป w…ตสฺpนมงQZ‹ฝเ.ฬบUล5h‘่FLe๑{ย๖rn#H๕]ณํ•@J!.N)!„Bˆ คF{b:Vjฬ๚ีXำWข—L?b*3„ื๑ูCOโŸ:Dธz5ฆ4๘ฆ?ฤ^t7šaใw72๘—๒rท‘ "บ๊7ˆฎ~/Zค€`เ~_*3๐ซฬฐะฬš@+šŠQ0%œvถMฒC๘] ็‚)'•“`*Hป้w0ง/ภk{…แ|^ถLtอ๛ˆฎ 4;N }๒ฏ๑:ผ้ฯ3สgb/ธ#1U\…fลAำยUำ๘]'p[_ภ9ฒ•ฮ๙๖J %„ฉ%B!„ศ ฉั'jซn Vฬฺe่ล5c‚)…JเuผN๖ะ/๑N ชฏt0ฅฤ7 {ํแœฎใ$๖ลk:ฒไญ๕r๕ฐ&ึŠw‡#พ:๚Oข2ƒ—ธ?ฌp*ŸE+ชย(ฌL9)ฎใธอ๛๐ฯG9ษซL้โ๋?‚YsJ๙xญ/0ณ?ฯว†!ถ๎ท‰,{'šร๏?I๒—฿uโ-ฒ1e‘E๗`ึ.WQuจ<•๊ร๏ƒD็ุv”“สูK %„$B!„ศ ฉณtkๆMa0Uณฝธ๚\0ฅ‚T^๛ซ8‡žย๋<„rำW.˜2Lw|k๎-h่x‡ูฏษˆ’+B7‰ญ๛ฐ&–iใœ"่mE9ร—9šGณ"hS0Šซ/ ฆบรSงขZคเŠoN๎'พ๑0ซขทiษ'G>}่ฤึ.‘ฅo ฅท•ไ/ฟทๅ ตปY9{ัa0UX9z)7ƒJvใuภm‡{bgN‚) ค„b‚[‚RB!„น๑†ิhMรžณ ณ~5fอ๔‚J4+h๘ษž0˜ฉ1ฅ<็-ืาฬ๑ป>=k= ๐:0ำ/ไๅ*n๑ ฟฝไ~4ภ๏oว๏m๗Mบ‰f'ภฐะ่%ี็V์#\)ั๏iฦmฺƒ฿yๅ9hv๔ŠmŽJป๕˜•๓Pพƒ๐ษ'ฟœํข›ฤ7~{๑ฝh†N }โแpeษ+๖„ฃก6ฦิyDScŠี+/K0ิ…๚pL5๎รซD)!„˜เr-”B!Dn\r u–abฯฝซnFีb๔ย)hฆ haแๆd7ษWศŽS%ฏTvAงะŠ‘ธ็‹X3ึ Tึ(๚ูฎ^อชซษ5#ฤ6๖ยปะ4= คzšมหผต>L้zAลyำ+รฐร๏iฦk~ฏใ สwฦฌณTf๘mŸย˜2ๅfpŽ๏ ต๕‘kร&v๋b/ธM7๐ฮ#๙๓‡†ปฏย—้hV4œสทไ0˜Š—ŽŒpS(7K0|ิก0˜jฺsUX ค„b‚หดRB!„นqูิู›ลšฝkๆ˜S %สGBึวI๖เถฝ„s๐—x‡ฯ>_wD$๎๛ ฌ้+Qพ‹ืฒŸแวฟ”ฟ\+Fถ?ยžทเ$~w๘ฮ๚-RšSฅำว˜ยw๑{[pwใu@ธเ๊r)'EถOb”ฯD9)œฃO“ฺอk3ึ*iฏ๓ร?•๎ฟŠ_ชฃู Œส9D–>ˆYฝ=QšžCn†`๐4ฉƒธMฯใตพpEƒ) ค„b‚หณRB!„น๑fฉัŽ›วž{+ๆŒต˜Sๆข”„ cชOพL๖ะ/๑ฯฟผฯŽRpร˜ำ–…Sย๒ตFั˜}ฟำุsnเ๗ฤ๏i G‘]QzXฬ\=V†^^?>x <พ6†็p^ รรธ์oQ^–๘ๆOa”LG9ร8‡Ÿ"ตใ๏๒ธ]6‚Rธํฏ‘๙_ไฆศธฆฃลŠFjLƒ9ํ๔x  …๛ุอ žย๋xทy/nห>๐ฯ/—๒ŽถRBq๑หฒRB!„น๑Vฉัœล^x7VŒ)ณัb%ฃฃs”›&่ภm}็่ำ—ผz™+ฆเมฟ‹fปY†g๓ณFัูํ‰’ธ๋sX3oF.Ao[HฝลZ[ฟ๒;ฃE่๑ฒpฤ”?๗bเใ๗Ÿฤmzทe?่šฆ]๒g+7CโฮฯกWฃ2Cd‚๔s฿อรvIธ๓OฐfฎเถฝL๒๑/กฎิศตKก่ฑŒฉ ˆ,บณvY8ฺํ์พ๖ฒxm/ใถ์รm{้-SH !ฤ๗ ค„B!rใJRฃ9;Žฝ๘^ฌบีณัbEhบสG9iผึpŽnร๏i๚ีฯ่๑R ๖`Lrำ8Gท‘z๚๋๙ษ‘ธ็ฯฐ๊ึŒRญ๘ผูQ.—๛(…/E/ฉ GP๘๘ง๐š๗โ6?BCำ๕7L•M‘ธ๗‹่…•จ๔ู?%ฝ็_๒ฐ] IgX๕kQม˜ฉกืโ™DำัๅS็?_6.DT^– ฟท๕Eผ–}ธํฏพฉ`J)!„˜เ2,”B!Dn\้@jดC/!ฒ๐nฌ๚ต่ๅ3ะ#…`เ๛(gฟฟฏ๕ฒ‡Ÿณšู๘j=QNม;พ‚Q1+ฌQtd ฉํๆo'7^Bมฝ_ยฌ]Ž๒‚žๆ7 ๅฎ๘oˆ๒ัโ%e๕ใFแ ม`'nำnๆฝa0๕+FL™! ๎ฝ ‚ ีG๖ีวศ์๛ทk—XqX<ฟn%ส›$SC5 ฝ`JL-นณj1Z$qฎฉ An๋KธMฯใw3ข๋ƒ) ค„b‚หฏRB!„นqตฉณ๔D9๖ขปฑfฎร(™ บฎศ—$่mลmูsd+มpืธjฝฐ2 คส๊๙ูป๏(9ฎ๛ภ๗฿[U{ฆ{ยƒH3)f‚$DQYTX{u,ู–d’`ฏณฝึP–ร๓yปฦฺฒฌณ๖ฃd[ฯkหส –@`ŽˆŒม Ožžะฑช๎}žˆ ™๎ม๏s%Q“บBwU}ฯญ[ฆœงฒ๗! ปถnืต“j!๕ึNผ•Wู‘.C๖){‹qยKƒ1จXทuรไญ}€ Sใ}๖Vพ๎'1กrfฮ1eยฉ๛'ู„. Q~๎‹”žื๚.ษ&Ro๙ชk1A๐#ไฟ๓gต๓๚2ํx+ฎ บๅx+ฎœฆ‚2z๘8๑g๑>Npf฿ด9ษๆSค„bžใฃ)!„Bˆ…qฑƒิไEuช…ุ5๗Yw+NถI€r1ฺว”ฦฺI›?‚๋ซ^ˆฏคแ‰“]…)S๓ Š~ถnืตำฐŒ๔[จ‚X":†๊^MกชO็sšVแ$›˜˜Pcะcฝ๘วžถaส/ฃ\0˜าธQาo*‘EPz๎P~?๊oปคZHฝํผWฺ u๐‡ไฟ๗—ตv‰„“]…ทb ั-ฏวkฟfฦD๕&ฌุธ{๔ ๎'z€'vv˜’ %„๓|ฺJB!„X ค&/Wป๖ํDึ„“iGEโ€ฒaช#่ย?๖•ƒ;!(ั๘๓/Nรr;i๖พJ๑๑ฌuํ4ฎ }ง์-ˆ~=tŒp่Xmผธh '‘EE“จdNผัŽd0=Opโy*]ป1…aงค฿ฺ‰Š5 ว๛)=/”๔ต๚. หHฟํOp—mย๘%{k่ชv_ov5ส+‰^๑zผUืL><&ยิ1*Gvใw?I8pdฮ9ฆ$H !ฤ$H !„B,…RฦNvีด0ตrrด‡ ส˜bŽ ๗:wŠ่ๅฏลi\)RzแK”ž|ฎk'ำN๚ํjoAฌ‡ฃk%HMˆ$pRญจH•hฤIdภ๑์ืŒA็Ž?Kๅเ!'u฿๏ b)๔X/ฅ'‰๒K฿ชฟํ2+V^๚6…‡?]๓ฏmZƒื~5ั-ฏว]~ขcพ IDATลู#ฆปm˜:๚8z่๘Œ9ฆŒ๑%H !ฤ\g(ค„B!ฦโฉ‰+j7ณŠุีo#ฒแTบerด‡ ส˜J†‘HSศQ|๊๓”_ (ต8O@{ีกƒ๔;'ำnƒิะq๔๐ฑฺ|ฑ^ 'ฝlž0ฅัใ่ nำj;Bjไ ล'คฒ{uท]œL;้wnำLฅ@๙ว_ง๘่฿ืฯ~ีฒพ:b๊>e—aซLPถaชk7~ืc„ร' ๔e„”BฬC‚”B!ฤYิ eOPnงฉรฮ1ตVœTหTภ BŸสกvnŸ—™งฆรA๓Zา๏ { bู>ipั็๚‰/:Šำฐ IขโiœDธSaสn=ึgƒิ‡๊๎=0;–_2ลวก–ฃuสซˆnพwล–ท๒๚„ƒGฉzฟ๋1‚แc4ฺ:ๅP!f•HB!„X‹ค&OQ‘nห:ขW฿Odอkpาญ œษ๏0…ฑ'๑ปŸย?ŒXป๚ณ๕ฆึ ค฿๙แคZ0ๅ1๔ศยมฃ๕ฑฃ8Nใrฆb จD#ส‹25บ&8๓ฅง๘3v{ิษ9ฝฒฮฉj(,=ลบพ5ิm€ท๊Zข›ถแฎธbV˜ ป(๏}่ไถ_๋”O@!„˜u6"AJ!„baิNš<DEธ-๋ˆ฿๒๓Dึ:ใซ&(cฦ๛ z๖แw=ŽiL9?๙ณตฆๅ›ix๛Ÿฃ’YLq=ฺCX๋#คฮฺ^Ÿ;ŽใแคZ๑V_g}โ] ํƒž}T๖่c5ฆe›Hฟใ/p’Mv๒งฟ@้น[2๏qw๙f"ซฏ'ฒq+n๋†‰9ฆ:ๅP!f’ %„Bฑ@j9HE.ป‹ิๅละ๙A๔hNบ งaูิ7้=ฺƒโ9ฃOœ|ใ—&N+ฉฅ0ๅu@๚ญŸดOฅ+ฃsgะน๕zสF๒Pฆ<ŽฉPฑ4*j'ี6AS&่=€๐‡TŽ>aPsKโญุbCa"ƒ)ๆ(>๕ฯ”_๘า’{ฏ{ห7ใญพศ๚[!ภ[ถนS>…bึัM‚”B!ฤยจ้RWG๊พ฿ืC็NQ|๚_Žฟ้ฝD:n@ลงพY‡่ั3๘วŸล๏zœเฬLฅ0qzI-„ฉศบ[IฝๅQ‘:?„99Uงg์ส‹น์.ป๚sง1~PvR๚hข:วฟ\ S๛๑๏ขาต{Z˜Zmใญบ–๔ŸBลา๖iŽO>H๙G_[J—W3ึฑปl* แงWง| !ฤฌOL RB!„ ฃ–ƒT์ช7“ผ๗7ภ๑‡OP๕ท๘Oก’Yผe›‰^๙&"ซฏ?kฃpด‡เฤณ๖ibง\3#ฆ"ท’zำN๘99]งg์.*09วW˜;‰:(T"ƒ“jEล’เฦPีง๒™ ‚ษ๔ย?(•#ปj"LEึผ†ิ[@E“่ ฅ'คผ็›K๑2kr<ะผ}Wง| !ฤฌOJ RB!„ ฃฆƒิตo'นํื@9„Cว(>๒w๘OM~I6ใ.ฟœ่–7Y๓T,=๕รFฃGNใ{šส‘„g^ยๅE]ž้sb้๑~t๎4zฌง>wวลIถเu@8|; އrLขขITฒ ๅล์S๙œ`์ฤ๔๙!ฆบร?ดkQ็Šฌฟิ›ศŽ\๏ง๘ุ?Pู๗%พ— %„s“ %„Bฑ@j6H)E์บB๒žŠp หฉใฯž๕ญNชwลbWผoอkPัไ๔Kot๎•ฎว๑>NุณoัยT๔ส7‘บ๗7ํ-ˆc}v„ิXo}๎8އำฐฏ*๔๐qยi”™๑m& Q‘„ Rฑ4N<ฎŒ_ชŽ˜:‚฿๕•C;!๔'ทBM„ฝ์n’o};rmฌโ๎ฯR9๐ƒ%พ— %„๓œ~HB!„Xตคโ7W[P„‡(<‚S/ฮ๛#NชoีตD7฿‹ท๊zT,5œp๘~ืฃ๘GŸ ์;8ํVพ…ปๆ~;โหqัฃ=่‘S่๑๚qงฉoูๅ„Cวะcฝ(ว๓Q^•ศขโ 8ฑ†j˜ยŽ˜ ่;ˆ฿กำขแล15cไฺ่Š~–สก‡—๔๛^‚”Bฬs๚!AJ!„baิlr<โ7 ‰?@ะ{€โรŸ&8๓า\งLNร2ฆ6mรkฟo˜v%ฎ s'๑ปว๏ฺMุwhมFLลฎ)’w ”šœ?J็๋sวq"ธญ๋q[ึvฃวPŽzู3:DนQ{+_2‹M3-Lๅ‡z๗แ{ฟk7ฆ46็6พfŒ\9Maื฿แwํ^า๏{ RB17 RB!„ คfƒ”๋‘ธ๙็ˆ฿๚>‚3/Qx๘ำ„ฝ^๎4’™ajy5Lƒท๊Tlz˜2„นS๘‡wแw=J8p๔"‡)E5๏ž๑ฅsงGNa ร๕นใธถMธM„ƒGซAส9งuat0ฆœx#(๛ณ&(c Cฝ Ž?CๅศnL17๏v~ตฆ\ณ“็ฟ๛ษ%พ— %„๓ก$H !„B,ŒZ Rสฟํฤozมฉ)์4แภ‘s๙iฮ SซฏทaชšYท๒ู นC;๑ป#์พ8aสq‰฿๔žษ_แ๐I๔ศฉYกฅŽธQถธMk์๒ ta๒ƒpNAjj; œศT˜Jfซ}taˆ ๗มฑง๑ปG†ๆฮฏT์บw‘ผ็ใS“็?้9็*[J$H !ฤ9k2๘Wฆโ7พ›ฤึ_e'ฯ/M8|๒•6f„ฉL;‘ต7ูp;๒-gฯ15tœJuฤ”:Ž +ฏ~yf๘ ‡บันำ˜๒X}๎8^ทe-n๓:ป<Gะ๙กWคฆถ“ั!J9จD•nลIdง} ๓C„ฝ๛ํSวžBœ™w;Ÿซ๘อ?KโŽูๅ่;Dแแฟ!8ฝgIฟ๏%H !ฤOุฃiœl;nหzป<9HM0Z9ฆRอี05kฤ”_"์;€d7‰็f…ฅณร”๒b$๎0ฑ๋ํญ”ฏl๒๚#AJ!ๆ9พIB!„X5ค–“ผ็ใD6n ผ็woLi๔bž†2#Lตฌ#ฒแN"k^ƒบมŽ˜ช”ํˆฉ}฿ล๏ฺฮ‚็_žT ‰{>>ุ๔๐ ‚#–๋rฟQฑค&&5๏?‚)^ 5มhBกRMจd NrฺS๙&ถO*‡~Hp๒Eย#ฬ5JJE$ถฑk฿ผฺษ๓๋‡)!„˜็๘&AJ!„baิlสด“ผ็Wˆฌฟ €๒‹_ก๘๘?bส 1 ๘ฌ0ีบศฦญD:^ƒฒ๖์05ะEe๏C๖ฉ|…a0๚์ๅiXf—งุยIยพƒ็u_Mฐวqฒซp›ึุๅ้?‚.ๆชGM2:Nฒษฮ3•ศ€ใN}=ฌล?ผหŽ˜๊;4c๛จXŠฤึ_žผ•า๏ฺMaืgf=ฝo้‘ %„๓฿$H !„B,ŒZ Rnำ~…ศš›(=๗๏”žฆRXศำRf„ฉถMD/Šืq#n๓T,=๙=ฦ/ู0ต๏ป๖ฉ|…ŒŸุยมฃ„GAืiJdp2ํำ‚ิaLqd๚ ฅeย”ยIdQษ&œDธSaŠ0 >Fๅะรวž&่‚ะGEโ$_๛๋Dทผธะ“็ื. RB1ฯ๑M‚”B!ฤยจู ีบไ=ฟ‚ท๚zJฯ”ž๚gฬขL>3Lyห7ูtชkqณ6L)ฦ`*ใ๖Vฑ฿งrไัษ[ ฆ~u2ฐฝะน“็4TMžฐ'špณํ8ูี„}ซฃืww2a(œD•lฒธ‘้฿aoต<ด ศ#˜ržฤœœซฒ๛{{ ๆ&AJ!ๆ9พIB!„X5ค–mฒAชJOฅงฟ€ sฮฅ9ยิๅฏ%ฒ๊:œL;*šดท‹)๔กฒ๏ปT๖ทi ษื:^ว ง~„๏ฏ฿ •lถA*ณ 0„ฝ1•<‹คฆถ }ภฑa*‘A%›Q^tฦw‡]๘วŸลkปlrป”๗|“โใ€)ไ–๔๛^‚”Bฬs$‘ %„Bฑ0j5Hy+ถธ็ใx+ฎ ๘๘?R~๖฿0aฅNW™ฆV^E๔๒ืโต_ƒ“Y‰Š$ภq ัๅQ‚“/๖!ฒvผ๖ซ๐=moq3a]๎7Nช'ำŽ“ic๚ยขฉณทฯD˜RัสโdWUoตฌ Œ Q^ €๒K฿ฆ๘ศg0ๅ’~฿KBˆyŽ ค„B!ฦ๐_รฺZ{]ชkH๕1ๅ›(>๚YJ/|ยZQ4+LญบŽ่ๅl˜j\aร”ใ—1ๅqT$f'DวฮUDX™s๔zเคpWโdV‚ัฝภ/ิุซœฆ*–Fลา8ฉ–™aช*์;D๑‰ Nฝธภ๓•-, RB1ฯ‘C‚”B!ฤยฺqwง2v2ต๒šผŽI๕หธm—†ยรŸกฃฏึ่-nณยTวD7฿‹ื~5NชMT+“O็๓?‹)ƒฎำR้eจL;nใrะA๏AŠ5๚j็1•h@ES๖้|ีH8A๕แ}‚สม$8บ@Ov\Xค„bž#†)!„Bˆ…•ฑ-ซัk%LEึBb๋/แถฌฃ)์kส{พYW'ฒๆ&ข[^ทโJTCสšรศ”FGz0ลฆฆN…ฉ^žำฐ'ณงa„A๏~ห5ชงญ[Sืh7ณ?{—ื๙AฃSู๛ยกcK*LIBˆyŽค„B!Gญ„ฉศ†;HlEฆ5  ?๘+ส{‚:@8|โฌ๙คj;Lู๕Vเค—aย a๏{ b}\nLฎ[S)โดฌรอฎbยŠ ‡ ะ…!‚๎ง(๏๙แะq;y{_ฏHBˆyŽค„B!Wnวถฌ1fŽ๛ล๘๛ัM๗ฟCธMซ!๔ษฆฒ๛ิร่ก9—็Š๛H๙œt›F‰ฯมT ่ฑ>ยแใ๚ณๆ•ชอ0ๅdVแ4ฎภIทb‚ฒ Rฺฏปํbทyœ= WŸศCESเz฿‰$8๕"ๅ=฿$์?\ืaJ‚”BฬM‚”B!Dศํุถฮำนะa*บ๙u$nœL;&(S๘๎_ุ‰ภ๋Tt๓ฝ$n ]žr=ึ‹Jdpp<ภ`ส๔่iยมใ`fฯ•U[aสษฎฦi\Ž“jล๘%ยพ5:ฟืห3aˆบ' ส{ฒ“ž'ฒ8้6T,^ๅFช?`ะลa‚S?ขฃฏ๖ฤ๘ฅบ›œ^‚”BฬM‚”B!DY่0ฝ๒M$n}Nใ Œ_$ะŸโw=Vท๋/บๅ$n{ฟ]žJยPั+฿„ทl“}โ›r˜ Sน“ƒ](ิไD่ีSej!L9MvฉT3ฆR ์;8๋–ร๚`ดถ๋?‘c๐ปŸุ‰่ต†jœRฑDSaJ‡่า(มฉฉ์๙A๏>๛Tพ:นŽ‘ %„s“ %„BQฃr;ถn3&ฺ‰ร=๓๏ฤฎนŸ๘-?‡“nร”๓ไฟIcOืํz‹]6ป< ห0•๙o}@Eโx+ถูp'nFT4m”1˜Jžp๘$aa”๋ีT˜ršืโ4,รI6ืูwจ.ƒ€z*‘œz”Qด ่ตถฃฆ’YT4Y Sีฏ๋]!8ฝ‡สพ๏ฺŸฏj~™%H !ฤ$H !„Bิธ‹ฆbืฝ‹๘M๏ฑs•ฦๆ'NพPท๋+v;‰฿๔^ป<ๅ1ฦฟ๑ };๒ฦq1Aฏm#‘uทแถmฒฟM˜า8a๎a฿a;Bวq}yๆu8 หQษฌ}}ฉฯ๙ฝœึ๕8‰,„>้=Vlด—&“หeรT 'ูlฃT$†rc6๊"่oริ๑g1Aํ>uP‚”BฬM‚”B!DธXa*~ใป‰ฝๆvN1ว๘ืˆเฬบ]Og/ฯทฃmฆzr<ะ!nห:"ทโถnDEโSกAแsฦhงs:I๖โ nหzข—…ดฆอ#eสc„Cว1ลภTGN]n๓Zœl*–B็ะƒณžX'ั4ชqN"3m.ฌW๗C๚จh'†Š%มูษ่™ฆz๖โŽส๏-สญ|ค„bžใ‚)!„Bˆฅใ•„ฉไถ_#zล๋Qฑแ`7c_๚ML!Wท๋ นํืˆny*š$PฟA*žมiXnoฅ,ๆ๛T'4ฟPื#ช:b*…Š7ุIะใี๙ร ฦ/ฃว๛โw?๘aป]/2 RB1ฯงถ)!„Bˆฅ+ทc:cL'Ž๛๙พ'๕†฿%rู(/Nะ{€๑ฏฮEฝํbKฝ๑๗‰lผ ๅลz๖QฟQ‘่๙&๔งยิ๚qณซgสW%์F็‡PฎwAFLน-๋pš์$๋:?€>ม…‹8 ฤ€J6ู[๋โvr๖p ๋"…ตj˜ŠฤQ‰,*€kœN&(cฦ๛ zœx–ส‘G1ฅ‹7˜)!„˜็ำZ‚”B!ฤา๗ra*๕ๆ?"ฒ”%8๓ใ_}LฅPทหšzห'ˆฌปๅFฮ์ก๘ฤƒฏz„ิl& ˆฌฝฉฆVอผ•ฏ4F8pฤฦ +_ŸNหฆ”E็๋4HTช'ŠŠฅ0๙ก˜œ]ู[๙"qT"ƒŠ7ฺSี‘m&ฌ`ฦz๖œxŽJื๎๊œ`zั%H !ฤœŸาค„B!.ี0ตว}วฤ—~'๑ึŒr\‚S?f๋ธ ท2],้๛oอM(วร?๙<ๅงฟ^๔ข-(วล๋ธศ†ญธ™•3oๅซŒฃว0ลtiโy ทu#NSส๑ะ…!๔๐I@ืี61Zใ4,วI7ฃ")tapเ(สqโ’ฃC”CลQ‰ N"สฉnร f| wม‰็๑>ฮ^ธe— %„s:KB!„ธ๔ไvlfLด‡{า๏๘sผŽP8๘'žcBฟ^OoIฟ๓ฯ๑V_o—็ุำ”Ÿ๗‹ค&ุ[ฤDV฿@dญ8+ฯšcJ๕bJฃ่า่y…)ทํ2คpะลat๎ไข<-๎Uญญq2+q’อv.ฌ|5Hน๎‚๎Fk”ฑ“ž'28‰์d˜"๔ั๙{+฿๑g๐=๋{๕ห.AJ!ๆT– %„Bq้สฏปทฅ๕W‘Uืc0O1ญO‚๊๓ไึ‹‘~วŸใญผƒฦ๏zœ๒ฟŠrฃ ๒๗MเฤRx7โuX15๓V>=)ๆะฅ1~าH4งคVกPvR๓ั๊o„Tˆ›]J6ก\{๋a8ุ…r#‹ฑ—`ŒA9ž<™ลI4UŸ๘่$ู่W Sฯ วz_๙ฒKBˆน?%H !„BB›1z‡฿๕ุu๙‡>uzŽจbiค–mฦ˜๐.ส{Z๐๐aดฦ‰ง๑V฿€ท๚ฬŠaJ—F0ใ˜B]Ÿ?L9nFฬjŒ ํ(ซ๑๊m)ธMkชAสณOป์Fy‘ล[0”rQฑTบ'ž™ Sฦ  Cง๗{ ิํ่ด๓]v RB1๗งฐ)!„B1กฒ๗กไฟ๓—8ฌญวื๏$›IฝO๑ฺ6bBŸสP9ธso ›bŒม‰7เญพHว 8 หf…ฉQฬh/บ˜ร”๓–g7‚บa*HๅNV'ฎำ •jF)ฏ… 5ใฒศgผ•jž9bสtq˜๐ฬ^*Gv๖ ์>}@‚”Bฬ๙ษ+AJ!„Bฬ–q๗Œ๑๊.L9+H฿)ๆต˜ Le฿w๑ปƒ™<{~F‡ธ้6๖kˆt\“^6mŽ)ƒ.Ž GํSฆ<aล~ษ‹แ6uT—งB8ิŠ&F_l&๐q›ืขา-๖ึรแเฑ R“—G๖?c จdNชe*Laะ…aย๘Gv๔์#<๚“—]‚”Bฬ‰+AJ!„โไvlหj๔vgGv๛ฮา_ๆปทWรTฆNmq›:HฝํฆL%OyฯทN>_3ฏะh›Y‰ื~ สซpฺฆFLUoำ#gl˜ ส C;Bชy-ฦ/๖ึ~dทeNชwฯ's IDATc4&?D8|l‘ๆ:ท} *Zฝ•/ู<-L)๔์ว?๒ม™ฝ/ฆ$H !ฤ<Ÿดค„B!^DˆRฦ>f4#F;.…05็๒ืhDp6’~ห'p2+1ๅqส/~• ็ฅš|ญNใrผUืแญธ'ีn5L้=ฺK0pSชNŽŽ๑ g๖ข\ฏ๎๖!Tp[6เค[0:ฤไ ‡Oิอฒจd3Nชkœฆสy‚žฝ๘]ป N๏!่:{ู%H !ฤŸญค„B!ๆvN!FยT ู*ผๅ[Hฝๅqาm่bŽ๒‹_&์;Xรgใnฆทผ[์Hœ‰H่โ0*GES˜๒8มษPฑTํ;&๐q[7ุRaS&>นhs{๗๋7ฅTช'™Eล@MjสyยพTŽn๋Fคาไญ‰j‘็๖:๏ๅ0ีง๒%›P‰ Nผqฺ\`ี0ีˆสแ]6L ว% RB1ืกO‚”B!ฤ”W=™ทๆ˜RA็%ฆึUริ๛kโ9.‘ŽIฝแ๗Qษ zฌŸาs†ฮฌฃณsทuส+q.ณ#ฆฆO~>>ˆ>Ž }Pn5่ิ๙ผ รฉR•"บ0ˆํฉป 5น<ฺ •ศ Yฆฆ~h"a฿aฎGฉt?๑@ๆ็?ื)ŸฎB1๋'AJ!„โ"ฬn 0aˆr#จD#*ˆJdง?A๐ S>m…b& RB!„ธ$]๔5›ๆEฅ*ณน๔ืํึmฦD;…bึัA‚”B!–ขšQg]น†ŸSJufท๏์^๚ๆโ„)MปแงI๚~PŠ g/ฅ'>_ฟgœฆ’๗*ธzฌ฿)ข\w๒L่ƒ๒p—ฃb จhๅFํื‚2„๛dพ  S“?ปPaJrซA*)ๆGฮ@Jy^o๋ย0ใ฿ค)!„˜๋่ AJ!„KInวถ๋qvิMˆ:๋ ๖า SC;๎๎TฦŽCๆ‚œุฦRฤoz/๑›~Pง~L้้ฆฆ&S:ฟ}งi ษป?Ž‹๋#:†Rjฮeš˜PiXŽŠ7‚ณ#ฆ”0~*taSฮ/`˜Rเxธห/GyqLq˜0w‚โ’<า…ใ฿~@‚”Bฬut %„Bˆฅ ทc:cL'Ž๛ฅq%{i„ฉŽmY~!ย”Š7ธๅ}ฤn๘)‚ฯQ|๚ (ืซ๕ใ6ฏ#ฑ๕—@)๔hฏ RำFGอล„!ส๕P้V๛DพX#ธ`0-E%oรTฅ0m\ฌ0ฅภเ.lo5, Žœ‚ ผไ?—tq”๑o}B‚”Bฬut %„BˆzถไBิYWดฆฮ๙ฤ6‘!q/ป๖ํ๘วžฆฟูงะี#'‚ฒŽฤฒปยhแ๐qิ9-ย่ๅxจT *‘ม‰ฅม๑ภLPฦ๘EL%ษaโE S (๎๒อ(7‚. ฃs'!ฌ,๙ฯ']c›,AJ!ๆ::HB!D=Z๒!jฦU-#F;œูํ;sK|ปf1;^ษvu’Mฤ๏๘0ฑซ €๔qส/|&'๓ฎ3nฏํ2โทฺUกGฮๆNN›œœN๗1Z[๙Rอhฤ‰72Œ_ฦT 6L†0~้"„)^ oูf;Va;กฟ๔฿บๅ<ใ฿๘# RB1ืัA‚”B!๊ษ%ขฮบบฝคยิyog'ีBโฎ_&บ๙u€ม?(ๅ=฿จ฿9คoลUฤo~ฏนS่‘ำฏ0ฐ)Œ16L%›Qษ&T,5๙U”0ๅผง0Œ Š(72๙ณฏ.L)ˆ$๐–oๅ  C่ก`‚%–5ๅc฿๘C RB1ืัA‚”B!๊$Pผโ‘3KŽ„ฉ99้6wŒ่ฆ{CๅเN*๛ฟ u{พ๋เu\OฦwๆNกGฮ œWุชqษฺ0•jAEโ“_5ASว”ฦl˜ ห L)T4‰ป|3 0๙!ยแใ`ย%V5•"c_ RB1ืัA‚”B!jๆf—wฆ฿ท.b๋6cขs…)'ำN๒ํDึฦP๛mรป๊vY1D7n%vอ่แ“„ฃ*HM^ุ๒b8ฉT";kฤT Sทqช0„ ๓Sฺ@,=ค ร่มฃ\œ'๚ีุ6๔หŒ}ํ๗$H !ฤ\G RB!„จญเ !๊œOไœ†ๅxํืYs#nำšc*‘ํTฑิƒK?9;LนM$๏ ผีืƒั”๗|“สมขผh}.คึD7฿K๔ส7ู>‰=s‘&iWLL>๎คšQฉf”;ตLPถฃฅJฃvฤ”>0ฅu5Hmฑฟซ0D8ptฒ…-e&จ0๖ี฿• %„sy$H !„ขŒ}แ#Yฟ๏€„จŸx๖ฆPัNชล†จwโ6u โ”ล ~๑˜๏๏t6=ธิWGnว0ฦ๋ฤaญผ–ไ}ฟ…ท๒*ค^2•?Dลำ๕นpa@๔ส7ฝโ>๛ฏร'ะฃ=x„ิY;(<;ว”“jืช +˜โฆ8‚.ไภ?1LญQัค.€ฐท์IBˆK๛”F‚”B!ขอ/nืนSฑง3ๅ=฿ดOณฮฺส‹ฃ’Mxซฎ%บinv•}Zšฦ/ข๓ƒ่แ๘'Ÿว๏z๒˜=ั™พ๋มฅพzr;๎€ำryg๒ ฟฝึ[พtH้๙/R9๘œdS}พ/BŸุ5oฏNาแะq๔X๏ER“;œ Sn'Š“l™ฆ|L1‡) ฃ‹#`ยyริYAjด‡0wฅ–~‘2aภุW~[‚”Bฬuค‘ %„BˆEผ\๛จN`-ฦ`*ยม.ฎว(๏๛ฆ“U„ืรI6แuHtใV–uvฎŸ‰”ํผ<น“๘'žล?๒˜ฝตk‚ๆaฅ*ูํ๎\๊k+์?ทํฒNtฐถ๔ฬฟR9๘Cœ†ถ๚|w>๑ ‘ wฺe[ะ U} ZุkฤIทฺ:žช#ฆ 9L1gGLaPฎ7ต๏b0aˆŠ%๑V^mwวI๔X—ฤRค„b3 RB!„Xh๙ฏม"๏้ŒฌฟmญJdf_cสใฝ๛๑ปง๒าท1aๅR=UรI5ใญฝ™่†;lˆJ6W็DR“ฃTยI‚๎งจ…๋ŸBา SYB{๑ษฺ๎™q2ํuน&๔‰฿๘n"๋n'HM-€ S m8‰&˜ OajSฎ†)=ฎŒQัิไฉpฐSฬ —;ุ่ฑ/–)!„˜๋,G‚”B!สไ|?žทึm]›YEdำ=DึˆŠ5ฬfขK#„=๛จIๅภธFTLp’Mx๋n%ฒ๎V–u8ษfT$Jat€)ขGNใูmCTaย๐ึั%ฆŠ}ถด็kฟๅ5m๘uOี฿DRกO์ฆ๗Y๓๛ฏCว1c=เธ‹๖’l˜RจxNช'ี2๓๕„!บ0„) ก‹9ะ6:ฉDf*H๕ฦT  %>6Z3๖ๅ&AJ!ๆ AJ!„๔‰งg|ม๑๐–]†ำด–่๗แญุ‚Š&gฐะ๙!‚ำ{จ์6๑g—๔บrR-DึŠท๚z–ต8้6T4eCT่cJc6Du=Fๅ่ใ๖‰g~^ษ9%ฆr;ถeษฌ๘=7ู๖๑บ Sa@–ŸณO ยกc่ฑพE!Uฝt`"xN„)'‘E%›ๆSล!๔h/ฆR@ล&Ÿฒ๔์ฟ่%ู'AJ!^ๆจ"AJ!„/lfLtืฝ์7:ฒMธห6ป๚m8M(/6ฒย=โ9*/}^ิ.!“#ขV]‹ำผงq*šB)ฃงBTp๊E*‡A๕c*ใฏ,Dอฆรฯ)ฅ:ณwv/ํq:•j3งq๙{Tผกๆ_ฏ Cทฝฏภ๊ฆว๛1HM^B03L98ษ,*ั„JfPntj‚2ฆ0 Jแ4,มษ0ก_หฑะฦพ๔›ค„bฎฃ‰)!„B\๘ ญŒ‰vโpฯy`uฤ”ท๒jbืฝ'ฝlฦ<5`0A=ึKpYJ/|;Y฿'cฑ4‘ wโญผทe=Nร2Tผๅxำnอ;Cpz~๗่ฑ>t~ฬE]"aชฆ’ธƒี‘E†pเ(:?XC!gV˜R.N"c'Ofg„)Œ๊S๕‚ำ{0•๑“ฃ/Uค„โeŽ"ค„Bqแ.๔_aˆšอ๕p[7Y{3ฑ๋…“ศุGะOปธ5~ับึ$ฅ็รN’\?ง`6Dญฟ oูๅธห.รi\‰ŠฅQnคขช#ขฮผDp๒Eย)๔x฿ไœ<•„ฉฺ`4‰?„ป|36Huก๓C58ฒhพ0ี„“lš•A็‡ะ#ง œ˜Cj้^HBˆ—9zHB!ฤซฟฐฟ@!jฦ.6L5uดุu๏Bล’S_ณW{˜Jžp่8แ]”u;ŸRอžy)T"Cdีuธห.ว[พ'ปzฺˆจSC๔๔ผDpfฏฝMkฌwฺล๛ฒaj{v๛ฮRวพ๐มm&t>ฅโษ;ฯšรl‘%๎๘0ne` แภaLqdrคQ ๎เฬ SN2ƒJ69ฆฆ t~]ฟ\นฅw]b Œ}้7$H !ฤ\G RB!„xฅ.|ˆš็"ืuq[ึป๊-Dทผ‰ฯบ๊ำ˜๒8A฿!*๛ฟOe฿wj์Œห†(oูๅx+ฎฤ]ฑทy *ึ`GDU_ฟ9Mะณฐ๗a๔h&ฌ,๎kืŒ์ppv,๕05๒ู๛ทaขŸrฒ+๎ฌSŠฤึ_ฤmYFงำ•ฦf4ูฝฤ`z˜Rั4Nถ'ู|ึwšฐ‚) ฃว!(MฟKfŸ’ %„/sด %„Bˆ๓•ฑm1ฆว}‚žธDธ+ฎ vๅ›‰lธcึ๙ h.ž~‰๒๏แy๔ฌ ไ…พ0w’Yึ vDิสซp›ืข’M3Cิh/a๏>‚พC„ฝะ#g?Dอv …ฉŽญH,๛7ป๒๊E Sส!ฑ๕—p›ื‚ ๚A%_O—€AWŠธMkp›ืุงf–ฦPฎ‡ŠMญ[ใ—0ลœ-(.โ{๖ย’ %„/s” %„BˆsฟP_œuึ L2k'>ฟ๊อxํืฮบ•ฯ€ั๙!‚S/R๓M‚S?Z๐ q•ฬเ6ญล[พฏj๛ไผdผ่ไญ†fฌŸ ๗a!‚ำ{GN/ฮญy็ใ’ Sw€xห'ฆ๖ŽE SŽKb๋Gq›Vƒ๚_จป๕hย`rย~BŸเฬKฟˆ“iวiX>cฤฃ J“#ฆLPFฉ๚Sค„โeฮ–$H !„โ'_˜ืFˆ:๋z=ณฏbWฝทํ2T4มŒ0†่๑~ฯR~๑+„]ไ*ˆ›]…ป ผีืแถl˜ข ˜๑‚C„‡ Ž?gC”๊kงจ†ฉKแB{2Lตญ๏P^tแฐ!yืGq2ํ†}ชฃ‡๊‹ั!ฒหQ‰,มฉaJฃจHยŽŒงQ๑ ส‹MŒ_ย†ะ๙ALPฉ๋05๚ค„bฮs& RB!„˜Bผ6CิYืํ-๋๐V_O์๊ทแdWอธฐƒ }๔ศ๎')ฟ๘U๔่™ RoภI/ณ#ขึ„บ'ู„๒โฃม/ขว๖ฦ๏~Šp่8u[’ๆ˜RAgv๛ฎ—๚๛aไณ๏๘ˆ“Y๓?U<•^?่ลlj\ }ย–๋nฝญ๑VlAลา` ~๗ใ`@EbvŽ)/ŽJ6ฃb)๛7mฤT%oGLๅ‡ํˆ)วฉป๗Œ)!„˜็I‚”B!fหํุ–5ฦ์จ๕5›ป|3‘57ป๊-จT3v4Kuฤ”1˜ Œ>Nฅ๋qส/|S~๕๓๑จX 'ูŒป๒*"๋nฑท&ฅšmˆย@ฅˆฮŸ ์=€฿$แเัฅทำ\"a*ทc[–ฬŠ฿s“mฟุaJE$๎๚(Nร2LXฉฉJญ4๎ฒMจx#่ิ‹เ—Q‘ฉัf6L%์๛6šBE“จHŒ‰นคLiฮ1Uฒ#ฆ—z Sค„bžใœ)!„BLฟุึ่ํสxqศิ๋rx+ฎ$ฒ6ขWพ '‘ืe*LiŒ_"์?Bๅะ)๏๙ๆ+˜ทIก"1TผoีuD6ฒ'ี ฃณ2บ0„>Aะณฯ†จธepัI˜บ@๋Qฃ6HฅZ0Aู)ํืแฺRธญ์-{: 8c;ฺษL~}ฦS๙"‰๊ˆฉด฿^ ”ฒsร•ว 8Š. bๅบ5ฟ๔ค„bžฃƒ)!„B,•5ƒใโญุBdใVข›_‡“lๅL}ฝ๚„ป g•—พMๅ๐ฎsบฐฦ๕pโxํW฿ผ'ู\ฝMPM>ฦ>ฬ$8cฎวป/ฝJsL){v๛#_Y๊๏ฆZ~๗BN|nย'Jโฮุ ๅ—{๗ƒ ๋๐Šร|บไไคๆAๅzgฟฟf„ฉ$Nชbฉj˜ชŽx ty Sฮcฦ๛1aeŽ฿U;$H !ฤ<‡ RB!ฤฅkI†จg: Iเถฌ'zล}D/ป•ศุั“W]ฬŽ๒K฿"8๙ย<ฟหAลาxซฏ#บ๙uvDTฒลVคL่cŠ#่‘ำ๘วŸกrh:wRv2อรJU:ณนฤ฿K๋Tช๕ฯœฦๅ๏นaสL;‰;>ŒJf1~ัސชว ๅx6H%ฒvคWฯ>œgพ7.Sa*@ES8ฉ๛4M/>9Gœ +”1ฅ1๔h5ฆ$H !ฤ<Ÿ๖ค„BˆKฯ’Qg๑(œtNฆุีo#ฒ๖&l4˜ฆtˆฮโw=Fy๏C„}ง~<ึ`Cิฆ{์dๅ้6๛จ๚‰UG๖เw?‰hW๕ฉy!u?a๙…$a๊ผฟˆผŽฤํฟ`CNฅ`GHีใ>ๅFq›:์rLŒ๔ามฬ0<๗—a*–ฦIตขขIpฃL<๑ะh฿>ฝฒ<Žํลhฺํ€‹O‚”Bฬ๓)/AJ!„ธด ํธป๓’ Qg๙8ธูี8™•ฤฎ};^๛5๖ษ_Sย=ึKๅะรTํฤอฎ"ฒ๎ึjˆZัสq1:œ Qวžม?๒ˆฅแ—ภhูัๆs)…ฉDห?;ูwพ’0eสye—“ธ๕}จDSฮฃ๊peธ1ๆ5จx#ฆR$์{พo\ฆยThรT2kฃTผa2>™ bรT%ํึD˜’ %„๓|บKB!. นwภฏ‡ตrคp›ืโ4uฟ๎]ธหฏฐ#žฆr„!ฦ/`"8ž}๒—๋M…จ‘3vฉƒ?ฐ!ช<^%ฮ‰ๆaฅ๔๖์๖/,ํ๗ึm*ฑS็ฆLiฏ*โท*ึ€)๖ฌฯ ๅ%pšVใฤ1•Aฯ^”z% 2-L้kฐŸวR8ฑp=ะ”0~SฮฃG{ฝจaJ‚”Bฬ๓ฉ.AJ!„Xฺ$Dฝฎิป๒Jโ74nSฬธp5SฅCti=ฺCpf/~ืn๔h:?do?ฏŒ?ง”๊ฬn฿ูฝด฿‡็ฆLqฏใFโ7ฝKcŠฃ‡P๕ค"I์jTผSฮ 5๏Q็วhƒo€Xoภ‰&ม๑์C *LฅhGL๕ฑXaJ‚”Bฬs&AJ!„Xชภขฮ๙„(–ถ“žoผ ทช๊E๋๔+Zฃ C๘]Sz‹่ฑ%D]0—P˜"๖yทฉฝใๅย”. Yw+๑ื *šฤs„‡ฉว"ฅขiœ์*ึสc=๛/Xš|‡pโ๖พXI€ใ‚ํhฉาจ S๙!ภ,h˜’ %„๓$H !„Kํ‚WBิ9Ÿy1ผuทเตmย]~นล‘ศฺงxM^0ฯ|"Ÿ)๔๎งฒ?ฉ์žฌฤ น=Yผๅ›?็ต_ฟ๙gป—๚๛”xห'็ Sz|่ฆป‰]S6H† ๛€S‡A*ึ€“Y…ŠฅะฅQย(วฝXอFฉxKู0ฅ” Sฅq๔xŸ T•"8๎cs‘ %„๓|bKB!–ส๎ึmฦD”uฌั๎๒อx+ฏย[q%n“ QxQ0ฦ>ญ+?สฑ#;"qฮz"_1Gpz•=฿ภ?ฌฌึW,าx+ถุmฒ| ns*ั๔9ศvๅEsK๛}[ Sm๋;&ž ว๚‰^qฑk฿Š&ัลบ๏p)*‘มษฌDES˜โA฿มA ผ8*วi\aŸฬ ti 3ฺcoศ ส`.๎oค„bžOj RB!Dฝ_ะnfLด‡{dmผชŠ%q[6ุ๘ฑ๒JผึีQQฬฤœ3c}} z๖ู[ค‚๑›Kdใ]จX’™a*@็๑=C๙G_#์?$ซ๙|ถHฤ†Aทm#สซq[ืใ$›ภ‹M„มณ๗Av^็ๆsฮ๎ฝo็t#gฬ”DRเHVฒdำดฦvูซ‘ึตฎ™ู0ฦlีฎ]ต;ณPีxw๕ฯjผฎ๕ฬ์he{g'ุ–-K'ˆ”(RL`‘นsบ}ำฮู?ฮEฃ@“ ˆะแ}J*ชิ$๎ํ๏๛}็<|฿฿;{๙P:๎กฬ_\ีbjๆ_}แะ๙มฎฒ๕ fv„pื็ศ์๙*ศE‘s›ฑUื‚n๊ฉตN‘Žž\Px_ฺ0)*ศบ๗ะุuux5˜ส ถZ„ธŠญ๎˜˜!%‚ฐฤ>@„” ‚ ฌLDD}€ Oฆฏu=^็Vž]x[ัu-(?ƒลBTยฬ‘Œž =A:yขข#Qย:ฒ2~^T˜ใช˜r๙La„x่*/{Li๒ส+3Œ.\ล๓๑;ถโตoภ๏‹ืพะ>ฝ็ลเุI’หo“ŸI&Oา–C๙‡งW๏wzžๆ๎฿ึAใฉ6f๖>Cธ๓35!5E2vzลEHYcะ ํ่ฦnT˜ร–&๏q+ฝlšิก๊[ั(/ผ๒1ี9lyโ 6*v1%BJa‰™)AAXYค“g๗W_ำƒั้็>nห3rAkฃ“iD็{๑;6ใ๗WQญdP(l\ฦฬ“ŽŸ"=N:zชvpอ๘`kAY2{Ÿฦ๋z˜ฒiŒ™นL|โ0•W•dโxญx๙>‚มG๑:6ก: ฌsทถฆ0ŠฉPJ ›FFคS—ส้ไ้›ž{ๅŸญv1e0๊?๑?wLณ๒3๎šฬ\บใํeทk บฑะแZ็ฦI'†๎ชบ๚^Rฐึ;IV฿žๅ‡˜j[šคโ*ง0ทๅuEH ‚ ,ฑO!%‚ +†ภAเใ6HวNQy๑I.ฟญฮษีYธž๎ ฒ IDATมษ4 ;๑:ทฌปฏ}“ซภ ฒ5๑Qม”&I'†HวN’ uญ=a๎ๆ^@๛่ฦN2;>n]ฟ8cสZla&ฯR=๚ทT_3๗šk/@ืทใตo xฟ{;บฑฎN'ULq S‡š4ฐ•ฅ[จLaขฟ๛ืข๑แ๏ฌๆหฝ๓ื๙`๋ƒสฯf:u[ž†4ZQฟƒ5)บฉะŽ r˜นฑšา๗๐=ฐฦIฒ\kv๙ภๅรU ุา4$ีšTpbJ„” ย๛5R‚ ‚ฐผ)‡<˜ศฏ๒{wตแฺ6ฉ’\|ƒส ุ้ilณ–[ฤTX‡ฎoลฺ๋Nฐ!ผŽอW[ม”‚ธŠ)Mb&ฯ‘Œ น๔6ค*Sk/่gะอฝdv,บฑJฝ@L%าั“DGš๊ต๖ฤ๓ัูftห:‚uเฏิ… ๋Pสรฆ‘ Š&˜๒”Q&yฉ&ฤงžIฮ<๗๙ฯ~c•_ษมไ๒;›ญฬŠ๒ƒ๕ฦmš ›๛jB*‹™รL • ฉ๗}oฦ‰&ะแ2ฆฒ ๔๗ื”งœSทvo!%‚ฐฤพM„” ‚ ,OฆํดึD๛_Vู2;>Mธ๋ณn๊›ฐHLU็ˆ‡^ค๒โ’ฮ\rU&kiCใgPนfผฎm„?vUDYืš—D˜าf๚<้่ โ๓G ญขฒP›DT%๕9tฎ<๏๊๚XƒŠ$—฿ก๚๖&>๙๊_ํฃย:ผ–~uฌ‹ส6ขด‡5ฉห์IcฌI0…Qle๖ฆDิuื?ฎŸ๙้šS3๚ƒญO}=่฿๗ิ-KิปŒMผ|?ชก๙ึรษณหBHอฟGcQJกฺั๕ญจฐ…a]ฆ<-Maใ ฤe>จ˜!%‚ฐฤM„” ‚ ,/ฎŠ(๏หื๓›zศ๗ยMO ปฎๆŸ€๛7๚ฅIโS?ฆ๒าฟรฬญ‹ๅจฐฟk;แึงๆ3‰Tญฬ&ถด_ธ๋๓lxd๗rS6‰g`^HNž[–ำญต(ช.๏Z๙๊[Y˜gส3ุาดซ่Kชฌ˜!%‚ฐฤถA„” ‚ ,›Cๆ’"๊Zผฮ-d๖ม†วn >LaŒุ่๗จพ๑-Lqb๎`4*ศโw๏ ๑ฉ๙Œจy•&ุส,fv„ไ๒[$็^ม&Tฆแฮj+PŠp๛งท<‰ส6]m—QsE๙&f๚ยjXะบกpำใ๘9 ‘หปj>k]€|a SwํOwจฝ๔ช˜๚แฏไ่๐๊พg<พ?๕๔๏.g1e“ฏe=ชฑฅLa”tjy ฉ๙๗l†Rู&7•ฏฎuAล”ล”ฆฐฅ™Zซi๔พŸcR‚ K์DH ‚ ยฝ>Tผˆบฟู}ฯเ๗๏ฝฆอค6๑m๊<ีทšุ่๗\สjุผ๘ผždv|บึšืแBล•ฎUเ13—H.พA|๎ืšํรบย&U@‘ู9‚Gœ [ะ„I0ณรDวพOๅ๕?c%OLTนfยญOฌฏu=ชฎๅgjBข๊๐qชELa„ป‘sfใ ษน#oฤ'๎7EL; )S#:Oฆ์ฺ‡[ฃย'ฆUL)N`หWฤิา‚U„” ยทXR‚ ‚pฏ‘nดปฒ4อๆฯ 6~„์พ/โ๕์Dyแb1WHGŽQ}๛ฏˆ‡^p๓ฏย-Ot.1eำฏe=บก”ยฬปช@ฅVะ•U51U๏rฆ๊Zิ”ฆฐล lTยš˜k3R‚ K]EH ‚ ย>4>u-™]Ÿ#๓๖ต|ฉbชZ$นp„สkJ:zUฌ\9l-c1ฅ2๕๘=ป uQM=ตpl—IDต„™#น๑นWฐi‚าmษ%บญZํฃณM„;?…ืฑๆX,ฆ’๑ำDoล๒Ÿศ็„>Aะw^๛FtC'„นซ“๓ส3.จ<*บ5JขZะฝ๛œญ-1๕ไW‚mŸฺrS6Mฎ )lMH]ญVเ•uญฉ*l@5ดนv้˜าvnUL-ธˆAXโฎ*BJA๎ึ!๑ฮ‰จkeAv๏/๎๚,^พฏ–/ต`โ[e–่๔Oจพ๖วค“็ภšซ‡ญe$ฆTX‡฿ปxํ๑๒ฝจl3ส ฐึ@Tv"jไ]ขำ?F้Zภ๛-Ljปซ่/฿Kธ“่–๕๓ญmตยฦUาัT_3ข‡—ืฦ1ศโ๗๏ซษมอ่ฦฎZ•š็ชิสณ˜าค›˜g“žiฒฌ>WWลิ~>เ๐ะ๊พ็ิฤิŽOvช…๎ๆ๕NSผึ๕่†vฌ5NTฮฏฐ ฉ๋พ n’dถUื‚ฮ5s]+_e[šํ1๛วญ)A„MEH ‚ ย>%u บฎ•ฬƒฟDธe?บพํบ‰oฆ<ํฺฤŽ|sYŸ+?ƒ฿ฟฟo/^๛&ผ|*ืŒ๒3XkฐQษ#$>๙ถ:ก“"+kฆ๑;ทl๛$^s7่ลm4Gr้-ชo~‡๘ฬO๎ํ[ ๋๐บถ >โึคฅ฿ษม+“๓*H*˜jัฤซW2uฬฒฝ6ฎ\~็oใฃ๓Wป˜*ลo^vื~C…ูเฎ_gc๐Zะ๕ญXc0…Late๛จซwY๐|Tฆ•kชต๒-จJ-ฯb*ณฟ๛ตฏ6๓ < Aฎู_ˆA„;วไก'mu-^็V|>๐k1™Ÿ๘ๆฺฤLqœ๊๋฿ข๚๖_aซ…{ท)๑3๘}๗แu๏ภ๏ŠnY‡ฎ…c_QvnŒd๔8ษนW0ลI@กย์ŠŒX“nมฆ'œ8\8‘ฯLy†ไยkT฿ษ…ืน›ีF*Sืพฟ~็ผถ NzกQQ37ๆฺ๑ข2ฆ:็ฆŽYณrฎSำ‡๖็3~้๗ฮํฯM1eลk@ืตธA ณ#ุโจUtq•็*ฆ2 จ๚tvแํbf‡ฟช›zสQแšง)AAธ‡ฟ'ฟbญอภryOมเ#d๎{ฟg*SฟHLู$ยฬSy๙฿?|Wƒต•โ๕๎ฦ๏ฺ†฿ตบMณ๒3n๚•ึผฑ“$_วF{MซสวV‹„;?Cธ๙ Tฆ๑š‰|)ฆ4E|ๆ'T฿๚้่I๎ค˜RAฏc~ฯ.ผฮญ.'ฺ๊5)ŒLกณMุ4คฒขDิuื_ฤิมXt RI3;Œ-Mญ.!u๕f^ˆสิปVพlำ•Ÿ|8(OFA„kn›"คAแv๖–Ÿˆบ–p็gศ์YWํไฎŠkฑq™trˆ๒5ษล7๎์&ฤฯธ๖ฏ๎ํN|ดot-/AึIธŒ™';Ir๙wต)ส V๕gศZKf๛ฯlxฬ‰ร…'๗4มฬ๘!ี7ฟ™แถŠ)/ภk฿่ไ`๗—U฿Aฮ<*cๆF1…๏…ุจฤrnอ๛ภื?ฎŸ{๙?$'Ÿ๛ว๙‡งW๏ฝj`ธ๏™ฏ๛žบ“ม็ึ‚ืถkฦฦeฬ์e—1ถชOXNL้l#dะนWA”'ค ย5ทKR‚ ‚p;wห_D]Kๆ_$ณ๋s่ๆ^”r5๘b+ณ$ร๏P~฿’Nœqงสต๙rxญ๋๑:ถเ๗ํY =ˆจโ้๘i’แw1ำ็ฑf๕‹จE‡๘$‚คL๖แ/แ๗ํEe๊ธv"_:}่๘จพ๚วุค๚แ^P๛nMฺ7แ๗๎v-“]ึDT\ม'0ณรฎ ส ฐๅ–๓tฦ‹)LTโ ฏ฿้นW™ˆฉ[ถเตo@e›]‹็ฬๅ{ฺ|ืพรฦธI{:€ด๚ี`รGส“Rแšง„)AA๘0‡น•'ขmย:ฒ*แึงXvoแD>‹)OŸy๒›T>”˜RA๏รุ๋Lะw^็VtCปQ0/=า๑3คฃวI'ฮ`MฒฆDิu‡ฺธ‚ ๋ษ>๘หx›QaŽEกษI„™๕ฃใ•kยk๊ม_w?มบั๙>Tถ้ชˆŠJฎ0ฑฅILiL,—พ†)LTโw๚_4ƒ?ี~ฏ๛๐bJาฎB*ำเ„ิ๔€ฟ†!%‚ฐฤSB„” ‚ แl5‹จk 6?Af็g๑{wฃย๚ล฿า„tโ4ๅ—๑ษ็ฎ฿\๘Tฎูตๆ <‚฿ตm>h^DUfHg.“ŽŸ"96*ขย:๙ ๔ฮ#x`ใใ.๓น*ฆ ถZ$พ๐ีื„ไา[ND…u่ฆ๛รkY‡ส6ขด5)6.A9)Uœภ”ฆ\›žp6MˆO=?’œy๎ท๓žฦjฟ๗บ˜R <ผฮMจ [™&บฐๆ>W"คA–xJˆA„๗>Œญ%ตx— ษ๗s„[žย๋ฒธM ‹+ครG)๔%น๐:x>*ฌw<?Šืต ุ‰ ๋œˆชต™้K$c'I.พŽ-Mกฒ๒AปU มภร>‚ฮ5ปvส+˜Sž&9๗2ัฑ ;6=ŽืฒU—w-‘ึbฃ’“O liSšu“ุธB|ๆงkDL=๙•`งฟnyข๓‰)/tRAฎVy~อU‰AXbซ)BJAnt๘Zร"ฺ๊อ‚Ÿ!๛๐ฏlx ฏuเšjœฺDพหoŸ1^๗Nฮ-่†NWฅ=W}Sรฬ\&=N|๎elyFDิmย&˜”ฬฎฯl๘ˆปฎืดZฺ4†$rาฐ–อeใ*ฆ2ใชขชฬคาšwKk b๊ฦืลZ”Ÿq~SžฦLžuำ็ึ"คA–ุcАA„…‡ญƒึ๊oˆˆบมฆ!—'๗่—A7u_|ŽIฑ6E)ดv:ี9la”d๘โ3/ธึผLƒ\ฬท*€ถ2 aŽ์}Oใ๗๏ปพีท็ปา’gM ี"้ฬEQท Wˆ‡^9•œ•?๐ฃรซ๛^YS;>ูฉ<ฦ“1เ‡x]Q~ˆ-อNœฬš๚\ˆAXb#BJAฎˆ({ํ}Yฎฦ{ฃ›{ษ=๖e‚‡Qนfฎถ๑X๗ŸคŠ™ฝLr๑Mขำ?†4F๙ู5wฝ‹[:ภbฃ บกฬ๎ฯ9 ไjหณ`ฌล'HGaซ%ทfศ~๐vbใ ษน#oฤ'๎7Wณ˜š>ด?๏m|์wฒปพ๐*ฬ^7ำšิๅ๕ํฯร–gHวOญนฯƒ)A„%v/"คA„ตŒˆจธq๐3x›ถ์ว๏ƒื6X›๔‹ค‡I1ณรT฿๘ษิ9W5ตฦฺt๎&ถZD7uโ๗์ม๋Šn๊BePฺฏUIฉkฤ”uแ๔ใงIg/CR]ฐ†ฒ7ผm๋ฒ†ฤTๆั/พ฿น™…bสฆ)˜”`๐ะ[ž%;นุaฏDH ‚ ,ฑฏ!%‚ ฌEDD}ภ Cล๋J0๘(^็ผๆ>Tฎๅ…W—J/ฮ.lT";E๕อ?วฦPaR%u๛ฐ•*ˆ฿ฟห๎ส๗กฒอnržMฑ•Z6T\F5tข;jqA8}‘ŽภฬŽ€I!uึiญ‰ฉŽ-ฟฌ2๕.ป,M6~”r-{ใ'ฏi%]ˆAXb)BJAXKˆˆ๚€… ‹ืต ^วtK?บฎๅgฐˆ+˜โคkŠJ๘๋@7uฃฬยใ8ถZ$~‡๊ซโrค$ะCaฃ"สฯโ๗๎ฦุ๋Šืบ•หปœk\ˆ์ษๅทH.4 xˆ`ใวะ]ตp๚+ ›ถ— ฟƒ-Mอฏ›p›ื-ฎ\|ใ๙๘ุ๗~-เ๐ะjพฯ†๛ž๙z0๐ศSfv˜ฬŸwฟiŠd์J๋5ต๎"คA–ุgАAึำ‡๖็ ๆ€R"Wใ&6~ฏ{~ฯ.Wี:€ฎk… &šโ fnœt๑้ŸNวฬ\kศ์}šฬ๎ŸญIฑ2ึ`หณฤ^ฃ๚ฺ?ท OธlTๅแ๗์ภ๋Šื6ˆฎoE๙Yฌ5•I #คร๏_8‚)ŒขณM‹+R”Gฐ๑cƒป5]No0ๅi’Koน๊+/@ฤิXวธB|๒น?9˜yเ—†V๋๏9๓๛?7˜}๔ห3๗โ—มb‹“คCาฒ'‚ ธ-‰)Aa53/ขฌMณ\‘๗ม๓๑{vใwn]$<r๎็qลbŸ&>*frˆtbW1บกฬฟDธ๙I'=Vใ˜Sœ >๗ ั‰gืZ๗ฮ-aฃ `๑:ทเwnqีj ํ.ดฐq37Fr๙’‹ฏป๖;/xoแง4แ–ง๐B็๒‹ฅ•I1ลqโ๓ฏBšึู3~x4์์^วfผ๖ ิุƒสฯ ญโ_|์กt๚ยฯ™ะาฒ'‚ ˆAV)3ฟ๗ู|—(๋‰ˆบฉsฒ‡ืพ ฟ{~ฯNผ๖่๚ถ๋Eิฤ’Ko‘ŽŸ"?J๏๙วzํษ>ซ๘๗ปJ๙j‹Mcฬฬe’ก‰†~ส’ฃใื06*ƒM๑Z๐:ทแwms•g™:สM3,NŽž น๘:้ิP๊ฆ+ฯlAšyเ‹๗ฃฒ ,*_Iาูaโกะูfd"฿ญฟTถ ะ ภo฿ ™:ื*Yš$น|”ไยkŽ;ุ๘ซzhต^†๘ฬ๓๛ฃc‡p๓ใ๗ซLšY~R‚ 7F„” ‚ฐบ๐ีBฬ ˆ‡^:\<าŸ{ูm[ า^๊ ์ตฌว๏แZ๔บถกฺˆR โ*ฆ4I:q†๔๒;$ฃ'œˆช>ะห๋"๓เ/ใwmuมๆjqจถ™ขz๔oHวOฃ‚ฌ|Žำโบฉฏk~๗ts*ฌGi›Dุ๒้ฤษล7Hง/b“Jญล๎^ฏR@e๊ษ<๐ห๘;PaŽลb*&น„™นŒM*Dˆ˜บ™๏—ส4ๅ…่ถAฎˆ[)` ฃุธŒ2kฬฆ0ซ/ั?ส8<ฝZ/ห๔กว๗‡ปž`ร#ปื‚˜!%‚pcDH ‚ ซSฯ๋บึX{ภฆQ3&มฦˆฯผ@rแ5โsฏˆ˜Zˆเ5๗บชจ]๘ฝ{ะ๕ํๆPJปส›า$fโ,ษ่1’แwIวN`+…๕ฒแถฟGf฿๑ฺjโi˜Šสค#วจผ๒ฐฦ ๕pำG7ฌf1%BJa‰])AaฅSหฏsวAฟ{๛€nDืๅม2_ษaJSDว@r๑ษล7ืด˜R~ุ…n]Oะฟฟw7บก๓5™:G:v’ไา›$ฃว?ดˆZ๐@A๖ฟOธใS่|_ญฝL-"ณ$—฿ก๚๖_AZ]3kcฅ^ว=x-จlณห฿Jlต€™พH2r 3užด0rหQ๏๛^โ ~ฯNยญOก[ึ_ณFWฑลqาูa7•ODฏC๛.ืหฆเg๐ZPu-๎ชE%lตI[žมฆษ๛Jืต#ฆžJฐํำ_ ท<ันล”)A„%v…"คA„•|ˆฑึ?ˆงts~็6ฝxmƒx๙uจบผ;ิ%Ulu3;B|ๆ'$็_!>ฦZj9R~U฿Šื6Hฐ๎ผ๎]่ฆ.TX็DTcห3คำ็IGO’\|d๘šw๓oHฃยzฒ มฆว๑šบOไ|~Uข7ฎ6>›ฆ`ท6เตฎGีต ผkาฺg๗2้่ าษณ˜ยศไเ๗๎"ฒีุY`51e 6ฎŽŸr!๊6]ป7#ํปI‡iŒ ฒxํPนฺ'.CTr๗กา6>ฐH\sbjว';WSฆœ)A„%v"คA„•xhฑึ?ˆf`๑กะCืทใwo'ุ๐ผŽM่†ฮซญ2IีŸ:Orแ5’ GHฦOCฏฝขrอxm๐ย๏ูn๊tYDJcMโฒlฆ/Œ"9๛ษ๐;๏V~๛ ฦkYGๆ_$xุฉ๋+Q (ฬ์0๑™็‰Zฎ‚•qฟaeฒžpำใ่ึAt] สฑึ`ฃ"ฆ0†™8C:v‚tf๘žH›DTw|š`ใวD>ฯc‘˜ชIFŽb็ฦืฏด^ฦ &k๑:6:Uห_ณq •ฑฅ 'ซ>dE›*q|๒_ญ๛๔;ซ๙ฒ๒Ÿ}-™ชยlฐ~R‚ K์†DH ‚ +…%Eิ ‰บพ ฟwมฆว๑;ทบ žL;Dง1้์0้่q’หo“\z3}WVฯล๒|Tุเชn6<ๆZ๓ปP™Š}ฅlf˜trˆ่ฤa’Ko;9ง4~๏ฒ๗"~ฯnTฎษตVบc8XK:yŽ๘ฤaโณ/ืฆมญศญส๓ฑT˜%ุ๘8^ บพ ๅgฐXˆสต‰†CคฃวœˆZญ‹ถR?Cf็gqk ˆ)“bสำตฯQดบ๘”/@g›PูfTฎ•mD)ฯ‰๏คโZ๔ๆฦnฉ"๊=ื!MˆŽ~w4>๖7ฟ•?๐์7V๏~>๓่—~฿๏ฬJS"คA–xœŠA–มไ‰งญ ฝฏˆบ/pbชแๆ'๐:ทึ„Œ๏าiB:u–ไ๒;ค#๏บlž™หฎลfฅข=”Ÿq"jำND5u_QWZภ ฃคใง‰O>K|๖e–O๛ข"ุ๔1ฒ{ฏs๓๕๙า˜tไ๑‰’ŒžจMƒ[[ฎZvYŠ ๋]_็–y๊jไYโKoaf/Cฒ2ดlฅ€ชk!ณ๓3๘}{ิธyyˆSลqฬ์ฆ4น,‡๑s฿ฑlบ.jhG‡๎๗ทILŒฉฐ…‘R๕ž๋P-xNฤิ @„” ยป#R‚ ย๒=ˆ<พ฿ฺ๐ š ‚,*ืL0๐0มๆ'๑7Aฦe'ก<ฌ‰Iวฯ^zห…xŸฦวWXล”ญ๑ฺ6n~๗NtsฯUe D%ฬุ้)ข“ฯŸ} ฬ๒อษ์Yย]?‹ื6xดทจLrแัฑ๏aๆ&–yฦ”ยฆ1:S?๘~ื6tC‡“iสรฆถ<ƒ™พH|แ5โณ/ก‚\-H|๙b+ts/แฮOใwํpSๅิีpzLJ:uŽt๔*˜ฦงœˆส4ข๋๒่ฆ7%๒Jkmrž-Mบฐrญ๏:ฌ%1๕เฏ‘฿ณ๓๓+-๘\„” ยOWR‚ ย๒;xuƒ‡^ถ k"ุ๐‚มGX๛ฐมy ึฆnฒๅทI.ผN:1ไ˜หพยCแตฎ'๕Y๎ตŠจฦ๙\—1sฃ$#ว‰O=Gr5lมŠุ(ฒ|‘pื็๐š{Ÿ[‹ญฬ~ž๘๘a๗EpSฒ ช 2๕ใu๏ภk์rEฺs"ฃZฤฬ^&9๑้็๔๋Vฤบ\ฉชณ•^็VยmŸภุ๋ไDอ‚|ฉt๊<6*aๆฦ!.ญผ’๒P9ืšง›บœ,ิ|จป)Œbซณุ4ญ‰จ{๓ฝฒี"ัฉ็ฯฤGฟ๓๋๙?:ผzŸ๛ร}ฯ|=่฿๗ิJS"คA–xฤАA–ฯAใฮˆจ๋~uyt};แฆแ๗‡n@g›k็็ฤUvŒŸ"=N|ๆา‰!ฬrmฏ}'ขถŒซบi๊vYY~ๆชˆ*Ž“Žž >๓๑ล#ุ๊ค+ญZEก2๕d๘%ยŸชิO๒œ IDATŸ/ฮถ37Nt๒Y’‹oบjœ{<๑อV‹จl~฿^ฎํ่ๆ^ท6ฺวฺิ šย(้๐;Dวƒ ผ0+nmๆลT๔Gฐ๑#่–๕ตVDภคุธˆญ–ฐี้ิ0+`€๖\>TึUEฉฐัU๊Yฺ๋+ #ุJkSิ‚๊ฝ{ญ‰ฯ๔ญ่ํ?๏DL-DH ‚ ,ฑ‹!%‚ ๛ƒลQื7๋ะ๙>|พ๏ฏษํฺ๕’ˆtrˆtbˆ่๘๗]ลTฅp๏^ˆn ๘1ผฎํx-จ\ตxY๋ช6Šคc'ˆ‡^$น|[žยฦU–OVิญ ห“}์+Nzไ๒ืd%ค3—ˆO>G|๑อEcwMT เe๐๛๏ร๏‰—๏ซญM‹j S!=N|ๆyWฅฆผ{.ะn๏:๙๘;๐ื=ˆ—๏Eu๎;e๔]žu-Š…แๅ|~EDe๊]‹^. sn๒a\ฦงฐ•ูZโ!ทพWkJLํ๚ทƒ ์^ฎbJ„” ย{ZR‚ ย=tCปห‡‰Jุ๊้ิyฬ๔Eขc฿%8ƒญ๏๕ผถ ƒโwnAทฌCืทข|—Ÿd“ ฆ8้ยสO˜t์fnฬ…ดฏฆ็ฝาxmƒไ๛ ~฿}จLใขเsา„t๒,ัปG2rฌึBv‡•(…฿ปวIยถม๙ตq“๓\~W2rœ๘K`ฌ“P+>[i‰๋‘DุธLธ๑ฃ๘,SI-3kๆฆ<ฝฌBฯUฆ‚:—Gืตข2๕X,ถ:็DZuŒqŸณ2Epํˆฉว๗‡ปžๅ(ฆDH ‚ ,๑!%‚ ƒรAkํAด๗ๅๅ๔พtC;^๛FยŸuีSM่บ—๗•0ล ฬิ9ฬ์0ีwฟ‹™<{w‚ฯ=ฏ}Ax[๐Z\%W˜C]™ฮVœ "z‘t4fๆ"ถZbEWDมเ#d๚UผŽอต‰{ ‚ฯใ*ุ้Iขท’tๆโษfบ"^๎๘][๑ฺ7ก: ศ:IW0sใคc'‰ฯ3;์ควมV  }ยญ๛๑๛๗ก:็ืษ&Ulq‚t๒ฌk%ฝ—ŸUkมZts7:ฟ•krี†ี9๗฿ส,6*ก<ๅฎEตH|แศข#฿๕รCซ๗๙ฒฤ”)A„#BJAธ‹…ๅ)ขฎE7uใun!ณใS๎ะŽฮ4:1—k“๊Nb ฃDวพ™พpg‚ฯ=ฟ}^๗Nฎmxํœ์ธ"V’*ฆ4M:q†ไ์หคgแ>*ฎฎŠจ› ณ๛๓d๖=ใrดNไณอ‘\>J๕ญ๏`K“ทIiะะ‰ืพฏs+บฑึ;•T]ตฺุ)’ G\n’ึ+Zh|(R) ๊Z6~ ฟwOญzฬeLน<ฆQาฑS คw1_สฯbJSxอฝxํPู&๗žH(•…าjuฌลSมถฯวpห๗ZL‰Aธ1"คA„ปp0X"๊jŒ๛ซืา๏ฆ‡m{ฎZชฑรตวi๏๊!zไ]า‰ณD'่ฆˆŽรด๖๐Zึใ๗์ฤ๋฿พ ีุ้*|ฎศŽา4f๒,๑๙ืœˆ?…ญใ*“{ฝz^Hๆ_$ณ็ จ๚V7epมด7Sš"น๘:ี#ๆBฤ…ก7น8 ”ซ k้ว๏ู…n์BePฺร&ฆ4‰™TฺUวY ~ˆื:เ*ข”r•P•Y๗ญฮaญA)ต6ึbญ‰ฉŸ์ผ•Š"คA–x4‹Anฦพ&ข~sี<0ณx๙~ผ๖๘ใตmภk๊ฉy ‹5‰ ญพ๐:ษฅ7‰ŽฎUฬฤaZiผ|^๛&]x];ะM]‹D”-O“Nž'=F:rŒdไถ<-ถ๗ภk@๖ก_!xคVณ J)MHง/๛ษลื฿#_ช&น :฿฿wŸนf”๖ไตส,f๚"ษๅทI'ฯaซ…Zหค์ฑW†$~๗v‚มGั-๋]kฌ็AšbขDeW 8s้ึ'*ฯญฏIA๛x›Qู&”า˜๘Šˆชb+ฌIWmEิ๛ฎET‰“ัwฟY}๑QภแU{s)ลo^vื~C…ูเnฝฆ)A„%ั"คA„ล๔กyƒ9 ฌM๓ju}บuฝ›vืทฏcบฉ่าุUx”ฆˆฯฝB|๎eโS?r•้ ฆฉi่&๙}๗แ๗๎พฺฅjำศ*3คS็]5ิศQาแw1ฅ)๙ฐ}}dež]W3žŠAšŒžpม็“CN\]๙ฉ1(@ทฎ'่฿‡ื6ˆสต ะ‰จ๊fv˜t๘]าฉsฎeำZ@ชข>ฐ ฑฟw7ม๚๑๒W'๒™ฤU,•ฆฐ•ฆ0สM‹>ํฃ‚6‰Pž็‚๏๋Zka๓Ulต€+ุส 6Mคญ๒สZฌ15}h>๓่—~฿๏ฬS"คAnŒ)Aแถl๎Wปˆบ๎ฌเD’ืตฟk‡ o๊rบ$‚4ม–ง‰‡^tbj่งW,x>บฎีๅD๕๏ล_wๅaMญkุ๊้I’Ko‘ ฟ-IEิ-ใ๙„[?Afื็\…ŒŸ]$ฆl‘^~›ส‘oขผSœtญy๋จMฮk?S I/a ฃ$c'1S็HงฮKEิํ!II•`หว ึ=€n๊Aีฆฺ$ย–ฆIg.ธษ}๏ีซ}”Ÿqž็ใwlAี/Qqษ‰จาิผฌnฐ"ฆn"คAnŒ)AแCmๆืšˆบๆ1Šฎoฏp๒บwเตฎG7ดสUKE.'>๛ษลืIงฮฃ๋ ึํร_๗ › —mtํ}i‚ญ03—IวOŸ•ไา›ุ๒Œ|ุnืŠ…ud๎๛yย-๛ัญ๋›ใdSR%?-Mข›{]^XเDqSœ ?M2~ 3}้๖ๅ ๓ุJึl๙ธซl่@9ภbใ ฆ8N:1Qqqฆ๒ภ]+eใwlB5t8ษ›VฑQ—ฑฅIl\ฎ… ๏ปQ%ฎผํ“ž~แZอb*๗ฬ7ƒ}O‰เsR‚ K์หDH ‚ ทฒy_"๊ิl~ื6‚G๐zwฃ;ัu-ต›๐fK“ุจ‚สิกฒอตษlพkKŠŠ˜ย้๘’ณ/Ÿอe wะAf฿/nzฺุ้*Yะสgบa›จXœภL'>Jrแ๘™šฬ๎ถR@7vl~ฟk‡›ศ็g‹ส˜ูห$c'ฐฅits P™tSบกํชˆŠซ—1ล l\u+k‘&DGฟ;๛›฿สx๖ซ๘ู6๎{ๆ๋ท[L‰AXb,BJA๘`๖'ฟbญHDิ ซจL~๏}?Jะฟw^<ก”ซฒY8uอššˆ%8C|ๆโกฑqE.ๅ@{๘ศ>๒k๘๛\{ ญ-ƒ™ฝLt๊วD๏สฯ,ส™๎<ถ:‡ืพ‰`๐ผŽ-.P ๗'.cๆ&ฐIkBีตึDT&q๛ ฃุD*ขnฯZ‰N<'b๊"BJa‰ณ)Aแๆ6่O~ลZ šน๏ƒ๖Qaแ๖Ÿ!ณ๋3่–u(ิ\‡›ฺv%‹ศฬ\&:q˜๊YZ๓๎๖ๅ‡่|™=_ภ๋†—๏Cež/SiB:uŽ๊฿ยฬ-–Šย]รFผฮ-51ต ir๙ๆภค˜๊,ถZฤ'jaๅJ.ํ^‹ต#ฆ๖…ป>Gม†Gv1%BJa‰™)Aแฝ7ไ"ข>(^K?แถOโ๗‡n๎Eีๅ)d_[7Ao๘(ษๅทIวO“N_tc่…;ด๓ัx๙^ย]Ÿร๏ฝ ๋]๕ŒI]6ธึ0ฯcก˜ฒI•t๔8ี7พญส lTๅ‘{7๐;ถิZ-<‰ๆง๑ูjuอฯ…ผี"ั๑gฯ‡Ÿ๚็มฦ}c๕>฿๎z๚woUL‰AXb[&BJAธ๑\DิE7๕๎๘~ฯNผๆ^—yใ…\™โf“ ˜ผึกดW‹+2˜ฉ คฃว‰ฯฟJ:vาจฃ’\ิถใQx๙~”ทืUDๅ๒N<ูิeอ’N]ภฮCX‡ืฑฏu}-ทhŽส$฿ ๚ฦŸcำXZม๎6*ƒ๒6>Fธ้๑ซk7฿ kAk@aS'ฅlRล'ฑqQึ่vฏGฃผะMBฬิป{šะš๔ ๒3‡W๏s๑ึฤ”)A„%ถg"คA„E็๛ลฏ˜j๕ ‹"ขn๊Iชั ํ„;?ƒ฿น า็ยฬ 5ˆNž%น๐:้๘I=๘๐:6กฺ]ๆI\ปั๔—'u๚'$ฃ'0…‘-bตv?แก›z6 ๆฎŠยา$fv[™Ee๊km•ื_+กูมภC่†ะ ฤ“I1ฅIHๆฐฉ\๘!>*๗|>tc:ไ~WฐๅLyส}—ŒA…9tC'*^ๅNึชmฅ€™ฅPZหE ๋‘ฆ บฑไๆk1;้n+3& 5fnœ๘ฬKozm}$๛ศ—ฏึ๋rณbJ„” ย!%‚ฐฦ™—฿๏๕ูyฃJIR‚ 7F„” ยe๚ะใ๛ญ ข๙ธืพ‘์_$๖ PkSˆซคำ็I‡฿%:๑Cฬ๔ELir [ฏe~^—-tEt„uNtค1ถ†ืฝm„๙C f๚ษุIโฯ’NŸ_[2D{xอฝx;Wd‹จฉ $็_ลฬ^ฦฤU”า\[๕พ‡@@y!แๆ'ะM=่ฆn๗:~าุeQŸมฬk'ฉชsฒ™ษ6โ๗ํล๏Šืฝร–g›\ฅYc+3nšแฤYผๆ'‘>๔=†4Aๅ๒x[\xฝบ:FัFeา๑ST฿ฆ8.ูEK]วZkžืนฟw7~็VT] hJ๎็Qษ $k?ธ@4]ืโZ2รWง=ืjฑU๗็›าŒศCภZ &Aeš\^Ce๊jmฎั|ušญฮaญฉUพฯYBiืๆ๊๙ฤD' :๒อ_ฯ8<ด:Ÿฑ๛๓™Gฟ๔๛~็๖gฎˆ)R‚ K<"DH ‚ ฌ ฆํ฿gญ>ดPD-z ๘ผ๎dv}ถึ>ถiฯวฦฬ์0fๆ"้๘W15sัฏฺ'คB7๗โwnร๏ืyตโญ!Mฐๅา้๓$฿ฤฬ^r-`ท”๏tอ?ฃ4ส 6~ฒฏฅผะI “:15z‚t๊ั‰g1ณร๎`ฝถถ0จ0็ฺบ:6นฟถฌsกไพปNถRภฬ\"พˆ๒3่๚Vnw๖–)McŠS}{๐ฺ7บC7๊ส้[-\z›สซษตŽI9Žป4•9'{ˆ๊‰ชou"<*9ั•ˆ๛Š๚j่yX 2ฎ4k%ULi [[ปแ๔สsำ๒rอ.#*S๏dRฯW”ูJk’›Qืโ…nสจMฑq…๘+kBL๋|f๖Oษ*BJแR‚ ซ›้C๛ญตั—o๊มiฤ๏ใฤTCGญ-อ๕ถq3uŽtbˆt๊<๑ษ็œ IฃUuอtS7^วๆZ๛ะ.tsฯbU™!พH:|ิU)Ufo“่XpศS^Mn@ธแ#xํั-๋ฎๆๆX‹)M‘Ž'>Jt์๛๓‡ฦUพuA…9ผฮญxmƒตษy่บึšˆ2ฎk๖2fๆ2x>*ฌๅ ๋ซ๋•N_Biฏg^พฏ–/uELLy†ไ์หT๏ฆฺ8W+6*๏#่฿‹฿ทwพ"Šธ๊๒โrM|คที฿YcภZtS—kๅ rNLีชHซุjSม&•5TีฆA{ฎช0เฆ^นืล%lตไ๎qit๎s‹E–ญ‰/๙Atไ›ฯไ^•yำ‡๖B2˜?๐ฃรฒ#Aธๆฉ BJau๒AEิuG”๚6unคซ˜ชoE๙ูy1•Nœ&=I:~Š๘์Kซ"๘\7uปœจž๘}{ัอฝฎeN๛ตJŠf๚"ษ่qฬไ9ฬ] FVุjะF0๘(^วft๋zืhมbฐๅ’แฃ$ŽŸx[;XฏบMKE7๗แwmร_๗^ว&t}สฯ`ฑP-a #ค“g][ž๊.ดb]9d+าย(สฯ8‘Y฿ถ ๘œซ๙N&:~•m\;7$cฐับฉ๋@ีทฮอ๊\MDอ`ำไŽ’ู4FyชฑปzžqีkเK\qblvk“U+ฆ, ”v"*ำ่*ขผšิหฎRญ<ํ๎๋w๘>gฃJœŒพ๛อ๊‹๔Vซ˜Anฐƒ!%‚ฐบ๘ฐ"๊Zผ–๕๘๋๎'ุ๔8^พUืZ›Jๆปœœ‰ำ$ร๏’\z“ไโn ปYAีJฃ๋\{^>‚%Dิ์0ุ้Iา๑!๎~—+\ฌ1(kP nYื6ผึืRcฌ5ุส,ษๅwˆฯผ@|ๆyH"lR]๙›• ‹n่ภk฿X›žท ะ๎ฆจกฑq37F:~ 37†n๊qmYw=ศ‰)k fvุฝ็ฎm่\๓ขเsาS&:๖}โกWต˜R:ภj—!๔๎ม_บฑ ฅผ๙๏–+ND%ีป:ะMไซG7ดฃ2 .|ป6ตo^’U็0…QWAt+์๎ ึX”RNฦๅš]lcฑIโฒk_,ฯ๕฿[ฤ” ยฺB„” ย*แv‹จE ?ใd@฿}๘๋ฤkpทึบส”ธB:1Dr๑ โs/‘\~ฌkYถhmD็๛๑๛๗บึฏ…"สค๓ญ_้๘iาฑSND™ไ&S ๏›ฯO๒Zึปlข4มš[ž&น๔&๑ะ‹ฤg_ยฦeH“๗™V~UืŠื>Hฐ๎A'ขšบ ผฒ\qNCตสคะษŽeNmำ•QนFŽ-ฎํsa๐yc&ฯQ}๋;คฃ'V•˜R:ภb]ตX๗N๕บึWฅkืฅXหmšฤฦ•{*{lš 2่บผ“œ™7‘ฯคุจ8m\bๅ&€)ฌIมZTฆก๔๎Bฬ.ธ฿ท<)MโžV†‰˜AXˆAXแLฺŸทึบ"๊บ‡F]ฟsซ ๙๎ูๅr๊Z Mฑi„J˜้ ฤ็^&><้ฤRศ๗D{จL^s~฿^‚มGk"ช ๅ๙๎pV-บŠจ‰!า‘c˜นQ'ุ–6M\fQ็žN6vบ1kฑัฆ4Er้-โก]๕Zฅฐ์~โ๙่lณ ผxฟgท ”๋ฏถx•งIง/`fG฿_฿ถlรจMySม๏qม็*ศ]SึUฅค#วจ๙Slqre‹)ํปฯ˜๖๐{๗ <์*ึด็$o\คŒ)Œaใ๒ฒZ3k :ื„ส4A˜C‡ .ว-M0Qข ถ:‹)ฯฎ jฉZี^ฃย:t};ชฎูM…Daโ"ฤleฮต]รฒš6hฃJ\y๛ฆ๑็๗Fž๖‚ ซR‚ +”้C๛๓s@Yšๆปzๆl์ย๏…ืนอUNulBๅ๒ฎB'q“ฑฬฬ0ั้็‰O3;ผ žxฺต~5uฌเaผ–šˆ œˆห˜ย(้ไ9’ ฏนช›e>อ&Qญ eบ๛ัM.เป–]dหำค3—HGŽŸ{…t๘จซ€X–2รCux๙^‚MOเ๗ํqฒ0ำเZผL์&็M_$- ฃPจบŠษ๘Iง/5๘}๗ก๓}ต๗ฝ@LEsฤg_ฆ๚ๆทGถ–kด2v”ž๛,zžหˆบ"ข<›&ฦn(ย(ถZXkิน๛E]ซkiำตฯ_uโ+๙f—๑๏ก˜—สCีๅkykYPสU|%Ule37ๆ*ง–‘ˆZtKขฃ฿อoๅ<๛ y๚ ‚ ฌDH ‚ ฌ0๎ฅˆZQ.wฉ{'~฿ผ–ผถWa 6.aๆฦ1…1ขใ฿'9๛ฒkyป๏ํใ5u >VQ๋\vส•T0ลIฬิy'mฆ/ ดฯฒช์zฟC[TFe๐๛๖ธชฏบVRฌ]Š)Nธึรัใ$—";9_ฑฯส ะM„?฿ปว ›l“kŸด้|ีš™น&นบ~+tvd^"ฮWต-œศWš">๕๑ู—kylห๙sจ\K(š`๐a‚E7uืฺ^๗ณธŠ)N`ฃJฏŒฆ7k ˜ิMญMžS~ต–Cาชซ*šวฦลe๖YTXkQตjPุAฆV]ƒI0•์ฬ0ึše+ขฎ[“j‘่ฤs"ฆAV"คAVหFD]‹๖๐jเ~^ผ:—“ฉฏUUH'ฯbf.ฝ๛]’‘w]X๎]:˜้ฆ.ยอOบ์ซ–~Tถู,-ฎอฐ4I:užไห$ใงQ~fJ4ถ2‹nhฏ‰ฉวPน&๐ย๙j37N:z‚d๘า‘cคS็1…ฑ{'=”Bๅšษ์tพ฿ษ&ํ_ญZ›รฬ\rmŠaŠ9@ฟfnย:ฮญ.ค]-h3)ฆ0๚ณ๗f1–]ื™ๆฟ‡s๎ำyž3rN&Gq‡”Yถ6บห ฬ •^ ไS€Tไj๔[็C?4ะ‚ ~่๎j@ๅjฯ’lฆIQโ˜ฬ$™๓๓<w>็์ฝ๛aˆL’I2‡ˆศึA$ษˆg฿sฯ_ฎ๕/W แิyz_บ5,ภU ผมoย9YืJกเ.ฏฌQ%QX†ุฅแKฮ„€ะ๕ํ4‰ฮK~v"_Tฅ๐๓<œ ป˜ข … ขถขฆยKQuก345ฏZ ้6ฺฑ-ฎ_๛;ฒ˜b†ู3ฐb†ูแ์X๕๙ŠNP;ภ7 {Ÿ†j๊#๙ใง6of๎2ฬาMืฮยฌNPฆั!ฺ๋เœ"Iึุ ‘jคCฝˆ’ฅU˜•qD“็ญLฤีRfoผiค‚+e!:ก;C๗= YำOฬา€ฐ๙y˜ล›ˆf/"šฟ ป:นอS"Y‡ฤฑ฿†๎Žื(g€ชึ Kิ๎iBภKลน={i฿BAำ6ฟY ~"ีmB˜์‚‹‡hแ:ต=ๆ๗ฉซแl๘ex^กj<Œ…[ถœขสีฎ^)gB™B0 IDATคะD  žKำ*\XŽงqฮวSHๅถฟ>M])สˆ"๑naรP^ƒ-ฎภ™๊ฎซช๗!‰ฉ๘w™ำo๏†avแNˆ…ร0ฬฮ%{ๆีำฮ้ั,ขพ๐`I5@ึ4รz ^฿3Pอƒโ,U€Xƒpโ พ๙ j)็6m"฿บˆRGใi€Mqˆ4๎„aฏŒ#œ๚vmยัดณฝ%:ึ/†‚ซไก2ฝP‡ใ)uwe9สฬZพp๒ฬ˜์L, –พGผ็แ พHkTำ แ%Iะ˜ถธ ป6U!ต๑{รํอ5ฺ ฎฒQำvˆคว_+0หท|ื0นi?ฝฏ1(ึภ๘ผแ—iต๔ต(„+ฏย–ฒ€ hญ[3gยx"_# ”oฆHธฐฉ ฉœ`ทๅ๕’ชำตTฅ“จ:ีU๓q•ZBศmyMพ&ี"ย๏}\ซ?อœลY90 ร์XH1 ร์@ฒg^}=Qปึ…ิถ@ค›เt๗4]L๙t@. ฝ‡h๎‚๋oาA.(ฦ™9๚4$มF^ƒn?ีL’~ฮ„pๅฬสขษอ_…Hึึ๎iษqg14`#ศ†.จๆAส.สtC(ฮ™๖0ป:I๙ๆฏา„Dm๎ฆ#ีฏ็I่o@ต AีทSลO~X`GXก6ร–ขณWฑๅ5ุ4tฯำPญรŸk!upีข๙kจ^๘ฏp•]ฟญ>๐[„e +xž‚๓“ BฤUฒฐลีฯ…•๏ํ5sฦ@ฆฟ†DŸฆส#kฉZ*(P๎YiuKชฅœต€3ฉFศD ห(D!‰ 9ˆฝS๙Uื„ลร0ฬฎƒ…ร0ฬb/ˆจฯ=f 3]ษ๘G บ๓(TำUKYg# ฌ ั%„ท~WYƒ‹ช4:kฟฝ„ฌi‚72t๛!ชฦชmฅรกฑˆสยฌN!š<ณxNi์อJฏEj@(จฦ^จฦจึ$ฆtฮ†@ทษๅ็\{ัุ์ิgึ๓aDƒHe ปŽร๋y’~fCDฒ๎3“๓ฬ๊$ฬสJิฦำ ๗๏ลdgบ๓Tc/pw6‘ณฐๅ,ฬ์%T>/€า[3‘ฯZ@๙P-ƒ๐zŸขuซiบ#yื3ขชjSr฿ญ™s€Le 5~ ต(ฏฟฏใฬ&T ฐๅตMส>ฃ้y"Q gCศtsœนฆเL[Zซไแชส๓ฺ‡๗‹)†a˜]tR`!ล0 ๓๘ู{"๊‹จฆ~ศบv๘Gพ ี:L‡l!mด–„็`ๆ.#œ๘ถธ๒ๅ:BBึตQK`A’umฑศP”cSษQX๙ิ˜•๑Xp‰}Q)p?งhVเ๕?™้‚j V>/E๋Q-Rxiมฅ 1•Ÿ๐MFฒบ๓t็qจถ‘;ก๒สฃ l•ใ๘>๚ฬบT‹งฮฟœษw3งฯŽ๑a†ู9ฐb†y dฯผ|ส9 $N๎ืk ฺม๋y’ฦล'jจีN'ย–Vฉrf๎2ฬU„3C(ŸไF๛!่๖ริj–ฌƒšชmชุ์4ข้ 0ซำpa‰EิุL!ผม ๛H %๊๎ˆฉา*์สlq ีO–&$–ฒ_T่T๛a่ถจฮcPอ4O'จ*ซZ€อฯรฎNRำืx}HxุตyจL7TศFึบ„pQณtแอทM‡Hึ%5พ|ฟG"J@5๕“ˆ๊8B"JiQqพI}’ป๖H๗SกตqีฆNlศ'gBส˜ชไ7ชฟ,cส9ะดผT…•'๋จ"สYj\QCx]พr]XL1 ร์8XH1 รl#ฑˆ…ฤk|5uภ ๐_ ฟ๙O6P;Š1ฐ…E˜ีIธJ€€ฌm‰ซ๊้ฐli2™ออ"šป ป:[ษลaๅ–/๎ !$ผม šกZ†ฉฉ•ฯYุJf้6์ส8‚๋gasณTม&tAจ–ก8ภ~˜ฺ๏ผ$ชƒ2l~fm€ุ_Šฐ็"<\P†ฌmฅเ๓xBaUšศ7wมๅŸมฌN%ฆ>๗}ข.,Ceบก{ž„๎<YืF๗VภU‹pa‰ค‡Xx<ฤ: ฟ"DR*YOไ3pQ.(“ค-.ึ@(gHะ ํoHa‘ส@( ็ \Xฅ ตrŽD ย_S ร0;R ร0‹จฏAjxฝOรz บ็$U=N}เฅ4*ฏ็ญอ!Zธป:gN…|-yg!;ฏYจึจถ@ช8ดZภU๓0+ใˆf.ย,^‡Hิฦ•Q)kศKั๗ +ฐล%˜• :`๋œต›๎ฬุRถฐฏ๗ศฆพ8ุ.1U-!š๚ีO?ช‚>—’ฎ4ผŽฃะ}OCึตSถWP5T\ลCSฺxฏ๘HฤD$—R๕~ dข–ฎฟเ‚2ตฦๆ็แ*kดFส‡ฌiฆ*5/X˜.(ภ–ฒ$่…bก๛จ๋R-"šฝ๔7ี๏“9}6หW„aๆ1l;YH1 รl,ข๐ก”j€?๔Mx#ฏBท„๐kปๅ…U—อ^D8uฎZL•/fฏ…๒แ„Ls’ฺ%;C๗‰ธสฃ8Kํ–^’Zศย*lify 6ฟU฿xษx๊ณู˜์ „าPวก๊;พ0‘ฯU๒n แ๕ณ€s$[ภ|ฒพฺ^MtGD•s,ขถB€XK๙’u€Ÿ†๔R”Ÿg#ธ [ฮRฅTชŒspA+TIUฮฮททR†t‚Z_ฅฆ`์  „eชพ‰สเฌ๒ญ_3‘สPีT"กSภ๚=โ่(ณซUh‹จ-‡ลร0ฬcx"ฒb†ู<ฒgN 8็F!ี๓ีธŸง„Hิ@ึถQ–ิะKP=”#ฅ|8XภDิฎ'$UแH VฉฌดŒh๚cDณ—`๓๓ยอlม:)’†^ ๐หะ]ว!“ ฑ”Š…ˆM9,,#ธ๕6dฒžฆ็ฑุ6ษ8 >ฯฯCึตQ–Wบ๑ฎ‰|wํ๙ฌญจ[X† J’k{ˆืรKBึwฦ-|wW‚: -/.รW`"ณฐ˜b†ูฦ )†a˜G‡Eิƒ>}$„—„ฌm…๎9 o๐Eจฦ>jUYQAถธ ››ก|kกš๚ก2]u€าthซๆaKYDS!š๙ฎด๚ฅ!ฮฬCH!เ‚2t๛!่พgh_*CdO๖Š…œƒ หฐล˜ี ภY:ls๐๒ถฏ›ญไอ|‚ไS ฒถ-ฎภนซิ0n3หcpA™sฝถ๋~‚ )เผฆ‰BฯeปgโŠOVโถฺุ<œ YLm#.จ„ม•๘_k~๗GฦWƒaf‹žŒ,ค†až์™S็Q๗๛ิาƒจkฅ๓มกš๚7D@Qถด ป2pโCjหห/Bึ4Qpv็Q่Ž#um4L’˜ฒลธย"ยษsˆf/Q•ลิ#œ…ิpBBึwภ๋}๊3๋DยฐD™6a…ยๆ๔ฦ41”yณ6KmdฮR๕šs\1ต ยรdgเ*yx}ฯ@6๖าšA_๋wคGqถฐ,ฆถl]X ‘ฌ…จ๏€๐RqvWH๗J~.ฮสP๕ก—‚ะ$})hพ@ƒ ‹$๔น v[p&Bp๙็ แีณฬ้7ฬW„af“Ÿ,ค†a•3ฏŽ งOCขฏฦW=qdm+ผoภZQ™รฒ‹ช”?”Bxใ-7฿‚+ๅ๐๙สY ีq^๗I่ฎ้ dบ *SK0หcˆฦ฿‡YพM!ฮ,ฆ่๐์ฌn„๎>I๋”n˜ะๆยj<9oั๘๛จ^๚GจๆAxฯมz™2ฃ4„Ÿข๏Uษo„›C ฟ€`๑ฑ๋f‹+ฐี<ผฎใ™žธŠ pQ™ึ ธ ี2D๕bqธq๐ฎ`๓ ฐ…@jมๆp3ึ„!ฉjP>ez…eช๐,ฦ-“Jร…U’Rฉจฅuด.,ัŸชpี"x‰ถW-ข๐ืฃใBุS™ำgว๘Š0 รlา“’…ร0ฬฃ“=s*caOณ˜บ7ฒฆเ‹ะฯB5๕Cึด@x & ฟ๙ฯอ ธ๑&‚๋gแ*ภF_=3ิFึ๓ผž“q@p ‰ฉ‚€ฃ™O~ถฐH c๏™{บ‚ Tหtฯ“Pอƒ5M^,–ข*\ifeแ๘{ฎ \em#฿ ฮาz๔?ภ+4ฒ^'bแ(แJY˜ๅ[0k๓ฺ'1%5็mถฐ Vจrฐก3^3ช64K7\{แญ_ยW 4’O+๘ ฒฎP๚๎w›—VaK+๗สžb`—ญ ’๕•ƒT๑‡วะ…•ธฺ้ณืุYCRสฏ…HึBz๔ูมE ,ำ็f9วีR[นX-ข๒มy฿ฬŸ2งq–ฏร0ฬ&>*YH1 รlbJ่ฯW้ แW {Nnˆ(๘)ศXDญมๆfN]@pๅgpฅUธ่>วอ  ีุีv^฿3ะ=OB$j คGbส†ฐk๓o แ๘{:๙ตขk_ธ*yศ†.x}ฯPv]+เง!„ค–ขrfeัฤn ฎฐt๏uZSฝOQKf฿ณ ]€๖)'G(ุ๒*์๒ฬ๊„Ÿฆเs8–~wม–`+y่ฮcP๕@"ฝqo™ล›ฎŸE4๑‰ษ)’*ำฤฟ๏ภซฉฬFnQlCH˜—a๓‹\อ๖ภKฃจฒ)Q ๘iศuฉkชโ,,ภ…eศ๛x๏ j•Mิ‘T๗S4=ั„ฐีMใ+็เชˆฯศEๆ‘?ซE„ท฿๛4ธ๘Wส"Šaf‹™,ค†a6Ÿr.’๕๐ผ uœZพ๊ZฉeE$ขึผiDำ#ผ๕6la ฎšฟ+ุ๗A~˜„๐S ]ะmก๛ž+ฆjฑส kaV'\? 3wๅฎ€็ taฒฆ‰Z๓Z†ฉบ&Yv6‚ซฌมฌN!š<‡pุตYธ ‡”ีšPภน๎}^๏ำTqีุ x‰๘Sซ“ˆ–nCฆ๊)เ™ืไ~๐ิšWสAw‚ฌk#aหCณxมอทMž‡อฯมๅ/_/!กปŽ!๑ฤภ๋{Ž๎qW๐นตฐๅ,eL•VYL}-’DTฒPปTp&"มWZ‚R|yอ.ปข็dm\ํ™„ฺจ0uQ…„~Pไเ๓G\dล0 ณ}ปR ร0[วS"Y๘eจŽ#P-ƒ”U“จฝ#ขึวฬ'ˆ&ฯมๆfaหYภšM๘๑"Yีุqบ9่๖รฑ˜`"8ภฎอ"ธ3D๓Wฉโรzบ(€๐ำะO@ท€ฬtSk‘๔เœกสต์4ยษM‡ษNWๅ=฿‰4t๏ำะGก;’ ๔า๔:œ+ญภฎอ#šฟ™j€H7ว•ผ?๙<ถฐ[ฮCต B5tR&ั]Qแญทอ| “›…ซ่พ๒†^Bโ‰?€๎:N•„w‰)pฅ,liฎ’g1uฏ๗นŸ‚H5A๘I@%(ฐ’ˆrๅ\9K"๋d๎Wทึ$!“qลT,Ÿœ ใlฉ<\~ฮTYL=่๕ญ\k!ผ๚๗ฤ"Šaf›ž ,ค†aถž์™SO:'ฯ@โตฝ๗$ฉฬ†Rญ ๋;โ๊‹จp™B'โujŽƒๅ•Or((ยฎอ!š8‡h๚c˜ี ุา*`ยG>ฐหฺๆxชbT๛aจฦ^’…ฮRๆW) W\B4{‘™—ไ๕๚บ˜์d]_um$yM@™^ท‰h๊lv ถฒ๖H‚ื?๚$N>T๋P|c1ๅ\X‚+Rพ” +๛^L9€L7าT ไ5ช Jิmด๒นสLnf๑ข™Oaณ“ Vณšเ Jส‡l๊ƒn?ี2Dำ๏โ)l+ฐ…E˜ล'ฯม,\‡ษMoYๅฺกPm๏ภ+P}๑๋j!1%W)ภๆ็c` ‹q%๖‹๗ŠD‚ ก{ ๋ปํQfuัไ9DณŸฦ๗ีา– แ%‘|แu๘#ง(|๎เsk(_ชดJำ้ทท†5สงชB/Imsส‡s.ฎผฬรWเย Vุฆ๗ซ32UฤA๊ยOCHMb*,SตVต[ฮBžj้L„เ๒ฯYD1 ร์„- )†a˜วG๖ฬซฏ;งG!ัฟ^ฏL7Bต€j? q„๒€Rw‰จ Hy@ณaฎ!šฟ›_xl"๊ ฏฟก^stภn์ฅรeไlKqศ๖์E3kณqxฐฟ๛\ี"U#ีwPฐ|Aศ8XUa K0หc”ต2†hสึ‹จฯฃผXLฝ ี<6หย@ศธตršฺ*k๑๋ท{ท•O(ธ๒œฉ@5๖Cfบํ“ˆสฮ š˜Z^ฎั}ต]/+•A๚ๅ๏มx"UOำื1UL•V` Kwๅํ€zg-„Twฆๆฅ›่3Žฺ๒‚ณZ ๖_ฉำ๋t้ Ul%(RR–U\-…jถœทํ–.จ„แ7C๚ท~๘yย0 ณถ>,ค†a?;]Lษt#d๓t๋t๗ จๆAˆdUm8Gํo๙E˜๙หˆฎSฅัฺC„`o๕SB…Uหผ็เ ฟีะh?žZ%a‹K0+ˆ&ฯมdงHา์’vpaฒถ^๗IจŽ#wrf„ UZ…]G4{ัโu˜ูKpQ๕๑.‹—„l่‚7 t๋0T็1j…๒RT1U-ยฎอยฎัิ8‘ฌง@๚=ำส'aหซpQบe2~O"ขh๖2ษจนKt_=–—จ šz๑฿Bw๘ยD>g" ๏..Q%ฮ†4ฝbŠBฤqง๊(xW]ฎธ˜ถผภ>6๕ลJั‚DD"‡ิS๘ฝซ่s ธ๖MN› *aดpๅ'ีw๒๛™ำgณผ๋`†ู!,R ร0;‡•3ฏŽ งOCขaG“ำ ]Pํ‡แ๕œ„l€Lg่ฐ์UDๅฉๅห,\C8y67Cู);๚้'็ {Ÿ‚ืt๓P ”ร"่PiKห0ซ“oพ ››‰ม{ศ ฮYxว ปOP^Vข.;ย•VaV'อ_…™ฟ‚h๖"URํ'ฃILีถย;๐ Mๅ๋:‹'M๊ฐ ››Yพg T}็–f๔l๑‚ั๛ฌธ–๏dฐ้œ )\~j\๕1lnv‡ผอ|่g‘|ๆโaฉ;bส9bP\อ/Pฺ๋.ฮZภ9’Pฉ๚Š#„UุJˆชpฅlฦงv์๏ฐ1`"]\ฆEย<\ดw'๒ฑˆb†ูแป!R ร0;‹์™S {๚qŠ)‘j€ฌk‡๎< ฏ็)ศๆ~jSั ๎ Šp๙ลธี๋*ขษ`Vฦwฅ~ ภ๓Pญเ๕=Y฿ แงโรต+.รไf\{6;M‚D'ฐ3ฺ’$,„sP-ร๐๚Ÿ%ฉ‘ฌง๐kยUr0ู™8#‹ฆบJ~็.‰๒ Sศ:สา]วกปžˆฬŽฤ[Xฆฉs—!”Y฿พ~๏9ฐ…EุjบใMิ4อๆ็iญฆฮ#œ8ป6ป3 /่o!qฟ‹Ešปƒฯ]Xฅjฉย"I›Pอๆโ p/Q ™ฌt’~ญฐJSใ฿‹&้+™Z“ฺ๋ใะ๓KBlTTบJ6?O๋บGชYD1 ร์’8 )†a˜ษ†˜๚฿o›œIค)ผ๓ผ็!›๚iคy\…ฐ [\†Yบh ตต-ํง!ต{#ง 2ะ]วจฒภฏูถฅุตyWN-nŸisn1%แL8Cํ‡ฯ“ิH5™pX-ภๆfa–n"šพ€h๚cšฤถ[P2ีีิu‚ฆ9vฅ–)ภธฐ ณ:‰h๖„—ข5;ตELP๘u~ถฒFฟSห „๔เœอ/ยฎŒ!แํwhภnธuา$ŸC๘ฟY( 1e l5Oฟs9ทƒe‡ธ๓XำLUQ^ B(8Sฅ๖ผฐW\†‹*;ง5๏Aอ๚Dพšˆd- $ิœ…‹ชฑ˜ZŸธ{'๒นjัโ๕‡Eร0ฬ.ู‚ณb†ููdฯœpฮBช?*#tขฆบใ0‘oA6๖R8ฎNppae#[ษฬ]Bx๛]˜ี‰ฝwฑฅ‚j่„w๐ืก๊ใ0๐v ูถle .ฟ[XDpํŸแยJ2ถฝgWอC5 ภf,Ÿกi_ฐ@P‚Y›‡Yบ…h๒CD“็aK+ปxM4dM3Tt,ฅT๋j่0]-ภfงจb*QYืถƒไ‡€ณ–Zฃชy่ฎ' š6ึห•ฒฐksฎฝเฺwMฌeหิะ…ไ3o๐Ej๋•๋ีCิฺ๋*ุx"฿ฮฉ,"I(€XD5P›žˆ\M%ุโ2\XปB3!DขžขมKาKฅkเ‚๒iๅๅr\ตˆp๊ม๙Ÿ|7s๚์๏†av,ค†av ›/ฆ„๒ าะ]วแผFQ5MYI.ŒCฐณSˆฆฮ#ธ๕Kุ์ิฟุสƒj์ƒไ7กบ Z†H๚๘iSลeุ์1eถบ%IPๅ“ษ:x฿€jZUhrส8ย[o#{oืส{_Amค]วก{ž„nl์‰CฆM+ญภไfaฎวZ้วXอB•Zfu.(ฦQCด^ฐี5ุ"ยซ„เ๚Yุโ๒ฮฯ^ปtวa$žCx}ฯศฏฟ‹ย8ทจDแ๔Q๙ฑ็9Aค2ต-‰ก)+(Q๐w9K!๚b๏}ฤ9k!S ษ:ภKA๚ฑ˜2lXชล;ำ-wp‹(†a˜]พฝc!ล0 ณปศž9๕คs๒ $^{่o"dฒบ็)x^j„L7วกท๔ท่ฎœƒออ œ๘มท`sำ{โภ@I/ ูิไ5จ–a ^Š2ฆฌู@feแญ_ลืoณฏmWตญะฝOAต ๔๚ดฏ(€+็`ฒSˆ&>D๕สOแJ9์ึษf๗uEฺ๊ {Ÿ†?๔U๓ี4“,\>_›ƒอ/ึฤ•Hnี๋aๅหฐ๙E่ฎใTั‹จ๕ื\{มๅŸ๎๎๊ตฏ@w?ฤ๑฿…๎:‘lุ˜bนัW-ภญอรูp›ลUฌ ?Y็*ลCย2l%T๓$ฅฐ๗?๏œU…y)^๔™ใช (ร–spีŽสฬbล0 ณG๖ฺ,ค†av'ู3/ŸŒ IDATrฮ}P1%’๕๐๚ž72TหP\e!6;p}ืyใ`ฟฏ–~ชํ ผม Aต าฃCถ5ฐฅ˜…kˆๆฎภฌN@ Uะ”!ฺ๋แ๕=M"ชฎmcช™3\e 67ƒh๚*็+ ทต…๐๑"๋;่ฝ|เสะ๒k(๗K >ทล%z*ช$j1e KฐฅU +oŒ๏-‡6วฎฦ๗ี>^฿ณ๐:t็qj‹‹ร้]ย…8P{!žฆธตkใฌƒLีCิถQธ๗zuก จjซ’lดง๗อ}D๙ dmDฒยฏขˆ+z.๛ไDQ ร0{lอBŠafwsฟbJคเ๕= oเPอƒTeใื|VDๅfM_@pๅ็ใภมฯ BiศDTผ็ :C5๕RB8 [ฮ"š๙ัิ˜ ค—คš>tจ จ๛$THœcU !–W๒ฐนiDำŸ ธ๒3ุrว~[+j‹“™nzoฟ ีิแ%II W-ยฌNยๆ็)๐Kp›สg๓‹V~ฒฑ๗ฎ,ขfuแ7๚%‰์ป๛ส9ะฏAต„Hg คGนEa ถธBูEAiKฺใึซ€dM3ๅ&y้X๊†ฐฅธต9ธ(ุ˜>ท๎ฃ๘:™Bz๕ํ$ฅผไฦDQq^[ฑTถนjแํ๗> .ีŸfNโ,?†a๖ศNŽ…ร0ฬ {ๆีืำฃ่ฬ}ช^s$7Z†!๋จ’DjQ•5ุตyD3Ÿ ธ๖Fพ\W•6๔เ๔R฿:LYF'ก{6ไˆsฎธ‚p๒ข‰`หk^ ฮ†_่ชไ!4t฿ณะํ‡ :ฉI}vr^4{แท`ฒ“ผVwก{ก๛ž…7๔U'yiฉฉbชš‡ษNรfg(ศ?‘„~ฤ jณ6WษBท‚ฬ๔ฤ“)›Z9)ฯหๆฆ)?W*‰#฿ฆ‰|อ‰๚;นEA*๔yTษmŠ0t.nปญmกV4ๅวR7ข บ•q’๎bฝ:‹๗ฤ@,ฆt2SP~\Mๆเย*\Xขฯขตyบ—ถฐฒEร0ฬ฿Wณb†ู[ฌ‹)‘ชํ๗_€๎<ี"ฒฎย่›H6tSปž”pA™‚ตซDำ็k ’ P™Nุโ*U่x)ศฦ^จLตRšvmŽฒยฆ.4,,qฅแ#เ๕=Cbช0DMๅYKaฺAฎœงjNg!ผ\X…P>Dบ๐๑”ล4K•<ฅWึโ๛g๓ƒ์๗๏วœ"๙—จฅJ4ค๛( ่บGUชไผ‰|,ข†a๖๙#……ร0ฬ!๗ฟ}gภ†ๅQH๕ว|5ถ๒้*!แฝo๘e่ž'!5สงV็(ซ+ž่ๅLWฮย,D8}fแฬโuQ[€l่„7๐x)ชd‹ซืเจ๊ฦ,฿B4uมต7`–o๓ๅฎ๛จถ‰ใฟo่%สําษฯ>ฐ&7ป6O๕pœ๕p&„๐า4ะม‹'๒ญท\%jo{/Wน๘7g2๖/ืๅ{?๘ั๋NˆQ ๔เ[็_๐,ฟ[†a๎ฺฑb†!!ตqพ.ภฺำ?“๏์›c๖ฬหงœ๓GYLmยƒ5Yีะีq^฿ณPM”ทข}š๎–)Hู8๓&š,QัโM˜๙ซวYผVมีR[ถZ$›๚เ๕?o๘จ๚vเ3ฬ4-ัWŽฟp์]ุ์lv†*ฉ˜ญCiศtT๓ ‘ืเ ฝ‘จ๛‚ฒๅl~ฎšาเsf{pฦP _ชžฺ๘’uสหมš3ฎZ<#R ูpไ‡qJZ7*คุxฆฒb†นวNŒ…ร0ฬg…ิ]‹ตv”ลs_T? Y ี~Pf”n„ะ 8W ไ(zeถธ yฒฑชyถgเT 0+ใˆๆฎ ผ๕6ฬโ พภ›บX"Q Yื‘Wกšh2@aุ&ค*ฉ) €3\aแํwN|ณ:Iแอ< `s‘2YOขฐ๗ixฯA6๕Q๋คณ€ กฅใŠCภ…UZทjvmžึ˜ณฃ. M^ศyร/žQ}gDบq฿ŠจuXH1 รcKฦBŠaๆK…ิ:๛PLฝ๚บsz๎๘สว(„๖!j[ Z†เœ‚j‚ฌi‚ะI88 (ร—`–วอ|Šp}ุ ป๋; S;Rm dS?ltฐ.ฎภฎN"šป„เๆฐู)พได\ยOAึถAw‡?๒T๛ajั`หซฐซSˆf/ย,„ฌkƒ๎~บ๛ษะ์๕ #[XD8๖ข‰s0หทaหYภZp5ฃฌ‰Bีะ๗,ผก—จยPyp6‚+ea–วxเ <q”Za•Gํฏa.,มU๓pฅ,ฏวcภ•ฐr๑ฏs๋dNŸ"๊{?๘ŸŸ„3g๎%ขึa!ล0 sG? )†a˜ฏRดษ†๛oกtงฯพฑr]XL}้ใ“๒‡’ $ขT๋dm3e@ภEุา*์ส8ย้ ˆฦ…YฝทPRฝะ]วแ พQ U฿‘ฌง๏W|˜ี D3Ÿ ผ๙6lq๙ฮ๋เ๗‰€จiืผ‘ืHd่[อร–‚‹ทq}Eชบuช๓(tื šส—จ'‰Uฉšญฐ„๐๖/N ณt ฎœใuy˜%๒’ต-ะฯร?๐T๓ „WV๒tM~„hๆภั„7WอC๗<oเจึaศd=ต[juA‘r–ซฅถW-"œ:Fp'ร~Q฿๓ัง๕(€ฏยBŠaๆฯR ร0๗'คึฑ9RvtŸ‰ฉำฑ˜jเ'ง„๐ำPญเ๚5่Ž#ตm^ŠD…เส9ุ์ย้O^? ณ2q_‚Bต A๗ณ%ขพ›9}v_<DDญรBŠaๆ;5R ร0&คึูob*{ๆTฦยžNŸทbJyะmแ<uฒฎยฏ”L[-ภฎอ!š:เสฯaV'๛€“๒ค‚j=ฏ็I่พg ๛ R Rpa f้ข™‹ว฿…™ปสแฺ_ถษIeเ |‰#฿†j;HQBา8๚โ2‚ko ๚้฿ยๆp?ยP$๋ {ž„?๘"T๋0Dบ 2ู(Mm|๙ุข้ฉํo๑\eโหฏ(ผแo"๙๔ฟŽ3ผR€ ‰mvmแท„—ข‰m_๛ํtื1x}ฯBึwาภ!แLWษรEธโ \Xโเ๓M`?ŠจืGG3‰HŸมˆจuXH1 รใัอBŠaๆแ„8‹œ‘8cฅๆ์๋ง๖EIฤSบใ0ผแ—ก;B6tำ๔(ฉแœ‚l~ั์ET/=lv’‚ฎ๖gฉฦ>่งแ๕> ี2HU9ฮัฯŒ˜ล๋'>Dp,l~๑มๅื^]ญd=ผมเ5จถˆD „P$:๒๓oม•ŸมdงแA๋Dฒžยถผ ีz€ฆ%&๋ ”Objm6;…hๆ"ขนKิสW-๐ยฌฃ4ผgx๊)#*Y!Mก,.#ธ/oฟCืฺO?ะ๚ธ(ฌ…๐5่ž'I๋ฤฦืœ (_*ฟgS๓ง"JW๔i%๑ะฯ=R ร0๗ุSฑb†yx!ตฑA฿Ÿbjภ97 ฉx/žชeะKะวก{ R Y†‹Eิข…๋.LvŠฤร=ฺพ Iํfฒฑ^๏3ะ]' Z†่n œ ลa้ ผ~ถ”#Œ๖฿๓]$๋แ๕=ะฏ‘(JึCH ฤถksวAxใM˜ี)ธฐศ๋$k[(รh๐๙8_ช๐SŸSf๙6UKอ]Yบ V๖๏ฆำKBw‡์ทกAค›๎–W๒oแํ_ยYKษ=๚ธJ"•?๒t็1ˆšๆ๘{ฺ;๙_•5ุย" `ฮ˜๚๚kZ-"ผงมลฟ๙=Q )†a˜{์ XH1 ร<บฺุฐณ˜ฺ3จฦ>xƒ/@wƒl๊ƒL7Qห”` ‹0‹7xf้li0แึผฉกบ ›๚(วจํ ‰)/ุ.ฌ6B8๕ข‰zฎ”[W๎‹{ุ?๘-๘#งhญjZ t‚DTnแ๘๛ว…]‡ญๆใชจM\žšf่็เ <~"QO9HRมeุฬย5D3Ÿ šฟณ2ถu๏•ธู๔RPm#๐GNAwวญฎ^’หซE„ท฿Ap,ตฅ ฑyB$ฆdฆศซPญ#ิช}ภZธ [\!9–มA๔_r 7Dิ_iๆ๔/๖P๙w?ัiXฑiู‰,ค†a๎ฑG`!ล0 ณyBj๋0.…้w๓ว๛ๅ’˜’?†ฤkป๙๗ ๐_„n?ี2YำBU/paถธณxแํw`ฏร–ถฏ๊Eiจฦ>จ–axฯB5๕C6๖‘(s.(ภU T15qแญ_ฦญb{๔Yฏ<๘C฿„7๔"Tห0d}U'9›F8๑ข‰a–วเสน-ฯฺR}ะ='ก{Ÿฆ  Tf#D…eุย2ขนKˆฆ. švmnOWฒ /ี:L๗S๗ ‰:8g Œpโ}„ท~ ณ6‘จsาถHช่ŽCะ฿€jุhน…ตฐAลx"ŸณEิ๗~๐ฃืฃ›;]–…ร0ฬ=๖ ,ค†a6_Hญณ?ลิหงœ๓Gw•˜ฒฆ™ฦวทP๛U]+– Dli…&MžC4w6?๗๘ฆ)ชiบผ‘W!k[Iฦฌgๅ”s0ks0ณŸ"šฝŒp}ธj~๏l^5”ญี๗,t๛!ศLฯต6‡h๒ย‰s”T\†ะwu้'tชuชใtื ่ึˆšธ5อ„@X†ญฌ‘”š๘แิ๙=|.t‚&F๖?Gmฆฝ้ Iำuaz๛ุ์ t<๕p{๖ฃฮZ่ฮฃ๐ึ๘5€Œƒฯซ…๘^_ื๙XDmฎˆZ‡…ร0ฬ=๖ ,ค†aถNHญรbjง>Dช^฿s$ขฺCึทC$โๅ๘pjWวM‚h๖S˜์4\9ท3^พ๒กZ‡กปNภ?๘-ˆšfศT <ฎœCดxั๔ว0KทM}”v๏ฆ%YyŒ*:ŽB5๕Qkžณ*?๕ขษ`–nยๆ(XqพึŽ#P-ระGกš)วH๙qฐv\อ6yแ๘๛ˆ&ฮํ‰i‰ชe^฿3ะ' š‡ jš!เช˜…ว฿ƒYเ[ ธ‹ *ร~•&๒5tฦแ้‚พfธ ป6U๖M๐นซ\k!ผ๚๗ดŸDิŸ๐/N)ธ3€8น•?‡…ร0ฬ=๖K,ค†aถ^HลฟXkG'฿ู7›า์™W_wNBnอ฿:?ำO@ฆก:Bท@w€l่ŠCฐU<&>ณ2Ij*ฬ๒mุย2vV๛› )ชm„ฝ|›2ŒtPŠชR*kˆฆ?A8๙!lvัๅ]%ฆD*vบ๋8tืq’;~zc*[8๕ข้O`ฎมไfvT>“L7Bw?ี2 ี~*ำY (ยตKซpA ั๔„ท฿E8ฎผฯUct๗ ่๎“ะG jš)j fe แd\ฑUwLKœซไ!ผผƒง่3 ถ5ฮ‰ด6a…&๒ญอรมํูเ๓;"๊,s๚อ๏—gำŸ๐/NI๋F…๒—&,ค†a๎ฑวc!ล0 ณญBjSSnดCตคvขๆ~ส๚‘šG5Oาf๖"ข๙ซ0 ื` K€ณ;h้Au7๐ ๘‡B'!”H8 [ZฅŠœฉ๓ฐ+c0หใqีฮ ศt#TหTวQ่๎' [†DD8๑!ขษsปโพ–๕ะGHD๕œ$ู&V`Vจฒะ,„ Š;๖wp•vบ๗)ˆdd*Cm–6‚-็`ณ3'ง฿qๆใ|gฎฉถ่ฎ'เ |ฒฎP0ฬ๊$ฬ%˜๙kฐ•ฮ—น1ฎR€j=@ฺัDพ8๘…eธฤ”-ฎBHฑk?ƒ‰\๙พQ฿๓ัฃๅ ๙ใ็ณb†นว–‚…ร0ฬcR๋๐Ÿ#eGฯพฑpฝณgNe,์iแ๔้ญS"QYื~บ็Iจึ49ฯKฤ“้Jp…ED ืaๆฏ"ถธด๛Dิsศบ6xฯB๗< y,ฎHPVQP†-ญPธ๖๔Ds—a๓ ธ; {_qฒชก ช๋8ๅzตC$๋hJซ0K7ฉ๕p๊#˜…๋ปtYญM*C-nวแ ฟแื@x)SฮมUrH?๑>ฬโ ˜…๋5‹^ป„L5@6PnูW ๋;)ฐุตY’นณ—Hๆ๎าj"Ui`ภภ M‰:j5lP‚\e ถผกvืD>Tย๐ฦ![?๛ๅ๙?pZ๘ใว๙:XH1 รckมBŠaๆ๑ ฉuXLmโฮKBึถAต€ื๗,TตyIช8 +ฐล%˜ฅ[ˆฎลSฟfฐSืีzเ ะํGจEฑฆ™ฤTT…+็`‹+0ณ)‹iๆำmŸ๚Fาฐบใ(ผกกฺQๅ,ฬ๒8ขูOŽฝป{Eิ~iHMSิ๒ๆx…ชูคŽ,l9‹hโ„“ม,‚อNm—‰ZจLtืqx^…j €vXธ"ฬส8ยษsฐนูฝำึ&$T๛!xฯCeบ(๘|#c.OŸ!ฅธ ธใƒฯ]P ฃ…+?ฉพ๛—฿ฯœ>›ฯ—"ขึa!ล0 sG- )†a˜#คึ!1ๅŸ>๛๚ฉ}qpศž95เœ…T|pส‡จi‚j L˜ถCuญ€Ÿ†ˆรŠmqfy fแยฟŠง~ํqค†๎<o่E่ถƒเ\Bb*ฌภ—aึๆ`ฎ!šั์Eธฐผ•[?Eญ_Gเœ‚n?LQ‰ฒ ขฉ๓nผต๋Z๓่:x ˆtt๛!่งแฟ ‘จฅPp็เlW^C8๖.ขฉ`oยi}ถr'„Ÿ&Yุu๐หะํG่= WสยfงŽฟ‡p๊ƒ"Tห0ผกoB5Bค๊ฉ]ฯธ  I„—s2‘ฯU‹งฮฟœษw3งฯŽํ—u๘~๔บbT;ตฒb†นว๎ƒ…ร0ฬ๎R'tต7 IDAT๋X‡q)ฤ่Oฟ๛›?/๋๓E1% :แผง!3wFดรQ๋Y~‘*ขฎฟ‰h๎\PฺผŒาะG ๛ ๛žn;Q !$เ@UAมีฆ)wใ๏Q(๗}\Oj< ๐ฏSE”_!œ5pๅย[oฃz๑`W'(ฏผF_ผˆบ็$ผ็ก;Cฆ3w‚้MWX†-,!šฟ 3wัฬ'ฐลๅ๚บ๋'~*ขาRื•s”ใuํŸแชลธ5ืhgBxวก๛žj์#‰'Iผx‚‰ฉ-ศืbต๓Eิ:,ค†a๎ฑลa!ล0 ณป„ิ:ึaผ)•/ใk?/๋”ู{๓๘บช๓P๛ูำ4อ๓dู–-’lใl,6† ‰Œ N.%้Fํmo“@sO’ๆถ๗~UšฆIšถHฐ€<ฒe[’5ฯณt$s๖ฐพ?ถl“ข$<ฮz~?ƒฮฐืz๗็๏zW†[•ค‚zฃ|m‰Qฒ5ญu๖มQaทa๙` ัฆุ}8แqฐm๙}>I๎Eฯ_Š(ภ(]ƒ–YŽš”y๖ม›ูสHใ3X]‡ฑzŽ๒ป*ฅo"za ž…ตn#l*Š๎qEิิ0fkDO>=า†0รเุ2อภ(ฌฦ(]^P…โKF๑ฅบMถmห]f9ํJฉ็ตcข–ฝ๐\…aRถ#แ "!ฌฮƒ˜-/แDBณ’dŒ~'ชŽžW‰^ธ-ญลH8WžDXaฤ๔่‘/^Eิ฿~๋ทŽ„&๋cIDA )‰D"™#๗”BJ"‘HbSHd$x๙๛jฺ็ฅ'ื้ช๒xผฤหlฺฆ&็1|% ชปs๔(ฮHัS/œk๖,+n^rเMD/จFKอC/]ƒ–QŠš nŸœH™"r๔ ฌž์มฆ฿zฟQถฯยMn๏ฃฤ ทถc#Bƒ˜0[^ฤjADBry‰แC/ZQr•lŸ๊๖า=`8SƒุรํX}ว>`}ˆ่๔™wƒชขฅใฉŠQฐ5%็lSrv—ๆ5๏+‚ขhว”“~+ BเYฐฝฐ 59ว•…ณป{ +‘ฮDย1฿–˜ŠWeNmw@๐{>ฒqdl<6ว …”D"‘ผ9ง‘BJ"‘HbWHe'๙^_CiZฆm๏zถฉ'xKeq$ผ"2ตอ™ญwFSอึWฑzํืฯ๖9’ผฃ$ม—ŒQ|Zๆ<๔ยjิิ\WL1+ฆฆ†qฆFˆžุŽ5p5!อ…,g!jrฎ+ข„ใ.Ÿ์>„ู๚*๖`3bf}8—ผณ๘๘…U่ลซะ๓– &คกxฮ-›ลlvซู๚ŽcตžmศmUป†ฮTZฝว0[^ฤiG๑&ษ ~ป๗ฅ๐$JBš+ฆr+ฯ๎b ฎด‘ป#_h๕ผvไ‘)ฬึืขวB ๎ฅธนว๏xๅ๕ฺ๕+ช‚รุุ30ศ๗|”ก‘ั˜‹R‰D2G.#…”D"‘ฤฎสK๖๓ฟฏฏฆ$DฤถyๆT฿{ํฤ.วq‚;๎ุ‰๏ฬฮ ˜=G๊œ๑ก:H•g๓…GMHC/]ƒžปุSพd* ธฅfฦA8วAM€๎!ŠจฎรX๛ฐ[SC๎๒<ษฬไิ”\๔ผฅ่…Uฎ˜Jฬœญฬg›ฦ‹H่lฯ/ล“เJ’ู๊Bซๆ้—qF:(ณฝŽd~๘NแIด๔RŒฒตh9 Qท*J8๎†ก!„่๔๏œ๏xQw๕อj„]‘›7o\ฟชฏว wpˆ๔Q๚‡FbrLRHI$ษiŒR‰DปBช05ฟปฎšโิDย–อำงบ๘๗ืO๙๓ฎจ๊lนmk[<ฤpฌพ6เเิ)BฟขทŽYT51mฌ]P…^Tb๘ฯ.IrปŸยม™ร๊iภ<ฝ{เฮdฟQ;กำ<จi…่9‹ะ –ฃe/t—โ้>(ช&fCeEฐ›0[_ล8((ช†Qven๑Jดฌ๙จdPup,œ่”›-2้6ขW4๗=‘VวธQŸRฐT่zธเ[ฎcใ๊•x=}Cร๛C?งw`(&ว&…”D"‘ฬ‘ฟH!%‘H$ฑ+คJIฝi9…ณB๊‰ืพ฿๎็ใ๎ณ4'ObJQช.ฯ์ 5จจzษUxnBฯYˆโK=':P‡์‰>ฬ๎ฃ˜M;ฑGปp&๛])"น๘!๒$ ฅ—ขฏpำงฃx’]ฯโVN9Cญฬ์}1=‚โMFสจ‹‹ŒœE่%Wก ฯํศwฆŠmf13AไะใGฬๆํ_Œgu†[nจฅvํ*|ร#วรฟ ซo &ว(…”D"‘ฬ‘ทH!%‘H$ฑ+คๆฅ'๓•ฺๅค$0cู๊x๗hž๓ตq(ฆJ…A)ฆ.PยเMFห,Cฯ[‚^X5[้‘2[% 8+>Dt {  ณ๓ V็~œ‰œ™ัณหฦ$5) -งฃhz๑*ิ”lU”Yq๘1%Dd ซ๋แƒ‚ชนั%‹žทฃจ5%wvG>aGลิpšš7Tl S* _œ๋๏๏ด๋ฏ^ƒฯ๋apd”๘ูct๔๔ลไXฅ’H$’9๒K)ค$‰$v…ิŒพRปŒผไfL‹_๋เ'‡Z~›„๘vT๓wnซ‹‡ุฮŠฉzTํyฆฟDม›Œ–Vˆ–[‰QT–]โOq{DอŒcฦ™่C๑๘ัาKQ3หPอSŽ™tลTว>ฌฮC8ใr๙ลˆSB=ณฝ`9zัJดดขsป†'ำฃn฿(_สฌtzร๚=gv™eวข-/‚•z‘qBC(šงr+zAuปšœT<‰๗ฦห๘ทƒ=ฌืi*ฟw‰๕Mฏa๓๚ต๘ฝ^†Fว๘ฯGงญซ'&ว,…”D"‘ฬ‘ฟH!%‘H$ฑ+ค*ฒR๙าฦeไ&๙™6-ihใกรญ๐}ยaVฉwTO}ˆฉ๕ตBx‚จl”gy$$ิ@zNza z๎โูFๆธ"jฐซ๏8V็ฌพFิ„tด Œฒu่น‹Q…(š‡3;ธ‰ฐ๛ณ}/VO๖H›ฌ–บ qJFK/v+nสึขe”ขxฯ๎nh6a๕ล๊kD๑$bVใYผ5! ดูส)7ช`8กAฬฮD›_ํU~’ ŠOb๕4ถ+Š ิํพ7^ฦ}พ"๊์o๓๚ตlฝ๖>/รcใ่ั_าาั“c—BJ"‘Hๆศcค’H$’ุR•ูืฦฅไ$๚™2->าส#GฮกHŠ)ษO < ๎ฎm9๎ฮz9‹ฮ‰จHgคซ๗˜ ปฏ๑M๏W“2ั‹V`”ญCห(EMสB๑๘Fฺแqœฉa์แvฬŽ}ุฝวฑG;ไคฟ๕(กxจฉ่น‹0สฏAฯZทrmz{คซ๋ f๋ซุ#ํœi<ฏ๘’ัณb”ญรSq;JีxW@XQœ๑ฌถืˆv’b๊"ฌ(๖`k'แแฏฦ“ˆ๘_ฏรQ‚oeำ‰๋ืญๆๆM๋I๐๙ใ_ž…ืaฏœ•H*":…3ัูuณ้์กV„9ฯ4ฯฬ(มSyF้รหo5>G "Sุงˆžุ=๛aห˜œ'"<ฒงฟหx฿ทu;วโe๏DDaรชnู\Kข?ั‰ ~๘“46ŸŽษ๙BJ"‘HๆศDฅ’H$’ุR+ 2๘หk*ษJ๔11นw3O|็5โXLีฟ•ๅ$1๓?[%“<[isFษ*๐$ธ"สœม™ล๊€–W๙฿Ÿใ. O`๕Ÿ r๔ Dtล๐K1๕{ˆWuว๗ิjˆzPช้gญซYฮoผžค„ฦ''๙ษ/ŸคแTKLฮ‹R‰D2GF*…”D"‘ฤฎบช0“บk*ษH๐26๙}งุtแv rเ0ŽSทใŽญq‘Dีืœ:E่u๏Z1ฅจ(žD๔œ…x*ทbฏ:WiใXˆ๐ๆ้Wˆž|{ฐ้ยํˆง๊(šŽ^ดฃฐ-wZz1hwW>ฮ‰)ณ๓f๛^Dh่ํ –wEœP5ดŒyx+ทb”ฏw+ื !lฤฬ8V_#‘Cฟภ๊oบ _ซeฮCฯY„wู๛PำK[ล”pโฬVO‘#ฟ;โ.ลDๆ“gˆgฅ:"จจส๋ฯwี๒%v๓f’M๒เฏžๆศ‰ฆ˜œ)ค$‰dŽtG )‰D"‰]!ตถ8‹ฟXท˜๔/cแ(?xํ$/œ๎ป_ตหqœ S1ฃo๘ะrใญŠ^ผ ล›ˆขเุs๓๔"วžยlF˜‘‹"‹โKA/XŽžท-gZF Šf „ใVโ„†ฐ‡Nmู๖"2wยCห,วปdv]bบปcกˆHkเ‘ƒbv˜ฉP\”๏7สึโYtƒปิ๒ท*ฆ86ฮ๔(f๛>ข ฟ;๛š๘E„'q&ฟ/B_–"๊ยฐr้bn{ฯR“˜…x่ืฯp๐๘‰˜œ')ค$‰dŽผP )‰D"‰]!ตพ4‡?]ปˆtฟ‡ั™(฿{ํป[๛/ๆWฦ˜Bิฃjทว๔ฝแGห]Œงโ:Œข_*Š๎แเD&ฑ:ixา]๒eNƒs๑—aอ/o Zึ|๔œEจgฤ”m‚ล™รj&ฺ๐$fวธธiฅnฃ๑’ีจฉนณหโ@˜ำุƒอDžt%วบ๘ว“ฝO๙<‹7ฃ๘oSถ…ฤ์ุ‡ูพ์๘ซhแIœ‰ŸŠฉกฏ๊vถลหธ?ฅ`ฉญซ๕*๊-๋;ชW๐๑๗m%%)‰ษฉ)~๖ิณ์;z<&็K )‰D"™#”BJ"‘HbWHm,หๅOึT๐{™‰๒ฏฏ4ฒง}เโ?€!~iชขn็ถญq๑๐5V_[*„ฦข˜า๓*๑,พ=jbŠแCว]๒ี}ุmT=ุŒ m^๚Dฤ“€šZ€Qผ-ณ -{!Z Tl'2‰˜ว๎?IคแษทีX=PS๓๐,จล(]šV„โMBAAXa์มข'ถปห#ก ทŒ๒ญœGน•x*oฤ˜w5ช/47Š)aE;๖avqf7>…wku[<‹(ก๋Aเข฿—Uฬ็“ทLjR2ก้)}zฏnˆษy“BJ"‘Hๆศฅ’H$’ุRื•็q็๊…|Fฆ#|{O#ฏu^ฒ๏w๗YšŒ31Uชrฅซ^P…gมต๎’ธ”<oข+ขฆGฑzŽmy ปbf์Š่ำคx“ัา‹1สึขe–ฃe” &eƒชนb*<39เ๛ษ็ฐš‰}ัก &ga”ฏว(Y–^์V!ฉยŠ`6=2V๗œ๑žูฅ‹—UGฯ_Šw้{c$PUฮŠ)!\6†ู๒V wGพูฑพ[ฤ”Q\21ฟdA9Ÿบๅf))„ฆงy์ู็ูsเpLฮŸR‰D2G&$…”D"‘ฤฎบqAŸ]ต€TŸมะT„o๏9ฮฎกK~๑'ฆึื แ ขฒ๑J;6ฝฐฃlญป.ญี›4+ขFฐzŽaถฟŽ'4„0gฎผฤฤ—์๖/šw5z๎bิ”\T_ h†ปฤpj{คซ๋0f๋+8cฑื๘\QPา1สึนQ้%จI™(ชŽฐM์กฬถืฑบ`อ๎8่\9ปู)D๔)^ƒมฉ0าq๔ _ถใฑเ๏ีSฟs[m\4๔ฝbฤ”ข ็/ร(^‰žท5ฃิญ^13†ี{ซcVIœ‰>Dt๚สOP|)ู่ 1สฏq—&็€nธอฝgขซฏซ๛(Vฯัุ—ข ๚ำะ‹W`”\…–555฿9ฯ1qF;1๗buฦnล™ฟ$}ขXTิฤt๔ผฅx–พ=wฑ๏JQฮฝๆL๖๎รDOlว ก๘’c๎:?'ข๚๏ิฝ7BแrŠจ3,,+แ๖พ๔ิTฆffx๒…yแีฝ19ŸRHI$ษ้„R‰DปB๊–สb>U=dฏม@(ฬ{้‡{G.๏ร›รธญR_bjรญB๕จ”\า/ึ ๔ฌ๙่ล+ั๓–ขe•ป" Q}X]]5ึˆ„bkb5ี›„–ปุญ&*ฌrล”ขธ=ฆ„ภ๋ฤ๊iภj฿‡=ิŒ3=z๙—ถอ•p๙R\iXฒ -ปย]žงyยฦ™์วj฿‹ูuุํ็55ถ#1าัRr%ขKnFห(E1|œ๋/ๅDxณ}‘ฃฟUu›๊_แK๘คˆโฒ๏2:ฟคˆฯ|่คL‡gxz็ห์ุ๓ZLฮซR‰D2G~$…”D"‘ฤฎ๚เ’>^5dฏN(ฬ?ํ>JC•แ€โSL]ปM=xฑล”b๘ะ2สะ๒*ั๓—ก็-9[y"f&ฐ๛1ป`๗7bด_‘‚ๆ-ื›ˆโID/ฌม(น =)jRึ์‰f#„ƒ3าีำ€y๚e์ัNฤฬ๘•ัห—‚žฝฝ`9z2ดฌ๙ {ภqSร˜]]กึ{gฒ/vDิง๎EMอว([‹งโ:ิ@แ›wไsœ๐8ๆ้=˜ํ{มŠp%๖–ŠWp็]_฿&%จr‰ๅ๚๏`^q!Ÿ๙ะ๛ษL 0Ž๐ฬ‹{xๆลWbrnฅ’H$’9๒)ค$‰$v…ิmหJนmy‰†Nh†ภ๑+ห‡qEU๊ž์ๆ{ใๅ|šS๕บบ@๑$ ฅกๅ,B/ฌBฯญDIธŒLc๕Ÿภ๊9๊๖:}E๖ˆzGใ๗Pา0JVกผaB ์(86๖p+V๏1ฬฆ]ุใ=ˆ๐\†\G๑%ฃe”ข็-uc•ตผ ฎˆ ปช๛ˆป‰“—•Iิ4yi฿!yz{Lฦ@ )‰D"y3RHI$ ฑ+ค>ทบ‚›* ๐jใS|ํนCtOLวjvi มง?ณ%.๖Y1Uช‚ชป$™๓๐.‚^Tใส UC˜๗!ฝทhำ.ฌžฃ—ฌRฌขฆๆฃ,ว[นีญ–JHCQuPUDt {จซง{่4VืAœ๐ไ๙๗/RToZ ฯย๋0ึขx“Xู&bzซ๗ัฆX ˆศค ศsจ๚่…Ux—ฝ-ฃ Ÿ#Eu๛€M c๕6m~1=๚Žล”O…์้ม๏2๗ญ@ฮฑx™๊/~๕›ฅaว ทวโ๑gฆ๘“O~„์,ขฆษžGx๘ษgb2RHI$ษ)R‰DปB๊Oื.bห‚|ผšF๛Xˆเs‡่›Œี†ฦ7ถฌผฏ"+%hจj[<œ{ใถฅVM[๔,ปyฃQถล—Œข๊ฎ˜qwc‹žี}DŠจ?œึณ—ฒฆฃg–ฃ็/ร˜w5jjž+ฆPˆบฯอ๎#X=Gฑ{Ž๕{vปS@ำQ“s๐.‚g๑*Šๆวย O`ถ=4fว)ขฮ7b$๔œ Œ…›ะ๓—ก&eบs +‚3ูีuˆhำ‹ ๐–๛Kลซˆฒmปดฝง/๘๛z๛๔L์.MKMแO?y9ู˜–ลk‡๒เฏžŽอ˜H!%‘H$oฮค’H$’ุR_ธz17”็ahmฃ“๏‡œŠ‡$ฏม76ฏ`Af –ใทซฝ/xcyA[|ฌใเ˜๐ณโUDฝx๐``๕’%A]ำฟุูวฟ๘!&งb๗‘šœฤŸ}๊ฃๆๆ`ูฏ9ฦO26o๑RHI$ษ›BJ"‘Hˆ]!UwM%›ๆๅah*งG&๙๊ŽƒŒLGb6ฉ>๗lฎก<#ำvฺุว?ฝx์>Ks‚;ทmm‹‡sั<๕|mค๑น{ญฦ™ว–่@  ็,Bหฉ@ฯšš^โŠ)วBDฆ\15าy๚eฬ๖ฝ8}๎๛’ณ1ๆoภณ -P่6ใFAุQœ‰^ยŽy๚eDtๆ—I~OfชเYxžล›ั2ๆก๚SA3fใยํฤj฿‹ูwEU฿๔๖xQ‚ม€ึ๋>ม›๋ึT/Mี5ฎ~ํว?cl2vซ๕’๘ยง?FQ^.–mณ๏่1๎์ื19)ค$‰dŽŸ})ค$‰$v…ิ\ฟ„๓rัU•–แ ๎~€๑ฐณqศH๐๒ตj(KOฦดžo้ๅ{Ž#ฦm•zG๕ิ๏V™c๕ท กQ)‘W่K{ะา‹ัrฃ็-Aห(CK+D๑ฅ€c#"“83ใ8ฃ˜]‡Q4ฝจ-ฃล—‚ขhณ"ช่๑฿=๕":‡;็]‚Hy็'#Œ IDAT๐,น O๙ดดboจฺo-ดฺ_ว์>‚š˜ŽBLTL }%Pทณ-^ๆ้ŒˆาT๊PI๘๛ถฒถz9†ฎำ30ศ๗๘รcใ1;พŸ/qJ๒๓ฐl›ƒว๙ัฯ“c‘BJ"‘Hๆ๘ฝ—BJ"‘HbWHอตKูPšƒฎช4 Mpืณ๛ EcทJ#'ษO๐†jJI˜ถอ๖ๆ^๕•ฦณO1uํ6!t)ฆ.h๖ฃขe”ข็/E/ฌA  ฆไบาC8+โV;)šป3œช!,ท"สly™่้=8ฝˆฐ์uฑQ3๐.?z๑ชู 5ฟ๘L๙“˜'ŸฟฯlŒ'๐นปฟ^‡ฃQI=๓฿nปy ืฌฌยc๔ ๑apd4fว่๕x๘โํงค0วq8t$๙ศใ19)ค$‰dŽ”L )‰D"‰]!๕ฅหธฆ$MU898มWžฯŒปKผ๒“๘๊๕U”’ˆุ6ฟ9ีอ๗_;๙ฆืSฯvK0ฮฯฑ๚ฺ€ƒSงฝ๎Ÿ’wˆf ฅ— Va”ฎEหœ‡๊KU็l)!แIฬŽ}DO= gz๔๗4?—\”Peฮร[yzQ+ ภ}ยŠร฿Osq็]_฿&%จ๒fIกฏgรU+๐z ๚‡๙มCา78ณc5t/n๛eE8Žร‘M๐แ_ฤไXค’H$’7#…”D"‘ปB๊ฎMหY[”…ฆ*4Œ๓ๅg๖ตc๗Aน(5‘ฟปฎŠขิDย–อS'ป๘แSฟ๓๕Ž ]U”เณŸ|o<œงRL]„Dศ“€Qrฦ‚Z๔ลจ‰้๎าฐ3މwณc/V๛^์‰>ฤฬธœผห€^ฐ oๅอ๗้๙K‚j ฐ-žฦ๛Dิnผ‰kVโ๓x่แ฿9=ƒ1;fUU๘หฯ|Šyล…Gp๔T3?๘้ฃ19)ค$‰dŽ&…”D"‘ฤฎ ^_อสยLT กŒฟอพ˜ŽรŒdพRปœผไfL‹วŽw๐ใƒ-็ผ*h๊v-วรy;V_[:+ฆn—W๑y&>†-k>zมr๗_๎b‹@ fฦฑzŽbท!ฆFฮพNM+Dั wวC!ๆ fkX]‡0; fฦถ)—๑],โXDฉŽ*ชฒ๑|฿sำฦkุผ~-~ฏ—กั1๓gำึำ๓๐ลmŸ`ai Acs+฿ษรฑy๏ัดM฿๛/๏”ดD"‘ผแ(…”D"‘ฤฎ๚ฺ 5ฌ(ศ@Ž๔๒ๅg๖วt*2S๙Rํ2r“L›6ด๑ำรญo็ฃv9ŽqวึธHฅ˜:„ว๐ฃฆข็-ล(ชAหซD๑&ก  ขSXฝวฐบb๕มnGXดŒRิ”<<๓ืฃๅ.FKษMwwไ3รsณ๕UฌžฃXGpฆ†])%sซ ƒQ฿๊{ทlXวึ Wใ๗นB๊ŸŠ–Žฎ˜ž/|๚ใTฬsW)ž<ฦw๎(ฆŽ_UUV-ซคfษโM+*+vส ["‘HŸI!%‘H$๐ญ]Gลฎึ>l'ถ๎‰๗lYAu^:‡{Gธู๋1‡ส์ฺ๋ฅไ$๙™2->าส#GษGฦ™˜Z_+„'ˆสFyUฯ&:†5%-งฃจ=JB( ˜aฌ“ุ}ว1;`๗Ÿrwุ๛ญงI ={ZFฦผkะาKPSrฦ็Žw›žทฝŽืˆีsิS’ทOœŠจฯ)Xj๋jฝŠzหŒ๋ืญๆๆM๋I๐๙ใ_&#nแฑcไajชR๗์ถอ‡โแ|–bสํฅ$gฃg/@/ชA/จBMฮvETt{ด{ฐ‰hหKX]‡ฯkนžท=o ฦ‚จ iจ‰™nล”mแL แL cuฤ8…ีsT6>ซ8ด+ŠYจ{๑๑x๖็ฟ,บq…ใฦี+y฿๕I๔๛็ว~อ‰ำm1=?๚ษจœ?EQhj๏ G\๑วผta9kซ—SRGjr2CG6ฉชฒS^่‰D๒†|M )‰D"!„ˆุ#ำNMฐฃน‡ฝ]CWqk๋J–ๅคแ๛ป‡๎ˆm฿R—ฮฐ„ฌD““๛ด๐ฤ‰ฮ ๗ผ+ธฯาœเฮm[โแผžS๗ข๎]นuจ:jb:ZF)F้๔ขจษ9(ช†0ร8“}Xง0[_ม๊:„ˆNฟๅฏะ‹j0 k0ๆo@1(T๋Šฉะ๖X7V็A์กฌco๋;โ ‡vEฑ‚บ๗ฦำฐ/คˆ:ร5+ซนu๓&’›˜เ'ฟ|ŠcM-ฑ=OŸ๘0KฬGQZ::๙็=ภ•๚ฒdA9ซ–UR^\D % Cื0-‹‰ะิฆฌ๔ด๒‚—H$’sH!%‘H$ภŒi ฆต†ง#ewk+ฆEแทฎค2;€์ํไkฯŽ้8ฌ*ศ ๎š%d&z›hฟ9ี}แŸใNL]ปM=๘ฎSŠŠโKAK+ฤ([‡Qบ55E5v'4ˆ=ุŒู๚ f^Dd๒ฅQถฃไ*Œาี๎Ž|žWL9๖xŸ+คบbตbฬฑ0‰Sต- ่aฝNSฉC%๕B~๖š๊e|x๋๕$%$2>9ษฟzŠฃ'›czพ๘ฃd๙ขจชJkg7๕?zหถฏจc\4ฏ”ซ–/กผคˆด”d<†(XถลDhŠถฎ4žt็mุ)/|‰D"yC๚&…”D"‘ภฎึ>Q‘™Jš฿ƒกฉfh*ยฑ1^h้ๅP๏ศuฬบช๒ถฎdqv*ถ#xตsoผp$ฆใฐฆ(‹ฟธz1 ^ฦยQ~๘๚)žk้ฝx_(ฤทฃš7ธs[ํX<œ็๏V1ฅxPSr1สึโYxjJ.ŠๆA8&bf {่4ๆ้=˜ญฏโL^ุ/ื <๓7`ฏB/ฌrฅ๋>Pu„cโŒ๗b๗5bฮVLู#เX๑}ร•"๊‚‹จ3ฌZVษm7o!91‘‰PˆŸ>๑5žŒ้yปใถPตx!ชชาึีรท๏}จi^ว6ฏธซk–3ฟค˜@J2^ข(X–อไิฝ์o8ฮ๋Gฐ›ใžปwสŒK"‘HรI!%‘H$p๗ณDfขีE™ฬOO!ี๏มฃ*8ย–อะt˜cc<ิร๑+ร]xuธq%‹ฒRฐมžŽAagl ฉซKฒ๙๓u‹I๗{›‰๒ฝืNฒซต๏ข~งpทU๊ีSbjฌพ6เเิ)Bฟhล—(…Aั ๐๘๑,„w๙-จIYฎˆ6":…=ิŠูผณ๕œะEฌtTTิ„4Œฒu่…ี่y•๎2>อB ฌœษAฌžฃ˜๛ฑ๛O\ในRqŠUŸ^ท;Oรพ"๊ 5•|์ฝ[IIJbb*ฤ#Omg฿ัใ1=Ÿ๙๐-ิT.BำT:บ{๙—๛สL๘๒V–ไณถf JKศคฮŠ(หฒMOำ?ศ# ์;zl5—R‰D๒ฆJ )‰D"ปž= ฆM›ฒด$ึgQš–DŠฯƒGSqAุฒ้อpดo”อฝœบผ ‹=:฿ุฒ‚…™)XŽเฅถ~qwCLวacY.Ÿ_SAš฿รศL”๏พาศหํ—ฆษผSฑ”ฝ((ž$< 6โญ€#J๗ ยŠเŒv9ฑณy7Nh๐า–๎E ธีRyKะฒ $คกhย6‘bjณ๋Vว~ฌ†๘่/5+ขTิ๚@ฮธจF<รw}}›P” สฅฉJ\^ฑ€OrฉIษ„ฆงx๔้ผv8ถnเ๛Xนด]ำ่่ํๅ;๗?ฤิ๔ฬe9–‚œlฎ^QEลผ2าx Šข`6S33๔ r่๘I^=t„H๔ทซธค’H$’9r')ค$‰ไœ:CฤฒฉศLeEA%iI${ Œ7TL๕Mฮpธw„g›บi ]–cN๑ณe๓3Rฐ‡ญ฿c๛มใบ๒<๎\ฝ€ฯรศt„o๏iไตฮมKz ยa•เ๖ฯnฉ‡sฌพ6 „ขj_Œ‰ฤล—ŒQบ๏า›ัาKQผ‰ จฎ๐ i|†่ษ็p&ภq€KŸ็($ดฌ๙…UhY ั2Kฯ๎ศ'ฬ0bfgzซc?fว>ฌ๎ฃ—ๅ8/:RD]2u†% ส๙ิ-7HI!4=อcฯ>ฯžฑ[๐n}W-_‚ฎ้t๖๕๑?ฬDh๊’CNfืฌจbั22ำx=TEลvlฆgยt๗ะpช™—๗"‰ฮ๙RHI$ษ9“R‰D๒f!u†จํP™`yneiI$y tUม‚ำSz†yฆฉ‡ฎ๑K› ง๙=|ms ๅ้)˜ถรฮึ>๙ฅc1‡๐ูU H๕ ME๘๖žใ—ญฉผ#hW%๘์g7฿ืภX}m้ฌ˜บJ<>ล—‚QXQqzฮ"wIœช!ž zข'ถใL #ฬธ๒5! -)zn%z๖ิิ|ิฤ ะ Dt'4ˆ˜วl}v)฿ษwวษว"๊s_น็VGฅR‹จ3,*/ใำทพ‡ดิTฆfฆ๙ีŽ]์{ ฆ็๔“๏ฟ‰ีUห0t๎พ๛“‡›˜ผ$฿‘ฮบšๅT.˜GfZŸื‹ฆjุถอt8L๏ภ GN6๑ฺก&ง~ …”D"‘ฬ‘฿I!%‘H$ฟ[HEภ‚ฌj๒า)$‘ไัั5q˜Šฺ๔…ฆู฿5ฬณอ=๔L\š%8™ ^พถน†าดdLๆน–^eOcLวแ=‹ น}ล|RผƒSaฟ—Žqฐ็๒6“—b๊2'*พd๔ๅxๆฏGหY„š˜ข๛>QำฃD›važ~{ฌ 13ยนโๆTMHs{Kๅ/Cฯ]Œ’†š๎VLEงq&๚p&๚1[_ม๊>Œ=ฺป'c[Q”`ผ‰จ;๎พงVuDPQ•—๓8–•p๛฿Gzj*ำแž|แEžeoLฯํว{#๋jช0tžAพ๗ภฯปธKๆ3ำฌฉ^ฦาๅdgฆใ๗๚ะ4หถ™ ‡แศษ&^=t๔ผๅ˜R‰D2Gž'…”D"‘œ‡šES*ฒRฉฮKง 5มSชŠๅฆขS์ํโน–^๚&/n‹œ$?มช) $aฺ6ฯ6๕๐WOฤtnญ,ๆ“ีๅ${uBa๏‹ ้ฝ"Žอด#œm;๎ุc๕๋k…๐Qน,ุŠ'=)Fูี่น‹Q“ณQ<‰ณ"jณ}fหKุรญ83c`_๙;ืฉ)น่U่๙Kั ชP}I($P5Dtgข{ฐ™hำ.์กำ๎ฒรXมฑ๏›Qm๑๔qฅˆจ3”—๑™ฝŸŒ@€™p˜งwฟฬ๖—^้9พํๆ-\ณฒ aะ30ฤ๗|„ม‘‹๓ปHNfŠๅTฮŸG^v&~ŸMUฑm‡™H„ม‘QN5ณgaF'&าgK!%‘H$sไ{RHI$ษ๙ ฉ3hชBev€ชt๒S$:บช` A(jั16ลพ๎!ถ7๕0<}qvสONเซืWQH"bๆT7฿-ถ—|xi)ซ*#ษฃำšแŸv7pฌŠ+ดุๅ8NPŠฉ‹”˜hดผJŒฒตฎˆ ขz“ยALb๕4`žƒ5ุŒ3ูถ;“ฉ(๎#P€^XใVLี ~ท)ปข!ฌ0ฮx/V๗"วƒ˜ฦ™พrวง"๊ฮปพYฐ๋ฏu†าย|วGn%3-ภL$ย๖—^ๅ้]/ว๔\่ฦู๋pี ผƒพมa~๐ะฃ๔ ^ุk"ม๏cช–-œOnVฦฌˆาp„C$ฅx„-ญผดรฃo๏7I )‰D"™#5’BJ"‘Hบ:ƒกฉ,ฮNeyŽ[1•`hจŠ‚ๅBQ“ฮฑ)^๏โ้S„"๖มน(5‘ฟปฎŠขิDย–อS'ป๘แS1‡U•๑‘ฅฅ$ฎ๚ึฎฃœฟR7ลิฝจฏ7ŽžSQถ-o‰ฐ— €˜ว๊kฤj฿‹ีgผa†cw2Uอ‘/ตฝ`z2Œโ•(†o6;SŽ…3ฺ…yzั–ก!œ้1ฎ˜ๆ็q*ข>ฅ`ฉะ๕ pE๖Z+ฮฯๅŽ>@VzแH”็^y_?ฟ;ฆ็ึอ›ุธf%>‡กแŸำำa6ป๐yฝฌซYNueyY™$๘}่šๆ๎ฌu+ขNดด๑๊ก#๔ผณ~†RHI$ษ›‘BJ"‘Hx๛B ฦ็ฆํฐถ8‹št๒“๐:šฆ#EL:ฦงxน}€ํM=L›fiQiZwmZNaJ"3–อฏ;๙ฏM1‡?ช)็•ล๘ ษiพน๓(อรW๚a๏Šชฮถถฦลƒ๙Xต„ะƒLL) ZFzQ zRทว’? a๗Ÿฤ์:ˆ{{ด™z๗Lฆชกx“ัา‹ฦ็หั‹V h@€pVgข—hใvฬ๖ฝn#๔่ิๅkฺ๎ฐKQœmRD]™ไd๓ว๛ 9้„ฃQvพบ_๎ˆm๒พ๋7rบซ๐y< ๒ร‡AW_;๚LฏวรUห—ฐr้b๒ณณHL๐ฃk:B8„#Q†วฦ9ีฺฮ+ำูAฦ!…”D"‘ฬ‘J!%‘H$๏LHa2bโัTฎ.ษฆ*/œD~CGULa"bา>bwk;[๛˜1ํw๔}๓า“นkำr๒“˜ฑl?ฮZb:Vฮ็‹‹๑๋ำ|ใ…รดކbโุม}–ๆฅ˜:OT]บ–ฟิWXใŠ(E่ ึ`3V๗aฌ๎ฃุรงแษw๏d**ช?-{!z๔ฅhน‹Qt/ยŠ "!œฑnขง^ภ๊:„3ูiซฤv)J4จ{)ฎจทƒฉ…/ฦย๑ๆeer็ว?Dnf‘จษ๎ฝ๘ล3ฯลt ณi7\ฝŸืรเศ๙ณวh๏้}[Ÿฅkkช—Qฝธ‚ข’ ่บ[1ฃŒŽOธ"๊ภ‘ทฟ )ค$‰dŽH )‰D"น0B๊ “ŸกqMq6ห๓าษJ๔แำ54ขŽ`2ฅyx’=์h๎ลrฎ` 2S๘๒ฦๅไ%๛™1-~~ฌŽ้8qีBSQˆOื่Ÿโ๋/ฆc,ถ*bม}ŠAp๛งทดฝฏ›ฑ๚ฺ€ƒSงฝ•ิ๓z“f &gฃgW VนKี3P aGฑ‡Nc๕6`uย๊?OฤัHAMส@ฯ_Ž^Tž]š^|NLEฆpฆ†pฦบ1OฟŒี{{ผ็โ๖ัŠSu็7พp&Eฆr็๖@vF:Ÿ๛๘‡ษฯฮ$jšผด<ต=ฆcฑ๕ฺซูฒa~ฏ—กั1๋‘_าฺี–>CำTV/_JueEyน$'& kB@ิ4Ÿ ฉฝƒื7ะ~qvน”BJ"‘Hๆศ|ค’H$’ +คฮ01I๑ฌ)ฮbYNY‰>ผบ†XŽรX8Jห๐$/w ๐BK/–๓ึaQV*ปqนI~ฆM‹GŽถ๑ะ‘ึ˜Žร็ืTฐua^Mฃ},ฤืž?Lฯฤtฬรซk|๓ฦ•ฟ(+ฅPีฑw๛๕s^bJำQาัฒสั ช1JVกฆไกจยฑqF;ฐšฐ:๖avxwWDjr6FษjŒาีhe(I™oS“ุฃ]8ฃ˜๛ฐ๛O`๕€p.ฤฉˆฒl;ะี[๗/๗>X6ฃฉฑviไ!?; ำฒxๅเa๚๕31“อ๋ืrำตืเ๗๙ใG?-็)EaๅาลฌZถ„โผ’1t˜Q4ทw๒๚แNตถ_ิqH!%‘H$sงฅ’H$’‹#คฮ01 ๘<ฌ-ฮbiNY‰^ผบๆ&ฤถร๘lลิฮำ}์n๋็|๏หKrอตKษM๒ŠZรŠ‚˜วnล8…ี{ {ฐg.3r8ฌ(ับxQ‘จนMืด๚๖žิ๏Sฆgbฏq~ %™?๛ิG)ศษฦด,^?|”~๕tLวๅบuW๑žMH๐๙็พ_ม๗ฏZVYขk:ฝ}๋br*๖ช3“๙๓O”ข\,b฿ัใุฏc:>ืฎ^ษ๛ฏ฿Hข฿ฯ่๘8๗?ไ๏•HKฮ็ชๅK˜WT@jr†n`Z&ใ“!ฺป{ypGN^ฺM@ค’H$’9า)ค$‰ไาฉ3LFLr“\]œอยฬา^ผgฤ”ๅŠฉฦ1vž๎ใ๕ฎ฿PY“Ÿฮ_ญ_BVขษˆษ}Z๘๕‰ฮ˜Žร_ญ_ยฦฒ\ Mฅed‚ฟ{๖ cแhฬ#ี็แžอ5”gค`ฺป[๛๘/C8Œ+ชR๗์g7วลCฬฮ๏” ฎ j™ๅท+บ!Dx{คซํ5ขMปpBƒ๒t>จ:8z2Œาี่ลซฮ‰)UsลTh{จณ}Ÿ+ฆF฿ฐ+กฬq›‹s%%จBษงn}ซ—/Aืtบ๚๚๙ื?ฬD(scJ๔๛๙๓OŒ’<,ๆภฑF๎๙ฏb:Nืฌฌๆ[6‘่O`lb‚?$ว›฿/qษ‚rV.]Lyq”ไณQฆe3 ัึหc8ึxYทR‰D๒fค’H$.ญ:C(bRœ–ศช‚Lfฆ’ๆ๗`จฎ˜Šฺ6CำŽ๗ฑซต}รozส‚ ๒š%d&z™ˆ˜ืพ&~sช;ฆใ๐7–ฒก,]Uišเ๎ํ˜Œ˜17Žtฟ—ฏmฎa^z2ฆํ๐ย้^๊_>~ฮฺUE ฦ‹˜ย6kEt*huo4๗=๙<ฮX—ผ๑ผญฬMEั Œา5h๙K1 ชPS๓Q /แJฟฉa์แvข-/b๗5bvผก๚lVLลฑˆบใ๎{j5D=(Ug'kช–a่:O~ฦ่D์5ิ๗y=ลํŸ ค วq8x$๕ศใ1ฏตีห๘ะึ๋IJHd|r’Ÿ๒)N5Ÿ๛‚าbึV/ฃผคˆด”d<†ข(˜–ลdhŠ๖ž^;มพฃว.kฆR‰D2GZ#…”D"‘\!u†Pฤค,=‰•™,ฬL!เ๓เัTfh*ยฑ1v4๗pดo๔์๛Vf๒ลk*ษH๐2Ž๒{›ุัำq๘าฦe\S’ฆ*œšเหฯ`ฦดbn™‰>พvC5ฅiษ˜ถอŽ–^พณง๑Mฏ‹715yงjญแž *ๅ]็fp Š/ฯยZดŒ2๔ลณbสŽ…ฐขgwไ‹žzซฏัํ/็"JuDPQ•7{๏ฌซฉยะuz๙?cxl<ๆฦh่:_๖ สŠ p‡#'š๘แรฟˆ้ธญZVษm7o!91‘‰PˆŸxšรง˜WTภบšๅฬ/)&-5ฏวƒข(X–ลไิ]ผ~ธ วqœหผ#…”D"‘ฬ‘ฮH!%‘H$—WHa*jQš–ศบโlสา’I๕ปbJมŒi38ฆก”ํอ=4Œsuq6~๕bาฦfข|๏ต“์jํ‹ํ8lZฮฺข,4Uแฤเ8_๚อ~ขถsใศI๒ผกš’@ฆm๓lS฿}๕ฤ๏|ฝ‡qœบwl‹‡•ฑ๚๕ตBx๊Qฉ’wŸwšษ)จIูx—„šš‡–ต5%วํ/e[8‘ฮไฮx฿x๔ไ3๕N฿ษ๚”?๙XลผโB„#8zช™๔ั˜Ž฿Š%‹๘่{o$%1‰‰ฉฯ๏yฏ๚U: IDATิ”d*สJHคโ;+ขlB3ำ๔๔์w|ี™ฐŸ™u๏•ฎzต%Yฝหใ ๔†B ‰๗๛๖๐ )ฺ…dูvฟษฆgC ฝwlllใnฒeษV๏ฝ]้๖2๓!ษ@ฬn7}ž?m™๓ž๗ฬ™็พ็๖Wื๒QU5มะิ๙AC)@ ๘œว!ค`jฉI|ม0Yฑvๆฆล‘mรn6b”%ผก0}./Uร z|\S”Nดลศฐ7ภo๖c{Kฏฆ๓๐“ๅeฬ›N‚ฃฝ#๐ฝ(žJฑG๐ำeค;l๘รaซ๋ไw{‘nSฅโยS‹ืฉชพ™tq:UกG†ฑ่2tQษศŽ4d[<’1ยI8Xฉx†+e{ย%ข๎{ "Cี๋+€;ึ฿^ฟv%‹ๆฬฤd4ะ?ภ๏Ÿ}‰!MฦฝฎศIŸŽชชิ44๑›ง_ะtห๒sน๕สK‰ฒวซŸ"ฬๆQแpทืKW_?‡ีณ๋@ภิ[{P)@ 8!ค€ฉ%ค& „รไว;(OŽ!=ฺ†dภ0!ฆ|มฎ@‡ลˆI'3่ ๐ซ]5์nำ๖ั+ห™‡ ้ๆ๗๖k2Žดจ~ฒผœ้QV|ก0๏ึu๐‡๊พฬ!„˜|ต;ƒ]l:†œๅN]\Fฅฮ1ญRŽL"๊opํ๊ๅ,ž7“ั@ฯภ x๎eบ๛4๗๏ผ…ผฬ TTj›๙๕Sฯk:Ÿ‹ๆฬไ†KWb2šUU'D”‚ว็ฅณง๊บFvฌยใ๕Mู8„‚ฯynBJ ฆฆ:๑ซชไลEQ–MFŒ ปั€~BLษ Ixƒ!~ฟ๗8๏j|Q๓‡VอdfJ,Pี3ฬ6hSHฅ;lxY)iB๊ญcํืพ/ฟลธข๒DHงTl]ทถๅBธ‡*WHช~=2Qโฎ๔QpชRจRgŠฌŒ๚๖[BD}AฎZน”ฅ ๆ`6้โฯฝBgoŸ&๛แ;n&oFuอญ๒‰g5GBl สK˜SRHBl ’$qEมๅ๑า;0Hีฑ:๖ชึฤŽˆBH มษ!%Lm!uโ†DN\$ณRbH‹ฒi2 “วะUUฅsิรถๆ^o่ขkิฃษ<|๕,ส’c8ิ=ฤ7ะd™ั6\VFjdP˜7jx|รWw ˜ฉ\๊PPึ 1๕UJ๘ I’*๋ทถ\Haฏซจp่}๚๕:™ฏล[ถk./BH มษ!%hKHtzX:#‰›K31๊uจชzb _XQqCดปุ฿9ศปu8})“$Iหฺู%8P€}ใๆCšOนq‘<ฐค”dปo0ฤKG[yๆPำi9ถชเ หT*ฒฑr๋บฅ็”ฌ‘สฅชชV ๋๎D๐Y.PpฯWœ5ษฺลฑ๚’…XL&†G๘๓‹ฏำกอ)ะ๗|ฅy9HฒDS[> กpxสถ7ยlๆขูe”ๅ็’‹ลlF'ฏ๙~F]nข์6ฬ&^Ÿw?ษ๛;๖h./BH ม็< !%ฺR=c^ๆO‹็ๆฒLlF=ž@ˆAฏŸh‹‰ƒY‚ะDลT๛ˆ›=ํlจ๋ฤMู˜๔ฒฬ#kgS˜EX…ฝm<ผฅJ“ใ)?>Š.)!ษfม ๑โ‘ž;|Zฯ!ฤิฬ,ข๎}๐กuช$Uศœ๐W/Zภฺลc1›q๒ุKฏำุึกษ>บ็ฦk(+ศE–eZ:บ๘ๅฯเง\;อ&๓หŠ™U”ORBV‹ฝNช*๘๚‡‡9ุB{O/W,_L\ดฏ฿ฯฦํปy๏ร]šห‹R@p2BH ฺR}. งวsCI==./Ol$+ฦNyr ษv ƒY–†ฦAฺF์h้esc7žเิSfฝŽ_ฌ™M~|$aEeW[?ผ๕ฐ&วSa‚ƒXRLขี‚;โ๙รอผxไฬธƒ PL•ซช\‰ฬ’ ๎FฅฐM’”๕Ž๕[]hกŸ)5ษŠ…๓ธlู""ฬ†FFx•7ฉoiำd_uUฬ,ฬGง“i๋์ๆ—O>‹ื็Ÿ2ํ3 ฬ/+ฆผ0ŸดคฌŸQCN'ว›[ู} Š๖๎^าS“๙ฦ ืใภ็ฐiื^1eO ฮ„ด'ค>.JOเบขt,=c^ูv˜บQฒb์ฌฮIฅ,9šซ๙3bjิคyhŒญ}ln์ฦš:ำ8ฌF=?_=‹ธHBŠสŽ–^๕รjMŽง’คh’bmfฦ!žฉjไีฃg๖ๅVQi•%ฉbใซฟฎู‘สEKUีXqAˆ)…m’จpฌ฿qมฝฬ~๋G_ญศTž)5ษา๙sธ|๙bฌ CN'Oพ๚ว›Z4ูgw^{ณ‹ ั๋tดw๗๐ซ'Ÿรๅ9๗]่๕:ๆ—•PšŸCzJ2ถˆtบ๑ฉy`€a็(uอญ์=T™้’ำ’น็ฦkIˆฦ๐ม๎ys๓6อๅE)@ 8!คํ ฉAŸE \]˜ŽEฏฃkฬร/ถฆq่“‡๒ใฃX‘•Lir ๑V3fฝY‚ ข2๊ P?0สž๖~67v +็<ฆ(ณ‡Wอ"+6’ขฐญน‡฿~T“ใฉ,9†ฝจˆ›™1ง6๒Fm๛ูqฆ˜ชDฆ์ผ ๎Q๗๘แฅฒขVHฒtV„ใ%sgqีส%X- Ž๒ิซoQุฌษพปชห˜WVŒ^งงฃท—_?๕<ฮ1ื9k,หฬ--bVQ>ำ’“ฐ[#ะ๋๔€J dศ9JCK>๚นUi)‰๑{ำu$ฦลเ ุถw?ฏฝฟEsyBJ NF)@ ~ดaฟ:น[๖Xœ‘ศๅำฐ่utŒบ๙๙–รด ŸาQœอ๒ฌ$Jฃ‰ท™1้tHร #พƒclo้eksaๅ}'ฤD˜xhๅL2c์ร [šบฉYฃษ๑4+%–ฟ[THผีฬจ?ศใ๛x็๘ู]FQiึo๚ฦ๊ื.„kxคr๑:UีW ŸูJšณ“šรทnนž”„8ม ;๗W๑ย;5—!คเd„เ๗๖ซ:Y;Bสฒ$3‰ตนฉ˜t:ZG\ำUtํuBfฅฦฒ:;…ผ๘(b,&Lz๙„˜๒๘ฉS{๛ฯjLษv ?[QNบร†?fC]ฟ{L“ใi^Z?ธธุ#พ๚ธžM ]็บY”˜ช\\!ฉ๚๕ศDM๙ฦ*ดJRจยฑรว/ด{๏}Tdจz}pNwOœ[Zฤ —ฎยnตโt๑์๏QuฌN“}zฺ•\2g&&ฃžA~๗์K๔ ž•s็f3งคำาpDฺ0่ €J0bิๅฆฅฃ‹}Gj8T{ 36ฺมทoฝ”„x‚กปV๑[4—!คเd„เ‡๏ํS๕ฒฌ™๖บ!–อHbuN † !Uฑ้ฝ.๏>ฦE้ ,อL"7>’hณ ฃNF’ภR๔๘ฉํa[su œ•˜าขฌtyำขฌ๘Ba9ม?ึๆ แย้๑ฟฐ€˜#ฟ๛่8[›zฆJ๓ถ!K๋7ฎ[u๏ิ6Rนิก ฌŸฒbJˆจ ฮฑˆšdVQ7]พšHซQท‹็ภมm ๑ซW-cษู˜Fz†๘ร๓/ำี{f`(ศสdniYำงแˆดa4‰`(ฤ˜Ms{'‡jณฟบ†/๛๊แˆด๓o"51`(ฤGUGx๚w5—!คเd„เผปO5๊ด#คผม0หณ’X‘•ŒAงฃexŒŸพฯ—ฺ[g&qqz๙๑Q8,FŒ:U8ฬ วOM๏[šz8ะufaฯˆถ๑เฒRา"ญxCaชm็ฯ๛๋59že$๒๙D[Œ {fฯ1ถท๔N-ข๒DHงTl]ทถๅ|ฟพOˆ)Iณฉั๙ฎˆZWQแ0๕’ฤฆRปส ๒ธๅŠตDฺlŒนผ๐ฮF๖ัๆvWฌXย๒s1›Œ๔ ๓ง^กฝ๛ฬฒฆงqัฌฒ eวd4 IกP˜Qท›๖๎๖ฉแใร_}ƒŠH›•๏}fา’ …C||๘(Oฝ๖ถๆ๒"„”@ |ฮปˆR@ =!ๅ…Y‘ยาฬ$ :™ฆก1~๒†ฝฏ|ฬe3’X’™DVฌ‡ล„Q–PT๐…ย x|ิ๔9ูPืIM฿ศ™yฑ‰ฑ๓ฃeฅคุ#๐CผVฦ“ดนจ๐’ฬ$พ=?‡ลศ7ภ๎ฎeื9Z›๋oบ‘ KLeจชZฌ;7U9 ฮ UyกcืUT8๔>zฬ”ฌV+อหแึซ.%สfวๅq๓โป›๘จชZ“}}ูาEฌผx>“‰ก๋ลWiํ์>ญ็ศHKaแฬRr2ฆ…ษhœQ!\ฝ}|TUอพ๊ยงธ‹ซ5ยยwฬ๔ไdBแ0๛ซkxโ•75—!คเd„เ๏฿๙X5้ušio0ฌฐ2;…ล™‰่e™†มQ~ผ๑ฃเ)W/K,ฮLbyV2™ั6"อ“S*`˜>ท๊ža66tqผ๔๎ฺ”ษKJIถ[๐Cผ\ส3UMšOหณ’นw^.ณ‘!ŸGwีž๕5นพด+Qy"ค3ฎ฿บn้ศ๙~ฝŸu1ฅเTฅPฅŒ\้Xฟ๕ผ๏฿O3ีEิ$ลนY~ีืˆฒqy<ผฒa3ปึdŸฏนd!k.น‹ูฤภ๐ฝMmงg—ฯดคD.šUFnf:ฑัQ˜F@"ใ๒z่๊ํ็Pอq๖:B <-็ด˜M|Ž[˜žšL8ฌpฐๆฝ๔บๆ๒"„”@ œŒR@ฏท?V-ํฉฐชฒ2+™E‰่e‰บQxw tZŽoา๋X™ฬล้‰d8ฌุอF ฒ„ช‚7ฆืๅฅช{ˆ๕]4 –s&8๘‡ลล$ฺ,ธƒ!^8ฬ GZ49žVๅคpฯœ\ขฬ=~*wึ๐๑YZ‹๋TPœa™JE6V^ bช\UๅJdฮฬฮnฐˆธ๗ม‡ึIชTฉ……ๅ ณg๐๕ซฟ†#2ทืรk๏oeวพƒš์๗อใฒฅ‹ˆ0[แ๑Wคพฅํ”Ž™œวย™ede=^%K2a%Œ๋ฅณง๊บv๎ฏยœึxLF฿ฟ๓V2าRP…ชฺ:๔ยซšห‹R@p2BH ๐ฟHต๔šiฏชชฌสIๅข้๑่d‰cฃhร~|ก๐i=ล cMN*๓ฆล“mรn2 —%”‰Šฉ^——]Cผ_฿E๋ˆ๋”ฮUœอY\Lขอฬ˜?ฤณUMผrดU“ใ้าT๎š“Cคษภ€ฯ์<สฮAํŒฏ NL-ZชชฦŠำ&ฆ„ˆZงJR… ้Zis~V&w\5ขฃขp{ฝผฑy~ด_“ฟt._พซลยฐำษฏพล๑ฆ–ฏtฌฤธฮ,ฃ0{qัฬ&ฒ,‡๑๘ผt๕ p๘X{Uใ๒xฮH—^อแžaMฦrSi&หณ’[Šฬhใย`คr๑:UีW  ฑข„Ÿ˜Q-ฺำ็,•$*ึ7–ู—5Cv๚4๎บ๎Jb<>/๏nษฆ]{5ห‚๒ฎ[ป[„็ุyช๋พะg‘v.š5^•‡ลlB'ห„ร ^ฟŸม!Žิ5ฐk#ccg-ฆ๕wFN๚tTUฅฆก‰฿<‚ๆ๒"„”@ œŒR@เญฝชอhะฮƒญชreม4fงฦ!Gz‡yเฝณ7ฝ$6ย41•/Žิศ" z๔ฒDXSํN7๛:ูX฿ษ€ว…Ž9/-Ž\\Hl„‰_€?}\ฯฆ†.MŽงk‹านฅlv“ž^—๛แช{ตY0๓“ๅeฬITvท ด>ฒํHลฦปW=~!พ˜บ€Eิ=?~x้ทXQ˜3c‰„D}K+•?ฃษXfLOใฎ๋ฎ$.ฺื็gร๖]lุพ[“ฑฬ))ไฦหVcทZuนx๖อ๗8T{ŒีbaัœrŠsณIŠ%ยlA–eUม็๗ำ70Dmc3;๖bhฤyึc๚ท—™ŠJmc3ฟ~๊yอๅE)@ 8!คธอฝjคI;B*Vธฎ8™)ฑH@Uฯ0?ฺp๖ื;Iฐ™น47™)1คFZฑuศ’DXQ ้pz๘จฝŸw๋:๛;^”žภ๗c12โ ๐ฝวูึฃษ๑tcI7–fb5่้uy๙ืซฉ้ำฆzdอlJ’ขQƒlำA•VTeฆ{ึ^/WC•‹+$Uู]โ.p%+j…$KK๎ป๕Šsฒ$‰†ถv*๛ Z|ดฬHKแ7\=.ค~฿ฑ‡wทํิd~fๆs๓kˆดฺuปxแํ์ฏฎฟ0›™_^ยฬย<’โใˆฐ˜ั๋t(ŠŠ/เงp˜ฺฦf๖:LOน[๏;n&oFว›Z๙ี“ฯj./BH มษ!%ภo์U#อฺRP˜›J3)KŽเP๗?xเœตgZ”••ู)ฬJ‰!%2‹AN‚ ขโ๒iwบฺููวฆ†.\อN€‹3๙๖|ข-F†ฝ~ฝ็;Zz59žn)หไ๚โ " zz\^ูv„ใNอล!r้Š(ภพŽq๓กOษ6EQ*.15Rนิก ฌ—T)ใBQ๗=P‘ก๊๕ภ“๖ญ[ฎฃ$7I–hj๋เ?{ๅ ญew&™ž’ฤ=7^C|L4>€อป๖๒ึ–ํšฬSi~ท^y)Q6;.›—ฤช๊ฯูdbniณ‹๒INŒวjฑ`ะ๋QŸ?ภเศว›Zู}ฐŠŽžพsำwoฟ‰‚์L$$๊ZZyTƒ•xBH ม็ฒ- +๋ถฎ["๎*ฺ็Q“}รีฬ,ฬC–eZ;บx๔‰g๑š‹5%1ž{oบŽฤธ|๖๎็ต๗ทh2oY™|šห‰ŽŒฤํ๕๐๚ฆm์>Pลผฒbส ๒ฦE”u\Dฉ*‚A†œN๊[ฺ๘จชšฦถŽ)ำทnนž’์OUโEัึ;ŒR@p2BH ๐ืwซั“fฺ๋„ธkv๖ฉ\9ง&8Xž•LIR4 V3&ฝ ) N_†กQvท๖ณนฑ›ี9)ฌ›Mคษ@ฟว์จแ@ื &วำณsธผ`ฝŽŽQ7?฿r˜–a—ๆโฐtb๕l๒โ# +*;[๛xd‘ฟ๙9Eๅ‰NฉbJ›ฌซจp่}๚๕:™ฯฎ›๕9u•ฬ,*@ฏำัฺีอฏžxฯงน˜“โc๙ึอื“‹?d๛วxyรfMๆ/73;ฏฝ‚˜จ(^/GŽืa13-)›ีŠA?พฃl dุ9JCk{ซชฉoi›ฒ1u%^ๅใO …5•!คเd„เพืvซฑฺRž`ˆปg็P๐…+Wฮ)ล‰ัฌฬNฆ(ัqBLย N_€ฦม1\๓งลa7่s๙๘๗Gฉ๊าไx๚ๆ\.หKรฌืั๎t๓ะUด;š‹รfิ๓๓ีณษ‰‹$ค(lo้ๅ~X…?/ฤ”ถ๘2"j’;ฎนœ9%…่uzฺป{๘ฯงžcฬํั\์ ฑ1w๋๕$ววูฑ๏/พ๛พ&๓˜>u;†ฟ?€^ฏวhะŸจˆrŽัษ๎ƒ‡9ึ4๕/ฯop5ๅฏฤBJ NF)@ @{Bส ๑9น_บrๅ\Sžรชœ โฃˆ0จ˜๒‡a…ƒ$ัํ๒๒oVSซอ้พ5/K๓R1้tด9ำๆCtŽj๏%=สlไแU3ษŠ$V๘ฐน‡฿q๔หHU ่L[ื-w›ฉษ=<\๑eDิ$ท_5ๆ•กื้้่้ๅืy็˜๖ชใข|๛ถHIˆ' ฒ๋ภaž{ƒ&sนjั._ถ“qrบzBDบ\ดttฑทชš๊บอฤ๔™Jผฮ.~๕ไsšซฤBJ NF)@ พ๕๊.5ฮjึL{ฝม๗ฬอ%7.๊+Uฎœkๆฅลฑ<+™๘(ข-ฦb Iภ ๑ิม&^ชnัไx๚ฮ‚|V็ค`า้hqQฑ๙=c^อลaโŸVฮdFŒ`XaKS7•;kพาฑTgXฆR‘•BLM๎}๐กuช$UศU>๋•—2ฟฌƒ^Ogoฟy๚†ฺ[ภ?ฦลwnป”„‚ก{ๆู7฿ำT ลนYฬ..$oF1Q‘H๗ำP8ฬฐs”–Ž.ญๅภัcšหฯx%^zŽ๖๎~๕ไsธ<ฺ’BH มษ!%ภ7_ฅ&hHH๙B!๎›GV์๘TชmM_ฑrๅsIF"‹2ษ‹$มjF/หใ๒BU้s๛8ิ=ฤ‡อฝš[{ X•ŒAงฃeุEลๆƒ๔นดทฎNผีฬ?ฎ,'#ฺN0fSc7ฟฺU{Jวbjjpช"j’›/_รย™e๔zบ๚๚๙ํ3/28ฌฝด:์vพ๓๕IKL$ฑทชšง_GmฯอLgAy Yำงแˆดa4?u/ํไญ>d฿‘อŽืฟฎฤ๛ฯงžgิฅญJ3‹iQ'~ี+*p˜AŸš65vkfMฉ\Tศ๒ฌd :™ๆก1~บ้ ƒฟๆ๒’hณPฑฒœt‡`8ฬ†๚.~ณ็๔TWจ NI–ึoผ{ีใโt๖๘ึพZ‘ฉVO]s+฿ธ๑j’ใใ †B์>Xลsomะ๔8พแาU,š3^‰ื?ภ๏žั^%žR@p2BH p๗ห;ิd{„fฺR๎›Ÿฯ๔(+p˜ww๒๛Žk:฿Yฯšœ :m#.yJ“ขY0-žฬ;v“รค˜ †่qyฉ๊ๆ†.š†ฦฆT,ฐธ˜E‰่e‰บq!ๅ „4—“Œh.+%-าŠ7ๆอฺv_Fฯฉ@Šฒ~ำ=kล‹)p฿Šฌ{L‰จIฎXฑ„ๅ ๆb6้ๆO/ผB{wฏๆ๚หd4๒ƒ;o!=-EQ8Tsœz๑ต)ัถฤธX.žUFA๖ bฃฃ0่d™pX9!ขช7ฐ๋`^ŸŸ่ศHพs๛ค&Ž/ะพท๊ฯผ๑ฎฆว๓ตkVฐxฒo`?<๛2ฺZ[P)@ 8!คธ๋ฅjJคv„TXQ๘๖|า&*W>มŸฮPๅสูโ‹ X™5น๘o๋ 8มAเ(1FrcฃศŒถa7ะหaUลฏ˜ฺ฿9ศ†๚.:œ๎)หKJธ8=,qผ”m7ึ\Nฒb์hY))๖ผมฏีถ๑ไฦณu๚mŠขT1๕ๅธ๏Š Uฏฏ๎<็ปl้"V]ผณษHะ๕โซดvvkฎ฿ z=?Xw+™ำRQ…รว๊๙ใ๓ฏœำ6%ฦลฐ ผ”ยœฤGGc6™ะ้dBก0Ÿžชjุ๋[Uอ˜๛“{_คอส๗พ~3iI‰šY๋oqีสฅ,]0gผopˆ?>๗ ฝ}ฺxู’`fa>ๅ…๙หๆ—‹๛™@ |๚)„”@ ภ?o=ข๖Œyฐ™ šhoXQ๘ฮ‚R##๐†ยผQฦใ๛4ƒOฏปิ44ฦ;ว;(Jpœ๘~ทY–(Lp0-สŠอจว “ )*ž`ˆ๎1๛;ูิะE‡๓nเฒRL‹G'K๋w๒ภ{๛ „อๅ$7.’–”’lทเ †x้h+ฯj:อb๊ pถEิ$k_ฤ๊Kb1™แฯ/พNsGงๆ๚Oง“Yฟ๎6fLOCUTŽิี๓๛g_>'m‰vฐ ผ„ข์$ฦวb1™‘e™p8Œื๏ฃo`ˆชใ๕์9x็ุษ;อii=ฌ/สหณ|แผOUโฝJ{wฯิ~ษ’$ส r™SRฤ๔”$"mถef“Qวเำ๗J!คบF=jเ({๛้๕b3๊งt{Uๅ; ๒OTฎผZำฦS5ƒฟ^wiC]'…ŸR“ xu2๙๑QคFE`5่ั๋dยŠŠ;ขร้f_็›บ้uyฯI,?Y^ฦผi๑่$จ้s๒ภ{๛)ฺ๛พอโ‡KJHฒY๐Cผxค…็7Ÿ“ถจจฏeuึuk[ฤ๋ึUT8ŒA}…$๑ƒsqU‹p้โ‹ฑ˜อ ŽŒ๐ุหoะฺุฎฝb ึ฿u;ูำงกช*ี๕๎™ฯj"mV.ž]Na๖ ’โNˆ(EQ๐๚๔qดพ‘ํ๛~ฎˆšฤl2๑;o!}b=ฌC5ว๘๓[๋หrูาEฌผx>“‰กโkดtvMู๖ฮ,ฬg^Yำ’“ˆดู0๔ห$Iฺ*๎Z@๐ฉ๏_!คTUUa…!ฏŸฦม1๖ด๗ำ>โฦ>E+ฆTu|อฅd๛ธ(xนบ•gชš4ƒNLs›\w้๚ฎฯR“ xDtไฤE’A„AN–ฦลT0Dˆ›ผWืyึwธ๛ูŠrๆคล!G๛F๘แป๛ะโทmQขƒณธ˜$›W ฤsUอผTrNคจ<า)บ˜ZWQแะ๛๔๋u2๋‘‰:WํXพp._[v f CN'Oผ๒&uอญš์ำ๕wFN๚tTUฅฆก‰฿<ยY9o„ลฬลณห)อห!)>‹ูŒNึ๏0๊๗ำ;8ฤฑฦfvจขh๘oฯ`ะ๓ƒ;?™~XU[วŸ^xUำใ}อ% YsษEXฬf†‡y์ๅ7hj๋˜Rm”e™น%…”ๆ็’‘šŒfร q+ 1ๆv/‹‹vlO\@๐ BH เ …UฃN V๖จprฐkˆ–Vริซ˜๚๎‚|’์ใขเ…รอผpD๏็?ZZสย้ŸLs๛ ฑ›‚xว฿ˆ/0>•/Ajิธ˜’%)*ฎ@ง‡=mผW฿‰๋,ํtWฑฒœูฉใB๊H๏0ผท_“9)MŠๆ๏/)&มffฬโ้CผVำ6%ฺvกŠฉฉ"ข&Y2o6WฌX‚ีbaุ้ไษW฿โX“6S๒;o!/3•ฺฦf~ิ๓g๔|f3๓สŠ™Y”OR|,6K:UฏˆแXc {ฆซ๏‹๏\(ห2wืิ˜~xบXyั|.]z๑ธ๘œจฤk˜"•xzฝŽน%E”ไ2-9 ปีŠAฏ  3ๆrัึรกc๕ห๎พ๎สญโ‰K >A)@ ถท๔ชูฑ‘D[Œไq1TFผG9ุ=D๓ ‹A7%ฺซช*๗_THโ„(xถช‰WŽถj:Ÿžๆvดw„mอฝฤฑ๗mง?@Uฯ0%‰ัฬLŽ!ษnSฒD(ฌ0๊า๎tณฃฅ—ปฯ๘Žwญšษฬ”X$ ชg˜mะฆ*OŽแ_RDผีฬ˜?ศ“y๓ุิ™Žฅ*8ร2•Šlฌบn้ศ๙~Ÿบ๗ม‡ึIชT9Dิ$‹ๆฬไ๊UKฑZ"ๅฉืฆฆA›ีš๗฿q y3า8ิสฏž|๖Œœวd42ฟฌ˜๒ย]Utธg˜:๚ษމJวjwบ้๕Rœไ 4)š›ณ^‡,APQ๕hco{?›บN๛xฟX3›ฒคhTเ`ื ?y &s27-Ž๕aย้ ๒ง๋xฟa๊.$|พ‰ฉฉ,ขNŒ‘า"nธtvซงkŒgx—รว๊5ู฿฿นF ณg !Q฿ฺFๅcOŸ–ใ๊t2sK‹™U”OZR"vkzP ƒ ŒR฿ฺฦช๊ำถ wLŒ เฬV{-.šUฦ5ซ—c‹ฏฤ๛ห๋๏pด์ŠO“ัศผฒbJ๓sHKLภnตขื ล@0ฤจหE[WGŽืำ78D„ล™ฯ !%'#„”@ ๐Y!ใHฅ'ฐtF9S๙ŒบqกPTœอรc์ขqhฃ๎์Š),๑ฝฤYวEมŸ๗ีณกพSำ9๘tUัก๎!u ‘m;ฅcถ;๔บฝ'FS’M‚อŒIงC’ 8!›†\์h้eKS๗i ๏‘ตณ)IR๛;๙ู&m ฉำใ๙ยb"LŒxฃใli๊™๒ํVTZeIชุx๗ชวตุ๏๗๘แฅ:ิJสฆz[gpำๅซ‰ดฺuปx๎อ ฌ9ฆษ๑~฿ญ7Pœ“…$I4ดตS๙ุ_8•วdIšmญ„้ษ‰ุmใSบTuผ’fxt”ฆถvS“Sภš†ฦ8า3ฬ๑ัำฮ4zYๆ…๙ใขภเี๑Acทฆs๐ฯkfS:QUt sฃ}#Lฒž–cท;๔{|”'ลPเ jยคื!๑Iๅ[รเ4vณฃฅ๗”vฤ“€นtE `_วธ๙&sฒ(#‘๏.ศ'ฺbdุเ7{ŽฑฝฅW3ํืš˜บ็ว/•ตB’ฅ%Z้ใ๒‚.>G.^|็3&>ํึๆL์š—๗™Šจ@0ภ๐่-]Tีgdิ๕7Eิ$BH ม็ผ!%_LHM2+%–YษไลGg5aึ๋คOอnqOๅC’ฮฤหตŠอhเพ๙yDOˆ‚_๏ฎeงEม$zYโ‘ตs(Lˆ:!qZ†]$ู,gไ|ํN7๎`ˆข๙๑QD[Lu2’ะธ˜ช้แรๆ^๖ถ91eิษ<ฒv6๙๑Q„•=ํ|หaMๆeyV2๗ฮหลa62ไ๑๓่ฎฺ/SŒmŠขTlบgํ”x)ผ๏Š Uฏฏ๎ิj‡ๅdq๛U—แˆŒฤๅ๑๐๊ฦุu@›๖7\Myaฒ,ำฺัลฃO<ƒ?BŸ-ฬžมา"ฒฆงeทc4่้ฤnkอ]ฌ9ฦฺ๊ณห=7^CYA.ฒ,ำาัลฃ?C ิ์…[šŸรญW^zFลgคอสา"J๒r>Sฅช @‘ฑ1š;ฉช=Žsฬ…Eิ$BH มษ!%|9!5Iyr +ณSศฏ˜2๋uศปนWLนจ๊ฆap๔ดถ5ค(D[LŸฟ]หž6ํŠ“^ว?ฏ™M~|$aEew[?]ฃโญๆ3zvง8L~ค˜฿]QU!3่๑Sำ็dKS7๛;ฟะ1อฑไMฤฒณตGถัd^ึไคr๗œขฬ~Uรว็ร%Nลิ๙ ข&)ศสไ๋ื\Ntd$nฏ‡ื7mc๛วฺU๒ฎ๋ฏbfa>:L[g7ฟ|๒Yผ>๘™์๔i\4ซŒำำˆŽดc4ว+gC!ฦ\nฺบ{ูwไ่YŸฦx๗๕WQฉX}โY|~ฟfวYqnท_๕5ข์v\ฏlุฬ๎ƒงG๔ญV.šUJA๖ Rโ‰ฐ˜ัษŸ์~82:>5๏h]ร—ชˆ๚k„‚“BJ ๘jBj’Y)ฑ,ฮLค(มAฌี|b7ทขโ๒iqqดw„c}ฮ๑นiงH ฌ`3sฯœ\ขฬ=~*wj[D๔bอ,rใ" MHœ~ทX‹้ฌœฟ้&V(IŠ&;ึŽรd๋}šŒๅฮkฏ`vq!zŽถ๎n~๕ไsธ=ฯำRY0ณ”์๔iฤDEa2Ž‹จP(ฬ˜MGoQ_uชณ‹(ศžมืฏฺ'โ๓ญl฿wj—QคอสยYe็d‘{’ˆvŽาฺุฮ‘บFฦ\๎ฏ,ข&BJ NF)@ เิ„ิ$eษ1,ษLค(1š๘ฯSฃ๗ Swjปั{ƒ!ฆ;lŸ;k8ะฉ]Qi2๐๐๊YdวFRถ5๗0๊ โ0ฯj;:F„•าคh2ฃํDYŒe ๐ร๔น}T๗ณฉก‹ฺ>็Šๅรๆ^mป6…ิ•ำ๘๚ฬ,์&}.พใ(UC็อu.„ิ๘zl9_๚0'c:๋ฎฝ‚‡ฯห;[wฐyืGšŒๅŽk.gNI!zž๖ž๓ษ็sป?๓7ำ’“X8ซ”ผฬtbQ˜F@"ใ๒z่์้็Pอ1๖T! ณXnฟ๚kฬ+-Bฏำำัำห>๕<ฃ.—fวY„๘Œ> โ3ึลผฒbŠsณIˆ&ยlAงำกช พ@€ก‘QZ8|ฌืwส"j!คเd„Nšค0มมŠฌdŠ$ุ,X&ฆ๒…ย c ]ฃ^ชz†จ%๘™uBdลฺนcB๔ป}๛๖ฃาฐ(ˆฑ˜๘งU3™c'Vุาิ?ค`3๊ฯI{บฦ๋0yh๕Lฒb" †วๅฺqT“yนถ([สf`7้้u๙๘ฟVS;|\๗BH:3ฆงqืuWํภ๋๓๓๖]lพ[“ฑvีeฬ/+—8ฝฝๆฉฟพSใY8ณŒ‚ฌ bใQฒ$Vยธฝ^:บ๛จฎk`ืC_xฉ3ษญW^สฒ z=]}}๚//0์ี์8;I|nูมๆ_N|ฦลD3ฏดˆขœ,bcˆ0›ั๋u„ร @€!็ธˆชชญร๋๓Ÿ65‰R@p2BH งWHMRลสฌ”q1e5c6่ัOฌ1ๅ „่๕pคg˜๚'/!ฆFA ขธต,๋„(๘ทํี้ัฎ(ˆทš๙ว•3ษˆถ ‡ูิุขจD๔็ด]c^๔:‰œุHfฤุฑ› ่e EU๑ร๔บ|์ไ†.Z†วซb#Lำส™dNศตปytW&๓rCI7•fb5่้uy๙ืซฉ9ล ฟฉ„RงNFZ ฿ธแ๊q!ๅ๗๓Ž=ผปmง&cนๅŠต,(/Œฤ1 ใ"*;“8‡ณษ„,ห„รa<>/]ฝTซcoU๕”šwำืึpัฌRŒ]}๖้ึ๎ต›5=ปฎฟŠX‡ฏฯว{๎dใŽ=_่ณq1ั,,/!?+“ฤธ,&3:LX™Q#ฃิทดqธถ_ pฺEิ$BH มษ!%œ!5I^\+ฒ“)IŠ&มjฦbะก“$BŠŠ;ขร้กฆw˜c_LL๘”'วpSi&6ฃž——ฆmQdทPฑขœt‡ 8ฬฦ๚. ฒŒQ'O‰๖๕น}ศไว;Hถb7ะ๋dยŠŠ'ขwฬหฎAฏ๏ย Vฎ5t๓ซตšฬห-e™\_œA„a|œ=ฒํว๛็อu/„ิฉ3=%‰{nผ†๘˜h|›wํๅญ-5หง%N฿เ05 MdNK!!&ณษ„N' …๑๘|๔ r๘X{UOษฉpืฏ]ษข931 t๗๐๛g_ขw@ปUด_E|ฦวDณpf)Y™ฤวFc1™‘eEQร#ิทดqคถ0xฦDิ$BH มษ!%œY!5IN\$+ฒ’)IŒ&ษnมbะ!Kแ 1ี5๊แh๏ี=#”0ฒ๔๙+ zฬ›ว ็‘(H‹Šเ'หห™eล ๓n]Vฃฝ$Mฉvx่ๅ๑ฉ|ฉ‘VlF=LhBLuz8ึ๏dvj,)๖ˆrํ7{Ži2/ท—gqmัt,=c~ฑ๕ศi฿5๒\"„ิiธv“ธ็ฆkIŒมฐuฯ>^฿คอw๎.[ล%sfb4C„ย!LF#:Y&V๐๚}๔ sไx=;๗WM้5™ฎYฝœ%๓fc2่โฯฝLWŸvwb2โ3)>–ๅใ"*6ฺลlB'ห(ŠŠ/เงh˜บฆVฏ' Ÿq5‰R@p2BH gGHM2#ฦฮŠฌdJ“ฃIฑG`1่ัIVU=c^v๑Q๛!Eมn2|ๆ๓n‹าฯ+QmใวหJI‹Rok'ฦbBšขํ๐๘1้e๒ใฃH‰Œภ:13<1•ฯ “1้d|!…w๋:๘ใšฬหบYู\Y8‹^G็จ‡_l=Lำะุys !u๊$'ฤq๏อื‘‹?dG๛yuใš‹รaแท@ึ๔4dYFUAUUUม็๗ำ;0Dmc3;๖ิฤZLWฎXยฒ…s1๔ ๓ว็_กฃงWณใ์‹ˆฯิฤๆ—“—•A\ดใ“ฉyแ๑ PืJu]@๐ฌ‰จI„‚“BJ 8ปBj’I1U–C’Bฤ„ิ)bสๅแPื{๛ )๊ 1ี๋๒ฒ|F๒y% fฤุypY))๖ผก0oิถaš๒ํ๒๚‘e‰ขฉ‘Vฌฦ๑ช7I’€ฐขRำ7ย?n>ฤจ?จนผ='‡+๒งaึ๋่pบyxหaZG\็อu/„ิฉ“ห}ท\ORธฺพ๏ /ฟทI3ํทZ,,(/กผ0๔ิdLฦ๑=Uผ๑ฉyต อ์9t„ํ์dzูาEฌบxf“‘ก๋ลWiํ์ึ์8๛Ÿฤgjb๓หKศ›‘N|t๔ฤ:_aๅีะาNck;พ@Y>7?u!%'#„”@ pn„ิ$ำVVfฅ035†$[Vใ๘Sa<มcชบ‡ูำึ‡;"จ(ฌฮI=ฏDAn\$,)%ูnม ๑jmIV‹&ฺ๎๔8ุ5DQขƒY)ฑคEYฑ๕'ชปผม0uNvต๕ฑฉก›1 ‰ฉoฮอๅฒผ4ฬzํN7๔AN๗ys !u๊ฤวDs฿ญ7’G d็*^x{ใ”oทลlbni1ณŠ๒INˆรf‰@ฏื๘`(ฤพ#5lฑ›ฎ^ํMu[ณ๘"ึ\ฒ‹ษฤภ๐ฝ๔:Mํšg‰q1|๋–๋IŽรฒc฿A๖VU3ฟฌ˜้ฤ9˜Lใ;*Š‚ื?>5ฏฑญฆถNมภ9A)@ 8!ค€s+ค&IŒ`UN 3“cI‰ฏ˜าM์่ๆ †้๓Rี3ฤ,ฯJๆาqQะๆt๓ะ‡่pz4 Qpq ‰6 ž`ˆWถ‘lทh*†vง›๊.อMeษŒคฯ,ศ +Œ๚ƒดŽธ?{๏วu๊ฟปณฝ` ,z๏ฝ›%ช’bษ‘-ษE๑M์ุq๛In๚Mrปพ7ืNs|ุฒ-ห*–ฌb‰”HŠTฅ(ฑ ขwA5› IDAT`ภb{›฿ @$%+†9๏_|agฮ์๙fฯฬซ๏|ฏyyed–H2ตๆฏ้‹›šธตกฃ$1๎๓ตG™ Eฏ˜๛^ฉ๓'ฯ้เ‹GI9•b‘}nวšฏ^ฏcKWM ”bต˜ัI้t†t:$Ih4เ๓๘ึฃO213งสyนi๋&n฿v5“Ÿ฿ฯw๒,Cใ“ชณ|W._|เใหqฆเ]\4ธœ9๏ี๙Jง‰ลxšœblrfMˆจ„‚๗#„”@ ฐ6„ิ %9nช+ฆป82‡ซAทœ1•ญOไฤษdฒ…ภ๕Z-ใ0s๗fC1ี~m…N๛uํฺL„)ž๊ฃฬaUuผ9แลŽ๓;)ฐšศr:ƒคั iฒ&d†ƒ์ŸœgืเฬYuVผ\|yK37ืฃ_Rน๋žp์Šน๏…:œ9vพ๔ฉ๛(+,DNฅ8p๔<๛โšงN'ฑฉณฎๆFสŠ ฐY-่$™L†ค,ณ่”eJ ๒1่๕ฬ๛็?elzF•๓ฒmำ>zใuXอf|฿๚yNŽŒฉ6ฮ\N_y่A ๒rP”4&๙PQˆลใx| ŽO0>=ทฆDิ BH ม๛BJ X[Bj…’ 7ิฑก4ŸR‡›A‡N›ํ่–ษdะ-ืม˜ฦ๘ฃ๏โ วU๛wป๘ฝkZ)ฐ™%dž86Fฅำฆบุ๋?97็ฏoงุn&ก(Œ/E0๊ดฺฬuZM6c*—๑…xsยห๎แYไ5(ฆ~gk3kณBjิโ/vf!šธb๎{!คฮŸ›•฿๚๔'(/*"ฅคxง็8?๘้ฯึฬ๘ดZ-ฺ[ุะBYQ!๖e”“๘A†ฦ&ุด—บŠ2nฝv+f“๚ทน]ปก›ปo†ีla)ไ?'†FTy-5ๅฅ\{ี:6ดท`ะgk)f2 ( ัxœ๙E'GวŸ™%•Zป™งBH ม๛BJ X›Bj…B›™k‹ุXๆฆ$gYLIฺีE‰”ยั9^89ล o@•฿บา<พzu nซ‰เฒชRก:0ต€7[~‘S<3สqฏŸํตลดๆโถ™0-ืช‘•4x’_˜}ฃs์CIฏP๊5ญlซ.B/i๖๙๓—ณK^1๗ฝR็ีbๆท?๓I*Š‹I) {๛x๘ฉ็ึฤุ:›ุAEq!9vzŽL’ฒŒ?bdrŠร}'Y\๒“RRด7ึsว๕ื`6™X๔๛๙žŠทนm้๎เž[ทcณX„B๐™่Rี54TWฒพญ™บส ๒œLF#šๅ…/)หŒOฯ286ม๐„:ๆH)@ x?BH k[Hญk6pkC)W•ๆำŸƒqYjd€h2…'ใธวฯพั9zๆ–T๕o,ห็wฏn!ฯb$—y์ุ(ตนvีลัม™Eผแฟ]E63แdŠGŽ๐“๑์ rฑ‹›๋Jh.po1bิIh€ค’ฦK2ดdืะ oMฌ"สฟmืUขำjZ ๒g/ReทภŸ‡R็ูdไw>s?ฅล(Jšร}|๗ษg.๋˜:›ุะึBuy)9ถฌˆZ‘มp˜ัฉŽž8‰ฯฤj1ฐ่ะ\[อGnธ6[w)เแงžc`t\•๓rUG+ฟz๛อุญVแ>ทƒฃ'T1๖ฦš*ฎjoกถฒgŽฃํ\ชFC:arnŽgwํ[^ีR@๐~„ิ!คVp™ีอิๆูัj4d22@:๑”ยb$ฮqฏŸWFๆ8:๋Sล5mญ(เห[špYŒ๘cIํฅ!/Guqtxึ—Rืถ-o?L๑ศ‘a~ฺ7q๚หbY>k‹it;p™ ซr1+ฆ๔{ผ:๊แอ ๏eฝž?ธฎkช ัi5 ,๙ำ—ฉข๛ู"„ิ๙c4่๙ฯ>@UY ้tšฃ'๘ฮใO_–ฑด7ึฑพญ…š๒2v}VDษฉP˜ฑฉŽใY๐ญŠจมตๅนzฌf3Kหu—๚UZwฉปฅ‰Oy+9VมH˜ว๖{Oฌ้17TWฒนซš๒ฒฌˆ2่ัhดค”ฑxฝNั`@Q๕๕sเ่1Uอ‰R@๐~„ิ%ค่๚vฎฎ*Dฏี“Š‚อ G/iIง3ฤS ั8ป†gืผ˜บฎบ/nj"ืl`iYH5ชPH๓แ ว๙ฝk[q[ณ๕ฐพh˜็๚?xKษี•\[UH“Aฎูธฺ™/ฉ(,F“œ๐๚yuฬร“—'c๊ทuฐตยคีะ?ไOv$žRฎ˜๛^ฉ๓Gง“๘สCR]^J:ฆง๗ุS—t M5Ul๊jงถbED้ )%E0YQณ…๗‰จ‚แ%ลสRwฉฃฉž๎บ‡อN(แษป8pดwMŽตฉถšMmT—•’๋ฐcXฮˆJ))"ัs ‹๔ ฑฎต™Š’ขU๑นHบึ!คเ}!%จOHษถถ,‹‚ม…ฏyุXžO™รJŽ1+ฆ2™ฌ˜šฤ้Ÿฐgx–ร3kSLX[ฬ็ฏjภi6เ‹&๘Qฯ(อ๙ีลั1ฯHดzX฿}w~qaไ๋ช นถชฦ|นfร๒ABQ๐E๔y์šแศ%‹zC›หณqvยเv\“ลืฯ!ค.ภƒคFรWํAj+สษd2โ฿}๒’œปถฒœญิV”Ÿ.2R)‚แ0Ss^๗ฤณฐ€ลl๚…ว Gc”ธ๙ุ-7žRw้g๔ ซr^ฺj๙ิมaทŽFyj็n:ผถN{c๋Z›—3ข‰Šข‰ล˜๕ฮำsrท๗”eพ๒ะƒิT”‘Ig860ศจjN„‚xŽBJ ิ'ควl,w#i ฯเ›o๖a3่i/สฅปุEEฎ ปQ^ซY‹ั}^?ฏy/[ฦอฯใๆ๚~}C“žลh‚GŽา์VŸ๊›๗ใ ว๙ส)๕ฐใ^ๅญใ5ภ5U…lฏ+ฆึeวนœ1uชX์๓xipšใ%3-ะ็๕๓;r%=7!ua๘๊ฏ=H]e™L†พก๕‘ว/๊๙ชJKุNCu%.‡cyk—†TJ!2ํ๑r์ไS&ƒแฌŽ‹'ศฯu๒๑oยfษึ]๚ั3/าsrP•sา\Wรงๅ#ไๆไ‰Eyๆๅฝผ๖๎แ51ถฮๆึต6S]V‚รnCฏหฮŸข(„cQฆ็ผ๔ ๓ึแโ‰๗บz~๕sŸขnY|๖๓๚ป‡T5'BH ม< !%๊Rฑฝ‹ e๙hใ^?฿|๓ลv3ํึ่vะ^”KMฎ ปษ€^ซ!ษn๓E๔ฯx}ฬ{ูkญpGcญฏ#วจg>็ฑccชฬ๊_เ ว๙ญอูzX๑$฿>0ภžแูณ>†^าฒญฆˆ๋ซ‹จฮต‘c2`XžฟxJม‰ำ3๋cืะ,'.nWลฟผฉ‹๕ฅู8๋๕๘๙ร๏^Q๗ฝR†฿}่ช*ษแฤ๐(๒ƒว.สyส‹ ูาICuyนฮีbื๏‰ŒyNŒŒ215‹ม ๛PวN$ev๗q vซ•`8ฬฃฯํเศ‰“ชœ“ฦš*>๓ฑโr8ˆฤb<ฟ็U๖พ}y๏฿ฮฆฎ๊hฅฒด˜[ถฦืฉ"qjฮKOzŽŸ&ขV๘สCPฟg}C#ผzเ jๆ#“žA!คเL„ิ'ค็Mฌ/อC›[โ_฿๎งะf>ํod%Mmžฎb5.๛ib#ฉ(๘bI‚ผ22{ูปบี\ฮงปkฑ๕xรq?6ฆส ฉมล Hœ/oiฦe6เ%๙ททOฒot๎Cหจ“ธนฎ„-nชฯ‹๑”ย\(Fฯœ—g๖….ส๕ีอt—dใฌgn‰?y๐Šบ๏…บ0๖g๎งฑฆ€“#ใำ๗ฝ ว/v็ณu]'อuีธœL#ZญEI‰ล˜š๓prdœฑ้๔ห >,rJมb2Vฑ็_โะ๑ชœ“บสr~ํปp9Dใ1^๛ป|๛ฒŒฅณฉmT”’•-6ŠD™๕ฮsฐ๗{๛H$~ฯ฿๙์4VW‘!C๐{฿~G5๓!„”@ |0BH ๊RงŠ‚#ณ>พฮึฎ‘’H)TๅฺุX๎ฆาiลi2dท‚‰Tถซะb=ร—OLำZษ5ุ:<แ8OฅูํT] ๛BฬGโ|i๓{ฺe?ฏyฮ๙˜ฝŽ[๊KุX๎ฮfผ๕่ด” ฤไžpœร3‹์œfยน ื๓7ทฌฃณุภัY๚าก+๊พB๊ย๐[ŸอตีhะprtŒo>|a„Ta~[บ;hฉซ!?ื‰ษhD’ดคR ัxŒน๙E๚G˜˜Cฃ9ฟs)้ Ž๎บ›P$ย/พฬ;=วU9'5ๅฅฺว๏&?ืI,‘`็koฑ๓ี7/ †Fร๚ถfบšฉ*+!วfEฏำYตฒต๒๐๑~9vYๅ;ฟ้OาT[ตg{: š๙Pาz‡F„‚3ื !ค@}B๊onYGWฑ‹ pxf‘|w|๋/.ฺ›ช]v6”ๅQ•kรฑ,ฆ +ฆ–โ ๆƒผ1๎ๅี1ฯ%ญ๔‰Žj๎kฏยขืแ วxฒwL•Bjฬf>็776f ดว’ำ›}D๔9Lnฌ-bkEๅN+๖ๅฎŠฉt†จœb.ใะ๔"/ อ0ธ0b๊oo]OgQ๎jœ—_Q๗ฝR†/=x-u5h4ว'๘ฟ฿}ไผŽ—๏สesW;-u5ๆป0MhตZE!ใ]๔qbx”ฑฉเยNi4ผ๋ŽๅBเžฑ›ทSๅœT–๓฿~๕cธ]Nโ‰$/ฟฑŸ๖พ~ัฯ+IZ6ดทาูิ@EIัฒˆสnŸ”S กH„ษY=ผำs9•:๛8๛ิrœกaplฒe| Š’ฆwxT)@ 8sํBJ ิ'คึ๕ด/‹‚ƒำ‹<|hˆ<‹๑ฌ>—j\vบJ\หลณ ่ตY1•Tฒbjh!ฤ[^๖ฮ‘J_ฏๆฎ>Z‰Yฏc6ใฉใ๊R ‘8ฟฑฑงษภb4ม7฿ไ”B฿ศ˜R@pๆา+„”@ จKH้ดถ ด8P2p`rž๗Œโ4ฮ้xแ„Lนำสฆr7ตyv\f#F้ฝŒ)<ษˆ/ฬ‘Y^๓^1๕น ๕ูTŽI'1Œ๐ฬ‰IUvู› EYŠ%O๋๘o๔qhz๑ขณศnๆึ๚RบK\”9ฌX๔:ดšlอ’p2+ฆ๖Oฮณs`š`Bqฆๅ๏o[Ÿณt†ท'๘๋W„:_ฎD!๕๋๗}ŒฮๆดZ-cS3|ใ{?")๒Xณšอl์lฃปฅ‘"w>VณI’Hงำฤ“ |~FวŸž!–H\‚+ั๐ภท‘ป†:ำ+…๙.พpว)v็“Hสผ๎ažฑ๋‚_ฏืฑฑฃฮๆ†S2ขtd2 งd‚แ“ณsผำำว‘ค/@ฆํฉq6:9อŽW฿ไBnูผ˜$ๅ'Fว…‚3W^!ค@]Bส(i๙ปึำไฮŠ‚ท&ๆyฒw ว9 ฉ"ษeหช˜ส51๊ดh8]Lฝ9แe๏๐, ๅย‰ฉฯolเ๖†2L:‰‰@„็NLจrห\8†?žไ3ห็#qพฺqŽฮ๚.๚น+œVnช+a]Iลv3ๆๅŒฉ”’&”™ Dxcห๎แYBg!ฆŒ:‰ฟปu=M๎”t†7'ๆ๙ปฝ=Wิ}/„ิ…แsฟ›ฎ–&$Iห๘๔,฿|๘Qโฟ@ ™Œ6uถำีาHIก›ูršˆ๒๙ƒœใp_?Zญƒ^wฉ‹นฃทฎvฆ{แ•ืูญขยูง’๏สๅ‹|œ’7IYๆอC=<๖ณ็ฟ๔l๊lงญฑŽ๒ขBlV :IdV3ขฦงg9ืฯกใtK็~๕W่niDซอฦู {_Sอ|$’2cBH ม™+ฏR@ .!eึ๋๘ป[ืัŸoŒ{yบo‚ฃ‚?œLQแดฐฎ$Ÿ†\fIสถ้Vาโ2รพฆๆู=4K<ฅœ๗9ฟดน‰[๊K0Jใ0/œœขQ…R๓ั8x’:kW;~ต^Žอ-]ฒ1T็ฺธฅพ”ฎVำib*˜[ ๓ๆธ—]รณฤ~Ag+‹^ว฿.วYj9ฮa฿ฑ+๊พB๊ย๐ู{๎d}[ :Ibbv–z๘วDb๏ฏ_ฆืุ้AGc=ๅ%…ซ"*“ษ“ซ"ช็ฤ Yฦj1]โงb Ÿศญไ9ฤโq^|๕ ^~}ฟ*็$ื‘รo}๊>J S)๖้แั็vœ๓๑Œ›บฺ้jn คภีb>MDBaFงฆ9าืฯแพ“ๅšบ๗.ึต6ฃ“$ฦgfy~ฯซฉ–ุ…G)@ ๘9KฏR@ .!e3๊๙››ืQŸŸC*ๆตQฯŸœฤfะ_ะ๓„2e ๋J๒hr;ษณœ.ฆVไฦ[๓์ž!&Ÿป˜๚ํ-อTWŒ^’[ ๓สศ,UN›๊โh1– —๙dg56ƒŽนpŒ๕j/ว=K>–๚ถืำU์ขภfฦค“ะj@Ngล“ -†ุ?9ฯฎก’ํf7๊๙›[ึQ——ณWG=|ต+๊พB๊ย๐™}” ํ-่$“ss๓๗#yฏำฃN’–ทๆ5QVTpZVM")ณ206ฮัฤโ‰K/ขVŠัp฿Gn!?ืI,‘`็koฑ๓ี7U9'96_๔'(+*$ฅค8ะsœ๔g๚8ณ‰ ํญtท4R\อlAง“ศd )หB!ฦฆf8ะำK๏ภ๐%ณ็vฟŠZถ์ลIฦ'…‚3ื^!ค@]Bสi2๐WทtS๋สAVา์cวภ4VรลูึJศุฬlฉpำ˜ŸCžลˆQ'eท๒-หqv;ุั9ยขNั _นบ…jŠัKZF|!L.Ph3ฉ.Ž๑$ม„ฬ}ํUX๔:<แ๐๊1Nx—mLอnช-กฝ(ที„Iwzถˆ/ฤ๋ใ๖ ฯV์ƒโ์ผ~Šบ๏…บ0ะจDHล LL !%g „”@  .!•g1๒ต›บฉvู‘•4{†gู=<ƒ๙"ืY %d๒ญ&ถVธit;ฒbJ’ะj5ซSใKa๖Oฮณwd–@์ลิ๏]ส๕ีE่ดZ†ƒ๔yุ/pฦืฅ ”” &d๎mญฤขื1Š๑๗๛zX^๖ฑตๆrs} - ฌฆ๗คโr}ฐแล{– ืg2™ŒณoผูwE๗BH]๎ฟ๓66wu ื้˜๑z๙ืGž ขธˆmT–‘cท&ขกฃ“ำ๊๋'Šฌ ตXฬ}wŒ•K<‘dฯ[xnฯซชœณษศ๏|ๆ~*J‹Q”4‡๛๚๙๎“ฯาฯY-fถtwะRWCiQAถะผ6ปญ2)ห๘C!F&ฆ8ุ{‚ใƒร—5ฮž~y/•dHEcq'ง…‚3W^!ค@]Bชภfโ/ทwS•kCV^še฿่&tIฮJศธญ&6–ๅำ\เ\อ˜าj •ฮJศŒ.…98ฝภหƒ3gีูํฎkใšชBtZ  Aฦ—ย่ดZีลQDNJศ|ฌฅณ^วl(ส฿๎ํah1ดfฦุY์โ–๚šWคโ)bสK2ธdฯศ,C‹ม๗ลู?ฟuโŠบ๏…บ0๗‘[ธz]'ฝžฅ@ษ9%nvzžS ^Nอะ{ryŸ ‰จๅ‡b4{๛Mๆนˆ'“์.ฯ์Rง?0่๕๎CPUVB:ๆ่‰พ๓๘ำ?๗๏ญ3Wฏ๋คญกŽBwV“yตพWR–Y Ÿไเ๑K–๕‹โlฦ;ฯS/ฝข!‰ฦšBJ ท๖ !%๊Rลv3ฑฝ‹Jง„ขฐs`†7'ผคK+pV2ฆ6”ๆัZเ$9๋fฅNQ8!3ˆp`jำฟPLษถถTธ‘ด๚็ƒxยฑ า&RK)„“2w5W`ึILณBjฤZscT๎ๆ†š"Žำ ื'Ri–b ฦ–ยิๆูษท˜Vใ์฿๎ฟข๎{!ค. ฟํ&ฎธƒ^ข((iฝNhSห"jršพกๆๆืœˆ:•{oป‰ข<I™}๒๔K{T9'’คๅ+=HME™t†ž“ƒ|๛ว?y฿฿9s์l์lฃฃฑž‚<ณ Jก๙คŒ/`xbŠG{›ธ์qvอ†nŒ=ณ๓ <ตsทjๆ#1<5#„”@ œR@€บ„T™รยธฑ‹ ‡•xJแล)L. —.OFQ(!“c4ฐฅยMKก“‚ๅ:EงŠฉ้`”“๓ผ2<หB๔ํเ์†N6Uธ‘4ะ็ 0แc”$ีลQBQ'SูTŽI'1ˆ๐ืฏ๔0๎ฏู1_[Uศี•48p™$mvk•’Fงี “ด$R ฯ๗O๒ํWิ}/„ิ๙S_UมwFIFC๖ฑ2ณ,ข"Lฬฬัำ?ศยšQ™LVzนณB๊ตw๓“ปT;7_งจซ('“ษะ;8ฬท~๔ฤ๊หs:ธชฃ5›•๏ยb2ฃำIคำiโษไr}ฏ)๖9ฦ๐๘ไšธžr#ืo\ั gnaQUsŠD™žBJ ฮ@)@ @]Bชาiใฯn่ lYH=ื?ษแ™ลหพล-”ฑ๕lญ( ฅะ‰jยฌ“–kL…’2ำ(๏L-๐สศž๐{mแb{ส๒ัวฝ~๚ผ~๕5—ำi"ษw4–aาIL"|mฯQฆ‘5?๖kซ ูVSD}^นfC6ใNฃAd2ฦ๙ญ›[บb๎{!คฮšŠ2ถtwPWQNAž ๒–แL&C0a|z–ฃฬz็1›ŒชธฆL๎นu;%๙$e™7ๅ๑^Rํ๎CะPUI† '†F๙—>Fžำมๆ๎šjช(r็c1™$-้t†D2‰/dh|‚ท๖221ตฆฎ็๎›ถฑm๓LžE?ูฑ ตผว„ยQFf„‚3BJ P—ชqู๙“m”ๆXˆฅžํ› วณ„คัฌ๏„Œอ gs…›ถ•Œ)ฝiนฦT8™b2แ๐ฬ"ป†f๑„c|ํฆnึ•ๆกzๆ–84ณH•ำฆ>ม‘ษ‘SึPŠQ’๗‡๙ฺ๎ฃฬ„ขชฟVฃแบ๊Bnฌ)ฆ!?+ฆ4หq%+iฆƒQŽ{ผ<4CŸืฏ๚๛^ฉOei1[บ;จฏช$ฯ้ภhะ/วˆ&ปๅ3)๓๒๛้ลจฒฦ้ sห”ธ‘S):|”?ฟSต๑ŸนŸฦšJF&ฆ› ฅถwž ‹ษ„Vซ%I“Lส,๚ ŒM๐ฮัใŒNMฏษ๋๙่ ืฒ}๋&LF๓พ%žฑ‹๔)Aื2มp„ั™9!คเ „ิ%ค๊๒r๘“mํ-ฤไOŸ ฟ*ึ ก„ŒEฏcSน›๖ข\Šl&Lz ญFƒฒ,ฆf‚Qอ,าQ”Ks€ฃณ>^๓ะ^˜ซสXŠศ)nญ/Aฟ,คrื‘ำฒมิภ5ู IDAT€Aา๒ษฮj>V…I'eหg2ค3O)xย1zๆ–ุ54ณ&:žณ€B๊ฌ)+*`sWMตีห"*++•ด‚ข(่uzดZ ๓พ%ใ๑ŸbณšU>vห” งRผ}๔?z๖Eีฦ๗o}๚ดิึ ีjI$“ค”fใ{Q๑d‚…%?ƒฃผ}๔3sk๚znฟjnพf3fฃ‘…%?Oผธ‹tZQล\Baฦf=BH ม!%จKH5บั๕ํูฬDๅO๔Ž1ผZณใ %dŒ:‰อๅn:Šr)ฒ›1Ÿ’1‘S่ตZฬz %แะฬ"/ ฮฐนญสXŠ+ 7ีฃ—$ฦ–B๙ฎ#,DโชปŽฦ|ผญbป%IEAงีขำjฒbJNแ‰ฤW3ึbแ๖_. „๚eป๓ูฒฎ“ๆฺj๒r˜ FดZ-Šข‰ล˜๑ฮ“LสิU•ฏŠ‚๏ไY,*ูฆwzฬF#ทoป‹ษŒฯ๏็{O=งบํzห๑ภ7@yq)%ลปว๚๘ำฯซ๊J l์lฃฑฆŠŠโขำj{Eb1Nฑศ1ฆ=^U]๖-นใ†kVใ์ษป‘Sฒ*ฦพ1แ๑ !%g „”@  .!ี^”ห๏_FกอD(‘โัža&Qี|ืก„ŒNซaKE]ล. lfrŒบำj -†8:๋ใ๘ผŸjงMUb*ษp]uzIห๐b?{๙0xRu๗D[a.บSโ์่{†gนฑถ˜ญ•”9,ุ z๔’–T:CTN1Œrpz‘]ร3Lึ~L !๕~\N[ืuาR[Mก;/+ขดZ%M,วณเcljšคœ"ื‘Cศfฃ‘p-Vณ_ ภรO=‡AฏS]ฬ+JšปnฺFeI1)Eแะ๑|๏'ฯชb์ฅ…l๊jงฑบท+ฃั€ค•XูษO$y๕Cผy๐ณ๓ ช\งทmฺภGoผซูฬR ภ;v!ห๊Rพ@IฯผR@pBH ๊Rล.~ฺVVก„ฬŒ0Œช๎;%d4ธฆฒ6•a^~อJ:MTV˜ F9:ทฤqฯ•*SืVขำj\๒g/"”U7?ล.~๏šV l๏ลู3}8Mnk(ecน›2‡๋rฆ›’ษI*L"šYd็เ4๐ฺฎ(„ิ{X-fฎปjญ๕ตๆ็V๔:Oเ]XdbึC<™ภiทฏ~nฅ]7]ีla)ไO?$iU๓)%อ]ฏงฒดEIsคฏŸ|๒™5=ๆฒขB6wตำXS•ญํe4db: $-™ L{<|;฿'žPŸ_]ะอ7o[ณ'w์"™Tว๕,๚Ly„‚3BJ P—ZWšวWฏnมm5Lศเ๐0sก˜jฟ๛ค’ๆ ฉqูะญ+ฅั€คษ Žจฌเ ว82๋ฃืใงยa]ำbJซีpue:ญ†… ฒ๓Q9ฅบyYW’วWฏy/ฮ>4ฤฯ๚OoŸo1r{c๋J๒ฒbส ;ญht0ย;S ฌŠเ”rBHอbasW;]-ๆปฐ˜ฬ๏ฝN$๐,๚˜œ™#)หุฌ–๗}~jึC4็cทˆอbม ๒รg^`๕X8+Rฉ4wnฟŽชฒา้4GO ๐วŸ^“cญ,-fSW; Uธœฺู^’ค%ฅ(ฤโ ๆ}่๕zŠ๒๓ะjตLฬฮ๒O˜HLฝkล–๎๎นu;6‹…@(ฤ“;vOจc;๔โR€ฉy!คเL„ิ%คฎ*ห็+Wทg1ˆห|๏ะ*‹fฏ`ั๋ธทญ’บผ”L†ฉ@„DJก4'+8tฺ์–ฐlญข8วๆ–่๕,QšcY“bJ'iูZแFาj่Ÿ๒ว;’H)ช›— ฅy|ๅ๊V๒ญู8๛ฯwู9๘มํเKs,TWยบ’zรuT——’Nง้้ไ=๖ิšcUY ;hจช ฯ้ฤd4 ัd‹ฬGใqๆ}Kฬxฝ„#1์V Wuดข“tLฮอ๑/?xŒ`8‚Zนชฃ•๛๎ธ9gแOํุM4ฎŽตoaษฯ๔ขR@pBH ๊R[*๖–f\#x’|wฅ˜zทaุ :๎iซคึ•ƒฌคู;:วพ‘9บK\ดๅRlท`3่ะ-gmฤไsหb๊ธืO‰ผฆฤ”AงesyVH๐๘ฃ‘•ด๊ๆeSน›฿ฺูLrœฟ์ž…Ÿฉฬตqs] ลฎีnŠ:ญYIJศŒ๛#ผ1๎aฯศแ51๕_QH๔zถtwะัTOYQ!V‹$‘NgH$“ูขื#c ŒŽS_U๑YQงฒ2ํ๑r฿ท`ทZ †ร<๚ีt?;•”ข๐‘mืRSQF&แุภ่“kblตe\ีัJ]eEvkžมํš—ฮfDy}ฬx็ั๋t8s์ŒMอ`1›ุิูŽ^งcฺใๅ_๘8Kม jืŠuญอ|โฃทcต ‡y๚ฅ=„ฃุ๊ฎ>๏[bfม'„”@ œR@€บ„ิตU…|qs.ณฅX’๏ผ3ฐ&ทC-zIหงบjจสต#+ ป†gyฆo‚B›IฃกนะI{a๎)Sาˆห)<‘8ฝsK๔yฺึ†˜2้%6–ป‘4pใ็w$ญยตvke_าŒหlภK๒ooŸd฿ู่ต‡ฏฮตqK})]%. ญ&LหSr:C0!3โ ๑๖ฤษ'๏ผ5+ "a{%’ฒ๚$yJIs๛u[ฉซฌ “ษp|h˜{ไ‰ห:ฆ๚ชŠeUNnNฮชˆJงำDใ1fฝ xั๋๕8s์ง EQุา‰Aฏcฺ3ฯฟ่ —ช]+:›xเฮษฑูE"<๒B*ษ๘๒..1ป(„”@ œ‰R@€บ„ิถš"~sc#Nณ_,ษw JชWHฅา>ฟฑ*งคขฐsp†NNแถšVFาhh*pะZเคiลnะ#"ฆๆฃ z=Y1ๅถ˜.ซ˜ฒtฌ/หG ๔zแŽwU9/ืV๒ฅอMไ.‹ฯู฿ฯ๋cžuŒฦ|๋Š้(สฅภfฦค“ะฎˆฉx’_ˆท&ๆู54C๒2d‘WR’คeC{+ฺZ(+*ภfต “ฒ ’rŸ?ศเ๘ƒฃใไๅ:ฯZDญ”ez†xเฎqุ์„"žx๑ebq๕m#Vาnฝf3๕U•dศะ74ยฟ๐๑ห2–๚ช 6uตS[Q†3วŽษp๊ึผฌˆš๑.`2NQ+Dc1–!ฎูะ…Aฏgv~o่Iผ‹>ีฎํu็ˆซฐ‹ฉ|ac#ฅ9b)…g๛&xkbž“—~NษdhฬwะU์ขยiลfิฃืjศd ก(ฬGโ๔y๔{8L†K"ฆœfล. +คLฅB๊ถ†ฌ๘ฬ1fล็?พqœƒำ‹ไุ๋K๓ุ^[L“Ižล€A’ะh ฉค๑ลœœ๒๊่o\d1uฅ ฉŽฆzึท6SSQ†รพ"ข@N-‹จษiF&ง0DDญpไฤI๎ปfr"ฑฯ๎ทFEม/็๚hฎซFƒ†ฑqพ๑ฝ]ิ๓ต7ึฑฎต™š๒2œ96๔:}ถXนข‰ล˜๑ฮ3ฟธ„คำ’็tž๕qEม็ฐกฝ“ัภผo‰xงŒฯฬชvhจฎไณ๗‰k9ฮ^ุ๛:ลล53>ƒ@EIu•ๅไนฐ˜M่$jVb พก0฿ต@ ฌ"„”@ 19•Yษึ•4แ„ฬL(สปS‹์c&ธvถŸRมงบjฐ๕xรqภIUl5yh5พฐฑbป…˜œโฉใœYภfะŸ๕1R้4Mn'ํEนTๅฺฒSห5ฆ’Šยb4ม o€๙6ƒ๎ขŠ)—ลHGQ.เะ๔"พ๋ฐ*็ๅฃMๅ|v]-๖e๑๙^?ฮแ™ [fkEืVาไvเ21HZเ=1ี็๕๓ฺ˜็œท 2ฎ!ี\[อฆฎvjสKqุํ๔:@CJI …›žexbƒ^AEิ วNrฯญq9Dใ1ž฿๓๓>uึ*บ๎ช๕ดิี ัhŸเฟ๛ศE9O{cWuดQUZ‚รnล ฯŠจTJ!‹ฒเ๓‰ฦt:I:—ุฦ ัีาˆูhbai‰|โFงฆUปVิWU๐ะ=wโr:‰ฦcผธ๗ ๆ.๏ฤt:ƒษhคฆข”š๒2 ๒r1MH’D:FNษ$’2IYๆ‘็v๐•ฯฟ@ ฌ"„”@ /ฮd\#&„คีRาDไ3ม(Gf}ผ22ว๘R๘ฒ๕žึJ๎๏ฌมnิแ ว๙ึ'Iฅี+ค$†/ljคุn&’L๑ฤฑ1Žy–ฐ่u๚XJ&CหNgฑ‹๊\v“รฒ˜J( พh’…ฝES๙Vm…Nาภม้rืUฮหฏดT๐`W-vฃo8ฮื_๋ฅgn้ขœฺ๋ชBฎซ.ค!฿หl@/iษœ!wฯrhๆยfCจ]HีWUฐฅปƒšŠ2rs์๔ูlrJ&Ž09็กx ฃแโˆจŽs๗Mศs:‰ลใผฐ๏uๆๆีYซ่š ดีืกัhž˜ไฟ๛๒YนญกŽอ]ํT”ใฐNQ)B‘พ@€h<Nส’s%“ฮเ…่ljภl2ฑ่๗๓Ÿ<ห๐๘คjืŠšŠ2~ํปศฯอ%ณ๓ต7™š๓r_ำ9‰ฦฑXLดึีR[Y†;7ฃั€Vฃ%I#ห2I9E,‘  ‘ฮd๘฿฿}Dtู‚3BJ ศvู๋,vq]U!ญ…NVfฝ„Vฃ!ษ•fCQz็์ใไ|เฒ๕พ๖*๎๋จฦfะ1Š๑๏T-ค๔’–ฯol ศf&œL๑่ั‚uา93•NSๅดฑก,ŸJง ‡ู€Aาf[งฃIƒ๔ฬ๚ฐ๕NLi ภjขฅ +ค™œ็k{Žชr^๎mหŠO›Aว\8ฦื_ํฅืsqทa]SUศMตลิๆูqš t™ฬJ]ฐlฦิKƒ3๔z.ŒSซช.+esw;๕Uไ9ซRCNฅGขLyผ M Iyฎ‹~Mรฃ|ไ†kษฯuK$ุ๙ฺ[LฯyT๗[ืuััXFซadbŠo|๏Gค”๓฿ฝฎต‰๎–&ชสJ–ทSf3@SJvฮb‰’V‹Vซ=๏๓e2มํ๕Xอf|?๕ฃใช]+ชJK๘ฏ nW6ฮvฝ๑6ฃS3HฺKgค"ั89v+ญ๕ตซ๕พŒ†ำET<™$Oโ…Hฅtหk™R@๐ฮBH AVHญป1฿มถš"ฺŠœ-XNS19[—่ธืฯซฃŽฮ^๚m)tึpo[%ฝŽูPŒ?p’ดŠหM:‰฿ธช›‰PBๆ‡GF[ ฏn฿:)…Z—๕e๙T9m8Lูโ็ูถRฏ(ภษ๙FtbJฃัP`3ัRเ@ษภ๓๕+๊RŸ่จๆพ๖*,zžpŒx๕'ผ_ฤ๊ดZถีq}u!ีนv&=Iปšๅๆ ว96ทฤฎก๚ฯS ซMHU–ณฉป†ชJ๒r—3ขE!2ํ๑229คqป\—์šFวนํบซO*ญUดนซƒฮๆดZ-ฃ“ำ|ใแ!ŸcำญVห๚ถf:›จ,->ฃฎWvฮๆ}K$e๙‚‰จSY๔่lชวjถฐ ๒ƒงŸ็ฤ๐จjืŠ๒โ"~พQ—K<‘dฯ[›X>“H4NฎรNGSUe%8ํ๖ีˆJZ!™LK$I$“„#QEแฬิ-!คเž…‚ำ…ิ Uน6ถีัU์ข$ว‚อ Cาพืษm1š•{Ff/Xฑ็ณแำต|ฌฅณ^วL(สฟฟ}5’›u:~cc=nซ‰`Bๆ{‡˜ ว.เ>Œ1ีU’Gห†รd@ฏ}ฏ^ัR,มภB“  ’–ฒœsSZญ†B›‰&ท%แญ‰yvo*็ๅฎ๎m}O|พ.]‹uฃคe{] [+ จฮต‘c2ฌึ‹ง<แ=ณK์ža๐วฅ!URเf๋บNšjซp9˜ FดZ ฉๅยืำs^ฦฆfHง3ไปœ—ซีj้nidcgeE…ุญึUY’”ณ5ข|Kฤ ๔:’tq3{ฒBช›ลJ โัgwpด@ตkEฑ;Ÿฯ฿/E๙y$’2ฏพsˆc'1~ˆgK")SVT@}U%•%EหR1{%ญR’I™`(L8;ซc !%๏G)@ เ ฉrอnจ)fSน›r‡%๛b|ส๖/<ษเBwฆุ;2Kโ"uพ๛๕ซ๘Hc&ฤT ยทœDซb!•cิ๓฿6ิใฒ๑ว“|๛ภก„|Qฯ‘ST:ญl*หJงู€qe.•4X’_ˆฃs>L:้ฌ3ฆt’–"›™๚R้4ฏy๘_ฏ๖ชr^~m}=w6—ฏŠฯฟyๅ(ฃ—ฑจŽQฯตล\]Y@นรบzฅาbrŠนPŒƒ3‹์šaย9ซcฎU!ๅvๅฒนซ–บ ๓๓0ณQŠ’&ใYXddrEQศuไ\๖X™˜žๅฺ๋NฃcชŒ๛๕m-ฌokA'ILฮฮ๑O฿1แh๔—๗:Z้jn<-#*“aตภาฑxƒโ‹จ—ด5ึ‘cตŒ„y์๙—8t„jืŠ‚<ฟ๙ภว)v็“”e^๗‡๚๚1 ์)Eกขธ˜†๊JJ ุ,๔ห[SJš”’"‘H GˆDch>D*!คเ!%œZมขืฑฝฎ˜ซJ๓ฉ>c๛—œNˆ'[ ๓ฮิป‡g‰$Stฌ_ุุศํฅ%‰‰@„oฝ๎"m๙ธ8M>ทกž\ณฅX’oฝ}’จœบ$็Žศ)*V6–ๅS›—อ˜2๊ดh85c*D|Iฃก4ว๒ ง—ดอิๆe…ิพั9๗kวU9/gŠฯฟzๅ่Y‹ž‹/7ี•ฐตาMiŽ›A‡n9c**ง˜ D88ฝศ๎กYfBฟX$ฌ5!ๅดนzCืชˆ2›Œูbษ้4ฑDฯ‚๑™Yโ๑$.gฮš‰•‰™9ฎูะuš(81<ขสธ๏jibcG+:Iวิœ‡มcร,b๕z›:ikจฃขคปี‚NาไTŠ`8‚gม‡œ’/IFิ™๘Z๋kษฑูE"<๑โหผำฃฮ฿#€\'_|๐W))p“”e<ิร;=ฝXฬฆ๓>ถVฃฅดจ€ฆฺjŠ๒๓ฐšอซ๓•RRศrŠx2I0&Ÿำ6K!คเ!%|8!uช|ุVSฤๆr7uงศ 9!”™ Dุ?1ฯพั9ฃ‰ 2ึ/mnโ–๚Œ’ฤธ?ฬทœ\bช|ษฐ๙์บ:œfพX’‚x๊าv \ษ˜ZW’G}~น&#I‹F๒)bjh!(ฑฐ˜2่$Šํfj\vd%อ+#ณ฿7๚T9/gŠฯฏํ>ยt0บfฦ็0้๙Hc9ส๒)sXฐ๊uHฺl๓H2ลd สม้v L{oญ)ปียึuด7ึS˜Ÿ‡ลdB’ด(้4๑Dฯ‚ษ™9ฒLŽอบๆberึรึuง‰‚ใƒCชŒ๛ฮฆ6vถฃื้˜๖x๙—>†?x๚VUฃAฯฦŽ6:›)-tc;CDBaๆๆI)) z%Q+๘AZjซqุํ„ฃžฺน‡ทซณฆ@ฎ#‡/=xฅ…ศฉ๛๔๐ึแl๓9ำ 7PQRD]e9…๙.ฬ&:IG&“ห•ฎyK ๑D๒ผ จ !%๏G)@ เ„ิฉlฉpsCM1uyv๒,FŒ:)†]INสฬฃผ;ณ๗qr]๗a๏๗ฮggfgg{_๔B€€HŠฝIฆ ญjR=rไ’(ฒ_ž_โฤqlวq^œYv$[qlห‘"Y–dI‘eหN‰^ป‹ล๖:;ฝž?fwY@ษ,v/|พ๑ร™;็œ๖|๑+žผ0๗ถ7๕Ÿฟa3w๕ฆqษ2ณE์…แซฺ๖๚rำ๔๒‰=Dฝn2…/<‡j˜๋2–’ชำ๕ณ;gS2Bฬ๗j1•ฏiŒe‹ -ึ‹hงCฏyœ2อaฑša๐ุ่,_zฮž)2ฏŸฟ๕ุ1ๆŠี 7ฮTะว=}อ์i‰ำpหศ’„nZ”T้B] ?<ฑk.ง“™…Eพ๒o“ษีป9z=๎•”พอ47ึำนœNy-5/W(1ฟ”AUU<ฯU้๖ำศŠ๔wu ‡)W+|ั'xๆศQพ+"ก ์ใฅต)…n่>~Šง%xs๗†iZxฝzฺZ้jkก1€ฯ๋YkZข้[DE IDATšฆSUr…"Šช]–s)„”@ \ŠR@ภRซ์m‰s[wšd˜ธ฿‹ื)#K`ฌDmL*Ÿอ2xa–‹oฑฯฏด…ปำธd–‹|ํฅ[ฏ}:ไใใป{{\,–k|ๅะฺ: ฉUŠŠF:์ck’DdE2ฎค๒™…•ดฬำ๓9‰tฐ.ฆผ.™ๆฐŸŽhอ0xtd–?~žB๊ีโณฤฟ?p”ลrmรŽท-เฎfฎki ๒ใ_‰˜า “ขข1™/๓ฤฮ’ฏi+ำ๕RAฏgpฮํ์ฺOsช๑ี"JUXฮๅ™˜ย4 >฿†ฟVfๆุปc+ญฉ—Eมั3Cุฑด–พnบn'n—ซ.คืwจTซ์พuๅ|% ๚8๕(Uำศ๋"ชฆ(๕›uQซไ‹%๚:ฺˆE"”ซU~๔๘S!ตสฮt7wฆุ–Š’ x๑นdR=จขฬซœšฯ๒ฤ…9ฮ-ๆ฿ิฑๅ;ทrkwN‡ƒัLฏลด๑ฃผ5เม]„V„ิ—žC฿ **A7ถฏŠ)๗Z๔›j˜k*cู็๓Mฑ5!ฅŸŸแ+‡ฮู๒ผผR|Ž-๙wŽฒ|™าNฏ$ฑ ๗๔6ณป9N*่ล๗1u1[โน‰ฅXSฏบ๚๚ถ}ƒm้ฟู!ื;vช*ห๙<็.\d๘โฝํm~[\+ณ ‹\ทu๓Š(ะ9r๒ GNžมaรศอM]|n.๓Kห;;D[บ‰ึฆ—ลa=k5"j Uำ๑n€ˆจืR(•้ikก!ฅRซ๒ใ'žแฑ็๖]แ๗z๙ง ฃ9n=}–GŸ=D4ฉ฿+Wj„Cถ๕๗าำJ,ฦํvฝJDีT•ZM%W, ๋ๆ9—BH มฅ!%\~!ตJ"ฬmMloŠญDmผ,ฆjšมbEแฬ|Ž'ฦๆ8>ป†Ž๙kทlใๆฮN‡ƒแฅ}rl#Šฑ ูEศใdพTRๆ{7ฆ๋[lJึ#ฆฒŒCzน^ุุr‘‘L‘;’ด†(†ม‡ฆ๘๊a{ถY—๏ส-]Mธไบ๘G^’๒ถ‘้iqw_3ปา 4ผx]N่ฆEAัอ88ฑp๛nฺzU7ˆห๙ยmAฟopตๆขj,็๓œg๘โษ†Mษ„ญฎ•นฅ%vnX/>หกใงl™J฿ีษญ๛ฎรใvฃj:Šชโ๓xV๊@ี#ขฒ๙"ณ‹K†Žืใp"jํนUฎะู’&‹Qญีx่้็xไ้็m๛ฎ๐ธ|แSะัฺŒiš;3ฤCO=G,zฯ—+5ขav ๔ััฺL4ยใv!I ร@ีTชŠŠขช”ส รเJ†๕ !%—"„”@ pๅ„ิ+ฅหm]M์jn 9\/ภ์”๋๕*jบAฆข0ด”gpt–#ำ™Ÿzฌ}๋vn๊hDvH -๘๎้‹(บ}…TOCˆm๏$่v2Wฌ๒•CCNHญm๐V"ฆ๖ถฤู!๐โ‘ๅz)ำขข๊๘\2ูAU7๘ัน)์{ ฉ_ปy7wฝ,>ํฃ/QR4อc แฎž4;Vล”S^ซ VPด“๏U Z–u›e1จj*ห๙ฃSœง!ถˆZe!ณฬถ^:Zา˜ฆษั3C<๗าqœฒš-Huำ~บ:p8ิCึZjrพภ์ยšฎ๐๙7ฌˆzYศTiK7‘lˆRU<{ˆ?๑Œm฿.ง“/|๚AบฺZ0M“็†๙ัเSฤฃ‘W}ฎRUH%โlํ๋ฆฝ9M$ฤํr!I`˜&šฆฃjลr…rตŠi^๗งR@๐:o^!คเส ฉUšC~nํNฑท%AKุOะใยๅฐE7ศTTF2žพ8ฯ3ใ ผ3๚฿พƒwด%‘็๓|๋๘v~’$"์ึv.'ณล*rhkƒฯจจh4๘<์oOฒ%!ฌ‹)‡H`˜ว็–๙ž<ลrUฑyyฅ๘<ฟTเื~‰ชฆ๖:šŠrwo3[ฃ$ƒ+b n—$้ชnkŠz[พX˜bhlœh(h[ตสโr–อ=]kขเ๘ู๓FoG;–eqzd”๏=ฟŸพŽv๚ปฺinL๐๛qญœ+0ั šขR,•)W*HŽ๕‰BJ .E)@ เ๊ ฉU|.'w๕คูš ป!Hฤ๋ฦต๒วฒfšjs%^˜Zโภศ eU็ท๎ลžึเิ|Ž??2Lุ๋ฒํฺoOลxฯๆ6|N™ฉB™?94„รfํนŠŠ†ฯๅไ=›ZyG[rM.Z+ขขช1™+๓โL†#ณถSฏŸg๒๋‡Žl˜b๓—ƒ}ญ nํnบ๎ๆซบA๙฿๘๖nนyเšYหlพ@w{+ฝํmX–ลฉแQ๐†R’$ัำFoGฉD>ฏwญธผiškR*W,๒ฤ๓G่lkม๋q๎ผิ•TผฆdEีx๚ศQพ๛ะ[_k_๘๔ƒ๔wv`a1rq‚Sรฃl่ฃ)'เ๓แtสX่†Žฆ้ิT•|ฑDฅZC^็R!คเuษBH ม๚ ฉUœทu7qC{’žxˆ˜ืƒว้@ิ•ขูS๙2‡&นพ%มถฆpb.หW‘xmป๖ปา ฬ@+^งฬdพ.ค์X yฆP!ไq๑›w์$๎๗`Qฏ+%KาZ1ํ’ช1Sจpd:รC็ง7tืบ็.๖ฎˆฯำ 9ี฿แZ๛‹ม4อซe๏็ใwoป~๋ๆม›๚ฎ™u,Kด77ัท" ฮŒ\เัgโqo,Q๎r:้lmฆฏณƒdC ฟื‹ผRหOQU2นŠชั’jฤใv“ษๅ8|Tฟm(คTU#‹ะ˜Dี4ž}้8฿ปGl}ญ}“ฐฉป‡Cขช(†ฑูถฺpUDe๓jŠบa"„‚KBJ X!๕J๖ท%นฃ'M_—งCB7MJชฮLกยK3c*_0๓๙ปwณปน.คŽฯe๙7BH]~7~๗ถwn}อฌcMQH6ฤ่๎เุ87๘4>ฏg]วๅ๕x่lmฆปญ•Tขฏวƒ์p`˜&Šข’ษๅ9?6ฮมc'˜œเ๖w\ฯ}ท฿Lภ็c9Ÿ็ิะัHศ–BJ7 ยมญฉบกs๘๘)พƒnํญ์ถ…ฮ6ZR8ๅบhฒ,‹ชขิETญFฎXBำ๔ [๋K)@ ธ!ค€)คVูัใๆฮšb4ผฮตข฿บi2ถ\โไ|–#SJชf+1e˜ทtฅธฝ;Kv0ถ\ไO_8ว†BjพT%ๆ๓๐/฿น•ฦ —ขข๑๕cXฎ(NŠŠฦถTŒ้A~—Œ,I–EY5˜.”96ณฬcฃณLn1๕๎นŽ้Žอ.๓ผtอ๗๋%คnฺฝcpำŠผนะ4h8ฬๆ.$$†/Ž๓รวžฤ๏[ŸTbฏวCw[+]m-4ฦcx=’ำ2ฉ) ™lžs.rุ่Iฆ็^๕[ฎฟŽ๗u+ŸŸlกภะ่E~nทหŽื7Ÿถtบapไไi๊od›๑o๊๎d๏๖-tทท ‡qป]ศyญขชฉฬ/-S*—1LkรBJ .E)@ `c ฉUzใa๎์I๓๎|ฏ่ไf˜&5อ`ฑขpv!วแษ%–ซŠ-ฤ”f˜ู“ๆึ๎&œฃ™_}aฏำiปkhก\#๎๓๐ซ๏B2เฅ h|ํฅjšมuอq&๓eฦs%ถฆb์J7 ๚ธe’„น"ฆf Žฯ-๓่๐ฬบŠฉ฿ฟw›bXภKำ~๓ภัk๎พ_/!u๓u;๛ป:ฎฅu$เ๗ฑตทI’Ÿเ๛ ๘|Wq ^‡พŽ6z:ฺˆว"ฏŠˆช) ‹หY†.Œs๐่ f—^๗87ํูลw฿Nะ๏'W(0:1…ๅฒฅฒL ฏวM{Kร09zๆ๑l๘qoํ๋a฿ฮmtดค‰†Bx.$ษaXX8ๅzแ๒L.วา๒บ+ฃ!%—"„”@ `!๕โ็_|ฯ>zใ!$๊Eณ%ภนR˜ทฆd* ็—๒œ\bกTะbJ5L๎ํkๆ)œ‰๓Kว ็๑บ์'คห5~ฟrำV๙šฦŸฟ8ŒDฝ“เ+™ฬ—น-ฒmELฅC~๎WGLอ—ชษp`t–๑l้๊qป๖ฒ-ล^œZโท;vอ๗๋%คnป{ฐงฃํZZG|^/๛{‘&ฆ๘#ธ RๅJ ฏวอ๖>๚:‰GWD”์@7 jŠยB&หนั1Ÿ8ล์ยาO=;v๏เƒ๗Aะ _,2>=‹ไpป์(คL<-iLำไ๘ู๓ูทvCŽีแธn๋fvoูD{sแ`ทห…$ีัEี4t@ำuย’C"“อ‘+1mฒ—BJ ^็oN!คภ>Bสใ”๙๗๎aS2ŒaมtพLYีi๛ y\ธ ่หU•‘Lง3LไสO๒(บม}›ฺธฑ=‰์8ทXเฯœวgC!•ฉ($พpำโ~นšสxaฟI<บ฿™ฬ—ห–ุaw:Nsธ.ฆœบiQัtJUŽฯfylt†‘L๑๊l%‰๔ฎ=lME1,8<นศ๏<~šป๏ืKHพoฯ`w[ห5ณŽšฆแ๗๙ุฑฉ‡รมุไ4๛ศใWด†TนRรใrฑ}S}]ํ$bQผn‡ร4จึj,.็8;rCวO1ฟ”yCวฝ~วV>๎ป ไKEfๆั ร–Bส4M.]m-Xฆล‰กaพ๚ญ๏nจ1บ].๖ํฦึพnฺ›ำ„\N0L0PU•|ฑLพXย๏๓าฺ”B–d๓ฒ๙†iฺโ|!%—"„”@ `!ๅw9๙ฝ{ฏฃ?F7-žน8ฯŸโฮžf๖ต&h๊bJv`Y ™&นชสpฆภฉน,ฃหE<จฮFUำนK๛“ศœYศ๓G๊วnd* ฉ —ฯ฿ฐ™ฟ‡\UๅO‘x้Œ๊w'๓eฆ zใแzฤTุGะํ\I7ฒจj:sล*'ๆฒ<~a–แฅย•$สใฝ{ุม0-N.๒O\s๗z ฉ;๗๏์lmพfึฑฆ(|>vmภแp0>5ร๗y๏่ฒทตฉฮvโั(ทI’0M“ชRc!“ๅฬศž้8™\Mบญ›๙่{๎!R(•˜_ZFQU\6”ไฆiโrน่nmมฒ,N ๐'๋;ใ]ๆ๕ฒืvถ๔’N&๘+" tรDืujชJกTฆRฉ 9(ชJ( ต)…S–ษ dry C)@ ฐ+BH ๖Ra‹฿ฝ็:zใatำไฉฑyเ้S๘\N๎์Iณท5NOCˆˆืหQฏญก™&…šฦxฎฤ‰น,รKydว๚ืจh:ฺม๕mIภ้…๙โศ†Œๆ๚‡ศVšB~~๙›h๐นษVUพ|๐mั-!:†fš,–kฤ|nzใแzฤ”ห‰์0-จi:sฅ'WฤิะbŠฬล็’๙ฝ{๖0 c˜ฯŽ/๐๛Ožผๆ๎๛๕R๗ฐoฐญน้šYวrฅJะ๏c๗ึอศฒƒ‰้Yพ๗๐ใx.cบpนRร๏๓ฒ} —พฮv"nWฝXนiRฉี˜_สpntŒ็Žž`9๗ึ๎]›xเฝ๏" R(—˜]XBืuœN› )งLw[gF.๐ๅฏ{]ว‹„ูป} ๛{I%โ|>œฮzM(ะัดบˆสKTชตWี‡RUp0@s*‰Sv’/•ศds่†a‹๓!„”@ \ŠR@€}„Tฬ็ๆท๏MOCอ0yblŽ๚ฬ้W}ฦ้ppkWŠฺ้M„ˆy=xœ$@5-ŠŠฦtพฬฑู,็—๒˜–…cฅk฿U฿ศช:ัลž–8pr.หืฺ2e/WSi๙๙ลฤ|n–ซ*Y๚a’7_Kgนชเuสt7„hV"ฆ๊ลฯkšมBนฦ‰น,O\˜ใฬB๎ฒฮ%่v๒ป๗ผ‰๗๔ลy๓Sงฎน๛~ฝ„ิฝ7ํlmJ]3๋X(–์ูถง,39;วwz ฯeหซ"jืๆ~z:ฺˆ…รx๎zjžaPฉี˜[\โ๔๐(/œ8ฆ#ข^หŽM}<๘พw †(UสLฬฬcYฆ-…”eZศฒƒ๎๖V†.\ไ๊[๋2–T"ฮ๕ทฐนท›ฦx ฟื‡,หX–‰ฆฏˆ(E%[(PSิืํ˜งj‘`€ฆd—ำIฑ\fq9+„”@ ุ!ค๛ฉ„฿รo฿ฝ›ฮXอ0xlt–/=w๖'~~[’ป›่O„i๐{๐ศ2‡„n˜U™Bฝp๖K3หฆyี  ๏๎aWบ€ใณห๕‰ฑ •V๘FษืTZ#>ทฏŸจือrEแKฯŸeGSŒศH]สVUd‡D_"LŠ˜rส๕TพšฆณXQ8=Ÿๅภศ์eSฏ‹฿ฝ๛:zV"๑ž›ใฟ<}๚šป๏ืKHฝ๛ๆ›“ืฬ:f๓Bม๛vlล);™š›็ป=†ห๕ึ๏ใrฅF0เg็ฆ>บ[iˆD^ีmญ\ญฒYๆไะ0‡Oœ&›ฟoฝ๓กiZhบ†ช้ิ…\กˆขjฏ+ขVั4H(@*‘ภํrRชT™[\5คภฦ!%ุGHฅ‚>~๋ฎ]tFƒจ†ม#ร3ทƒ็ม๏ํi‰s[W›’โ/^งŒ,ฑาัMgฆPๅ๘2‡'ัM๋ช‰ฉ‚ข๑ฉ๋zููรŽฮd๘ฮฉq6iใฺนดG๕Dผ.2…/>{†mI|—Aฐ-WdIb กeฅˆ+ป+.”kœ]ศq`d–S๓ูท๕[ พบ๘์nก&ƒf๙โณgฎน๛~ฝ„ิ{nฝi0•ˆ_3๋ธดœ% ฒ็v\N'ำ๓ |๗กวp:฿}\ฎิˆ„ƒl๋๋yED” ึ"ขๆ—2œๆเั“หๅห:—-ฝ|โ๛ˆ†ร”ซFฦ'qสฮŸ*J64–EoG;’$1<>มๅ7ธ๚๗ดทฒg๛๚:๊=w=ฝา2ัดบˆชึjไ %4]C๋ซ๋:‘Pˆฦx ทหEนZcf~Atู#„”@ `!ี๒๓›w๎ค#D1 :?อŸzร฿฿ใๆฮ›b4ผ๘\2ฉžVั JUNฮๅxaj‰’ช]q1Uจฉ|foS1Lเล้%~xfู!ู๎**ฑ doa‹ฅฒย}๖4ทv5!_ฦ”ศลr งรม@ฒ1XํฎhAM7X*ื8ฝใษฑ9Žฮ,ฟฅ฿Hผ๖]ปึ"๑Œฮ๒G?%ฯฎฌ—z๏m๏lŒ7\3๋8ฟ”! rร๎ธœNf๙๎รฝฉ๛ธ\ฉั ณฅท›ž๖6ขแn— Iช‹จrญส์"งG.๐์‹วจTkWd.›zบ๘ไ๗‹D(Wซœวใv!ห๖R†i2ะูไŸไ‹๙ L๓สฝ๎6๕tฑw๛zฺZ‰†C+้•†iขjŠขR]‰ˆ2 ๓M‰>]ื‰†ร$bQBช#ไ7n฿Ak8@U7๘ัูIลแท~ผh[ปS์Nวi๛ื๊ญฆ-Wฮ/x~b‘…R๕ฒ‹ฉ‚ข๑น๋๛ูม0-N.๒๐๙i ฉพD˜๏๊&ไqฑPช๑GฯŸๅถฎ+Mmก\รแ€พx„ฮX๐+ฤิ๊9<ท˜gpt๖ ‹ฉๆฐŸ฿ผใญGโู…๕Rธ๋ถมX$|อฌใไฬัpˆ›ฏฟลb†๏<|€ŸvฏŠจ›๚้lm&z9"JืuJี ณ K=3ฤ‹'ฯPฎVฏส\๚:๙๔KC4JฅVๅฬ๐>฿ซบฝูหดะMƒM8ฦงf๘รฏ}EU/ห๑‰=ถฐk๓ํอMDBA\N’†aฎีˆ*–ห”+ีทZg&ัp†hฏขjŒOฯb!"คภฎ!%ุGHu7„๘ทท๏ 9ไงช|ฬ8๕า่>n*่ใถ๎&ฎoMะ๖๔ธp;$,@ัM–ซ #™/อdฯ– ธ/O฿’ช๑น๋H†1L‹gวxdxฦ–){UMง?แ„x๗ํƒัp่šYว‹S3Dร!nฟฏอา2฿y่ผŽ((Wj$ใ1ถ๖๕ะตๅvีEทn่”+Uf9vfˆร'NQSิซ:—๎๖V>๓ม๗‘ˆลจึjœ—ฬ=i๖ถ$่‡ˆxธ^!ฆŠŠฦลl‰sY†—๒ศoqใQำ >ทฟŸž†0ša๒ไุFfถ่Z`K*ส‡ทuโw9™+U๙ณ#รุ–\—๑ฬ–ชH@"LW,Dฤ๛ฒ\T ƒๅชสะbžgวxf|แU…_‰๗ƒ3|ํฅ‘k๎พ_/!๕ัw5 ๘ฏ™uBjk*สฏฒฆ ’ช๓ญใc|็ิล+๖{N‡ฤ-]MุHoทo€ฎ†z7ทวFgy|tŸหiปkH5Lถฅข|pk>—“ูb…ฏฝ4สพึฤบŽkฆXซ]ื๕บqษ,ซ.ฆฒ5•‘ฅ"O]œใ™‹๕๊๕HผํคC+‰ทQX/!๕ภ}๗ ๚ฝkfฯŽ\  sฯอ7เ๓xXสๆ๘‹๏H0ภพiKงkฉyšฎS,•™š_เลSgy๑ิ™+Rh๛ญะ–N๑๓๙๑5Uๅฤนaย’ ำˆ ร@ี4ถ๔๖เv9™ž_ไ+฿๘6™\ }ฟ)g๏๖ญl๎้ข1€฿๋E–e,หDำuTMGQTฒ…<5E{S๓*>‡ฦD^w]HMอฮc˜BH ]BJ ฐฺัใพyA/EE็วF๙™‰ซ๒๛’D_"L๏ม#ห8บaRRuฆ Žอfxiz4฿Ptอ0๙…tFƒจ†ม#ร3 ^˜ปl5ชฎ&šaฒ#ใ-๘œ23ล ฿8z=-๑ 1พ้b0ูšŠั$บRc  ฆ›dk ฃ™"ฮ’ญชkโณข]นHผ๕fฝ„ิวs๏ ืในfึ๑ิ๙"auหM๘ผ^J• s‹K4D#kQ–ต"ขสeฆ็8r๊ /:‹nj.อฉ$Ÿ๛่I%จฉ*'ฯ  lูhAื๋ัK[๛zึบ้7ฟรBf๙ง~ฏตฉ‘};ทำืูNc<†ืใAvิ›]hš†บ’š—-Qีซ#ขV๑ธ4%โx=jŠยิ<†!„”@ ุ!ค๛ฉอ _๏J2เฅจh|ํฅQ~tn๒ชŽแบ–8ทu5ฑ9!๎๗เu9‘%0,‹ฒj0Sจpr>หกษE4ใง‹)ด๘…}ฏ๊ๆ6xaŽ๐e๎ๆwU6ฆษฎtœ๗nnร็”™*”๙ู๋nุPใœ.TะL“ญฉ(ฑQ_]LI€bิ๋„อ—ชดGƒDผ.Jชฮ_ŸใoN^ผๆ๎๛๕RŸx฿ปWฃ…ฎŽŸ"ัใฝw‚ๅฦฒL ำDvศ€ต51;ฯฑณ๕ฎyMDญ’N&๘ค)GQตต”=; )MืQTm}=๕๎‡Kพ๚อ๏2ปธ๔บŸ๏้hcฯถอ๔uดF๐x8$ฆeึE”ฆQฉ)ไ %4]ฟช"jทหEบ1QBJ lR@€}„ิž–8ฟzำVEใฯ ๓ะ๙้uหถTŒ[บRlKลHฝ๘\2Iยด,*šมBฉสษนGฆ—(*ฺ๋Šฉšn๐/nุL๋J7ทฟšโษฑ9ข^ทํฎ!รฒธฎ9ฮ}ญx2S๙2฿9=ฮŽTlCŽwบPA7M’zใแตˆ)Iช‹BY’p8$jšม<:zESCื‹๕RŸบพAง,_kธธผLตฆ๒ฮฝปH'“8–Uฏ๗ฃ้…R™‹S3?wžOม47๖ฃถ1ภ/>๘!าษชฆqb%BสŽิk;ฉl๋๏ญw?ฬ,๓฿ฟ๕=ฆ็_ฬ`sO{ทoกปญ•h8„วํฦแ0Lณ~ Eฅช(ไ๒E ำ\ตŠ้$Jฎ ฉ้๙… +7_‹R@p)BH ๖R๛’‹7๗{ศีT์…aŒฬฌ๋˜บBั“fGSŒtศOภ-#K†5Mgฉขpv1ฯ “K,Ujkbสด,rU•_ฟm-แz๑์žเ้ฑy๖Kgฒ,‹=- =ะ‚G–™ศ—๙ม™ ถ6F7๔ธง e4รค/a &ๆ]ฉ&IH+็iC=5T5-สІื%ใ‘e$ 4ำขXS].๒ยิFfm)ฆ„๚‡ษdsศฒƒŽ–fา‰KDTฎPโยไลR™ปท)”J|๋Gs๔ฬ9]แ`€‰Ÿฃต)…nิ#ค~{FH-็๒ไ E๎บ้ธ]ฎW›wบa ฏtฬ+”ห”+U6z*ฉ์pะาิธ!5ทธ„ข !%vE)@ ภ>B๊๎f>{}Qฏ›LEแŸ=รแฉฅ =fฏSๆŽž4{[โ๔%ยDVŠg[Vฝ๘j๑lE7๘_ว.๐๔ลyšร๖พณ3ลm]Mธd–‹<>:KOCศv๓x~r‘_~ว&V"๑Œฬ๕นูา%๎๗\"ฆฦseM.๒่ศ eี>bJฉŸ&3 Htทตะ”L๐๙p:e, TM%[(rabŠ#'ฯpfไป6๐ภ{฿E8คX.๓ํ?ย‘“glwํ~>ษŸฃ-„n+Bส~‚<_,‘อ็นn๋ใฑตˆ'ำดะ }ญXyฎXขZซญHฦ์pะ’ชG|ี…นฅ Šชฺb์BH มฅ!%ุGHฝซฟ…ฯ์้#โuฑTV๘โณง92ฑษFBโๆฮ7u4า๓ี‹g;V6J–e1]จr`dš‘ๅ†iฎ?ปpKWŠ[ปšp:Œf |ป‰C”*eพ๓๗8t”ํฎ}ฟืห็?๕อitรเิะ0~ป)หขPช ้D‚dผ€ฯ‡,;ึžฑีšBฅZcนGQดuํ˜๗Vp8ดฎ )Ue~)CMQl1v!คเR„์#คณฉO]ืCศใbฑ\ใๆ4Gg–mท๛Zั“f{SŒ„฿ณ๖ฏ๗๚Šุ˜)T8:›แ่ฬ2šaฎuๆ่ึฤอ)œรKž›˜ง=b?!๕โL†ฯํ๋'๊uณ\Q๘ร็ฮฒPช’x)*1Ÿ‡;’lJFH๘ฝx2‡„f˜u1•/spb‘'ฦๆXฎlอขR/“+qส2m้้ฦ$~Ÿง,ื›h*น•ˆจCวOq~l’๏oํ๋แ๗฿G$ขTฉ๐ท<ฮs/ทต๏๕ธ๙Ÿ|€ŽึfLำไไะ~฿ฦฎiง๋๙b ‡$‘N%H6ฤ๐z<ศ–eญ<_%4]g~)S?ื6Qซ8$ญ้zสžข*ฬ/-Sต’852z๛‡๎ฝ๓ @๐๒๓Q)@ €ฟ๕คตTู๘ิฟฅํ๊!ไqฒPช๑OŸโฤ\ึถ๋๑]=|dGฝx6–…iCชง๒•Uƒูb…“sYN.ฺBLู“ๆฆŽFd‡ฤะbรSKดฺ0๐๘2ŸOฤ๋"SQ๘โณgศืT"^๗ฺgVลิ;ฺ“lIFˆ๊bJ^Iๅ+)ใน2/N/๑ุ่,™ x !ู|E[บ‰ฆdŸื‹์ฑฌบˆสๆ _œเลSg_WDญฒนท›Oฑp˜rตย}‚งตต๏r9๙ยงคซญeร )]7ึ ทค’4D"xฏง\Q5Ue9—g๘โ/œ8อ่ฤิ?xผ๎N>๕ณ๏!‰PฎV๙ัใO๑ฤก#ถป๖eูมฏ|๚ctททb™'ฯเ๓z6ิu RญโrนhJฦ‰†Bxn ร4ฉ) ™,รcใไ‹%๎น๙ยม Šช257nถ}gผJHi*‹™,ๅjuCŽ3๐ใqป๑z~O{+Ÿ๙ะ๛‰GฃTk5z๊Yyๆ -ŸKฟ๒™ัืัŽeYœน€หๅ\ซsท†ฎ8Nใ1"ก .ง IชKชrญสยา2C.21;iึ#  ฅ2ก@€ฟgˆ†ริ…ูล%TMรฎผVHer9Šๅสบหแp๔๛๐yฆeแ$’v6ลhz\ธ ่&ูชยpฆภั™eฦs%.็บฮ็พMญ์iIเNฮgอ‰พข๎’]ธ˜+๑ั]kฉก๐ิi|ฎ7Wนจhx]27ถ7ึ#ฆ^!ฆ ำคฌ๊Lๅ+ผ4“แ๑ัYฆ W๚ว คJๅ ท‹–ฆT=5ฯใมแxนฦP&—g่ยEŸ8ลฤฬ[ฮึf>๛แ๛IฤขT…GŸ9ศ฿?๙ฌ-ŸK_๘๔ƒ๔wv`aqvt ‡$!หWฟธฆ้่†฿๋!ู#เZ)Fฎ้ๅJ…ูล%ฮ3>=ปึMo•ชขโr:๙ไ๗‹DPT•ูล%Uลฮดง›๊BJำXฮๅษK๋& eูAะ๊ˆ(ง,c&ชฆขจีšยb6ห—็_‹.{@๐„เ—pะบซ7อŽฆฉ Ÿห‰ำ!กฏlœg ีต๓dพผnใ'{๚xฯๆ6|N™ฉB™0x‚‹ู’mื๕บ*บyIญ"ร4้KDุัฃ#$์uแrิ7_ši’ฏฉŒfŠœœฯ2’)เ–ืงƒิ๛6ทฑป9Žœ˜ห2‘+t;mw^& e>ผญฟซ.คห3ง๑ฝลฎ\EEรใ”ู฿–`Wบฦ oญF˜aZTดzDโฑูežaๆ*ŠฉkUH้บNพT&๐ำœ&‹^"ขณYฮ_็เฑ“Lฯ/ผํ฿์hN๓ูOฒ!FMQy์นCh๐i[>—>ษ่๎`่ยELำฤๅบzQ˜หน<บnะ˜h )'เ๗แ”๋ฯMื)–หLฬฬ12>มไ์<๎Ÿ โ ำBื๕ตTJES™_ฬุข+Oธm+BJีt–syrลโURk"สํฦ๋๕เvน ำDำ4UฃRSศๅ hบŽํโฟล7„‚ื>ื…ธ็ฯฑบbA๎๊mfWบTจพqฎื0ชw}›.”96ปฬc#๋#ฆ้๕ฬ@+^งฬdพฬ๏<~|]ูๅ•]WSร’๔k)บAo<ฬึT”พx˜จฯฝ๖Yอ0)(น2ว็–^*ผกณหษ๛ทดณ+ภ๑ูeๆJUผฒฺซฯซ|`k๛Jjh…/>wฯœGQัp:$nhodG:F:ไฟDLอซผ8“แภศ,SWแบพึ„ิโ™\žŽๆ4]ฤcผžWคๆ) ‹หYฮ]ธศแใง.‹ˆZฅต)ล?่hŒวจฉ*ƒฯฟภ{า–ฯฅ‰ŸcSO'Ccั4็สG:ๆ‹%jŠJsc‚ฦDŸoฅะƒ๙RuEฮ0v#ฏ!5ทธฤR†žถVบ;IฤขxW"ข ำ ZSศdsœใะ๑“ฬ.,]๖y57&๙ฯ}Tขšช๒ไก๙ฃƒถ|.๒ว?ย–n$$†/ŽSU|^๏•๙1หขPฎ ้:้D|ญะผ,หX–‰ขึEิ๘๔ '†ฮ“ษ๘฿๘XTM็๗฿G"Eัด •๎ o\^QCJีtr…™\‡รqๅ~ิฒe™P0€ฯใมํvฝ*"Jี4jŠŠขจ”*etร|ร!%ฏ๓\BJ .RซดGีำฬฮ๔kปพYฏฺ8?:2ร๘UH๛g๏ุฤ=}อธe™‹นฟuเ๓%๛n.ุูล‡^™๖๔›ซUTT4ƒ>nhOาŸจ‹)ฏSฦแะ_Q@๛ุ\†ฃำหจ†yE  pkW„ิ‹ำr5{ ฉLEyUj่<‡๛2o๘ŠŠ†[vp}k=•ฏ)ไ#เvฎl๒^Ž˜:>ปฬใฃณWคVš…ิโ๓™ez่๋jฏ‹(ทI’0M“๊JDิ™‘ /ต„ใภป๏ผ•ถhบi’Hฅษๆ๋ทอ[Bขซฝฟื‡i™ค29โฉิІฮ›ฆ…K–hŒDฮQฒ,cYZ5ฌผฌiไ E,~M๏+„”@ ผฬบ.„”@ ผบZข+์็ๆvvF้๛(nฎJEว…|p^โSืnaฯ๊v—ฬh2หง"ฉR๎˜๔ั๋ธ๛ฬJœgฟก@๒œfTvwGhkXฮ[jท, …2Gb)ฬ$ศ”๕Sฒ$q฿ภ*ถด5`9ฐwj‘xAซหP๓œnœี๚ู็Oโv]๘J/ล%ณน5ยๆึFบ#~หb๊๔ุšK๒๘XŒกลฬผzRฑล8๑dšuซ{้_ีKSค*3ชส%ญLl1มเศ{#~+ขฮฅน!ยo}เ=tถถ`˜&ฯพxˆ๙ัฯ๊r]๚๘๏bฦuศฒฬ๘ิ ษL†p0๘†%Gฑ\Fqปioiฆ!ยฃจศีVีRYc1‘dxbŠมแQ4xC"j วqธ๏Ž[ioiฦ0L้ ™|n„!IญQ>?ฆe‘ฬdHคา+"ค*"Jฆฑ!Œฯใมซช(Šฒ\qจ:eM_Q๖๋|†BJ ^f]BJ ^ปZข5่ๅ๖u]์์jฎด๒QัQ6Lb๙Gbic(žYฑใฃ๋ธ~unYf$‘ๅ?=Yอจ๋~nH๛?>w๗ ด^ไ4ฅฺถญฝ‘ฎฐฟา&IX”M‹Dกฬ‰ล ฯOว‰สoXLนe‰๛๚ุิมฒž›Zd!_ฆมซึธ “ึuข, ฉ!—|ั>฿ใ’Yืak[‘ภYโทl˜,สO๓ศศ,'€˜ช![Œ“Lg่๏๋emoM aผชŠ$UDTฑ\b!‘โ่ษ๖>vQ*ขฮฅ!โw>x?]mญฆษCG๘๊~Z—๋าฏฟ็ถoZ,หœšž%žL ‡ฮ[rhš†ชชดE›ˆ„‚จŠŠ$Igศฤ$ใำณ่บผ‚ีˆ–es฿ทาูล0M’้,้\ŽzE’$:ขอ~,ห&™ฃRE IDATฮOฅpปฯ_›ฆ…%ำ ใ๓z๑( nลฝ\qXฎถๅiบ~^"j !คเeึu!คเ๕ ฉ%Zƒ^nํ๏ไ๒ฮfบ#‚๊้ŠŽrตขใp,ลใcs_xใb๊฿]?ภต}mธe‰“๑,๚๐ tณnฏ๛น!ํ+]‰“ำ \ฒฬๅMหํ–!Uมํ’q‡ฒi‘,jœŒgynj‘XฎtbJuษ;ะวฦ–0–ํ๐ฬdๅข~O‹fYgต†~๖๙!T๗ลo=T\2๋ฃaถต75vถํP2-๒%Ž-คytdŽม…ื/aj]Hลใคฒ9ึ๖vำฟช‡ฆHชR5ฌผาšwdh˜็ฝคข! ๐ปฟ๖^บ0-“}‡๑ๅ๏ธ.ืฅ๗vlˆห%393G,ž 1~]๏aY6ฆeโvนi‹6PJ๕฿, ฅ๓๑CใLอฮc;ึ9หฒน๗Ž[่lmมด,R™ ฉl )Y’hmn" `6‰ิ๙ )ำดpป]4E*Qช[AQ”ำญyบNIำชQฅ๓QK!%/E)@ เ…ิmA7ฎigwwtYLYัฑXิ8:Ÿ:๏็%รžm\ำ‚K–8ฑ˜ๅ?์eำช๋พา~ก+qlวA’$6D#์่hค+ ไQ–s‘4ำ&UึŽg94—b<•วฏผ>ใqปธo`๋ฃaLแ้‰fณEฺƒพบำq–[Cว’9>ทw๛าeaษ’ฤ๚h˜MtEUท,แTซ*S)ž‹q$๖ฺิjUHลใไ๒๚zบ่๏ํก!Ri๏’คŠˆ*–สฬ-ฦๅ้‡j"ค:เ๗๑{z/ฝ˜–ลฃƒ|๑;?ฌหu้ร๏บ››qป\Lฮอ1;ฟHSCไ5ฝึ0MLหย๏๕าาุ@0@ฉฮรด(‹ฬ.ฤ99>มไ์ ใ>๋x ‹{๏ผ™๎ถ6,"•ษ’ฬdฉW$ ตน‰p(ˆmฤSiโฉ4ส๋RgŠ(ฏg)#JA’dlB7Ltร —//•Xฉg%!คเeึu!คเ ฉ%Z^n\ำฮ•=-t…„0๚7๗ฌขฟ9Œi<9>ฯLถ@w8PWcb;Hpร๊๖ๅึะฯํยง\๚,,วqุิฺภึ๖Fz#‚žŠ˜ฒซb*^(sl>อSงๆya6๑๊็ZcBjID๕๗๕ฒบง‹†PZษฐ1ซU5s๓‹e๏กฃ5Nํ๓z๘ฝฎ,หๆลม|แ฿ฏหu้C๗ผ][7ใvน™Šล˜ž›งนฑแWพ&•ษb&mั&ฺZš ๘|ธ]•9c˜&นBษู9†OM2=ท€z‘ฒๅtเ;nกงฃหถHgs$า๊วqhmnข!ยqS)โษŠ๒๊•ญฆiก*nย!|^/ชข *n ’eZบก“ษ.ˆไBJ ^ŠR@ภส ฉ%ผ*ทฎ๋ไŠ๎(=‘j5ฮREวbตขใใ๓šKพๆ๗๔Mูำ‚ .ค๙w žื๑?ผv 7,…ด'ฒ|v๏I|สลฉฤัL‹5M!ถถ7ฒฎ9LƒO]–O†e“ำ &ำฦ’ŒฤsHฏาITผk`k›ย–อ/ฦcLฆ ฌn ึี˜่–ว-Ÿี๚O๛NโWj+œ}]4ฬถถFV5—ซ–ฤTขจq|1อใฃฑ_)ฆjEH-‰จuซWัืIc8ดœ3dZ&…b‰นล8‡ŽŸd‘Ar…ฺ%อฃ*|๒ร๏gUw'ถmsppˆ๓ญ๏ีๅบ๔มwลpปLฯฯ393Gดฉ๑e6›/ iญ-ด47.‹(วqะ ƒl>ฯไlŒcรฃฤ๘}ท…ทฌ๋{๛-ฌ๊์ภฒ-2น<๑K1ถRุถMKS#M‘HEH%S,ฆRจฟBH™ฆ…ช*ๅ๑ * Š] +wlLำDำuฒ๙"๙BW]์ฯ!คเฅ!%ฌผZ"์Qธ}};;›—œ—*:ดฅŠŽ…4Ožš็…™Wฏ่xเ–์์Š"GๆS๛‡ิ๕uใ๋ธฎฏา>ฯ๒น}C]|ไu“?ปบข๔7‡i๒ฉx.$IZSณู"‡็S -d0l๙eXB…{ทฌbuSรฒy|lŽSฉ<๋šรu5&eำ" บyหชV\ฒฤะbEHjqท@ึ4‡ธฌณ‰† a‚rF>Xผ 1ด˜แััน—S—ZHลใหeึ๔tณบป‹†pจฺ:Tyˆฮ—Šฤโ<~’ฝ‡ŽR,—k๖พqป]มG>ภ๊ž.lๆ๐‰a้฿ฉหu้oฟ“+ทoEqป™™_เิ๔,-อg)ว![(b˜&ัfZฃM๘<^\.Žcฃ้้\މ™9Ÿ8I2]‘๓ฮ‡bYใพofUw'Žm“ษXLฅจW,ห"ฺุHดฑ‡…dŠx"…ชพTH-UD5V3ขwฅ"ชา๚zZDer ฅา?v!คเฅ!%\8!ตDƒWๅฆต\ั}ลŠŽ‹ๅลูWฎ˜๚‹[/ใฒฮf$เP,ลY} ฉรึsฤวUน$ว’ำ Zƒ>ฎ๊‰ฒ>กู๏ม๋v!หฆeS0Lf2Eฦผ8“Dท์ณะ#^•wm้ฅฏ1„aY<2:วx2ฯฆ–H]I^7i๒ซgต†~~0มZRKฉถอฺๆ0;;›้m๒ชจฒ„CU5N,dx๒ิ<ฯO-.ฟ๎R ฉ‰™Y๚บ:YำE$ชถQiอ+–˜™_ไศะ0ฯฎ่iม%มฑ๙4g๐%—ื‚nูlˆ†ูัD_cฅbส]อำญฅVพ ฯL,๐๔ฤย%R๐ตo๏ู=ฐ้๑U]„ƒมๅ หฒศ‹Lฮล8zr”็Fำบบw>๕ัะฟชวq862ส?~๕[uน.ฝ๛ฎ[นv็TEav!ฮ่ฤ$ถใเ๗y้jkฅ1ฦฃชห"Jำtโฉ4#SฅT*_rตDพXโ]ทฤฺว!W(‹'๊VHฆIs$Bkด ‰…d’…xGล4-ผ•H(ˆ฿๋EQ”j |ED™ี๓2ู<…b Iพธื@)@ x)BH OH-แW฿ม5ฝญฌjจ๎๘ๆ’qœำฮƒ ž:งขใฟพ“mํ8ภ 3 >ศ‹u}ฯอฤ๚ง}รDผต!>ršAPUุีฬ@[#ํ!3ฤTษจd‰ฅ80“ มง๒ฮอฝฌjขYœa$‘ใฒช@ฌ%H€•ึะฃ๓iพp >„ิšiฑ>ak{#อกŠ๘ญfธ้–Eฒค3ฯ๒ฤุrูE}@\H$๗DBมว+KืEfๆx๑ุ ฌ;ตฤ'?๒~ึ๗ญยมแ๘ศ8•oิๅy{ว-\ท๋2<ชB*“er.F{4J$ฤฃจศฒŒe[”ส‹‰$'OM28<†n5#ข–ศKผ๓ึYทช‡\กาzฑeฬJก๋ a:ขQ$Ib!‘d!‘$เ๗ำโ๓xฮQT[๓ช"*—งP,"ษ๒%9v!คเฅ!%\|!ต„OqsKW๖ดฐ๚WTt<=ฑภณ ๕ปhkภLวyเัƒu}ธeปบขHT2ฑพฐ„ฐทถฤGN3pหWtทฐญฃ‘ฎฐŸ€๊ฦ%หXถSmน,3•.ฐ.&๊๗R6-~24อษx–+บฃu5&‹ล2kC\Ui =K๑ๅGk3C๊UะL‹พฦ ปปฃ๔5i๐V‚๋+ญ|6ษ’vcWุQวู<ฎ&๙B™๙EdัALำช๋๙{zึฌ`h์๓KR—็qฯm7qำีปQ –eaูึ้–/หขX.ฑH121ษ๘๔ บn"ืจเ)–สผํฆู๋ฐบ‡|กฤ์ย๒%’2oxN๋: แ]ญญHฒD6Wภq์สŽyชŠRอณ,ร4)k™|ฅ"๊RŸณR@๐R„.Zย๋vqsWvทฐถZักV3ฆ–*:FYV5้Ž๘ฑุ;ตศ_ฟo˜'วcิ๋Jืw์dk[E|˜I๐อ#ใuU‰cฺ๖้ฌขฆ>u9,ุดmE๑dž‘Dh๚jœ&3.๋lZ—3qพst๕ฆ™๏Eรค'dggำ7ฌiฟจˆณฟ#;[–฿็ฅ!ฤใ๑ ธ]ธ].$I^ฎŽ*keๆใI๒ลbอK6!คเฅ!%ิฎZย-Kนก›\Oะฃ,WIูถƒnูdส:ฃษฟ<5ฯ/ฦ็1ํฺorห2ํŽlj`ูฯM-๒รใSu[‰“ื ไ๚ญด‡|ุŽCAทP]žjศฎaูไ5ƒ™l‘แD–‚fา๐ ิุ_๓ว’9ฎ^ีสๆึ–ฯO.๒“กi\uบ+ื+1—+2™ส฿๘ˆR+wn๏น‡ํ›ึ#ห2งฆg๙ฬƒ_ล0ฬš9พฮึvmฬฦตซimnฤ็๑โrนpM7H็rLLฯbZ—mˆฯ๋%‘N๓…>Z—cbšoูนƒ›7, ฉุ้|MgHY–…ฯ๋ฅ1ฦซชธ๎ๅ]๓tร\1‰D:อ|~๋e๔7‡pะ, Y’–ย™ัชbj,™็๑ฑ9~yjพฆ+ฆผn๕๖lh cูOO,๐ำ“3u[‰S4L๘๚ฺƒ>๒บษำ ธe‰ ัอ~^ท Y–0-›‚a2›-rh.ลX*GGะGO$P็1’ศr๊v6VวๅูษE~6<ณ\™๗f!–+1‘ส !ต‚|์พwฐc๓F\.™‰™9๎‹_ฃฌ้—ธVuupลถ๚๛zˆ66,W?ูŽฎ$3Yฦ&ง8p๔8'ฦNqำีปy๋ืแ๗๚Hฆำ<๘ึeปTชŒฎฺฑ•หถlยํr‘/kVHูถ฿๋%แ๕จ(n7n— วก’ฅ่†AฉT&ฺิˆโv“ฮๆˆงRหู~ตŠใ8!คเ%!%ิ‡Šผ๙-;่k aXว2” “ตอ!ฝr๏นผ*>&f็๘Ÿ_๚:ลR๙’ฯ๚ีซุ9ฐ‰U=4FยxTuYDišN<•fxb’‡ŸžY~ž+w๑ถ›ฎ'เ๓‘สd๘าw„ปN+7maืึอ์ฺบทห]ูe/6TCBสถm>แPฏวƒโvใ’]€ƒaZฆฆ้คณ9rล"^J_W'ชขษ็Iค2ฆYำใเุใ‹BH มน!%ิ‡j ๚๘ฯท์`UCอฒxxx–ื๖ตr]_๋šร4๚TT— Iชdฅห:ฃ‰{งใ<::‡VCหGผ*y๋eฌmcX6OŽวxbอ‰ล อ~ฯ%S'YnZำqZŽฯ๓ฤx์M7็ eฦBHญ$บ็mหโc*ใ}้ไ …‹~›๛ืฐ{Vww ๑(*ฒ,cูฅฒฦB"ษะุ)๖df~แ%ฏฟnืeผใึ=|~Rู,_๋V”;์ุผ+ทT„TฉศฬR œm>B!TUฉŠจŠ([QฅฒF&—งฌiธ\.4]ว๏๕าูŽช(ไ EๆI,หช้qฐm›ฟ๙โื…‚sBJ จ!ี๖๓้›wะ  Y?šแณ{‡*‹9pm_W๗ถฐฉตฆช˜‚สno™ฒมฉtžg'xtdŽr ˆฉfฟ‡?ฟๅ2V7…0,›วF็xfrกn๏!หq๘ิ[6ำ๐’ี F9 ห:+หด$ฉr๎AUมงธ๐).\ฒŒe;”M‹Dฑฬะb–ฃ๓)ผ๊ESC๑ ทฎ๋dmSEb<ฦSงๆ฿ts>^(3*„ิŠ๒มwลU๑1=?ฯ?|๙›คsน‹๖๙;6m`ืึอ๔vถ …PI’0-“bฉฬB"ษเศ๛ ฒHพโ๛\s๙v๎นํ&‚~?้l–ฏ|'ิsว๊ึ ๋ธjว6Tล]mู[X ๔Rเุ6~Ÿ†pZiอsนd ำฤ0 สšF*›ฃฌ้gUง้บ฿็ฅปฝ ช/•˜-โิ๘^ณ–e๓™/ !%็"„”@ PBชท!ภŸธ๎H€ฒi๑ใกi>ฟ๏ไK~๎สž๖ฌiฏfฉ•Pm@ทฒe‰t_žš็๑ฑฅK8๔๒ภอ—ฑบ1ˆnY|dŽ็ง๋๚>๚ไ5›‰tr†3‰บฝ‡dIโ“ืlขษ๏!]ึ™Hะ-ล๕ซ โQ จ.ผŠฟ…ฒToVฺ.‡ใYF’Y\’DgศAฯใ๘b†ป6tำืฤฐ,™ใน:…/Gฒจ1,„ิŠr[o็šหทก* ณ ‹ใWฟE<•พ Ÿๅ๕xธjวVถฌ_KW[+Aฟy'6รดศ‹ฬฮ/r๔ไฯ:Bฉฌฝๆ๗ตu3๏น๋6Bู|žฏ๐!Lหฌqูฐบ๋v_^ฉ(*–^ถM๑‚โ8~ยม^Oeื<—\‘๏บab˜fต"*‹ฆฟ2ฏห4MBมญMMx=*%Mgj6V๓Rฆi๒ท_†R@pBH ๕!ค๚›C‡=่ ๙)&฿œไห/Žพ๊๋vwGูณบญš|ี$0l‡ฌf0žฬฑ&ฮ##sUL๕DูM้ฉV|dhšƒsษบฝ‡—ฬ๏^ฝ‰&ŸJบค3•) Y6๎ื˜ีโ8๖T๒ฅŠฟ๊FYสSฑ+;(žJๅNdqh๚.ศy .ฆน{cฯYYe๛ฆใoบ9Ÿ*้œ\L !ต‚ผ๛ฮ[นvืTEan1ฮฺทek๙ูฝm36ฌฃญฅ™€ฯ‡ๅฎฬำ$—ฯ39ใศะ๛;ฏฐ๋ห6oไฝw฿N8$[ศ๓=Œn่u;.ซzนแสxU•Bฑฤ๔ERŽใ๒๛ ‡‚xTทห\QKญy%M#อฝbEิน˜ฆI$ขน1‚ฯใAำ &f็จ๕็0๘Ÿ_๙ฆR@pBH ๕!คึGร๛ถั๒Q4L๕่_;4๖š_ฟป;สต}mln๕{๑œ!ฆršมd:ฯฉ8ฬ’ฟbชฏ1ศŸธ๎p€’i๑รใSOีํ=ไuป๘ํซ6า่SI•tฆ3• ฉื†lู!BƒWลงธ๑*๒i1eูไ4ƒฉL‘กxวจ฿ณข็qd>ล=[V•U๖ยlโM7็ำeก!คV’wvื_ฑช‹'๘ื•นล•‘™ญอM์ฺบ™อkhmnย๏๓โvนซU6™\ŽษูŽ๒โเะ๚ฌํื๓ท฿I8$W(๐ญŸœRน\ทใฒบป‹›ฎพฏง"คfๆ/|ๆRภ็#ฒ$ขฮฉˆา ƒRนL:›C7ฬืตƒกeY„‚Z๐zฝ่†ฮฤฬv?ฯhบฮ๚๊ท„‚sBJ จ!ตฉ5ยŸ\ฟ•๖ผn๒อรใ|๓ศฉื>;:šุณฆ-ญ Dณ*ฆršมtฆภณ“‹<6:Gบ|แช^ฎโ๋ฤbฆn๏ก€๊ๆWl มง’.WŸi;œ๏fVฆๅเW]D^|Š …๊’‘$ รฒษkณน"ร๑y ู๏YWo„}3q>ฐ}อrVูNLq8–zำอ๙lูเ๘BJฉไท์aฯUป๐ช*๓‰$๔/฿yรํam-์บ… k๚hmnฤ็๑โrนฐmอะษไ๒œšže฿แc]‘๓ุบกŸผใ."ม๙bo?๔(…bฑnวฅงฃ[ฏฝ ŸวKกTษบฟ๐$$~แ`UQP7ฒ์ยq์ำ"JำHฅณๆ๋QKX–Mะ๏ฃฅz/ิ‹*—5๋฿BJ ฮ!„”@ ิ‡hkเฏ฿J[ะKN3๙๚ก1พslโบฺนym›[hYSฒ„น\…Sเ๙ฉ8OŒอ/j+~>ขไ†ญt†}t“o=ลH"Wท๗Pุฃ๐๑+ึำเUษ”*]ถใผแ์`ำr๐ธeZ^ชีUSฎ๊X “ูl‘รฑcษํA฿ @ๅฤซŸBj%น๛ฆ๋—+q)>อ๏25;ฏ๗๊๋๎d๗ึ-๔๗๕ะาุˆวฃ"K2ถcฃ้:ฉLŽัษ)=ฮะุฉ=-๋ึ๒มwEC8LพXไป?F6Ÿฏq้lkๅŽ๋ฎม็๕R(Uvู[ั‡ $‚Uล๏๕เQ=ธ\2ถ] +ื “RนL2“ล4ญ๓QKุี๚–ไรฮ@‘ IDATคฆ๋Lฮลฐํฺ/–Jใฟ|G)@ 8๗wˆR@PBj{G๖ฺ-ดฝไ4ƒฏใ๛ƒ“o๘}ฺธym'[ฺNWLน$0m‡œn0•.ฐ&มฃฃsฤ +ืถฒนตwภrลื7s*Uฟ}>•ณk=ฏBบคs*ฝฒ็bZช[ฆู๏! ธ๑ธ]x2ฒ$a;Eรb.Wไh,อ๑ล ก๓SŽฮ๒;WozำTฎฝyเXLฉ•ไฎ=ืrห[ฎฤ็๑ฐ˜L๓ฯ฿๚งff_ื{lXำวฮMฌํํก1ยฃž!ข4ลTŠ‘SS์;rŒSำณไ<6๕ฏแื๙Vรa ฅ"฿๙คฒูบ—ถๆf๎บ๑Z^ลR‰ฉุส์J'ก`ชโ๕จxT—\5ฯ0ะชญyฉl๎ ‹จ%วม๋Qi‹6/ ฉฉนV ฉ|ฑศgฟ๑]!คเ฿%BH A}ฉห;›๙ิต›i xษj_|a„Ÿ˜^ฑ๗hkเ†ีํlmoค5เลซธ‘ซb*ฏLgŠผ0“เกแ’+P1ตตฝ‘?บn`นโ๋k‡F™ฮิo[LKภหGv๖๖(คซไำrpษั€ฟโฎไL!ฆJ†ลbกฬฑ…4'24๛=ฏKLth†?ผnหYYeร‰์›nฮt“ฃฑคR+ศํื]อํีJœx*ลพ}ฦฆf^ำkทฌ[ห๎m[่๋๎$ โUU$Iฦฒ-Jๅ2 ‰Ccงุwไณ๓vืวk๚๘ะ=oฃ1กP*๑ฃวž$žช฿ถีๆ†๎พ๙>ลr‰ฉน7&คdI"๐ใ๕x๐จj5ฐUฉˆชŠจbนL*“ลฒ์Qห8ชขะฺR 5ื˜ŠอcYต-คr๙Ÿ๛ึ๗„‚sBJ จ!ตป;ส'ฏูL4เ!S6๘็รlxfล?gcK„›ึvฐตฝ‘ถช˜rU3ฆ zฅ=l฿tœGFgYศŸลิŽŽ&ํu[h T*พพ๔ย(ฑ|ฉn๏ก๖]ถ–G![6Kๆ*%ำreh๖yจ >ล…Oqแ’+ญ2%ำ"YิZฬpt>Eฤซพช˜า-›‡Gf^’U6^ว•kฏDั092'„ิJrห[ฎไฮ‚฿๋#‘N๓เฟ€‘‰ฉ_๙šห6od็ภ&z;;ˆ„‚จŠ‚$I˜–IฑTf!‘dpdŒGW|วพWb๊U|๘]wำ‰P,—๘๑ใO]ดฯพDB!y๋~Jๅ2“็ูF้’e~^งRฅจธ.,หฎŠ(bน’eู+,ขฮภํrัีึ‚ืใฅฌkฬฤ0-ซฆว “ห๑๙o@)@ 8!ค€๚RW๕ถ๐๛Woขษ๏!]ึ๙“<6:wม>oC4ยอ ด5าฌˆ)ท\ ิ^ส-:Pmๅ›อพสฆ]]อม[ถ, ถ/&qฒช.]a?ุฑ–ว]Rฉ‹—‡%KŠ_uแuป๐)nWeW+อดHuFYาxฎWSษ’ฦ๓S‹ั๕ด}ไ4“9<ฦdบ๐ฆ›๓%รโ๐\Bฉไฦซv๓ึฏ#เ๓‘ฬd๘โw~ศษ๑—ๆ)Š›[ท0ฐพŸUํ„‚ท€i™J%ๆa๏แฃd๓๗[ปช‡๛vš(•หไฟ$ถBป^ ~?๗q3A€RนฬTlวถyญw.Y&่๗แ๑xชUQ .น,ฏ:บaP,iค2,น`"j๙x\2mmx=สบฦ์"†iึ๔ค2Y๙;?BJ ฮA)@  >„ิต}m๖Ui๒ฉคK:๐ žŸฟเŸ฿ๆ–ถต7ั๔โWธd หถ)่&3U1๕๘ุ๋jนปชท…฿ปzอUม๖O{O’ีŒบฝ‡zผo๛‚ช›LYg,•ฟRฏHุฃโฏVK๙ซb @3mาeแx–ัdY‚ฮฌืNe _ฬœ•U๖ีƒcฬd‹oบ9ฏ™g…ZIฎ฿}9oฟๅ>?ฉL†/}๏วœ_wŸืร•ท2ฐพŸฮถ‚~?JU^ฆEพXdf~c'Gy๎เสฺฅิkzบ๘่}๏ ฺุ@Iำ๘ูSฯ2›ฏq๑z<ผ็ฎ”ด23ฑ ำD~•]9]ฒL0เฏfDUDิRž—ahzฅ5/ษ]ะŠจ—;ฎฎ๖V|ี ฉน๙E๔R‰t†ฟ๛#!คเ„๊CHฐบO\นFŸJฒค๓๗ฯ็้‰…‹๖๙kšBผถƒMด…|๘ืrnั’˜:8—ไแแูืT1u]_ฟu†`๛ว็‡(fCkšBผgk~ลMVซTHI—่XBžJ_@qใW(ี‡OรถษT3ฎFYlฺƒ>Ž/f˜ษ๘รkทœ•U๖FZ3kฒyq&.„ิ ๒–;–[ราู,_๙O86-ขสšNYืษๅ X–ลJ๔ur–า4ๆใ่Fm1c1™ไK฿ฉR@pBH ๕!คn^มวwฏงมง’,jณวynr๑ขG_c[๚;ูHGุฟ,ฆ,กh˜ฤr%^˜M๒๓‘Yฆ3ฏjณgM;ฟyลiม๖ฟž=ŽQใมดฟŠuอa๎X…_q“ั N]เ ฉื‚e;= ^Ÿโฦใ–QซS†e“ำMฆ3N,fศ”uEผfœีJ๙ฯ†W$ฤพึ0,›„ZQฎฺฑ•{ซญa™\Ž?ก`€อkhmnZQŽใ ™\Žษู๛Žใะ๑“5s=ํ|{hijคฌ้<๚์^NMฯิํธศ’ฬ๛฿q'‘`ˆ’Vfn!Nฉ\ฦํv/ŒiZธd™†Hฟื‹ช*จสูญyeญ๒_พXผค™Mฒ$ัำัถœ![Œฃ้ต-คๆใ พ๒ร‡„‚sBJ จ!uบNอฎ๕Dผ ‰ขฦgžd฿๔ฅห5้k rำšvt6ั๖Ÿำสg1Ÿ/qp.ษฃ#ณ/Š}K'ฟพ{ Š`๛gŽcื๑๏ค-๎ู‹Oq“ีtฦ’๙‹]8๐Š˜–ƒ_u๕{๑).ฒณŸฐGaฑPๆožไ…™ฤ%?ฎU AnZมๅMด‡|T7.Y>ซb๊ะ\’วF็Mž๚พ}}นŽˆW!^ะ๘g๋๚ฺาฺภ7๗โsปศhใษ\อฉ%Lหมใ–i x๑ซnTWEL™ –ี ข~^ท‹ิ› •๒•ฐ‡S‹BHญ w\ wํนUQ‡สwL ฑัtT&หศฤŽูฐ๓Zกณต…฿x๏ฝดE›ะtƒ_์=ภ๐ฉ‰บำฒ๙ะ;฿Jc$BกT$ถ P*ก(nB!|^>ฏUQ‘$ฉ* 4]งฌiไ ElปvชWeIขปฝ ŸืKYืYˆ'(iต]ล9=7ฯ7zD)@ 8!คธ้Ÿ~ภe๓’LคV๑๎M=|่ฒต„ชB๊ฟ?uŒƒsตณygศฯ-๋:ุูฅ3์[ฎ˜ฒ(&sน‡cIฉˆฉทn์ๆร—Ÿlใ—วpฟJศn-ณตฝ‘ป7๖เuปศh:ใ—(ิต= :จn™fฟ‡€โฦใvแqหศ’„ใ€,’Dูฐ๘๚ก1ฮ% y”7ีœท‡}BHญื๔ฑs`[ึญฅฉ!‚T5ฑถmS,—‰งา Oฒ๏ศ1&fj?‹ฉ-ฺฬ'wํ-อhบมS๛_dhlผnวG7L>ฎปiŠD(–KLฬฤdhŽD–CหeI^ฎ`+k:Z5#ชซVฯRšฎฑORฌa!U*k<ฝะ??ง>๐@Z|ใ‚ำ!%U๖<๘Dƒา?#K|ธ๏ž-ฝผ๛ZB7๓๙2฿SG9Kีร\ะวํ๋;ูััLwฤO`YL9” ‹๙|™รฑ$–ํpบ.B7 ี๓๑\ค]š.;:šธkC๗i!UC-{ฏ„i9ธd‰hภƒฟ*ฆ–ฦ ภq e๖Mวู? YาTb๊๙ษ!ค[ึญๅŠm[XีIC(Tg;‹ษ๛c‘Af๋ๆผZ››๘อ๗Ggk0๘ๅƒซq*i:ฝ๗ํD0-ซRๅvใU=gดๆ้”สšฎ“/–jช"๊\ฮnูำYH&)–joใ…RYc๘ิฤใ?๕์ว๗_?pJ|ห‚—"„”@ œรž๊s[๒ต&ฆฝต๛ทญ&จบ‰ๅK?ฟ8สเBํฑต#ไใฆต์์jฆ; xF+_ษ0ั-›GA‘%ๆ๒%฿'โWu{฿์์jๆŽ๕]x\.2ๅj…”TวnZH๖(Dผ*ํA๏้*—ชHL5†โž›\dฑP~Sˆ)!คฮใ‹ฃ—mศฮอ๔tด QI’ฐl Yชd“้†ฮOž๘%=๙Lcscฟ๕wำีึ‚n˜<๛โ!Žžฉห๑*หtถต๐๎;oล๋๑Tฏƒ$I˜ฆEYื—eTกXฌ‹?‰Jจนฯ๋E3t) ฅRอŸQ@๐:ึt!คเๅฉ51๕ํซy๗@ีอ\ฎฤ_โC‹™šฟŽํ!7ฎigww ]a?Aีโ’+๙2U้QิM>ปwˆSฉ|ŠŽQn_ื‰๊ชŸ ฉs1ญŠ,ผขงฏโยqlวม%WฦK3-Eัd–}ำqฆา‚u,ฆ„zํ(Š›+ถ 0ฐ~-ฝํ„‚A”๊.mฆeR(•ศๅ‹D๐z<JE~๐ศ/xr฿ uw_4Fยฮ฿Cgk+†i๒มรฎซs(ห๘>ถm่gsZšฯjฅ,”JhบAYำศJ8ิฯ๓ภi!ๅAำ โฉ4๙b๑’Wฉฌ18:ถ๗—ฯ์ฝ_ˆ(@ xkบR@๐ซนํมŸ๏ภv>p)ใƒ;ึ๒ฎ-•]ๆrEหGId๋ๆ:Fn๎๏ไสž(a?zVkุlฎฤ™ฯO-’ำŒบSW๕ดpKŠหEถFCอ_ ฆํฐ>&์Q(๊&ฑ|‰ฏZฉfsU2พtห&UาId9K1žศแQ๊ฏRฉWว็๕pีŽm ฌ_Kgk ฟฅฺZk˜๙B้๙Ž%™ษ๐ทAS$BกTโว?ลใฯํซป๛" ๒;ฟv?mm˜–ษCG9x|จ.ŽฝP, Xฟ–U=4FยxชปๆIReญMฆ3,&ST3็๋๏แ่้hฏ)ร ‘สห็นT nฉฌqth๘่“/๚=Z.ฏsMBJ ^ท|ก=ฒ,?ภ%Sนผyท™l‘๒ฤaฦฮุตฎn๖ผ wฎ๏ๆญปi TZรภฒสฆลbกฬั๙๛งค๊(ณ่-ซZนqMŠK&[g-{gโ8ะ฿"ไQXศ—y|,FkะKณ฿CGศGฤซขTร็ &[68•ฮs$–โd๑4<๓|อP ภ๏~่~zฺ1-“Gyแุ๑š>ๆBฑL8`†uฌํํ!VE”„eูศฒ„$ษ˜–ษ‹ƒ'P ^Zทฟ{ซBJ7L้4™\~น์b!D”@ ผq„‚ืษฅSตnyท้Lฟ|0้|^ว฿ฝzwฌ๏Bqษุvฅ5ฬ-KX”M‹DฑฬเB%ณจฤิu}mฐบฝ"ค๊ดe*bp}4BศSษ*{dx–ๆ0ฒ,aชหEgฤGฃืƒวฝ$ฆršมtฆภมน$'38Tย‡k!ค^J[ด‰][ทฐน ญอ๘ฝๅ8บก“ษๅ™œฑ๗๐QŸ8ปmMOฝ๏D)•ห์ฉg๘ูSฯึ๘|‡฿KoGฆeqเ่ Žึไฑ.‰จ›6ฐบป‹†pช,หงBฉD2•กฉ1B8@7 NŽO`;ตŽ…Tg;>OEH%3า™’|qึ›RYใภเ‰…็ฝ_ˆ(@ xc!%็ษm๓ุŽ๓€,ฑ๊b|o\ฑž;ืWvq›ฬ๘‹ว2)ึํ๕๛ฤ•–Cภำe๙|‰ถ oน5ฬฎVLล‹eN,fxfขถลิ ซน~unนพ…”ใภบhธžŸ+๑ศ่,ข‘ๅv=ำr(š&>ท›ฎฐ&ฟฏU Iถlrบมlถฤ ณq^œญ์ฆXซc&„ิiบฺZูฝm Vฏขฅน ŸืƒKฎฬCอะษd๓ŒOฯฐ๏๐1_aวนพฎN>๖๎wาาิ@Iำxไ้็๙ษฟฌป9เ๓z๘ฝฎ,หๆลม์;|ดฆŽฑP,ำ 1ฐพŸ5ฝ4†CจŠŠ$iVvา‹-ฦ9x$#S|๘]w/‡ดNNašชZฟ๙oง…”A*›#™ฮ _เ๊Lร0yศฑ…}ษ็๊ฯ฿‚เ#„”@ ผA.–˜๚ญ+7r๛๚N<.้<๙ัƒฬๅJu{~๛ชห!เงาy๛SGูาึศ5ฝญ๔4yTนาฮW6-โใ i๖Nวkr—ทืดsm_nYชfHีง’‘XยฏTย๓›cSK๙œ๊ำrศ๋ช[ฆ' ู๏ล๋vแ’ภr บษLถศแน{ง1kPL !kzปู5ฐ™พข x<*ฒ$c;6šฎ“Lg™˜ไ…c'89>๑+฿ซงฃ฿-M”5GŸห{ฒ๎ๆ€GU๘ฟŸพ๎Nlๆะ๑“wัึDYืyโน|‘'๊oธ]มG>ภ๊ž.lๆ๐‰až}๑ะ%=ฆ%5ฐพŸพ๎."กภฒˆฒ,‹|ฉศิ<รใ“Lฬฮ.๏€P,kึ–3ฑ&g็(k:๎3~ฆฎ^่๎hล็๑bZ&้lŽลd—kๅ…”ฆ้ฦไ\์มท฿|รoˆo9@pึt!ค`ๅุ๓เ ฒญหๆ$™ศJพ๗'ฏูฬMk+กูใษŸ~ไEEญnฏีงฎยžjๆาh2หONฬ0ะvZ|ฬๅKธ$่oณถ)Dศซขศาฒ˜Ju†โ๖N/2—ฝ๔b๊ถu\ีSR9อ`ฌN+คT—L_ch9<™ษึGรฏ๚:ำrˆหธd‰พ† ํ!?ี…K’ฐ‡ฒaฑXฌTนํ›Ž“(^๚๖ห…ิ–ukนb๛ซบ:hฮฒ,‹’Vf!‘โฤ่8๛ 2ปฐ๘บปฃ%สoผ๏^ฺฃอhบม“{๐‡ซป9 ห2Ÿ๚่Xำc;99ฬำ^’c)ห47Fุฑiซบ:+Q’$aš&๙b‘™๙Gฦ˜Ž-,๏€x&ลR™w฿uซ:+™X3ฑy ฅR] ฉฎ๖^฿B*ตบฟhšnœšž๛ฮ๗žyโ>๐@Z|ป‚ ดฆ !%+ฯ…Sxํ–ๅะ์ัd–?{๘Eาeฝnฏั_7ภuีฬฅแx–Ÿ ฯฐๅe*qb๙Ž[#ฌn ๖ชจ.วqะL‹dI็d<ห3 ฬ็/˜บc}W๔ดเ’ [6๊v—=หE_cp9<L‚ีMมื๚%1e;kšBt†7๎3rมE“๑ ฯMลYธ„c๖‹’$‰หทlไ๒-›่ํl?Cj€iU๒†)Ž ฒ๏ศ ๑d๊ผ>ง-ฺฤoพ๏>:Zข่†มS๛^ไ=R—๋ำง>๖A๚{{p‡ฃรฃr ๕๓ ล2ญอMlะฯชฎ"ก Šป2f†i’/™]Xไ่ษfb‹จ๊+หฅ|๑g๏พใใ:๋DN™>าŒzณ%Y๎*.)NO์!$ค„ะbฒ–ฅ„ฝห.q~„ป๗ {—ป\n–5,BIHœุ๊Ž‹mYฝKฃ้ํฬœrI‰Yž‘ž๗?๐bญ™93ฃัœฯ>ฯ๗คธ๏ึ[h˜ฺ‚8<>A<‘œี€3ง๏g$j*ห๑บ่†A(%š %B” ยฆ‹ %‚pแฬf˜zsภyไ้}ฤดlม>7sCื4Tขศ'&ข<}j๘๗ฉiรฑ†eาZUBฃ฿‹ฯiห…)@ำM‚)“(ป'Š$็‚ผ Q]Q†ืใฦ4M&Cกะyญ๘Jฅ5:{๛ž๛๕žw‹%‚0‡Ÿ้"H ‚ \xท?ูจrป,๑เนฦงnhใฺฉ€s2ๅoณTV/ุ็ไ๏6ญ™ูโv|"ยoปFhฎ๐ัŸŽฅฐ,‹U>–”z๑9ํุฆNฌ2†I(ฅq|"ย‘ }แฤœEŽทฏZฬฅueH@8ก?œ(ศR^ปE>๗ฬ๐cใjŠ]็u›๑4‰ฌAฃ฿Cๆ‚MฟfแT†ฮษ(‡GCtc8ินYน1_ƒ”้ไŠumดฎXFme9ท{f+WV7ˆ' ŽŽsธณ‹‡Hkณณาฒิ๏ใ#๏น—ฺส๐์Wไป?ฒ ?Ÿ>เฌ\าˆ…ล๑ฎ^v์ฺsA๏/ฅiิTTะผฌijt<”ฆVD%่ฅณท‘1gq…ผh<มฝท2ณq<$n’$*หJ)๖zฮ;HM‡จ็^๙ะWm๏฿VAๆ๘3])A„นs>a๊ำืpU๋็oŸ‹f˜๛\|ๆฦต3[ŽŒ…yพgŒีgพˆl8–ฤด`YY+ส‹๑อlๅ› SiS๛†๔…ธm6Lฑz1๋ksA*˜ส0Mไ๋R์ฐQW์žž2กบศ5+ท=Kก้&ีล.šJN‰Yำ$šฮาŽsp4Dg ‚rO˜็[*).ๆฒ5อดญXFUy— UUฐ,ศ๊Yข๑รฃt?ษCGั cV๏฿_TฤGw‹ชชะ ]‡๙๖OUฟ{ปXูิภ‰๎>ž{u๗นŸTZฃบขœถ•หY\]I‘ืsฺึผh<มภศ(GO๕04:†ห้8๋๛ˆฤณ๕&–7ิca† EฃH…XฬษฉŠ?พโ",ำ" 3 6ศLžwขA๒เ3])A„นทq๛“6SzLBzว™ฬ#›ึrE}.เ๐7Oพ†n๎gx๛M๋ธlQ9ph,ฤห}ใฌ*?๛]ƒั†™[1ตฌฌ˜—ดญ|แt†๎`Œ]๔Gx.P˜บณนžต5ฅL&5Fbษ‚|]N;5E๎™แ๙]ม•^็ฌ‡ฆ›คu"ปZŸ‹ง‡*#ำ"ฆeŠ$80โD ‚eYศเไyพฉช๒2.okf๕ฒ&*หJp;](Š‚eYdฒ"ฑ8}C#์9x„ƒ':/ุฑy<|๔๗ฯ\อํตCG๙ๆO~Qฟพ๛Yฝt '{๛๘ํ+ปf๕๖Sišส ฺV.cQuE๖ฉฯฆLV'30:NวฑŒOฯ)DM‹ล“ผใ–3+พ‚แมHด †–—๘)).ฦฒ,&Ba&‚มฉซ๑็๐‰ฮร/ํ=๔พฏ}แำA.*คA.ข›rฃ,หํภ ์฿~v*เศภ‘๑0๋ื ๚ุแ–๕3+Š:FC์œ`Yi๑9฿`4A:kฐฆฆ”ฆา"J\ถI)ั IDATvSยดฉmaมป'่ ฯ~˜บปฅ5ี%X@ ‘f4ž*ศืฅิๅ ส๋šž฿JPแq\๛า ‹DVวฅ*,๒น)u;pชJ๎ b†I,“e8šb๐$๛†ƒฆ9ซณม =H-ชฎไ๒5ญฌXา@Ei .งEฮ ืฒยั=Cผv่(GOu_๐c๓ธ\|๔๏šนšพ#วุŸ?+ศ฿ƒผ็>š—5!I}<๓๒ซณrป้L†ลีUฌljคพฆš"ฏUQ‹ฌฎ‹'่ๅHg7#ใ็ขฆล)nฟ้zV65"!ŠF „ {LRฉฏ˜2ฟ ‹‰`ˆ‰ษ๖ทุฦ8ข^ุื๑ฑว?ศA„ผ ‚” B8“0๕น›ืฯฬ(:4โo~ณท ๙ ›/a]M)p`$ศแ KJผ็}ปƒัรคนาฯฒฒbS[๙$ ฒFnลTg JวHn^‘ห6;๓Š๎im ญชงง r†T™;คฆ‡็F”น๔>uร"žษbWd๛<”yœ8UEรฒHdt†ฃ):Fƒ์ 5f'LjZZฟˆKšYึฐ˜rฟ‡รŽ,ษ˜–‰–ษ G9ีืฯรว่์ํŸณcs:|มhจซม0L=ฮื๔ำ‚|๚๐ป฿I๋๒eH’DWOฟ*็๓9“ัY\› Q‹ชซ๐ž6ืKŸYลvฌซ‡ั‰ภฌ„จi‰dš[7^“ lHDb1ฦฯ๑JŠ๙ขคธˆ๒’’?คDˆAศo"H ‚ ไ‘›r#ฒ˜ k฿{ใŠข#A๎ฉ}}ฌnน”ถฉE{‡&9:ฆฏๆ๖๛ Fhบษส +ห‹)q9ฐ+ฏฯ+Šค3tใž<๏Aฺ’$qokอ•~L`4šd"ฉไU๖*ฤas*•ึุ{๔๘๘ž‡>๕ต/|fป๘v!‚ŸDAศC›ฟ๔6ำฒฺe‰†้ํ+Š๖O๒™ง๗์๑ษ’ฤฃ[/ฅe*เผ6 3ฅฎุ=๋๗5Iึ VU๘XYแฃิ•›WนS‘t–pœืtŸc˜R$‰wถ5า\้รฐ`8’d2™F.ภ"UๅuQ๎vL ฯ2‘Hใsฺๆ๔1่†E ™F–$–”xฉ.rใฑ+(’„aYคณคฦั๑\˜šLj็ฆ %HตฎXส†5ญ4ิีโ+๒โฐ$ร0HฆำLL9ึหk‡Ž02ธh๏E‘yx{fฎๆv๐D'_๛ไgิ๏ป‹ตซW ห2=C<๒ซg1>ซิUUะฒ๙๗ฒด~1–eqธ๓_ฮ ๒3๊ก{๏d}๓JdYฆop˜ง_EVฯžม{ึ พฆ†•M ิUWโuนงฎt8ข๔ s่ฤIม ขฆi™,7lธt&ฐ%’)†ฦฦ :HนNj++ค\'Oค=้ฟเ๛I|{A( "H ‚ €-ฬถ/mนไฑ–*ฟozEั็~[ธrจ _r)ซ*Š1L‹W๚'‰&ฉ๐\๘“ณH‚xFงตสฯŠrฅn;EA–ฆฎ๐–ฮะŠำ1ไิdU–ฯเxd๎iidๅิ๑๔…„ำิ RPWไฆฤํ@ŽŽ‡‰jYงยT4ฅ/œ Šส๏ Š๙คJ>.kkฆmล2*หK๑8]S[ผ^z=02Fวฑผv๘(†aๆ๕{่c๏+›8ัว๚ๆw ๒s๖}wฦๅkZP•ัQ~๛๒.’้ำƒิโ๊jV-[BuE— U™ž•!‰า30Dว๑“Dc‰‹ขฆ้†ษeญซูฐถUQIฆำ ŽŽ…คRi Y’[๓๒%_ดl"D ‚ 0คA ิฦํ;ฒ™yX…ฯฺc/u;๘ฏ7ฏงฉดˆฌa๒\๗šnโฝˆ[ร" &“kชKXUแฃย๋ฤ95c*k˜ฤด,‘๛†'้šŒ๖ณ^ปสญ ,--&k˜tNFIft์ja)รดh,ษลศ]อศ๋ญ‡šnึ Š์vj‹]”L ญ—ศmมŒkY# :ฆVบฆ…</fชฎ(ใฒถV/]BeY)nงsj‹—I&›โี?<ย๎ƒ‡9tโTมผ‡ฝ๗ณzู$$N๖๖๑ๅํ฿)ศฯื๗q+ึถaSUวฦ๘ํ+ปI$“duƒe ‹YฑคแดQ–e’ฮdˆLmอ๔๑D๒ข†จ7^ฏ[ฝ‚+ืญมnSIฆา `Jฅ5:{๛ž{๎ล}๕ั๖^๑-@ก๐‰ %‚Pเ6nฒQ5ไvYโมByฬ'Ÿปy=KJผd ƒgN`ZnลŸU4I0‘Lณถบtj+Ÿ—ช ห๚T˜๊$่ qd<„M–๑9lีาภ’า"ฒ†ม๑‰i8ง+๖]์ืฦ/~ง}ๆjŽฒ$ฤpvฐHdu\ชBฯM™;ทาm๚u‹gt†ขIŒLฒo(ˆหฆ\” ี/_xีšึ็–/i ฒฌงร"็†ดk™ แXŒž!๖:สฑS๗y๔g๏น—–eK‘$‰S}<ถโWอn฿ส•๋ึ`SU†วว๙Ÿ’ีห–ฐz้jซ*p9(rnF”–อŽฤ8ีืฯก“]ฤโ‰ผQำ, ZV,ๅšKึbทูHคา ŽŽฬk!B” ย%‚” ยHM‡จ๖u|์๑ฯ?ฒAaAJažบ๙๑'7สฒN†ฉ†/lZรขb)เว(u;๒zขษ@$มh>u}ีE.โŸ๎ฃถุ]ฯ็@$มH,ลวฏ^ืa˜Yy`˜i •5Hfuยฉ &๙๛wื˜š!ๅŸ Rป&fŽiพyฅoœ฿ž๔“๗ดc.๏๗ƒ|~ฃ๓6Hฝ๏ฮธ|M ชข28:ฦฟว๗ˆฦy๗8ซ+สธr].D••๘q9_ฟฺaZำศ๊:ท EV˜๙ทืํ*ุืฅบผœญ7\ƒวๅ"•ฮอบAJำ2ู๗๘ฟ{๐?…AคA€›ํฉ^Y’ฺŸz่–ํ ํุo๙๚Scา.ษ๘ๆ๊>ืึ”๒WืตPแqำฒ๐P/ ~oม>‡?;>ภ฿ะFตืE2ซ๓b๏อ•~$ NปŠ*K่ๆt˜า๓2L™ฆEcฉฟำAึ0ุ5 ุ9?WHํ๐รC="Hอฒwฝ} Wญ_‹ฆ246มฟ~๛y1+ซถช‚+ืญaๅ’*JKp:ศฒฤ๔ีก0งz8ูว่D€ตซWฒ๕บซq9B!?†้(ุืฅขด”6]‹วๅ&•N346Žašsvš–ษ๖Ž๘‰Wv|x{{{X|๋A@)A6) ภด่[ˆajใ๖~ูฬ<ฌ˜<<a๊ฒบ2>qM3'Q-ห๖ฐคคจ`Ÿฟ_žไฏฏoู‚ธg0€SUฐ+2ฅn^ปŠfรe“งฏ๎6ต•O'‘ั™ศ“0eYะXโล๏ด“1 ^ํŸภ็ฒฯห๗กIพ฿ั-‚ิ,ป๏m›นๆาตุm6†ว|๕;?d"บhgqMึถฑrIๅ%~;ฒ$cš&)Mc"ขซ€๎!2ู “แ-ห–r๋ฦkp;]รaถ๘็8์…gK}>nฟ้zผn)-อะ่)ขA„ท"‚” ฏฉi&t`šฯ๕ษ๊ล6Wa๊Šล|๊ี”น„ำพฐ—eฅ…ค~}r่ด-ˆGฦr'เN›€,I”ธ์ธT—Mมฉ*(ฒ„iYคu“tึ žษHค1/b˜zcา ƒ”ฬำ u`$ศw๖w‰ 5ห๎ูz3ื]ถ‡ฦ่ฤ$_๎ Lฮ๙ใX\Sอ•๋s!ชิ๏รaทฃศ2†i’JkŒ&950H๏ภ๐LˆšŠฤXฑค6]‡วๅ"‰๐ปญpทฏ๚ŠผผใๆMy<ค5กฑqtรธ`๗—Jkt๖๖=๗๋=;๏!JA๘CDAเwƒิyM ๅQ-K_(NZ7ฐ‘ฒ$แsุqU\๊t˜’0-ะtƒิ‡ฉ7ฉ}ใ”yๆg:6แ฿_;)‚ิ,{๛ฆ๋ธ้๊+p:์LC<ƒŸะ?นœN๎|#โbาšฦศD€L6{ท›Jk>ัy๘…}{๓์@Aฮ‚R‚ œyšfZ|CWฬ๖…ฆ6ozฆ๕pร๙ฮ–uOƒิษ@”วwŸAj–zร5rํ•ธก0๖ร'่ž๕๛YXฯ†5-,kจว๏+š Qฆi’Lงะ?HฯภฆuvรปำZ†_1๗ฝํผn‘xŒ๏์I ำ(ุืล้pp๗–›().&ญฅ L’ึ2Hาน}–ˆ%‚ ฬคA8๛ ๕†๘ฆn~ษฒ,ทsŽa๊ถU‹x๐’e;lL$า|`oAฉ—๚ฦ๘ุUซ)า>Iๆ‚ิYn๑™S.›‚ฆโถฉSรฯsaJำ ™, ฌaฮz˜2-h*๕โsุIfu^้งยใœ—๏แฎ`Œ๓๊qคf๛ณ๔ฺ+ูz5ธœN&รaG?Z4;–7ึsๅบ6š๊แ/.ยiท#I2†aHฅpขงแัณQำ2Yฏอo฿LฑวK4็{ฟ๘ Y=[ฐฏ‹f็žญ7Q๊๓‘าrณด’ฉฒ|vsฑDˆAf“R‚ œ{ฐL"†ฬcฆllวถ ๊jB็ฆั\ฯ{ื5Qไฐ1O๓ƒC…คvLœ6ค} ’ c˜(็8sFFขศaรm=LฉŠŒiZh†IF7Hduฦใ้Y SฆKKs+คโW๚ฦฉ๔ฮฯ ีŠ๓ฟwAj–tีถ้ZNมp˜ํ?9ฝ็}ป+–4pลฺVš๊QR\ŒรnC’dtC'‘J161ษฑฎFฦ8฿ญu†aโฐyเ๖ญ{ฝDq~๘ซงIkZมพ.6ีฦ=[oขฬ๏'ญiŒƒฤ)”3ิžJk์=z||ฯCŸ๚ฺ>ณ]|kAfƒR‚ œ_šถ ริฟ=u'๐˜,ัp&žึX„ืฎ2O๑ฃCฝฌฎ๐์๑๏ ฬ iงณ๔‡ใ่ฆล๙ฮ@–ฆย”วฎL]™Oล6ฆfVLes3ฆ2๚๙‡)ำ„ฅeE๘œ6bšฮ+cTy]๓๒=N๐/ฏAj–mผโ2~ใ๕3Wง๛ฦO~ม‰๎sพฝ–ๅKนคeK๋Sโ+ยnณาi+ขŽw๗า;8|ฦqๅ~–[ (2๏พใV|"b‰?z๒’ฉTมพ.ชขrฯึ›(/)!ฅฅ รฤ EyหŸ3 ƒ‡EˆA.คA˜ 5s2ณ€ริๆฏ?ฝอดฌ๖?ฆ๎_ณ„๛ฺ๑ุUFbน ี\YธAj๏๐$z๙ .;‘t†พpรดfiGe1uU>—}*Lษ–Yำœน*_ ‘F;0•[!5คฒผา?Aี<]!5I๒ฯ/Aj–]wูzqหF<.7กฉซำ;ี}ึทำบb—ถฎฆi๑"|E^์6ิU๓r!jhlœฮž~z††PHT9วฏศผ๗oรWTD<™เ?๓,๑Dข`_EVธg๋MT”–ึ2ŒMNO$QU๕Œฆeฒ๛Ž๛C๗๙๑-AAธDA`vƒิN๎๛dIj๊ก[ถ/ผ็๓ญริ{ึ5qOK.›สH,ษ๔๔ ฉ#Aไ๒ๅ๘v"้,ฝกุน—eAฑำ†KU๐8l3aสด k˜dŒฉ0•ิHgณ S–Hฐฤ๏ล๏ฒีฒ์ PๆžŸCอGข)้ฅร"Hอฒซ/Yห]›oฤ๋vŽF๙๖ฯ~ลแ“]g๓-ห—rลฺVีžขt žJ2<6ม๑ฎ^FGQd๙‚‡$ษผ๛๖ญ๘‹‹‰'“<๑๔sDbฑยย/Iณ๕fชสJIg2 ŽŒ‘ึ2ุlฟค4-“ํ๙๑ฏ์๘๐๖๖๖๕TAๆ๘๏“R‚ &HM[จajใ๖~ูฬ<ฌ˜<,ษœ6 ๊มK–๑Žๆz\ชยP4ษOŽ๖ั\ภA๊เXhๆช‘t–žะ…=qต€"ป ทMมcWqmุงร”™ส—ฬL$าgฆLำB–%^J\นฐvp4H‘ร6/฿ŸcฑใEคf†ตญ{๋อง]ฎใ๘ษ?๚sญ+–ฒamu5๘ŠŠขtโษ$รใ้์bhlญˆzำd$ธใVJŠ‹Iค’์™็ F"๛บศฒฬ]›oคบผ -“ฅgpˆlVวn๗[„(Aaฎ‰ %‚ภ… R3'"Lอ„ฉ‡.[ฮํซใT# ~zl  ‡šฯ\50šฮา𛕖^ป:ตZJมcทaWd,๋๕Sgฆtำยฎศ,๖yฆfae8>มฉ*๓๒}9‘H๓ฯAj–]ฺบš๛oB‘ว3suบGมฟพyPkย IDAT๋šWาดธ_‘›š $บกO$p๔T#cุm๊\~EๆทR๊๓‘Hฅ๘ลณ/… ๖uq:œผmใ5ิT”“ษf้์ภ0 v;ฉดFะ่๗EˆAๆšR‚ ฬMzƒ็MำlŸ๋แ‹ํa๊CWฎ๐ฝmๅ"ŠB$มฯŽ๖๔ ฉ๏[ฟ”ข9RำLำขศiรcSqูTผ๖๐s€Œa’1L™,“ษ ษŒ{รTึฐpช2u>ฯฬีปC1ไู„•g‚IGwAj–ญ[ฝ๒wฎN๗ฺกฃงY–ธคe5๋Vฏคกฎ†bฏw&6้†A,‘dptŒฝ aทซแH$x๛J~’้ฟฺ๑cษ‚}]Šฝ^nผjuUdฒY๖Qฐฏ‹ชจถ้:๊ช*ษ๊:ป:}ใป?yปQ‚ ยล$‚” -HM{Yz๘ฉmทXHฯy2ซ7ฺฅฝ7{๐ู๎Q–๘ฝ{,ฝแ8๗ฏY‚ืฎัฒtO^œ 5อ0-Š6ผSWไ๓ุีV>^_1•สๆ†Ÿ'ฆย”ฆจฒฬาฒ"N;มคฦ‘๑๐ผ!ีฒ|ทDšeญ+–๒w6uuบ$?y๊Yv๎๏`๊U\ฑฎ•EีUงญˆส๊:ัx‚กัqž่ddb—#้[๗ฟ}3ๅ%~RšฦS/๎dptฌ`_—ด–แทยโ๊๊็uC฿ๆฐ{ล_~AแbAJ‹ค0-พก+f๛Žm[ิ‰ย๑@ค๑ห/๛โๅue๏Z\ ซค" ๎mkฤmS‰jo…ิ›ฆ…วฎRๆvเRUœ6ิŠฉŒiข้&้ฌฮxB#šฮ ศ+ส}๘œ6&“{‡&ฉ๔:็ๅ๛.‘ั๙3๛Ešeซ—5๑พ;o›~๐x'.ง“๚ฺjŠ<l๊tˆสฮlอ;|ฒ‹‘๑ œ{ˆ๗ถ…ŠR?i-ร3ฏ์ขohธ0฿๋ษ4แh๔ล?}๗;พiQA!Oˆ %‚@~ฉi 5L๘“WV๘ฟti]ู†B Sรฑ$๗ด4เสณ 5m:L•ปธl 6Eฦฎผพ•/5H๋ฃฑ %^Š6&i^๊gI‰w^พ฿RYƒฯ>ฝOฉYถชฉ‘๗฿};ฅพb รDหdPUิp้QรนญyCใใ๙ฑ"๊๗ธ๗ึอT–•ึ2<๛๊z ๊ตH$ำ‚ก—รษไ#‘‚ BžAJ Rำraส๐ŽmิU 1L%Rนบ>oƒิดำVLูr๘loš1ๅRTYb<‘ๆฉฮaZช๓๒}ฆ้&ŸyjฏRณศfSy๛ฆ๋น๑ชหฑlไพbZXVnET4ž wp˜c]=ŒŒpปy|4๏z3Uๅฅค3ž฿ต—S}๑:$’iฦBกX"๙~ขA„|&‚” ๙ค,“ˆ!๓ุฒŠฒวพv๛ฅ"Lๅฉ@Rใ๖ี‹qฉ Q-Cw0N>_œN7-ผv•R—๗ิ|)ป"#H ้ฟ:9DฉำN‘sอ‘ส&๗คfƒfใ’ึี\ึฺLC]5E๏ฬ๛_ื ‚‘รฃt?ษฤdปถZpฯญ7S]^†–ษ๒ย๎ฝœ์ํห๋‡<ข๑ฤ฿ํ Ÿู.ฒ ‚ ๙N)A๒7HM[Yๆ‹|jSc5^ืcŠ,…ึk๓๔ถฟ็๓WีWิๅk˜ ฆ4พj1NUษ๋RoฆnปB…ว‰หฆโRs๙ฆฮว ฅ4z‚qbZงช`Wๅy๓พ2L‹ฟ}๒5คฮƒรngรšึฎ^I]UEชช’”ป๊ใ๐๘8ฟ|๎EB‘Xa„จ)pฯ–›จฉ('“อ๒โž๏๎ษหวšษ๊ ŽOˆ%‚ คAศ ี\้็ฏohฅสใŠ$ฒ๚cล[๛ย{rajหŠฺ:_> ?"Z†ทญ\„C)ฌ 5M7,ชLu‘‹ Y’ศํถฒศ˜‘T†pŒมpงMกบศU๐๏'ห‚OzR็ภaทsี๚5ดฌXสโ๊*ผn7ŠขY@QdY!“อ๐›wฒ็เ*หJ ๊M ๎ูr#ต•dฒY^ืมัฮฎผ{œฉค |๙หŸซGฤ_rAกะˆ %‚@ฉถ๊๊บVชผNbšฮทt๕=qt ฉ‡nูพะ^ซ?๑๊Gฏ\\uล๙ณ"žัูฒขถ`ƒไ†|๋ฆษ†E8m †iข›6YF’ kZDำบƒ1๚ร ๔ฎwR]‘โž}์^=pˆมแณบmฏอวฎ™ูK{ๅลื๖ใv: ๋ย‚-ื_MS",ำโเ‰N^ูw`ึ๏F„(Aa!AJR[–ื๑ะeห๑9m_~ๅ({็qฒe}9ฃ8ฺwlธ ฎึtกย”CUธขพE‚˜ฆำŠsc˜~—ล>ช,“ศd9ˆขศg^ึtรB‘%*งfL9Uปช LmๅKduย ๚#‰ฉซ๗]0ตะ‚Tฉ฿ว5—ฌeีา%นญy'ฒ,cZf.D…#œ์้g๗มรgขฆนN>๖wัPW;3{้…ื๖ใrุ ๊๗มฒ`หuWฑด~1–eqธ๓/ฝถึn?‘L3 $โ‰ฟฺ>ณ]A"คAศ ๕ถ•‹ุv้2Š6&i้ฅฃ์ž<ฟ.“ˆ!๓˜)[ˆajIi๑ทฎชฏจ›0ๅฒ)\พธˆerƒฝ iZ”ธิปsณฐ2Y:ฯ2HM›SๅฉSUF–$Œฉ05I2ŽcSeJ]g๕ฬB Rฅ%\นฎๆๅKฉ(๕Ÿถ"*ั„ยt๖๔๓๊ƒ ŒŒื}9์v>๑เ4Lmu;p๔/์ู‡รn+จ฿ห‚อื^ษฒ†z,หโศฉ.^ณ๏ผo7“ีŸ!JAAJศ ๕Žๆzปฎ‰"‡๑xš๑า:F‚ณsโต€ริๆฏ?ฝญม๏๙๙†)]ๅาEๅน ฅe้ `ฒ ํ ฆศ5ณ๕๐ไdU>๗ฝ‡บa!KPแuโฑูpูoZ15MัŠa“J=sปŠfพฉšŠr6ฌme๕ฒ%”—”เr:Pร0gBิษ๎>vuf`dtV๎ำฆช|bปYฒธำ49xผ“็w๏ลnS ๊๗ม0L6_w+ฐฐ8vช‡็wฟv^ท™Jj๑๑P๐/๛฿ลuAA)A„ฉ0‘฿A๊๎–XD‘Ce,žๆ_8ฤแฑูmG–I™๖งฺุย{sajหŠฺ:฿9l-*rุธคฎ ˆjY๚ 0HYTxœTyฏฉ@U9aXบa!หLอ˜z=Lฉฒ„nš$2:#ฑ]ม( Qแvฮส1๓5HีT–ณaM+ซ–.กฒฌงร"+ฆAZำ˜ G8ัีหซ146>ซ๗-ห2Ÿภ{ff/:ูษ๓ป๗ก*rA>duƒ-ื]ลŠ% œ่๎ๅนW๗œำmฅ’Z<e$xt{{๛‚Š‚ ‚๐VDA ƒิ}mทf ›สX<ล{แ0Gว/ฬyiั'KR๛Sฒ}กฝไว/u Uิปg๓s>—u5ฅภT ล‘คย;J“ ฏ EสGg †mรnXHR.Lyํ6ช‚ห&ฃศ2†i’ศ G“Sa *=ฎ ฆๆ[ชญฌเสumฌhj ข4ขdIžบjžF ๆXWปbd"pมŽ๑“xฯi[v์ฺ{N[?/&-“eหuWณji#'zzyv็๎ณบ ขAแญ‰ %‚@ฉwฏmโžึ6•‘XŠ/=ˆศฝฯ…ฆ6n฿แ฿PS๔อๅพži˜๒O) ˆฆ3๔†โgtuบ|SใuQ๊qฮl=์œŒbป+[tร *NผงชโTsWๅ3-‹Dฦ`$–ค'รฐฌ ฆๆKชซชไŠumฌZฺHน฿รaฯ…(ห$ญiŒ‚œ่้c็ฦม ~Œo{7ห฿ฐีmวฎื .ะฆด [ฎปŠๆeMHHt๖๖๑ฬ+ปฮ่gษ4‘hโK"D ‚ ย[AJR๏[ฟ”ปš๋qูT†cIพธใ ง&็ๆJn&t`šฯ๕‰๛ลv6aชฬํ ญบ ˆL)ฅƒT]‘›ทcf๋แฉษู]!๕fบaa‘ NปŠหฆโฒๅ†Ÿ›–E2k0Kq*ร4M*f9LzZ\Sอ•๋Xัุ@™฿—[%หฆA*ญ11ไhW{ftbrฮŽ๑ใ>pฺVทgw๎)ธ •Lฅูrีด,[Š$Iœ๊๋็™W^ๅญพ6'’iยั่wรมะงฟ๚h{ฏ๘ห*‚ oM)A๒?Hmปtwฌฎวฅ* E“|แนzๆJnฯ›ฆูพริe•ž/7W๚฿‡Ÿ—{œดV๙1H*Co8~^รภ/ห‚ล~~gn†VTหา=›“YNบaM=นซ๒นl .›2ต•ฯ"™ีง่ ลษ&ๅณ4cชPƒTC] Wฌmcyc=ๅ%~v{.D)-อฤdˆฃงบูีq˜๑ษเœฟ—>๚พwถีํ้—w\ %’lฝฺV,C’%บ๛y๊ฅพ๏อ"D ‚ ยนAJRผ|ทญ\„SUŒ$๘‡็:่'.ึรY a๊ษฦตๅฅsmuษ]ง…))7{ฉฅสaA8ฅัŠ_ญn’iAฃ฿ƒฯiŸูzุšฐ6ฝ•ฏz˜rช ช’ S้ฌฮ่T˜าM“2ื๙…ฉB R‹jงBิbสน%Iาi+ขŽœ๊f็ƒร‘‹๖^๚ศ{๏;mซ“/๎,ธกๆัx‚-ื]อšUห‘e™ž!ž~้U ำ˜๙7‰dš@0๔r<}ฏQ‚ ‚p๖DA ƒิ‡6ฌไึ•u8…H‚๚ E“๕1YX?อสึร;ถm]P'boS’$Qๅuฒบา‡aZ„ำz‚q์j)ำbIiลฬึรžะล[้%~—ทMลฅ*8mนซ๒™Sa*M_8Nฦ0)s9ฮ)LJZZฟˆหืดฐผฑžRŸป63ฌ<™N3:เXWฏ์๋ ]๔๗า‡฿}/ญหงถบ๕๐ซ/aS•‚๚}Gclฝึฎ^,ห๔๓๔Kฏข๚Lˆ '“<๙Gv ‚ ยน}฿AJ!ƒิGฎ\ลๆๅต8…พpœ๖฿`4–สa๑ ]1b˜j)๑๎ึ† U^ซ*Š1L‹P*CO(†ฃภNภ ำขi*HMo=์ _YX2>——ชไโ”M™~žฮŒ'านS†E‰ห~Va*฿ƒิา๚E\นฎฅ ‹)).>mktˆ:าูล๎ƒG.๊Šจ7๛ะ๗ะถb๙ฬVท_<๗bมฉ`8สึ๋ฏf]๓*Eฆoh„g^~•ฮž~ขAa–ˆ %‚@ฉ]ฝš›—ึ`SzC1>๛ฬ&้ผzŒ 5L๑g6>z๋eํ+ส‹oะงƒT0†ำV˜Aส็ดอl=์'๒f๖,I;r๘<ถพq+฿DRฃ/Gำอ3S๙ค–7ึsลฺVš๊Q๊๓แฐค\ˆJคRŒ&9t๒{:ŽŠF๓๎ฝ๔ม๛๎šYYิ30ฤฯŸ}กเ‚T f๋๕ืpIหjTEกoxค๏ฑo~w?_ํ@A„Y!‚” ๙คพฆ™MM5ุ™๎`Œฟf?มค–—U‡ฯ™ฒฑ6.จห๋ฆตQ7อ๖pโ†๑DW!ฉฉฐ6M’oณจeIขศaร3ตZสmSQำดH๋นSแ8iฤ๏|๋0•oAjUS#—ถ5ณฌ~1%พbv อฌˆ Lr๐D'ฏ๎?D,‘ศ๗าC๗ษ๚ๆ•ศrneัฯ~ปU)ฌ฿‡๑ษ[ฏฟšหฺš๛TEmWyป๘K)‚ ณK)A๒?Hๅต-ะT*หtMFyไ้}Dาูผ}ผ–Iฤyl!†ฉ'O๙lื่ฟพzQตวฎฬใ~s ฆ2 ๅaz#ŸรŽž‹RN›ŠM–ฐ,H๋‰4}ก8Yำขุa๛ฝa*_‚T๓ฒ&.i]อฒ๚ล๘‹‹ฐlนaๅ†A"bd<ท5๏ๅฝHฆาy^ฺvฯo\YฤOŸูQPCอ3Y๎มแ๛฿vหgฏฟ’ํโ/ค ‚ \"H ‚ A๊ฟ\฿สuUจฒLg ส฿=ต—xFฯ๛็u!‡ฉอ_zฦฆ๊/]ืXYYa๊๕ eG7M‚I‘X Iส๓๗เwุsซฅ์*ฎฉ0eZ ้d.L้ฆ…ื~z˜บุAชyYึถฒdQพ"๏ิึ< 0H$S sไdป:O& ๆฝุป๏0ป๎๚ภ๏{ฮํm๎๔*iFฃ^Fอ]ถี โฤุdƒึ ๙%›‚Ilร€m„ˆeI xBIHศbY’-ษEถl๕^f4ฝท๋)๛วฬศ˜bd{ส=šฯ๋/|๏นง|๏นš๗sฮ๗ั๏ผ—›–/มฎฺ้่้ๅ฿๖@QฌคRษLผซo๐k_๛'–_F!„brIB๒?H=ธn9k็”ก*6. D๙ฬcคrบe๖ฏiฑ)ถ๖ฟe๗ฬ[ฃajหผส25/7๚๙ •ำ FRzโ)lฺื—รŽฯiว๗sa*ญ้ ง2t„ค5CลeWง-H-Ÿ7๗ภญ+—S[]E(่ว้pކ(M#žJา?ศ‰๓—8z๖<‰dสrc๛nYฑ ปjงณทŸ์;€ขไ๗HJ%3๑‘h=ษมปร!„bาIB๒?H=ดกf•ข*6.๔G๘๔ณวศ๊†ๅ๖ณaาฆุl31L=ดฤ7๎™_QCuไ้ฑกฎะ-H %3๔'R–ื~งฟslŽ)งงช`šฃsL %3tFำคŸ8ฝพ~V๕‚ภ๘ญyำ4ฉ}œ<‰ืNŸ#ษXvฌ่}นmeป๎~e๏๓(yzฉ„(!„b๚HB๒?H=ฒqทฬ*Eตมนพ0Ÿฺs รย็๏ู!#W4ฮ ๙vฯคqถ~๗มะ‡jžXPผ7฿ยิ›ƒ”ฮ@"ร`2mู}mšpู๑:๘ฦ&?wช &ฃท๒ %3\่oุ2ฟ๚เTฎW:“]๏tุhบN<‘คป€ำฏ๐๊ษ3dฒYห๓฿ญmฑบงรAw๒์๓yw๋g"™&SorไฟKˆB!ฆ‡)!„ ƒTใฆ•ฌฉ)Aฮ๖…๙ิžฃ–฿๓Š|f}•๏กdNk ธgาxหว0ešP[่'ไv’ัG'Jf,ฟฏ ำ$8>ว”รŽ฿iว>6ŸQFื7x๖){†aฎืtํ@<‘คฃง3—›8r๒ ู\๎†฿๗ฝg wดงรAฯภ ๒์syณnใ!*<<๒™'v6ถสฏŸB1}$H !๙คพฐyซซ‹ฑง{G๘๔ณว,ฝฟ–๐เ๚ๅT๘=$s?:z่{'šง๚๖ฉ้ถ~๗มะ}‹*ฅก"ดaบ'>ล ีKNgo˜}m&ท]ล7vKŸCU6(6”Žนกpd}Woใ็.p๔์yr9ํ†ื๗nฤ7ฏฦๅtะ;8ฤ๗์Ÿ๖u’%„Bไ RBAฉวถฎfeeงz†yh๏qK๏๏%e!>y๗2ส9>ยฯด2 c†ฉ=ต๗-ช{j:ร”aยฑ[๖าฺhŠdฒ7พ6 Ÿk4H9UeCน฿3ฅcํษ๚gฯะ4†ฯุฒuทฎมํtา78ฬž?Z<งA"™fpxไๅp2๙๐“=<ฃฮ+B!Dพ“ %„ไ๚โถ5ฌจ(ฤNt๑ศพ–฿ห+ ๙ฤ]ห(๗ป‰e4๑T3zฎh`๒ดฆwloIใp:ร”๙ssHฅ4žh’X6wร๎k0yฎฉ{รƒ๋Nๅ็~ไแวึซpเFว๏x7oฟทหIะ?ณoส็ผ“%„Bไ? RBAฉืฐผผ8ึ5Hใ“–฿++‹๘ซป–R๊sหไ๘ฮ๑f~zฑใ—^7Sริๆ'๗ฌฟgัฌฏ฿\Sฒlชย”aย"?.'ฉœFW4Iโผ์็MวS๖fBบgl^{+—‹แ0?|f/S๕๏M QB!„uHBเกฝวอื;๓๓Dmณ๑•ํkXRย^๏เ ฯŸฒ๔พฉบ˜ึ.ฅฤ็"’ฮ๑วฎฐ็rืฏ}ฝ„ฉษSฆ uE \’9ฮH‚ิ |[Hš,๎พƒmwŽวๅfpd„l๒ƒT"™ฆodค#O|๖ฌ: IDAT[?ฒ[~ี„Bˆ'AJ!€๖pย<ืๆ@s'{†๓j์Šย—ทฏaqYบa๒jว8m้}๋ฌRโŽล{]„ำYพฺežk๎yห๗˜]a—ก8wฑ~F=ฆ}*ย”9~…”I"ซัNั%HMด™ค6ฏฝ•{ึ‰วํf(ๆ๛๗ูI›C*›ำ่์%„BX)!„tร4ำšฮ`2อนพ0{ฏtsพ??š‡หฎ๒ฅmkXTD7Lท๐ฅƒึRwฬ)ใฯo_L‘วI8•ๅ๏\โPK๏uฝw&‡ฉญOํฑ~nล—๏ช-+›่0e˜P?คโYึ‘8šaะ๛S‚ิไุpอผwใ]x†#้ง{€‰๗f*™‰๗ ๅW?—฿–_1!„ยz$H !ั ำฉ*†IZำ้‹ง8ำ;ยฆ. Fฆu|N;o]อ‚’ ša๒Rk_yแฌฅ๗๗บบ ไึ…zœ งฒ|ใ• ผึถ–1ฆžฟkใLฏ“ฆL๊‹bึ‘๚ o R“ใ๎[ึ๐M๋๐y<ŒD"ใฉT2‰ฦฟั“นปฑ1ŒB!,I‚”Bวบ†ฬ9…~.ล†aBZำ้ฅ8ี3ฬ+]ดŒฤงe‚.m]อผโ ša๐BK_}ัฺAjc}%|หBn'รษ _;|#๏hY†I›bณ5๎ฝห๎™6nวริ–y•eชb{Wหฝe/@A,“ใ๊p Rm&ฉตkV๒;[7เ๓x Gฃ|๏฿6!ห•%„BX$H !๐฿}|ฯ‚jnฎ)กฎะ฿ๅภฎุ0L“Tn4L๏โู+tFSบn…'_ุฒŠ๚ข 9เ`K/๛า9K๏๏m๓ซนฆ๙ธ &2|ํ๐yํค๒35Lญ฿}0ดถฆเ๑{ๆWิใP๏d&`๊ GƒT4“ใ๊p์†฿wค&วmซ๘m๑{}Db1พ๗๏?{W“š'’i"ัฤ—%D !„7 RBl}jฏ r;ู4ฏ’f•2;ไว๏ดcWtร ‘ี้'9ึ9ฤฆnบฃษ)Yทฏ‹/lYEma€œฎ๓\s๋๐K๏๏฿ZTร‡Wฯ#่r0H๓?_:ว‰๎‰™L0iุ฿ถd&แ๕ป†>P๓ฤ‚โเฝo7L&จ6s }„Nย้,ญร๑ัJu“ 59nnXส}๏ูBภ็#๑Ÿ>K&›EU•ทตœD2M8ง๐๐ศgžุูุ*ฟTB!ฤE‚”B๐FWโuฑy^ทฮ*ฅบภ;ฆอ0Id5บข ^๏ไนๆzcฉI]ทrฟ‡ฦอ+™๒“ำu๖^้ๆฏ^ด๔ภ’ูมสz.;๑4_}๑,ง{G&๚c†ั8ีมaบ‡ฉyE฿ฟ๙ฅtรฤก*ฬ.๐›hพe$ŽM‚ิ„› Aj๕าE{ท๔๙‰&โเ๎%–Hโt\฿x”%„Bฬ ค„‚_RใJ|nถอฏโ–šRช‚ผ;vล†nšฤณํแGปูwฅ›กdfRึญ*เๅณ›V0'ไ'ฃ๋์นลG.YzฒZ>ธขฟำNo<ล฿ผp–s}“v'ฮ S{j๏[T๗TCEhรo Sšaโถ+ิGƒิH*Kˆ\!5fBZฑx๙}๏!่๗K$๘แ3๛G๐ธ]o๙พD2อเ๐ศห๑h๔%D !„7> RBมฏRใสณฐšU•ลTx๑:T› อ0‰gst„ผึ9ศ3—ปˆgrบnณ |<ฒqณ |ค5Ÿ]๊ไฏ_ถ๔เŠ:๎[V‹ื1คv:รฅIšแ!{wl99“ฦ๖๕„ฉœnเuุฉz =๙ฐ=ฟแ๗ฉษฑ|แ<เ๗PO&๘๑ณฯำ;0ˆ฿๋๙•ฏQแd๒แ'{๘ B!„˜$H !ฟ9H›๒ณi^%ซชŠจ x๑8์จ6ศ&๑LŽ๖H‚—๚ูwฅ›dN›uซ-๔๓ะ†j‚>Ršฮ\่เฉcW,ฝฟหชz~gษl<;=ฑ$_7.ปŠ ะ ƒp:K๓PŒ—๛9ะƒfผฝZTZภงึ-งย๏!™ำ๘แ™Vพบลา๛๛On]ศ๖ีธT•ถpœ/<Š๎hrฺึว4ˆ่ ป ลนk&†ฉตuO~จกฎพ2เ!เrะOsq Bะํธกท]‚ิไ˜_;›๗พขPˆd:ลฯพฤพรฏตูLณQB”B!ฦIB}ืPQศฦ๚J–•R๊sแฒ^a’ี "cWLผฺห ญ}\๏๙wiyˆฟพ{~๑ฌฦ๗Oต๐ฃณญ–฿v"ถฮฏย1ค๗Ÿค/žš๖๕šษa๊‰#—v|hลฦ€ห>ง/žๆL๏%>ื ฝอค&วู5ื฿}?%……คา้ศ3๏๚ฝ{65ส/B!~ž)!„`โ‚ิธUUEl™Wลขาล^n๛่\<`8•กi0ฦพฆnŽt ฦe5T๒‰ป–Qๆwหh|๏d3?9฿n้w,fS}%Uฅu$ฦg๗`0™ษ›๕3 "(4๎ป๋ฎ™๖]0LsGO,ีxฒ{xNeะsCoซฉษQ[]ล๗} RZฺ•สdv๙<ž0B!„ฟ@‚”B0๑AjญณJูT_ษ‚’ …ฎ๑0ฅ†ฉ a^ํๅตฮม_ปŒUUEๅK)๕น‰er<}ผ™ธุa้—w.e]]Uกy8ส#{ONg๓n= “6ลfk{–3ํ;๑ํื.jIy่QCฝa๏“ 5_"5•ป>๚ก์ช()‘%„Bˆ_K‚”B0yA ภUWฮ]ตๅ,() ะใฤกŒ†ฉฌฎ3˜ฬpพ/ฬก–^Žv า๛ืT๓๑ตK)๑นˆfr๚pCอ Šƒ๗ˆaJ‚ิ„{:cืุุ(!J!„ฟ๙๏$ RB1นAฺ๊ X?ท‚;kหYP$ไvโT 2บฮ`"รน0๛›บ9ำ;rํ}ทิ”๐ฑตK(๖บงณ<๙๚๖7u[z?ธn9k็”ก*6.F๙๔ณวIๅดผ_oรค ำุ1ีcบจaJ‚ิ„yฺฆiO์ll•_!„B\๗฿Gค„bj‚ิ8ลfc]]9๋+ฉ+ Pเ Sฆi’ส้ $าœํa_S7๚#1ปŒ?ฟc1E'แT–ฟ?r‰C-ฝ–฿mhเถYฅจŠ‹sŒฌnXi†ั8ริ,ญ๎ขาะ{}NปๅทG‚ิป&!J!„๏˜)!„`jƒิ8งชฐฑพ’ปjหฉ+๔p;q*6 RšN<ลฉž†’i~g้ =NFRY๎ี‹ผฺุg้ศฦ2ซี็๚ย|jฯ1 Uผ‡>ปiEใขภม™๔}Yฟ{Oํ}‹๊žjจmฐr˜’ ๕ฮ˜†yศPlO>๖๐Œ๗B!„˜Xค„‚้ Rใ<•ญ๓ซนmV)s \ca*ำˆg5B'.Ua(•ๅ๋‡ฯ๓J๛€ฅ๗wใๆ•ฌฉ.Aฮ๔๐เžc–Žš/l\ษ์฿กดฆ๏๐9ํญ3้{c๕0%A๊ํ‘%„Bˆ‰$AJ!˜ 5ฮ๏rฐu^ทิ”P[ไ'เt` SŠ ฐูHๅ4พyไฯX|R๓GทฌbUU16เT๏ŸyึšAjNศฯรจ)๐‘ึt~zฑใ้'Ž_j<ธc{๋L๚ฌ฿ฝง๖๓f๔ๆš’eV Sคฎ„(!„BL RBA~ฉqE๋+น}v)5>‚.ชbระ4้Š&9ิาวพฆnบฃIK๎๏วทฎfEe'{†yx๏qKnG]กŸ‡6ฌ :่%ฅ้๛…vvkย0yZSฆ6?นg=‹f}*aJ‚ิ[3 อfš฿z‘๒+!„Bˆ‰&AJ!ศฏ 5ฎฤ๋b๓*ถ/จฆา๏มfRบaฯๆ่Œ$yฝs็š{่‹ง,ตฟwn[ร๒ŠBLเXืŸย’ใฆพ(ภg64P๐’สi๋๙vพ{ข๙ฺืเ๓†โupว๚๐L๚>Y%LI๚ี$D !„b*HB๒3H๛ƒ•s๙`CNปŠišืnแำ “DNฃm$ฮฑฎ!žนE$อ›/o_ราฒpดsฯ?wา’ใfAIื5P๐สi่\x๒๊›^cDt…]35LmœWฯwี–•ๅc˜’ ๕ "ฆอ|@B”B!ฆไ๏ RB‘฿A๊๗–ี๒มu๘v’YกT†B ฏรŽbm์ŠฉŽp‚W;x๖r๑ฌ–ท๛ฺฎ(์พ†%e่&iเฑง,9n•๐ฉuหฉ๐{Hๆ4~xฆ•๏Ÿn๙•ฏษaj๋S๛vฌŸ[๑ๅ| SคฦDtƒ]š[ตปฑqFM!„BL RBA~ฉ6ิq฿๒Zผ;ฝ๑฿=ัL}Q€••ETข™n's‹”๙o SัLŽ–แ/ท๕๓\sMฯ›ํ๑9ํ<พu5 J‚h†ษKญ}|ๅ…ณ–7ห+ ๙ฤ]ห(๗ป‰e4๑T3zฎ๚Z€I›bณ5ฮฤ0๕ะ฿ธg~๕G=ี1๋1“ƒ”ฎ๑y QB!„˜Nค„‚Rดบž,™ƒวฎาK๒Rk%^๗Xš&UanQ€RŸท]EฑAฮ0‰ฆณ\Œ๒jวฯ5๗ำi฿žทƒวถฌฆพ8ˆfj้ๅผxฮ’ใfEeu็Rสnb™฿=ัฬฟ_่x[ห˜ฉaj๎ƒก7ิ<ฑ 8x๏t…ฉคžถiZใ;[ๅฬ/„Bˆ้$AJ!ศ๏ uš๙ผw๑,‚”i˜‡C฿!!J!„๙F‚”B฿A๊On]ศ๖ีธT•ถpœŽH‚ฌ๖›oฝS>‡Y!E.ปr-L '3\ SG:ฆt{*>ทi%sB~2บฮณ—ป๙๛#-9nnฉ)แck—P์uNgy๒๕+์o๊žจลฯะ0ตง๖พEuOME˜บ‘ƒ”i˜‡ ลึ๘ไcฯจ๑#„B๋ %„ไw๚ณฑu~Uฅ=ง+’$ปIสvNMศKก…SUฐู ฃ %3\่sจฅ—ื:งd{j ||vใ f๘Hk:?ปิษท_ฟlษqs๛์RSไuNeyโตKผฺ;กŸa˜<ญฉFใม[gาwr*ยิค$D !„ย*$H !๙คโŽลlชฏฤกชtDโt„“๏่ฉy‡ŠSU™๒๒8qช ฆ ]g(™แ|_˜W{9=4ฉS[่็ก ิ}ค4ธะมSวฎXrY[ฮŸถˆB“‘T–ฟ{๕"/ถ๖Mสgอิ0ตu๗พ•็U}๗ๆš’eฆnค e@› ;$D !„ย*$H !๙ค>~็RึืUเP:# ฺย๑๋บe๏ื๑:UN'7Nล†aBZำLฆ9฿แูห]œ๏Ÿœงมื๘ฬ†ช^R9Ÿ\h็;ว›-9nึีU๐งท.$ไq2œส๒ฟ_นภแIž›kฆ†ฉอO๎YฯขY_Ÿศ0u#)ฺlฆู๘ญวู-gr!„BX‰)!„ ฟƒิ'๎Zฦuๅุ…ฎh‚–แ89xWหดู ฤ็&ไqt9๐9cWL™คr:‰4g{Gุิอฅศ„nฯ‚’ ฎk 2เ!™ำ๘๑ู6๑ิUKŽ›๕•๑- น '3|ํ๐…)™“ห4ˆ่ ป ลน๋เŽ๕แ™๔]ศ0eๅ %!J!„V'AJ!ศ๏ ๕ษป—qgm9vลFw4E๓pMŸ˜ีUUทŸCล๋tเPl˜&ค4พxŠS=ร์ฝาอีแุ„|ๆ’ฒŸผ{ๅ~‰œฦNท๐ƒ3ญ–7[ๆW๑‘›Pเv0”ฬฐ๋ๅ๓ผ>Esqม Sg๏6ึW~๓ฎฺฒฒwฆ,ค "บมฎ'w>(gn!„BX™)!„ ฟƒิงื7pว์Rิฑ ี4E7&vuUลFuะ‹ฯๅภ๏ดใuุฑ+6Œฑ+ฆ๚โ)Žwณ๏J7mแ๘ป๚ฌeๅ…๕ห(๗ป‰e4้ิUๅ\›%วอ{T๓_ošOะๅ`0‘แo_>วฑฎก)_™ฆถ>ตoว๚น_~'aสRAj,Dinmื๎ฦฦuŒ…Bqc’ %„ไwzhCทอz#H]Œ2Y็n‡ชP๔t9๐:ฦย”ช &ษœFo,ลฑฎ!๖5uัIพฃฯXQYฤ'๎ZJฉฯM,“ใ;'š๙้…KŽ›๗.šล‡Wืp9Hค๙Ÿ/ใD๗๐ดญaาฆุl{๏฿ฒ{ๆ}‡Gริ–y•eชbปฎ๗X"HIˆB!ฤ J‚”B฿A๊ณWp๓ฌR ;–ไ๒`&ymv•ช€‡ ‰หฎเwฺฑ+ ša’ศjtG“ํไนฆบco/Lญฉ.ๆตK(๕น‰frรั+‡พxŠฏผp–๓yy1ห!ร0ง:พLท฿ฆ๒0HIˆB!ฤŒ!AJ!ศ๏ ๕๘ึีฌจ,FƒTำ4ฉk?6Mg^q€ช รŽjƒœaฯไ่ˆ$xนญŸMฤณฺฏ\ฦuๅ้ญ‹(๔8Ie๙ฦซyฉตฯ’ใๆC+๊๘ฝeตxvzใ)v:รฅH>ฏ๒ S{j๏[T๗TCEhรฯO|ž/Aส4ฬC†bk|๒ฑ‡gิqB!ฤฬ&AJ!ศ๏ ๕ฅmkhจ(ฤบ" š†bำพNYรภก(ฬ- Pแw†)ลFV7ˆerดฤyกตƒW{Iๆฆ6ึW๒วท, ไv2œฬ๐ฟ^นภซํ–7ธฒž{—ฮฦใฐำK๒ลƒghŠZaีกุุปcหษ™๔=S๓ŠJ}๎iRข„B1“IB๒7Hู€obYy่ฮ“ 5.gธ์*u…~สv๕ฺSัt–ๆแฏดฐฏฉ›œnŒ๎๋๙Uท›Pเv0˜ศ๐ตร็yฝsะ’ใๆรซ็๑Kfใฑซtว’<~เ4W‡c–YรไiM5๎ุ:“พ๏๋w๏ฉต๋J#ฆฑ{:‚”b˜ข„B1ำIB๒7H)6_พ†ฅๅ!ts๔ ฉๆก เP๊Š”๙ธ์*6@3 "้MรQ^iเนๆถฮฏbวšy]i๖ฅ๓๏ฒไธนอ|ปxปJg4มใNำ:ทvธ์๊ำ_ฺพบqYYaซœ „B!ฤT %„ไorจ ;ทญaqYบaาMru(ฏภ1ท]aNแa ซDาYš‡bฤณทฮ*!เrะO๓?^:วฉžaKŽ›ผ€{ึเถซtD<๚):" หm‡฿i็๑ญk˜_|Z3Œv5,g!„B1™$H !๙ค<•/n]รยาเhŠ$-qK˜อfร๋T™]เฃุ๋บvลTF7ศ๊^‡ŠjณัO๑ีฮrถฯšใปe!๏YXKUi$๘ยs'้Š&-ทn'mYE}qœnDถ๔๎๚๒แ+ป๎X/aJ!„BLฮ฿ ค„"ƒ”฿i็ฑญซYPR€ftF’ดXhŽ"ปjรcท3;ไฃะใผฆฐูH็4พ{โ*?:jษq๓฿o[ฤึ๙UธT•ถpœฦ็NาKYn;Šผ.พฐys‹ไtƒW{๘ฯGt…]†โ”0%„B!&œ)!„ ƒTมc[VS_ R Z†ญ7G‘กโTUf…ผ”๙ุำ4้Oค9ู3ฬ -}–›ฯo_ฬ–y•8T•ึ‘8ฯ ?žถ๑)๕น๙ๆ•ิศ้:๛›{๘๚แ ฃวศ ข+์zญrฆB!„E‚”BฟAชศใโ [ธrฅ+jอ 5ฮiWฉ/ P๔`ปJJ7L2บฮP2ร๙พ0๛›{,3งิว๎Xยฦ๚JชBหpŒฯ๎?มP2cนใR๎๗ะธy%sB~rบฮณWบ๙ปW/พ้5†I›bณ5๎ฝหn9c!„BˆwK‚”BฟA๊ฏ\้Œ&iตp๐9ํฌฉ.ฦiWั ร4qจ ฆi’ึtโiฮ๕‡ู{ฅ›๓๙}งุว๏\ส๚บ ชB๓p”ฯ๎;มH*kนcR๔๒ู+˜๒“ัu๖\๎โ‰#—~ๅk%L !„Bˆ‰ AJ!ศ฿ U๐ะธiๅตPะuฉวษา๒.Ue$•ก;–ข2เ!่vโTl&ค5พxŠำฝ#์o๊ๆ๒`4/ทๅw-ใ๎บr์ŠBำP”‡๗'šษY๎˜ฬ*๐๑ศฦฬ*๐‘ึt~vฉ“oฟ~๙-฿c˜ดa;๖d๛A9ƒ!„BˆทK‚”BฟAช:่ๅณ›V2ปภwรฉRฟ›…%AชJ{8ฮ—กกขf•RW เrเS9xŠS=#์k๊ฮป' ~๒๎eY[Ž]ฑqyp4HลณšๅŽImกŸ‡64P๔‘าt~zกƒ8vๅz฿~ศ0ŒF SB!„โํ %„ไoš๒๑ศ†ิŒ]นาณ~ช*๐2ทะ?6xŒฟ;r ]%‘ีhจ,dEEu…~.vล†nšคrฃWL๋โู+tFyฑ-ฎ[ฮฺ9eจŠKQ>ณ๗ฉœnนcR_เ3จ xIๅ4~rก๏o~ป‹‘0%„B!ฎ›)!„ ƒT]กŸ‡6ฌ :่%ฅ้๔D“ดŽX;Hอ)๔3ซภ‡CUธ:ใ๏\ฤ๋ฐฟ๑ๆ•YS]ฬฌ~ง‡ช &ษœFO,ษฑฎ!๖7uำIN๋ถ<ดกf•ข*6.Dxpฯ1ฒบaนcฒ $ศƒ๋จ xHๅ4~tฎ๕ฬQฌ๘ซบด<ฤ_฿ฝŒ ฟ‡xVใ๛งZ๘ัูึ Yถ„)!„B๑+/AJ!`็ก3ๆก–^๔<ปบeIYˆOฝŒŠภh(่ป‚ิา๒ล^ืตพyไAท๓7พฯญช,( าP>zล”ืaGฑf˜ฤณ9:#I^m`ฯ•.โS๔คปฦอ+YS=คฮ๔๐เžc–<& …|โฎe”๙ฤ2฿;ูฬOฮทO์‡˜ๆืฒชซ๑เŽ๕a9ใ!„B RB๔ฦRfำP”W:8xต—\žฬดฌ<ฤ_฿ฝœ๒ฑPะOา>’ฐ๔พ^VQH‘ื…jƒs}aพ๕๚eBืคฒบAฮ0ธcvซ*‹จxFร”bCำ ข™‘/ต๖๑|sฯค?๑๎ั-ซXUUŒ 8ี;ยgžตfZYYฤ_ต”RŸ›X&วwŽ7๓ำ‹9ฆADWุe(ฮ]ฆ„B!f6 RBฆifuƒH:Kหpœ๚8tตwฺ'จ^QYฤ_นt์ส•=ฑakฉฅๅ!Š}nเl_˜oฟ~‰B๋m-#–ษแPึฮ)ceee>7๎ฑ[๙rcaชu$ฮหmฃajฒž|๗๘ึีฌจ,เTฯ0ํ=nษcฒฆบ˜ฏ]J‰ฯE4“ใŽ^แ™ห]“๖yฆ„B!„)!„R9t`๔*œp:K๋HœZ๚xกตŒฆOหzญฎ*ๆใw.กิ็&šษัMN๛“ๅญ%ๅ!J}nlภ้ž:vๅบฏ๚EฑLรฮํณKiจ(คฬ๏มmWQl3Lข้,MC1Žt ฐฟฉ{ยใทญaEE!&pข{ˆG๖ฐไ1นนฆ„ึ.กุ๋"’ฮ๑ไ๋—ูืิ=้ŸkDPhwึ]rB!„˜Y$H !pจฅืœ_คะใฤฉŽฌaIei‰๑J๛ฎN•6o >vวJ|ฃก ;šค;jํ ตธ,D™฿ ภษžaพsผ‰‚wคฦล29.ทอ.eyy!e~7.UลfƒX`ผ:็ฅึ>\ํ™ฐ'แํพ†ๅๅฃA๊XืŸoอ u์Rโ๖ลy]„SYพ๙ฺ%\ํฒฯ7Lฺ›ญq๏[vหูH!„bf %„ภถง๖šwฬ)ใฮฺr–Pไ}#L฿vu8ฦ๋ƒ์k๊!•ำฆdฝnUสว๎ ้,]ั$ฝั”ฅ๗๕ขฒ*Lเxื฿;u•เ[๚TพH:;iกเOn]HแX(่Š$่[7Hูlฐฐด€ส€็Zฤ๙๑ู6|N๛ค|^,“ฃ*่แ–šR•P่qแTl6ศhฃa๊|˜Z๚8า๑๖ย”SUุน} ‹J ะ “W;xภiK—๕•๑- น '3|ํ๐…ทฝ?&ษ!ร0%L !„Bx$H !ฟ:H[YYฤๆyU,*ฝbสmWQฦžๆ6zลTœื:9pต‡‘ิฤ†ฉ_ ั$RŠbc~qช 0yฅ}€ฟะŽวaŸิฯerฬ)๔ฑบชd,L9qช ฆ Y]g(™แ|„W{8ึ5t]หtUพดm Kƒ่†ษหm์nƒ‰ _;|žื;๓i%L !„B`$H !oคฦญฎ*ๆ๎บr––…(๖นฏ=อM3Lโ™ญแ8วป†ุ{ฅ›๐]1ตe~นinCษ ‘C‰Œe๗ณชุ˜_ค2เA‹8?ปิ‰ฎNษ็ว29j :ซ”yลB๎ฑ0ค5มฤ่S{ฏtqถ/–ห๒9ํ<พu5 J‚h†ษKญ}|ๅ…ณ–<.๗,ฌaวšy]i๖ฅ๓๏สป๕41-ง˜ฑฝUฮZB!„ึ&AJ!ธพ 5nEe๋๊สYZ^H้ฏSั$ฏwฒ๗Jืปพb๊CAg4ษฐ…ƒ”CUจ/P๐ข‡Zzู฿ิƒSUฆt=9ฺŸg—RW ภใฤฉุ0TNง?‘ๆl฿๛›บนะ๙•หบ<ถu5๓Šƒh†ม -}|๕Ekฉ๗/žลYUOภๅ ?žๆผtŽS=รyปพ†ษำšj4J˜B!„ฐ. RBม Rใ–”…ุT_ษา๒e~žฑ[๙4 –อัM๑Z็š{้‹ฟณ'ใฝo๑,h, $าtF’ 'ญคœv•๚ข9เภี^ํล1ลAj\&ง3ฏ$ศš๊b๊ NŠ ำ„”ฆำOqชg„}M]4 ล๔ษฃ[WQ_$งฦต๙า9K—{—ฮแC+ๆpู้‹ง๙›ฮrถo$๏ื[ย”B!„uIBYทธฌ€อ๕Uฃaส็ฦํฐc›c*žี่Š&9ฺ9:วTO์ํ…ฉ฿Y:›ผขZ(่Œ$งฒ–ฯn‡สขๅ~9]gs/ต๖aW”i]ฏŒฆณธ,DCE!s‹\์Š ร4Iๅt๚โiNtฑฏฉ›ึ‘8ล^_ุผŠบข9เ๙ๆพv๘ผ%ห}หk๙†:|;}๑_yแ,็๛ร–Y >o(ฮ]wฌหูL!„ย$H !๏.H[XRภฆy•,ฏ(คฬ็ฦใPQm64ร$‘ำ่Œ$9ั=ฤฆ๎๋Sใกภ๏ดำOัIฑp๒\ส่:{ฏt๓jวชอ–๋g˜&๓Šƒฌฎ*fNก€ำ]Uะ “dNฃ/–โX๗๛ฎt“ึt>ฟyต…ัธึิรื_น`ษใ๒กuฒZผŽัqถ๓ะ. D,ต ฆADWุ%aJ!„ย$H !คฦอ/ ฒฉพ’ๅๅ…TฆบฃINtณ๏J7ฑไ[.๋ƒ+๊ธ๏็BAg$I4m ๅuฺฉ+ P๎s“ึtžนษ๑ฎ!lyคฎ8bcAiีUET}๘vช‚6ฆบฃI.DXS]LUภ{-ฎซ-y\pe=๗.วaง'–ไ‹ฯะ4ตไถH˜B!„ฐ RBมฤฉqs‹lชฏคกฒช€รŽj4Id5zc)Žwณฟฉ›ŽHโบBAg4I,ณ์~๖น์ิ๚)๓yHk:?ฝุม™ซศ†%ๅฌฌ,ข*่ล7v;ฆ>v+ŸCUpฉ iอเ™หป”ีUลิ๘๐9ํŒ฿l˜ส้\ŒpธฝŸM=ฤ,ฆ>z๓๎YXƒฎาI๐…็Oั๙kฎฺณ* SB!„๙E‚”B05Aj\uะห–๙Uฌช,ฆ*8zล”:๖DทdNง/–โT๏่S๋+yฯ‚ัPะIะM’ดp*๐8ฉ ๙(๖บIๆ4~tถอrsล29tำไ‹gณnnN๕'ๆtƒh&G[8ฮKญธฺc‰€๘งท.b‚*\ชJ[8ฮž;๕็7ณฐC†a4๎ศ๖ƒrๆB!„˜>ค„‚ฉ Rใช‚^6ฯซdUe15^|N๛ุSฃ๓๕'า˜&ิxq( mแ8ั$ฉœnู๒8™S่งฤ๋"–ั๘ม™ZGฌwkXw4‰วa็s›VPๆsc9รDตูPmฃ;šษั<ๅีŽ๖_้&ฃyป=~๛bถฬซฤ1ค๗Ÿค/žบัฟ๖‡ฒŠฑใเŽํญrB!„˜zค„‚้ Rใช‚^6ฬญเฆ๊ช ผ๘v์ส่LำฤฎŒึMั2#mแ U่u1งภGฑฯE,“ใŸN]ฅ#bฝ+qzbI|ŸZทœส€‡Œฎำ6’ภeW(๗{pูU่S‘tŽซร1ท๗๓\sน< Sqวb6ีฉ–แŸ‚มdๆ††ฑAฎ”B!„˜ค„‚้ Rใส6ึWpKM)Uมฑ0ฅ*ืๆ(สh:W‡c„ำYโkถWไs1ปภGฑืE4“ใ{'ฏาต^๊ฅ๐ป์|๊๎ๅ”๛=$r?8ยน0›๊+Y^^Hฉ฿ฎฃa*œฮru8ฮก–^ต๔ข๙๓๛๛๑;—ฒพฎ‡ชะ<ๅณ๛N0’ส๐฿{ RB!„ำG‚”BAj\กวษถี\]ย‚’ ฎฑจaษฌFค„‚ Rฎ[ฮฺฺrŠTN'ฃ๋๘ช‚a˜ค5มdšŽp‚ดfไ}˜*ธฉ ๚yœŒคฒŸฃWˆคญwkุ@"Mศํไฏ๎ZJฉฯM,“ใ;ว›๙้ลŽ_๙๚ตsสธซถœEฅz\ืžฬ—ีu†’Y.๔‡yกต#ำsลิงื7pว์RTลฦล(Ÿy๖ุ่x78 RB!„ำG‚”BฟA๊3๋ธ},\Œ๑bkทฬ*กฆภGะ5ฆLs4L $าtDd4รศฯs{EะCuภKศใd8™แG/[r>ฌมDšBฏ‹ฏ]BฉฯM4“ใŽ^แ™ห]o๙พป๋สนซถœ…%zœcว2บฮp2ร๙๛›บ9ู3<ฅ๓ะ†n›5:ฮ.๔Gx๐ูcy9๙๚D“ %„B1}$H !๙คูธ‚[f•ขฺเ|„๗ลeWู2ฏŠf•2ปะOภๅภกุฎ…กd†ถpM7๓.*TฝTฝธ %3|๋ตห$sึ RCษ ล^ฌ]BฑืE$ใฝฬ+ฟ๙‡ธณถœM๓*ฉ/ ปb๊็รโ๙{ฏtqฎ/<ฅใLฮ๗‡๙ไžcฬ„HB!„˜>ค„‚ RŸด’›jJP€sa>๙ฬัkอ๋ฐณu~7ี”0ทะOภํฤกุ0ฬั[ม†“:" ฒบAVห0U๔R]เ%่r0H๓ํื/“สY๏ึฐ‘T†bฏ›ฑ˜"ฏ‹p:หท^ปฬ๓อ=ืฝ ‡ชฐ~n๋๊*จ+๔t;qŽฟดฆำŸHsบg˜M=\ŒL๊๖4n^ษš๊ัqvถ/ฬง๖฿{ RB!„ำG‚”BฟA๊๓›Wฑฆบpฆw„Ÿ=๖Kฏ๑:์lจฏเถYฅฬ- ผ)lduแT–ฎH’”ฆM{˜ช zฉ)๐p9่ง๙ึ๋—ษhV RYสn๖ลyœ„SYศ%ต๔พํe_๑v๛์R๊~!,ฆ5XŠำฝร์ปาM๓plRถ็ั-ซXU5:ฮN๗Ž๐้_1ฮnDค„B!ฆ)!„ ƒิฯ‡‚“=ร<ด๗๘ฏ}ญฎrw]9wืU0'ไ#ไvŽ dดังบuG“$sำฆFƒ”€หN_<อ7_ปdษนŠ"้,ๅ~ถEŽMะW/๒Rk฿;^ๆ๘oทฬ*ฝโอๅภฎุะMHๅ4๚โiNt๑์•.ฺร‰ žวทฎfEeง~ร8ป‘HB!„˜>ค„‚ Ro]อสส"LเD๗์;๑฿ใRึฯญไฮฺ2j Œ…) S#้ แ$Y]'ฅ้0…[^๔2;ไร๋ฐำO๑อื.กึ๛ŠฆsT=ษ- G'hOe๙๚แ๓ผา๎Ÿ’Wเvฒฑพ‚;f—1+ไ#0๖TEอ0Iๆ4zc)Žw ฑทฉ›ฮศฤ„ฉ/n[รŠŠยท5ฮnค„B!ฆ)!„ ƒิฮmkX> Žu ๑นื œชย†น•6ปtl๒l'e4Le๕ั0ีI‘ีu’9ฉ๘9จ z™๒แqุ้‰)ร‚ฟCัLŽš —ฒ€ษP2ร:|#๖E›ๆUr๛์2ชƒ^N;vUA7LYฎh‚c]C์o๊ฆ'–zwใl๛–—bวบirF|๏%H !„BL RBA~)›อฦ—ทฏaiY8ฺ9ศ็Ÿ{๛กภฎุXWWมsส˜_$ไy๓S‘t–žX’TN'™ำ'๕้jUA/s x์*ฑ$Oนdษ๑ฯๆ˜]เ็›ๆSเv0˜ศ๐ตร็yฝspย?+ไv๒[‹jธนฆ„ช ŸรŽชุFรTNฃ3’เhื{.u1œสผฃq๖•ํkX26ฮ^๏เ ฯŸš฿{ RB!„ำ๘๗Ž)!„ศฯ eWl์~Kส ะMxญc€G฿E(ฐwึ–ณ~n๕ลŠ<.\๊WL…ำYzb)ูฉœ1)aชชภKmศฎาM๐อื.cณเxId5j ์X3๏ฺ๖ๅ๓๏šดฯฌxุ6ฟšUUEิŒ๖จุ@7Lโูั0๕jวฯ^๎"šษฝqฆฐs๛šัqf˜้ไฑค„B!ฤไ’ %„ไgrฉ _ฺพ†Eฅฃกเ•๖พx๐๔ป^ฎbณฑvNูต0U่vแฒ+ุxs˜2L“‘Tc็xช.๐2g,HตG|๓ศEิฑญ$™ำ˜[เVี R_}๑งz†'ณg‡|lžWล๊ชb*ๆลณ“๒Yk็”ฑvN‹JC{฿ฆ9dV#–ั่‰%฿U˜ช RUฅu$ฮ“ฏ_FUฌwำ^ZำYXRภWิแwฺ้ง๘›ฮrฎ/<ๅ๋2ฟ$ศฆ๚JVVQๆ๗เถซ(6ศ&ฑt–ฆกฏv ฐฟฉ›ฌnา๛.o]อผโัq๖BK_คq–o$H !„BL RBA~ฉษฃ[WQ_$งj้์wt\i^เ๏M•ซTฅR*IVvฮvปsป: /ณ K\e†ษMำ“ ห ;ภ–xู]Xย๎พคmwปฃsJถ%U)”Jชœ๎ฝ๏Uฒ3ำูvU๕>็ฬ9sfฌาญ็+ฉพ็yžหz๑์mžบ[ุ5aeK€ฐว‰Sื*K๙,›lฑDถd’ฬ—ˆฅs”ฟEx;• ๅวะTF็SูัaqฉBูdu[ฌ๏รc่Lงs|ๅะiฮฯ,ึ์˜Vท5ฑgฐ“๕!Zฝ.\๚จธ˜/1:Ÿโล+ำ์‰Qถฌš^g๕B‚”B!DํHB๊3H…=Nžฺณ™f?%ำbH”ฏฝt๎Ž|๏-]a๖ FXฺูT Sš†ช*฿0cชD,•ฃ๔.ยTwะKศ‡ฎช ว“ท#X ๘kจhZฌm๒แตฝx h*ว—žโา\ฒๆวถฎ=ฤ#ห;YำึD›ืu#*V๗‰งุ?ๅ…๑lฎ้uVkค„B!jG‚”BPŸAชอ็โษ›้ ๙(™&ฯ G๙๚ห็๏่1ีๆแVทฏฯ˜R([6ูR™tฑLถXb*๙ฮยิฒ —พ]Uธ4—ไžWAซ^”L‹๕!พoMnC'šส๒ล็O1Oีอ1nŒ4๓่๒NV/Eล›ยิ|ฎศๅน$๛Gฃ ว“5ฟฮjE‚”B!DํHB๊3HEn~u๗&zƒ> ฆษ3—ฆ๘W/ิไXถt…ูืฮฺถ -ีY7K๛ๅJerฅสSSษ์[ฆžjาT… ณIๆLc)ำฒู ๑=ซ{p๋“ษJOีฑณฌ•ฌlmzรฦ๕…ฒE"W`<‘f0์งลใช๙uvงIB!„จ RBA}ฉ๎&Ÿต‰ž&/๙ฒษ?_šเ_ปTำcZืb๗`„5ํAฺช๛ฝ1L™$ซK๙ e๓›พพ'่ฃฏู‡ฆภน™E๊ิ(ธ‰”ilŠ4๓ซ–แา5&3|ภ)ฎ,ค๋๖˜๊kฏl\฿ึDณ‰CSฑํสŒ)]Uะ5•Bูไ.\ซ๙uvงHB!„จ RBA}ฉ ฯ์@w5H…kษ‘หuqlkƒ์์dM{Vฏ ทฎกU๗˜สVรTชX S๙า0ี๒ั๒กgg๘๓cรธ ฝแฎถูๆ;Wvใา5ฎ-fxjI&3u์๕ตณc ƒๅแ!ท‡ฆ‚ข T฿ื๘B†ฏฟ|žำฑฤพ— %„BQ;ค„‚๚ Rอ~>ตc]นฒษw๎*zlธฎŽqM[]ƒึ-อ˜2tด๊Sน’IถT&S,3•ส’/™๔Uƒ”œŠ%๘ำฃร\FC^3[ปย<พข งฆqe!อS๛N2•ส6ฤฑซŠย๖vv DXัR SŠR™ฉV2-&“YฮN/๐๐็f>ฐ๗ฝ)!„Bˆฺ‘ %„ิg ๘ิŽ๕Drฅ2w๖*qbค.วoUk;"ฌ๏ัแsแ24TEมฌ†ฉtฑDฎlข) 'ฃ๓๑๋—h๑บ๒šูึยcห;1ชA๊ษฝ'˜N็๊=84•kc??ฐฎ—ฎaุ6– ๙ฒษt:วฉX‚ฝรSu๑ม[M‚”B!DํHB๊3Hญlmโฏงร็&[*๓ืgฦ๙ห“cu=ŽCแป#l่ัแwใพiฦTฆTฦPU††iู›Š๓ฏ]$โ๗4ไ5soO+{#šฦx"ล{O0—ษ7๛Xูาฤ'wฌ'โwcฺP4MtUEW•J˜*•™ฮไ9>g๏pด.7nฏ$H !„BิŽ)!„ >ƒิšถ ฟ๒๐::|nาล2ujŒฟ>=ใ9๖ณ{ฐ“ !ฺn<†Žฎ*ุถ Šถอd2วŸ?…ฎ*๘ทl๏6vD04•ั๙O์=ฮ|ถะp๏cM[_ูพŽ๖j๘<=ภฅk๔}๘บฆVgบ•™N็865ฯณ—'นบi๘๛^‚”B!DํHB๊3Hญ๏๑หญฃ็"U(๓฿OŽ๐ฟฮ^mจqํ๙ุ3ิษฆH3m>7ง†ฝІใ)NF็yebหฒ*Lm๏kg{†ฆ2O๒™็Žณ˜/6ตฟฎ=ฤวถ฿ธฮวษQ๖Dู5แ6บ›<๘†ฆRถlฒฅ2ัd–ฃ“q๖ŽL1ฑ˜mุ๛^‚”B!DํHB๊3HmŒ4๓หญฅี๋"U(๑็วG๘๛๓ืr|{‚^[ล‡Vu_ชž ˜–EถdMf9K๐สีส ฆv๔w๐P;บชry.ษgž;FชPjธsณ1าฬ/=ธ–6_ๅ:๛‹ฃŸs•๐t9x|Ew/kฅปษƒท:ำอดm2E“‰ล วฆโƒิถ๎~แ5„=N๓%๘ศ%žป<ีฐc์w|แั- …˜ถอฤb†Bูค+P บZYVูซ(ฯ้X‚—ฏอ/™uฆ๖ urO+šชpa6ษ'Ÿ9Jกl6yนซ+ฬ/<ฐ–oๅ:๛“#—yๆ๒ไทท]{†:ูาฆซษƒ็ฆM๋Sล“‹Y_™a฿ศ‹๙๚Sค„B!jG‚”BPŸA๊พžV~พี4{œ,ไ‹มซ90kุ1น<๕ศf›”L‹็วbJ&A/aƒ ห‰ฯกฃk*V5Lลชa๊•kณไJๅบ S.๏ไe• u~f‘OŠฆษ3—งะT—ฆ]7N]ฃ3เฦ๋ะ๑•h7…ฉูl3ำ ^ป6วbพXำ0๕]+ปฺู‚ œ™^เใOiศ๓๒P_;?w๏*Bี๐๙ปฏ\เลw>Wถ4ฑ{(ย†Žm>7.]C] S๙"ฃ๓)^พ:หแ)Šu4‹L‚”B!DํHB๊3HํŒ๐ำwฏ ่r0Ÿ-๐ต—ฮ๓๊ตู†ใฎ€‡'voขงษKม4y๚า$>‡๒-ญำะ่๐น๑;o„ฉ๋K๙ส&๑Lžณ3 ผte–dก6a๊Cซ–ฑต+ŒœŽ%๘ฤ3G๒ผ์่เg–ยgถภ๏ผ|žWฎพท๋lu[{†:Y฿ขอ็ยฅWbcษดXฬWfL‹qp,Fูช-'AJ!„ขv$H !๕ค[ลOตœ&—ม\ฆภื^:ว๋s ;ฦฝAŸูน๎&/๙ฒษแ)<†–_ใะ5"~~งปฆŒj˜ส—Mๆ2yฮฯ.๒๒ีYนย S฿ณz›;+A๊DtžO?{ฌ!ฯหžกN~jr‚.๑lฏ>วk๏๓:[฿โัๅฌn า๊uโิ5EกX6IไŠŒฬงxnxŠ—ฎิvฦŸ)!„Bˆฺ‘ %„ิg๚ฮ•ุึ!NƒูLž฿~๑วฆโ ;ฦอ~>ตc]นฒษกั†ฆพฃฏ54•Hภ฿aเq่ธ CUฐm(˜&ณ™<็f921วl&Gยิ๗ฎ้acคจฉฯ4hz|E%|œ•๐๙‡ฯrt๒ึ\g[ปย์Œฐช5Hุใภกi( M‹๙\‹ณIล8\ฃ0%AJ!„ขv$H !๐งG‡ํ็วbL%ณusLbMjำ~งมL:ฯoฝx–“ั๙†ใๅ->๙๐z"~นR™—฿รฒ0]S่ x๐:Œ๋๛Lี=ฆŠฆI<[เฬ"G&็ˆฅrท5L}ฺ^6t„ฐc“qžุ{ผ!ฯห‡V-ใ_oฤ_ Ÿ้ลณŸบตืู=m<ิ฿ฮชึ&šNีนฆฮอ,๐ย๘๔{^*๘^IB!„จ RB,ไ‹๖T2ห‰่<Fc\Iคk~L฿ฟถ—8€฿ฉ3ฮ๓‡ฮpf:ัฐcผชต‰?ผžˆ฿MฆXๆ๕‰9ฬ๗ธฎฉDn|ฝฆŽj˜*˜&๓ู"—ๆyๅฺ์m S?ฐฎuํA,เ่ไO๎=ั็ๅ{ื๔๐/7 โw๊ฬค๓ๆ g8ป=ืูC}ํl๏ogEKอn†ฆbCL7ฝc3%H !„BิŽ)!„,ถ-&[2‰ฆฒœ‰-๐XŒ‹ณ‹5;ฆฌ๏ใ#๚๑9tbฉฟ~่ ็fvŒืถ๙ุ๖ut๘ค‹eŽผ ตDืฺ}nี=ฆ|N‡ฆb6…ฒI<[ไr<ษKWf˜N฿บ0ฅ( ?ธฎ—5m• ๕๚ตYžฺฒ!ฯห‡ืUยงฯกK็๘อCg83}{ฏณ๛ฺู3a0์'่rเะ5l{i_ฐสŒฉg/O๖+AJ!„ขv$H !,”lกก* –m“+U๖%:;ณภกฑ้š,•๛่ฦ>ผฎกMๅ๘สมำ\œ[lุ1iๆ—\K›ฯEชPโ่Tำผ5ฟƒ4Uกร๏ฦ๏4๐:>‡~}ชByiฟขEŽMลนบy฿aJS~`}kฺš0mx๕๊,Ÿ?ะ˜A๊‡6๔๓‘๕}x ้tŽฏ:อ๙™้ชสށ๎oง?ไงษU‰‰KณฆำyNว์žโยm รค„B!jG‚”Bไจฝ)าLgภƒฯกฃฉ7žไฯVbฦั่-์๙๘‘อƒ|฿š†ฮT*ห—ž?ลp<ีฐcผฅ3ฬ/>ธ†Vฏ‹dกฤฑษ๘๛ž!๕TUฉ.ๅ3p~งกุฏ(‘+pi.ษ๑hœ+ ผo๓”ฟ7cจ• ตชต ำฒy๙๊,_|TCž—nเรko„ฯ/<ลฅนไ๛NMe๗P'๗๗ถั๒p9ฎ๏ –/›LงsœŠ&ุ;2ลๅ[|\ค„B!jG‚”BPyส^gภรŽ๎๊ำ้๗เsี=nn,Ž'94>อแ๑in๗Oฯ:ฤ๗ฌ๎มญkL&ณ|แภIฦ๊`oซ๗j[w แ5ดx,ๆKœˆฦ)›ทgUU!ไv`ะไ!่บyฟ"‹…|ๅ\™œใ๊Bฯป SN]ใึ๕ฒข%€iูพ2ร—žnศ๓๒แ๓‹N12็รงะxdจ“{–ต2ะ์ฏฦDำ†\ฉฬt:ฯ‰hœg.Oฒ=$H !„BิŽ)!„ ค–{ศํ`็@„{–ตฒฌษS๙`|ำ๒ฏ…|‘หsI^Ÿ˜ใ๙ั(ำบ-ว๔SV๐]+ปq้‹>wเ$W2 ;ฦ๗๕ด๒๓๗ญฆูใd!_ไT,Aฉlึ๏ฉ(เ24ผ†Nฤ๏!่vเ\:—ฆลBฎศ่|Š—ฏอpm1๛ŽgLน ฏํeyK€ฒe๑ย๘4ฟq่LCž—฿บœ๏^ฝฌnยgภiฐk0ยฝm,k๒^ฟส–MฎT&–สqt*ฮแฉ๗}?HB!„จ RBมƒิกณ{(ยถฎ๚›}4นื—•,‹ล|‘๑Dšื'ๆุ7%S,฿าc๚™ปW๒+ปpjW3<ต๏“ษlรŽ๑C}ํฝซน$rEฮN/P(›wๆ—.]รm่t*3ฆœบŠยอ3ฆR›Œ3šHใ1ดท|=ฏฃคร• up,ฦoฝpถ!ฯKฝ†ฯ หมžกN๎๏mฅ+เล็ะัซ3ฆฒฅ2‹ŽNฦู7e*๕๎ RB!„ต#AJ!๘ึAj‰กU6_พwY+Cแภ๕˜PฒlR…‹^น:หมฑ๑lแ–ำฯปŠG—wโิ4ฎ,คyr฿ bฉ\รŽ๑ฮ~ๆ๎•ๆsE.ฬ.+šw๔–fL95ฎ&!—‡ฆข(Pบ)LŠฮ3Oแ|“0ๅw|บ^š”L‹ฃQพz๘\Cž—zŸM.ƒ๏ZนŒปบ[่n๒เ5t4ต๒๐Lฑฬตล,G'็x๚าไปพ๗$H !„BิŽ)!„เญƒิอ๎๋ie็@„กฐŸฐว‰SืP…’i‘.–˜Jf92็เh์}จ๙๛Vณg(‚กiŒ'R๊ฬf๒ ;ฦ,๏ไ'๏ZNะๅ ž-0O’.”ks0ีS†ชา๔rฟ1L-ๆKŒ%RŸšg8žฤฉฟ1L59+Aช/ไงdš์‰๒;/oศ๓า(แณ็ๆัๅlํ ำ๐โuhhŠBูฒIหL&+Q๘™หS,ๆ‹๏่5%H !„BิŽ)!„เฉ%wu…ู1aek€ฐว…Kืะ0ซณ6&“YNF2๗ใ๙…ึฐs ‚กฉŒฮงxb๏qๆoั์ซZ๘Ž]๘]ห 8 f3yฦi’๙RอหะU\šFw“ทซK๙,›ไMห2วiีงBn฿ทถ— ’i๒p”ฏฟ˜A๊แ3อฏ๎=^ืแsY“—=Cl้j&โ๗เฉฮ˜*›ฉB‰k‹^บ:ร‘(‹os}IB!„จ RBมปRK6FšyจฏuํAZฝ.††ชT–eK&ฑTŽ3ำ žqav๑]ฝ๖/=ธ–‡:ะU•‘x’ฯ๛ญhZค๒EฦiŽOอs~v๎€็z*˜&ฯ\šโ๗_ฝะ็ๅๆ๐96Ÿโณ >{C>๊dsg˜vŸ ๗7„ฉ๑Dš—ฎฮผๅnค„B!jG‚”B๐ƒิ’-v tฐพ#Tตq#LๅK&ณู็ฆx~,ฦษ่;zอm_วC}ํ่ชสๅน$Ÿ~๖(้bนaว๘๛ื๖๒ร๐;uฆำy&๋+H-14‡ฎฒฌ:cสกiจสยฦๆS วS฿JwภKม4๙ง‹แk—๒ผาƒkู฿ก-…ฯใ๏xษ[=l๖๓ศ๒N6Eši๓บp:ชeห&Y(1O๒๊ต9žป<๙MOฤ” %„BQ;ค„‚๗ค–๔…|์่๏`Sg3สฬบฆbY6๙ฒI<[เโ"Fข™Œฟๅk}โแ๕<ะ†ฆ*\œM๒ฉg’+™ ;ฦ?ธพฺะฯก3ฮqm1หbฎ~ร‡ฎ)่ี=ฆZฝ.œšVูcสฒษหธ งฆ’+›ร… ๘๕ฦ R{h๕฿>Ÿ;Fบะx3๑Vถ6ฑg0ย†ฅ0ฅkื๗KJŒฮงx๑Je)_ฉฆ$H !„BิŽ)!„เึฉ%~ดsWW ]>งก*ุ@กlฯŽ'ya|šฏฬ๐ญ~z็๎]ึŠฆ*\˜]ไOฅ๘ 3<ษG7๐แuฝxŒส ฉk‹’ 0GQบJo“—V_%Lฉ  ((€iูœŒอ๓๋ฯ0Ÿkผ=พnŸ—ๆ’|๒™cไJ;om{G†:YคีW S@ฑ๚ลั๙4๛Gขผ0>Mู4%H !„Bิ๊๏l RBq๋ƒิ’&—ƒƒ๎^ึBO“ฟำภจnŒ]4-นJ˜zmbŽ็Gcส7f@}vืF๎^ึŠฆภู้>๔Qฌ™#›๙พ5=ธส ฉซ R4GงฎัไฅอWYฆ6/™\š[ไต‰9Œฦ˜k ง!~jว๎๋น>?๙๔ัoZฺึˆ6t„xtykฺšn<ณz฿ลsŽNฦ๙ำใ;๔z^~ !„Bิเฯk RBq๛‚ิทกณg0ย]- 4๛hr90ิJ˜*Yษ|‰๑…สำ๖O‘)–yr๗&ถvท gฆ๘๘ำGzŒb๋r>ดzn]#–ฮqu!Mบะx3qL&่vฐต3|=.ฺPูLปXโฺB†ฃSq๖G"L>ฯอ,๒‰งPถ>8ยށVทiv;ph60ฮ๑Wงฦvวื>/?…B!๎< RBมํRKtUeว@๗๕ด2๖r9q๊jeๆFuำ์‰ล ฏ^›e[W ๋:B(ภฉX‚O>sดกว๘งทญเ;Wvใชฉ๑Dšlƒnา~e!อ๗ฎ้!์qbSูWJS”๋›iง‹%ฆ’YŽLฦy๚าd]?ต๎Wwoโฎj๘<;ณภว๙ฤฟ ่mใกพvVถ6แu่ฤณๆ๔๘ฮ?ผy๙ („Bq็IB๎\บู=หZู5ayุOณวY„Yน>ำฦPU<†Žeœˆฮ๓้g5๔์=ซxlE'NM#šส2žศ4์^Eษ,ญ่คร็&W2Kคq้ีฅ|ฺ๕0U&šส๒๚ฤ๛FขฤRนบ{/ฟถg3[ปย(ภ้X‚O4x๘|;๖ตำ๔ฯๆ9—%{B!„5"AJ!จMZrWW˜‡:XีฺDุSู„YS–~J+`DS9~m฿ ฦ้†ใw฿jŠ`hฑTŽฑDŠ|ƒ>50–ฮฑgจ“vŸ‹Tกฬ89ส™้ป;Y฿ข็ยm่่ชBูฒHหL%ณ›Šณ$ฦฤbฆnห็ูฬๆฮJ:K๐ฉxบ™บ••ญLหๆ๐•พ|๐๔ทอ}/AJ!„ขv$H !๕คBnO=ฒ™มๆ%ำโ๙ฑฟโู7]Uyธฟ๛zฺj๑r9q๊* PดlR…“‹^ฝ6วมฑณ™|M฿ำฏํูฬึฎ0 Kๅธ0ธAJQ6w6r;˜ฯ๙๚ห็Iๆ‹ธ ]ฟ–ฎ)ฌi ฒฆ-Hw“ท:cชฒ๙yพd2“ษs*–เ๙ัุ-บขฯก๓๙Goฬฤ{a|š฿8tๆๆพ— %„BQรฟฉ%H !Dฉ“งูL_ศOษ4ู7ๅw^:ฆžeญ์่`EK€fงฆกช eำ"U,1•ฬql2ฮ๓c1&j๔Dทฯ?บ…M‘fฆำ9.4๐ )CSู t9˜ฯ๘—ฯ“+™84๕}ฝๆ๒p€๕!–Uร”ฎU–๒ๅKefณฮN'ุ;ฝeaชษe๐๙Gถ0X‰wp,ฦoฝp๖ๆพ— %„BQ;ค„‚๚ Rํ>7O๎ูD_ะGั4y๖๒ฟ๛ส…ทบญ]av๔wฐชต‰ฐื…KืะชOt+3•ฬq26ฯ‘(cw๘‰n_|l+;Bุ4~rฺ๋C4น โู_=|ำฒัTๅ}ฟถฎ* …lŠ4ำUฤๆง+ฮd๒œŸY`๏p”3ำ‰๗๕ฝš•๐9ะ์งdZ๒ีร็พm๎{ RB!„ต#AJ!จฟ ี้๗๐ฤ๎๔}L“ง/M๒_^ฝ๘Žฟ~}Gˆ‡๚ฺY฿ขอ๋ยmhจJeXถd2“ฮq:ถภมฑ[ฟ ์[ฒพ๔๘Vึท‡ฐ€ูtŽ๓ ค|NีmANƒนL฿>|…สRพ[ฦ†ญMlŠTfLy—žฎhCพl2—ษsvฆrOอฟงoัโu๑ิžMืgโํ‰๒Ÿ฿b&)!„Bˆฺ‘ %„ิ_๊n๒๒ฤฎ,k๒’/›ำล ่๕K๏๚u›์Œฐก#D‡฿ƒืกก) fuใ์นls๏3jผชข๐•วทฒถ=ˆi7~jr;Xูภ๏4˜อไ๙ํฯขฉ๊m๙^ฆmณบ5ศ๚๖ ฝ!พj˜ฒl(”Mๆฒyฮอ,๒ยXŒ#“๑w๕ฺK3๑zƒ>Jฆษ3—ง๘ฝw0๏ƒB‚”B!DํHB๊/H๕†||f็บ^re“8?9z๙ฝฟ^ะวรํlŽ„้ xฎ๏Oดด l>Wเา\’#Q^Ÿ˜ปๅ๏วะTพุVVท5aZv%„M/4์๕า์q2๖ใwฬค๓|๕๐น[ฒ\๏ญุถอPK€ อ๔…|n SK็๐ย์"Fข๏8Lu<<ฑ๋ฝฯฤktค„B!jG‚”BPAj ูฯงwn ำ๏!W6๙฿็ฎ๐็วF๗๋ถ๛์่`[w ]>งCUฐBูb>W`8žไ๐•Mcข฿nCใ‹neekำฒ‰g œmเ ี๊sั๒ใw๊LWƒ”~›ƒิำถ๙ฺูฆ7่ล๏rเะTlฆP6‰g \œKฒox๊mริฒ&/Ÿฝ3๑•)!„Bˆฺ‘ %„ิ_Zเ“o โw“+•๙ณW๘o'Foู๋๛{†"JOะKภi W—œ•,‹…\‘แx’#“q๖Dษ—อ๗๕|Nƒ/<ฒ…ๅ-•งนล3…;ฒwีําpำ๔แs่ฤR9พ๖า๙;ค–M‹กฐŸญ]-๔VฯกQ}ส_%Lน0ปภ‹ใ3ผtuๆ[พฦญž‰ืh$H !„BิŽ)!„ ‚ิชึ&>๐z"~7™b™ฟ>=ฮ_žปๅ฿วmh์ŒpWW Ca?M.ฦMa*U(1žH๓๚ฤ{‡งHห๏้๛]>๗่f›”L‹™LŽKณษ†ฝ^บš<,k๒โ1tb้_{้๕qปำŠฆษ๒p#!๚C~š\7โbั4™ฯน8ปศแ+3ผxe†›๏ใLผs๎*vl๘ๆพ— %„BQ;ค„‚๚ Rkƒ|l๛::|nาล2yrŒฟ93~พŸฎ*l๏๏เž6†ย~Bn'N]EŠ–MบPbb1รks‹1“ฮฟซืo๑8๙ต=›้oฎ<อm*•c4žjุ๋ฅ'่ฅ+เมm่DSY๓ห็ฏG Z)”Lร~6Fš ๛ บšŠmWยT"_dx.ลก๑/Žฯ`ูvu&z"~ฯm™‰W๏$H !„BิŽ)!„ ‚ิ†Žฟะ:ฺ|.R…2ํฤ๛ี;๒ฝ๏Yึสฮ–ท{œ85 UU(›้b™ษd–ใSqžqm1๓Ž^s้in}AEำไ๊B†ซ ™†ฝ^๚š}DบฦT*หื_:ฎฉuqlน’IoะหหZh๖ฌ๎1/[$๒Fโ)๖DIไŠืรgถt๛fโี+ RB!„ต#AJ!จฟ ตนณ™๘เZZฝ.R…vl„ธpํŽร–ฎ0;๚;XฺDุใฤe่hJeSํLัd*™ๅ๔t‚#QF็฿zถำ7>อm,‘fj1ฐืห`ุO›ฯ[ื˜Hf๘W.ิlษ›ษ^S- „•0ฅณฒOุt:GOะG“ห ],๓Wงฦ๘๋ำใ฿6๗ฝ)!„Bˆฺ‘ %„ิ_ฺฺๆXK‹ืIฒPโOŽ\ๆ้K“59–uํ!ถ๗ทณฎ=Dปฯ…ะPหถษ–Lfา9Nว84{ำ'็๕ฝ|v็FบซOsปO2๛.—ี“ก–m^.]cb1ร๏ฝzกๆK๖Lถdา๔ฒฅ+ฬP8p}ฦ”ข@ูฒัUUศ—L฿ใ#ทuihฝ‘ %„BQ;ค„‚๚ R๗,kๅ฿฿ฟšฐวษBพศฟ~™ฝรS5=ฆf?ป#l่๑{๐:44Eมด!_*3—-p~v‘ƒฃ1ŽMล฿๐ต!Ÿน‘ฎ@e๓์๓ณ ,d‹ {ฝ …ด๛]85ซ‹~• ืŸpWฏ2ล2}!/#aVถนช๛„)ชjPผ IDAT `ู6W2ู+ตใฦๆูงงค ๅ†ฝ^ร:ชA๊สBš๒๊ลบRKR…M๎Yึสฺ๖aฅ๚•-‹๙l‘๑…4‡ฦb›ฆP6?ฐ๗ฝ)!„Bˆฺ‘ %„ิ_ฺ฿ฮฯณАA"Wไw_นภ‹ใำu5fํ>7๗ทณญป…๎&/>งCUฐBู"‘+p9žไฅ+3Lง๓|l๛:"~7™b™ฃSqJๅฦ3๖๑ป14๑Dš?|ญq‚ิ’Tกฤถ๎>ฒพงฎaุ6( Eำ"™/2–Hsx|šƒcำdKๅ}/AJ!„ขv$H !๕คv F๘7Vt;˜ฯ๙ฯ/ใๅซณu9v>งม#Cถuทา๔pื๗S*Y•อณg2yzƒ^ฮสๆูฏึ้{yงnRc๓)๐ต‹8tญแ‡ฎ*}•ฅกEห&S(แ24œš†ข@ษฒIๅ‹Œฬงx}bŽฝรัT˜’ %„BQ;ค„‚๚ R u๒“–t9ˆg |ํ๐น๋ห฿๊•Kืุ5แฎฎ0ห[4U7ฯถํส“๙–6ฯ.”M๑ยงัฐืหPK€ŸCSO๑Gฏ_ฤก5^r๊?w๏*šซ3๑๖Ot;Xำ$์q~S˜บฒแีkณ<7ฯKWfฺุfGซ‚ดxธt (Z6ฉB‰k ^พ:รั(‹๙Rร๗ค„B!jG‚”BPA๊Cซ–๑ฏท โwฬf๒งฯr|jพแฦ๕๎๎v FX฿ขลใDฉFฉฒe“.–ศห,ไ‹,ไŠ 5ใๆๆ uy.ษ=r g.ู๓; อ+บฬg |ํฅ๓ผzํฦrส‘f๖ EXีฺD‹ว…KืPU…’iUยิb†Wฎฮ๒XŒ๙lกแฟ)!„Bˆฺ‘ %„ิ_๚5=หMƒ๘:3้<ฟ๙ยNล ;พjำ ูPู<ฦฒAU*K๙2E“™Lމล,ูR‡Z›ƒUƒ”ฆ*\œM๒ฦฉซ w^Bn?yื š\๑lฏ>ว๋฿bi่ฦH3ป#ฌim"์ญ„)ญบ”/](qe!รัษ9๖D‰7P˜’ %„BQ;ค„‚๚ R^ืหoภ็ะ‰ฅsฦก3œ^hุ๑}จฏŸฝwaทƒ‚i‘ศ๑:t<††ช(XถMถd2“ฮse!iู(u~†ย:n4Uแย์"ztธ!gHตz]ุึ!ี™xฟ}๘วbi่ฦH3๛ฺYืขตฆTๅฦŒทk ŽNลy๚โ$ ๙bฟ RB!„ต#AJ!จฟ ๕C๚๙ศ๚>ผh*วWžๆย์bรŽ๏ฮ~ๆ๎{‰%๐:tZฝ.Bn'^‡†ฆ(˜6ไKeโูW3M หชฟ฿SCแM4ฮอ,๒gว†q5`๊๐ป๙ัอ7–†ๆ g9}๛ฅกkฺ‚์่`}Gˆ6Ÿw5L•,›Lฑฬd2หkณ์Ž2›ษืํ๛— %„BQ;ค„๘่_ฒ็๊่ƒ๓G7 ๐k{q• ๕ๅƒงธ4—lุ๑ฦงŽ%า”M CS นx^Cวmh่šŠeูL“xถภไb–|ูคdZu๓~†ย~:›ผจภน™ุ.ฃ๑‚Tw“‡nฌ, ฎ. =.–†ฎjmb๗`„u!ฺฝ.†ŽVc*],3•ฬrdrŽ#1ฆRูบ{ค„B!jG‚”BTข‚}t2ฮพแh]|pั-ƒ|๏๊†ฮT*หœbd>ีฐใ{๓S็ณEฎ.ฆ)šํฦพKM.Cรm่ธ ‡ช`…ฒE"W`2™EQrjซkจ%@gภƒ œžN๐฿OŒ6ไ’ฝพฺะ}i่ฏ<รน™wฟ4tEK€=C•S7…)ำฒศหL,f96gH”ษd„) RB!„ต#AJ!€ฒeู™b™ฉd๎๚็k‹™šฯOl]ฮ‡V/รญkL$3|แภ)ฦ้†฿›Ÿธ/26Ÿฦดmt๕;Eู6\n]รkธFu“๓’Uyฒ[ถT&[,Mๅ0kดœoy5H)ภฉX‚ฟ:5†ก5ฆๆƒa??ธฎQ R_>xš‹๏ci่P8ภฎม6Eši๓นฏ๏fZ6ูRe)฿‰่<ฯ^žbชย”)!„Bˆฺ‘ %„T‚TeฃสS฿&“ND็ู7\›0๕ำV๐+ปq้ื3|nษšฒ๗๋ๆง.ๆK ว“(Š‚ชผY(ฐ๑ป šœ<†ŽำPo„)ำ"S*“+™ค‹%ขษ_ฮทคNF็๙ณWะฅแฮหส–&พmOuih–/<อๅ[ฐ4ด?ไcฯP'"!"~ฯ7…ฉX*วัฉ8{‡ฃLิ๐บ– %„BQ;ค„ธ8ทhw๘8{V๒๘Š.œšฦ•…4Oํ?Y3Jซ\฿w}iุbพศp<…ฆพ}ภ)[6>‡Nุใฌ,ใำTšŠBeํlฉLพd’*–ˆฅrไKๆy?ห[t<ุภ๑ฉ8็U” Rkฺ‚‹5=ธuฉT–/8ล่-\ฺ๒ฑs0ยๆH3‘€ฯ–๒™Lงsี๐;ลX fJB!„จ RBิ฿ถ๗ vฒ1าLgภsำS฿์7|p~nxŠ+wเƒ๓}๏*]‰Cำ_H๓ไLงs ;พ?ผฑŸจ. [,y‡AjIูดqญ^Cรกi84UU(›ึ๕ูR‹๙"‰\‘Lฑ|[฿ฯŠ๊ )8:็๏ฯ7fZ฿zราะฯ8u[ฎ๏ ƒ6w6ำแwใu่hช๚†S'ฃ๓์‰ัฝา$H !„BิŽ)!„“gm€ž —]6w†้ T>8๋ชJน๚มy&ใd4มพ‘)†ใท๏ƒ๓ฟฟ5ป#šฦx"ลฯg.[hุ๑‘อƒ|฿šสาฐ๗ค–”Mงฎ๖ธ๐:tœšŠCW฿g39ฎ-fษ•สื—๙j+Zt๚=Xภ‘‰9ยต†\ฒท)า†ฅกOํ?y[—ะu๚=์Yakg ๗๕S– นj˜:Kฐo$สp๖?UR‚”B!DํHBnฉ%]ป‡*œ#7^CGื*3:๎ฤ็_|p-;๚;04•‘๙$O8๛7ftไซ3:Nลr~ๆp•ํ๋xฐฏ]Uธ4—ไำฯปํ๛"N7oาžศธฒแVฎp[ Sอ'>‡KW๑Tใ†mไห&๓ู‹Y๒e๓}=•OS†n R/]ญ)ทัxA๊พžึบXฺ๎sณkฐƒmญt๘บฆbY6นฒษL:วู™๖ G97ณpหพฏ)!„Bˆฺ‘ %„ผ}บ๙ƒ๓ฮถuท\S7ฯ่˜อ83x฿œ?นc๗๗ดขฉ f“|๊™ฃไหfรŽ๏า&ํ†V™!uๅอz3MN‡†หะ๑:FuฟชBู"‘/0น˜EUน"๏๖๗`%H๙้ x([6‡ฏฬ๐๗็ฏแu่ w^์kฟพ4tt>ลgŸ;Vำฅกa“ƒ๎YึJW“Ÿร@W์๊lทสŒฉฯฦ8Kผ๏๏'AJ!„ขv$H !๏๑๔ั†฿m_วC}•Mฺ๏tZRถlผทกใะTšŠข(ืรTพฆฆS9rฅoฝDากkt<๔…*A๊ภh”ธ0AศํhจsbZ6.๏ไ64Uแโl’Oึ้าะ หมรืำJOะGภi`ด?ุ\ฆภลูE๖D฿U˜’ %„BQ;ค„‚๗คnเผk0ยo>ฃใย์"๛Fฆ8>๕ๆ3ฆ>๗ศf6w†Q€“ฑŸzฆฑƒิ'^=|ฬg‹Œ/คั๏pZR6m\†Fซทฆœš†CSQU…ฒi‘/›ค %๓Eนโ7อrฟ›พŸ’iฒw$ส?^˜ ์q6ิ9)™฿ตช๛ KC?๑ฬั๗ตแ๛ํๆs่์Œ๐@O=A/~—‡ช`S ฟูf94>อซืf฿๖๕$H !„BิŽ)!„เึฉ%M.ƒ‡๛;ธฟทž&/—ใ]อ่๘ยฃ[ุiเDtžฯ<{ฌกว๗ๆ=ฑๆณEฦitMฉ้1•Mงฎ๖ธ๐:tœšŠCWQ•สำ3E“ูLމล,นฒ‰^…eht<๔}M“g.O๑ฯ'h๑บ๊œM‹๏YฝŒป—ตข)pvz?}ซ.p: U6?๏ UfL้ีe˜Esi)฿"/]™แ๐•™7} RB!„ต#AJ!ธ๕Aj‰วะู=แž6zƒสŒ)Mลถo|p>7ณศ ฿0ฃใKmeCG86็‰ฝวz|o+‘+0–ศ`ิ8H-)›6บฆ๖8๑.CรuS˜ส•*{%า85…Nฟ› ‚i๒๔ฅI๙โ$mพฦ R๙ฒษ‡ื๖ฒตปฒ4๔ฬ๔๚HCฝ—ฎฑgจ“m- …•๐[รญhšฬ็Š\žKrh|šรใำ|ใM.AJ!„ขv$H !ท/H-q:{ช3:๚฿bFวแ+3ผ|e†/?~ฺ๋ƒXภั‰9žwขกว๗ษ=›ธซซ˜ฯฏฃ ตคlฺ( ดTgLน ทกกฉ*ฆU™ู–ศศหZ<.๒e“บ8มำ—&้๐ป๊œไJe>ฒพŸ-]•ฅกงb >ู KC]บฦ๖v๊kง/ไ#่rเะิ๊R>‹Dฎภp<ล‘(/_นฆ$H !„BิŽ)!„เ๖ฉ›?8๏ŠpOw+ƒีŽ๊SK3:†ใIzƒ>บ›<˜6ผvm–ฯํ?ูะใ{๓žX๓นW™š/ู{3Kaชูใฤ็0p้*CGฏ.น,”-tUAืTŠe“u๎*ฯ\šค3เiจs’)–๙่ฆ6U—†žŒฮ๓้_jh*๗wฐฝฏfM.GeVP0-ๆณŽLฦ๙฿็ฎ0ฑ˜• %„BQCค„‚;ค–8uํ}ํl๏ึ3:TUมก*”-›—ฎฮ๐…งz|ฟ๐่6Ešฑฉ,ูปฒฉูฆๆ๏Vภ้ภ๋ะp:CวPEAlๆฺb–ฟ=s…k‹†ึ0็$],๑#›‡ุX]z|*ฮgŸ;ธŸ Meว@๔ด1ะ์'่v`จ•๛k:ใžใŸ/MJB!„จ! RBมRKี4ฃรกฉ7‚0“ฮ๑วฏ_ๆะXŒF‰ๅวทฒพ=t=H]]ศ 5Hฐmธ \บ†ฯat(ีMฮห–E<[`l>อ้X‚ห๑$FuVN=KJ๘ึก๋็ๅศไO๎=๑บฏuUaว@„{–ตฐผ%€วะ‰g ™+<7<%AJ!„ข†$H !ต RKี๗๗ด1๖ำ๊uกVƒ‡iูLgr\žK๒๒ีYล([๓ณ[Uพ๒๘Vึถ1mHd \[lฌ ตฤฒlŠฆลๆฮf\†^๙m(™ฉB‰ซ‹NF ฯ%กŽ฿bฒPโ'๏Zฮฺถส^eฏ_›ๅฉ_๚ฆ์๛;่ z‰g œŠอห’=!„BˆZ&AJ!jค–่ชยwฌ์ๆวถ แsืgI-…ล|‘‘๙/ŽOsplšฒeีุ๊ชสW฿ส๊ถ&Lห&‘+2ฑ˜Amภ Mๅุiฆร๏ฦฒm2E‡ฆเิ5EกdZค %&“YNO'ธ8“คh™ืcฝHJิถฌikยดแีซณ|ภษoซ๛^‚”B!DํHB๊'H4ป<๕ศf†ย~l f%fผ9๓bพศ่|šฃQ^Ÿฎ๋S.]ใKmeekำฒ™ฯ™Lfiะล\ฆภบŽ >7้b™รWfะU…•-M„=N\บ†ช*”M‹LฉฬT2ห‰่<วฆโห~งQ๏#](๑ำwฏdU๕ผผ|u–/>๊๊พ— %„BQ;ค„‚๚ R-^OํูD_ศOษ49;ณHฎTf0์'ไrโิU hZ,Tริ‹Wฆ98ฃhึ฿Œ)ŸC็ neyK€ฒe1Ÿ+MๆP4H-ไŠฌn า๎s‘*”929‡Kื๐์yt%w}็OํUwืฺRki๕ฆnป๏ฦX†8!&ยd&v ž $$มa˜็<99Cกำฃ์CN†yf’ฦ6  ุํ}มฝช7ต๖ๅ๎ทถ_ี๓วฝ’ป—n[Rj}^็p’CิาญบUาอ๛~฿าUคu iCCJWกซ2dIZZE5Yฎโนษ<›ํ‰ศรTล๕๑๋1ุž„๘—Sำ๘โรฯฎฉ๛žAŠˆˆˆ(: RDDhฎ ต.eแ๗^‰ น!๐ภัqำ gp๙บ,ฎํiวึถ Z,บข@’๊ณ‹๒ถ‹ใs%<:6‹๏Ÿ€ใ‹ฆ9ทYSว็nฟ ›2๐D€…šƒษr-ถืJล๕1ุžAGาDษ๑pdถO05พกศฺ“’šSS`žฆjžภLลฦ๓Syป๗*llMร๕'าMWโคl_`sk}๐|ั๑plฎOชฒ๔5พ!I๕cO้,Mฅ)Pd"a๛sU‡gŠxไฬ f*๖ช‡)๘ภ ƒุZ…Ÿ˜ฤ๐๙5u฿3HE‡AŠˆอค๚sI|jฯ่อ&a๛็๐:6ŽŽคน๔5%วCWฺย }ูEz}จ67Qด]œสW๐ร“S๘่$jžู๑tฆLUุุ’‚'fชf*vlฏ?1Kก=i `{8ฑP‚+่_็}ญฅ0ฅยTXš M‘†!_`ฎ๊เ่\ Žอเlกบjaส๑~๚mKก๐{ฃ๘“๙ษšบ๏คˆˆˆˆขร ED„ๆ R๘ไž+ะ“I ๆ ๏Nใ๛'ฆะ–0~๊kKއŽค‰7m่ฤถ๖ ZCต%I‚+”l'๓๋ฉi<|b%ว[๕ใYŸNเำท] น\!0]ฑ1Wuโ{ฑ„๕hุš0ท]œZจภ4Ezญ†ฌก#ฉ+05 Uถ8จฏoป<:[ฤ3“๓81_†ฉ)+zŽ/๐มถc  :6/่…5u฿3HE‡AŠˆอคถดฅ๑๛Cปฑ>@อ๓๑?r?:=ƒKลSr60 ŒQŸ/•ิT$tš\S^P‚โษ…2ž™\ภัูโRดZnž๐มถ7ซ์O|hM๗ RDDDD~ฎf""jฎ 5ุžม}ท๎FwฺBี๓๑?ž;…วฯฮ!cพ๖Vฎล0uMO.๏ฬข=aย8'L•ง๓e[จไxhต \฿ืŽ9ด'อ๓VL•c… ~tz฿=>ผํฎุ๑œปโห๑&ส5V๐็ญ†\ 9KGฎ>?q‘=j‰/B$tํ –ฆภP่Š I’เ‰eวรxฉŠg'๓84S€+dIzรว`๛ฟyำeKณส๙ะลcGืิ}ฯ EDDD)""4Wฺน.‡฿}ห.ฌK™(9>้Qš) ฉซฝJއฌฉใฆ\™Cวb˜’%๘Kซp*xไฬ,FF'0ปณถตg๑๑[wa}ฦBอ˜(UQŒ`–ีrhI!g๊(ิ๊3บ‚ฐTฝ7ย! UFGาDBWก+๕0ฅ4ซŠ็cผXลำ“๓x์\?xCะkžผใผYeร[S๗=ƒQtคˆˆะ\A๊Š๎V|์อ;ะ™2Qr<อSฃ8>W‚๕†\—CรMุฑ๎ลSŠTj\ษ๕p&_มcg็๐ใ˜]ฦงเ]™ร๏ฝe'บาชžภxฑŠฒ๏ ตฑ%ฌฉ!_sq2_^ึ๏ํ‹บ*ฃ-a ฉฉ0T†*C–$aˆj#๊=7™วฃcณจy๋ Sืรวnูyฌฒฟ~๒๘šบ๏คˆˆˆˆขร ED„ๆ RWฏoรoฝ๙rt$M๙ฤ1Œชะ—aธ๕b˜บฎทปบZะ™4aj*ไF˜*ปฦ UwVู฿>5Š๏'งืิ}ฯ EDDD)""4Wบuc>xร6ดX:ๆk.๔ว/ภ๑W'เ”š"ใฦพ\ูŠui MYš[ดฆžš˜วGว/hลิ-๋๐กF`+;ฺไ•@ IDAT>Nๆห๐ƒ๘)SUะ—M"กฉ(:๕RRDฏ% ดQ฿ฦ—ิT$tZckง(4f\=;น€ฃsลฅmŸsUYSรoฟyวyณสฯกฑ5u฿3HE‡AŠˆอคn๗_7ˆœฅcพ๊เห?>ด๊+ŠJŽE–pS'ฎ่jAw&ฑฆDข๊๙˜,ี๐ฤ๘<<6ŽฑBๅฟืะฆ.๚๕๕ภVr}œ˜/!ˆ๑฿ž„ฆข'“@BSQp<œ\แRB!R††S‡ฅฉ0Tyiพ'”\c… žŸว 3yฬUฌK™K[) ถ‡ลทœ]S๗=ƒQtคˆˆะ\A๊Žญ๋๑kื"kj˜ซ:๘ฏ?:ูŠขล0uCoฎ\฿Š๕๓โV>ฉr OMฬใ;วฦqโež8ทwหzผ๏บญศ™:Jއั๙2Bฤ๗oOJืฐ>mมาTฃ๓eHRsผ6_„H่ ฺ&,Mก*ะ’TŸ Vv<Œ—ชx์์๒5ฟtๅ&ด5f•ํ;x_S๗=ƒQtคˆˆะ\A๊ฮmฝธ๗š-ศf*6พใCAด/ฏไxะื๖ถใช๎Vtฅ-$uŠ,Ÿทb๊้‰y|๗๘Žฯฟ8่๛mƒ=๘ีkถ"kjซ>|%คu ™,UAม๑pbพิ4Aj‘/BชŒŽค‰„ฎBW๊a๊™`EวC{ย€ฉ*Xจน๘ณGใแ“k๊พg""""Šƒš+Hฝ๓ฒ>๒U›‘^ R?:ั$ฟซƒ0„.+ุั•ลฎuญXŸฑ–VL!P๓|L”jxfr฿9VS?ปฝ๗\]lEวร๑นๆ 8#k๊่JY0Uวล‰ˆ†š__„ะUm IM…ก*0Tฒ$! Y Iฐ=ฟz™k๊พg""""Šƒš+HŽ~ป+6#mจ˜*ืWH5ใ๏jM‘qyg—wถ 7›@r)L…จySeฯLฮC!๎ฺุƒดกขไx8:[„,วทHต˜::ฯ RMดe๏•๘"„"KhOH4ยิโ๛abบbใเุ,พb OMฬฏ‰๛žAŠˆˆˆ(: RDDhฎ u๗ฎผg๗Fคt“ๅพฃCM}๎ Eฦ`G;ืๅะ›M"uฮVพš็รา†M–Ph)%ฦAชี2ะ™2a( vc…TLว! Ckฌ๔2!5^bHœซ:8<[ภ๗ŽOเฑณs—๔}ฯ EDDD)""4W๚ล+6โ๎H๊*&J5|๙ว‡‡ก+2ถถgฐปซ=™Rบ M‘๋ซปัร๖N,”Pv|จJ<ฃT{ยDGา€ฎฤg…ิK๙ข ฏ๏๋€ฉ)รAB‘๋๏—ใ ฬU]Ÿ/โ๛'ฆ๐ร“Sธ?-0HE‡AŠˆอค•›qืŽ~XšŠ‰R_๑aฤiA‘"I์ศโส๎ฌฯ$5๔๓ถ†EวรLล†/ยุ…ฉŽค‰๖„MQPlากๆยB ถg14T]“ๅrฆ^_อฆศWXจน86Wฤฃcณ9>G—ฬ}ฯ EDDD)""4Wบ๗๊-๘นห๛aฉ ฮซ๘ณGAŽa๑ร:ฒx๛`:“๕ญa!„pEว(ปๆช\?ˆM˜๊LZhKะล˜mู;ถดฅ‘648~€C3(ฒ„ดก!khH่*4นฆผ @ั๖p2_ฦมฑYฆ›ฮซxfb฿ภ‰…rlฮƒQtคˆˆะ\A๊บ้2พฅšขเไB~๐๐า,Ÿธ™ฏ:๘ูํฝธm๓โ๑”๐ง?>KSQq}์๊jมญh„ฉ๚S๙๊3‹ฟพ•oฎ๊ ๆ‰ฆ S๋ำ ดX๕AํEวํPsU–ฑฉ%ซคž™ZXฺš๗r|BWdด% $tฆชภPeศ’„ Q๕ฆJ5<;ต€๏Ÿ˜ยOฆ๓Mคˆˆˆˆขร EDเ๖}ไ%ูfx-yำๅx๋ๆnhŠŒ๓%|ํเ‘ุฎ*ุ.~v{†6vASdŸ/โฯ~|I]]๚WุึžมฎฎlnM#m๊ะd้œ0 ๊๙˜ญฺจนั‡ฉžL9ณคJއั˜ฎา-iXชาุฒทpAื™/Bศฒ„Žค„ฆยTUXš E’ ยถ'0Su๐ยtŸ˜ฤ“ใ๓M{คˆˆˆˆขร ED`hx$'๎G•:L๖›wเึsฮพƒG_uๅJ3+ปฑญทl\U–qtถˆฏ{0HE‡AŠˆ่e P…|ฟ,แžีูฟu8Gf‹ุ๘11 RŽ/๐ฮห๚pc_Yยก™พ๒ศadM‚ํฆึ4ฎ^฿†ญ)dM}i––ิรTล๕ฑPsPv}จซT…๚ฒIdM€ผํโtพหR)]Co6CQP๕|™-.หqd  Mตฆืฎ+ไk.Žฮ๑ุุ,พ;: ว‘ž)"""ข่0HฝŠ(ยิ'†vใฆฮ_>~ บชฤ๒๙A€wl๏ร๕}P$เ๙ฉ<พ๚่ดX๚ุิ–ฦ๎ฎ ถg]ฺส๗b˜ชy๕แ็ืฟจmgฏG6‰L#Hอื\œ-Vb๙d =™4EAอ๓qxถ๐บWHฝTึฟฟฉสH้บz^L,ฺNๅห886‹‡Žฃ์๚‘œ)"""ข่0H]€กแz ธuฅึ์น7๔ืฮOฆ ๘›'วvห^†x็๖>\ ภณS ุw๐Z,ใขฟWล๓ฑ1—ย5=mุา–A‹ฅŸท•ฯjžภlี^ัSนิา–รนชƒ‰R5–๏MฮิัN@Sไ๚ ฉ™ไe>g"‘าUไ, Mก*ะ7Qr<œ-T๐ศ™YŒœ˜ฤlล^๋“AŠˆˆˆ(2 RDDa๏พCฒ,฿ SŸนํJ\ภ๓ำyํSฃฑj?wYฎZ฿ ภำ“ ๘oEฮิ_๗๗ซธ>zณ ิ฿‰MญiดX๚า–F7เxj~}ลิJ„ฉ  ˜ญุ˜,ืb๙พดZึฅ,hŠŒŠ็แศLqลV—๙"„ฅ)hOš็‡)I‚/”\ใลž8;‡‘“+ฌฮช3)"""ข่0Hฝ+ฆp๏Uธฆงpž\ภืŸ=UŽ๏ ฉปvlภ•ญ<51ฟzโุอz-ฯว†\ื๖ดcK[นฦV>Iช?ฑฏโ๚๕x>JŽทlฑe1Hฆห5L—ํXฮjKิƒ”*หจธŽฬW|ปฃ/BชŒŽค‰„ฆBo„)EDขโ๚/ึ๐๔ไช๎๋S2$lhI!mจ!0^จbฎj/๛์ฅีฐ.eก=a@‘%”GๆV/H-๒EYฺf#Lฉฐ4Š$A„!lO`ถ๊เ'ำ๕0๕ไ๘ฒ|)"""ข่0H-ฃๅS๙mืœทข่Ÿล7H9พภ/_ฝ—wf!Bเ‘ำ3๘ฦ๓งิี๛™%วรบ”…7m่ฤึถ ฺ: U$IpE€ฒใกๆีท๓•]ขถษ’„\ )C…BŒช˜ฏ9+พีm%tฅhKP$ ไ๘8:[„ชDsพ!K@kย@Rื`ฉ ,Mชศ‚ถ/0_spdถˆ๏Ÿภมฑูe๙น RDDDDัa""Zท๏เฃpฟ$#{Qฟ”% _|๛5็ญ(๚ๆแฑุžGธ๗๊-ุ‘B๋้ำ งai๊Š์’ใก+mแฦพ ถgัšะa( dฉ„ทช๋ก์๚จy%วป 0ฅH6œคNๅ+ศ๎ชฏ,zฃB=้Zd%ืรัูbS<อQ‚„ฌฉ!กฉ05 M…&K8~€๙šƒcsE๐ไ4~pr ม๘ร EDDDแ็>)"ข•14<’“๗ฃJ€^h˜Rd _|๛ตK+Š=3ƒGฮฦ๖ธBเWฎูŠม๖, ภONแ[GฮยT•U{ ‹aฺ๊žv\ึ™E[ย€S^#LU\5_ hฟz˜R% -)คt ~เฤBEƒฆฤ,H…@_6‰œฅCPt<›+5ีq„2F=LYš‚คฆ. ๗๗‚๙š‹ฃsE<~v฿=>3คˆˆˆˆขร EDดย.&LŠŒ/ผl๏ศB!~tzํฑปBเ}ืbs[~เแัI1‰้ฒš฿“AŠˆˆˆ(: RDDซlh๘ภ€*ไ๛_.Lฅ Ÿฟjlmฏฏ(๚ม‰)Œœ˜Œํฑz"ภฎ฿†-)ธBเกc๘แฉฉฅญWQ*9ฒฆŽ›7t6ถ๒™ฐTฒ,มo„ฉ๚S๙๒ถ ItEF.…„ฆย‡f ฐ}cท .‡ล •3u„ ถ‹“ ๅX g๗EKSะž4—ย”&หK๏[ู๕qถXล“ใsฤ™BๅฟƒQtคˆˆ"๒ra*k๊๘Waskž๐๐‰IเไTlั>xร6lศฅเoว#c3Pคๆ ‹+ฆฎ๋mรฮu-hOึWLษเ!jž…š‹’ใกโ๙ุั™ƒีRฯOเ ฑช3ฑ–รนA*Pจน8•/ว๊iพaจ2ฺ“&’š ]‘กซ  aˆŠ+0^ฌโ™ษy|otฃ๓ฅŸ๚ RDDDDัa""Šุร^‰ ทถ% |v๏Uุุุโ๖ใ๘ืำำฑ=6๘เ ั—Mย๖พyx OŽฯA’š/|”Cว }ํุฑ.‡ฮ”u^˜*ปfส6:’&ฒฆG<;™‡˜Zผ‚”„ุุ’B‹ฅC„@พๆเLพ9fO ๊aJSdด% $u†ชภTeศ’„ Q๕ฆห5<7•วร'&๑T~้฿2HE‡AŠˆจI์w`จ+“ธฎบuq‹ƒว&๐ศ™™ุ“C|่†m่อ$Q๓๙…3xvjกฉ_sษ๑ิUุื]-X—ฒฮส'Kาาฃ3๕ี^–ฯR-–„ศ.ฮชˆaZโ‹’t$M$uฆชยาd(’ถ็cถ๊เ…™ฤใs RDDDDb""j2ว็KCนิŽท~๛ศ8;;฿ƒ ธ ๋ำ ิ<๋…ำ๘ษt!/ฝไxฐ4o๊๏ภฎฎt&MXšบดญ-0WฑqถTยz|‹‹—ฉ๙š‹๑b’๛g1LีWLiฐT–ฆ@UdAGฬU-โ;วฦ๗|mืŒ๐ทั๊c""jRืื7ษNNmHZ<ศ๘เ ฑ>cกโ๚๘ฦ๓งpdถซc(9LMม}ธฎท=™ไFน ย๚Vพš ๆ๙(ุn,ยิ‹Aส€˜ฏ:˜(ี.‰ u๕'!cjHh ,M…ฅฉะe !ว0Wต๗๔f“#mCDDDมg5)"ขๆvว๏Mฺg{2V_ย”,บa;บาสฎ|๖$Žฟฬp้8(9ึฅ,ๆ›.Cช๑>, AจyUฯGพๆ"@๓}RนFšญุ˜ฎุ—์=ศ,MERS‘ะจฒŒแU–G๘[†ˆˆˆh๕1Hลฤb˜ฺิš๊ำ9ฏY‘d|่ฦmX—2Qr|รณ'prก๗ oป๘[wa}&?Qt<่ŠŒ„ฆ.=ญๆ ุพ@ู๕1[ฑ†€ช4ืาฃ 1ะ’Fฮาแ‰ณU3—pZ:๎0Dฺะ5uXชŠ0 ๗ไ,}„ฟ]ˆˆˆˆVƒQŒ ไฒZ๘;™ไGR†šj๖ืซส2~ใฦm่Hš(9๎้QŒชฑ=Eวร}ท๎BWสBี๓๑ƒ“Sธผ3Irf)oช,มร”฿”a*B ดฆ3 xB`บbcฎ๊ฌ™๛ศB$4Oœ๓หWoแo"""ขีว EDCCร#นžคr_ปi|ธ™ร”ฉส๘ภ๕๕ Ut<อ“ว1Qชล๖ผW\ฟ๗–K[ŽอยT่ŠŒึ„”ฎ"กi็?อจy>*ฎ™& Sa ดค3uธB`ฒTร‚ํฎฉ{จๆ |ๆม'๘”="""ขˆ0HลXณ‡)KS๑๋ืข-a oป๘ซ'ŽวzkXอ๘ท์\ฺ‚๘ิภิ€,Ihฑ๊ม,Mฉ*Pd Aย๖ุž@ู๕0[ฑDฆฮ RŽ˜(ีP`""""ขUฤ EDt >0ะe&จ3iผท™Ÿง ๏ฟnญ–Ž|อลวaก฿ญaฎ๐ฑ7๏@gชพ๑๘| Ž/`จสy_งHR}NQc€ถฅ)S!?hฬ˜๒0Sฑซฆฮ Rถ/0^ฌขไzk๊žฑ=O3HE†AŠˆ่าla*c่x฿u[ัb้Xจน๘‹วŽฦz%Ž๘ํ7๏Xš‰5:_†ใ ่๊ห™_ S ญพbสj<อB8~}๘๙โV>„ซฆฮ R5_เlฑŠ ƒญ")"ขKPณ„ฉVหภฏ\ณ9Kว|ีมื;‚ฒใว๖ผaˆ฿บyฺ“ŠŽ‡S eุพ€๖O=”% YCGBWaฉ‹aJBŽ/P[ๅ0๕า 5Vจ ๊๙k๊ฑ}O?ภ EDDD)"ขKุ}†ฺ’ๆ็ึฅฌ›ฃSํI๗^ฝYSรlลมื‰u๘|ไๆหั–0Pฐ=œษW`๛โ‚’ YS;gล”บฆ\ภ๖|Tผ๚S๙<ฑra*M-)dM5ฯว้|ถk๊p|O1HE๗ูšAŠˆ่าU˜๊L™ธ็๊-ศf*6พ๖่ิ๘†U–๐oบ ญ!ํc…j=HษŽร”ฅ)Hh*šฺ~^SŽ/Pq=ฬTx"X๖0„ภฆึฒ†Žช็ใdพ Ok๊žpŸzเq)"""ขˆ0Hญ!{๗ส%ฬ/ญO[;W#Lญฯ$๐๏ฏ„ดกaบlใฯใ eจ ~ใฦํKCฺฯ*pEE~}มH†„ดก!กฟฆTEF„pDืจx>ฆห๖ฒ†ฉ 6ทึWH•]'ส๐)""""Z= RDDkะ๛ผ7mhŸํษX}+ฆDbcK ๏ฝbRบŠษr ่‘XฏฤIh*>xร6ดX:๒ถ‡ำ๙2 „;‘ิSI]i<™O…ึSK+ฆผ๚Œ)ืใa*€อmidM %วว‰…‚5๖yภเ RDDDDQa""ZรริฆึTŸƒน/–lฯโ฿๎@RW1Qชแซ†ˆ๑฿ดกแื "g้(ุ.Nๅ+Ai™vิ…!OๅS`้0%KCภ ‚ฅง๒อVl8o LีWH-)ว็Kk๎ฺg""""Šƒแ=๐๛ปRึง 5ตœฮu9ผ{วXšŠ‰R_}๔HฌWโไLฟvVไLรษ…Vโhยศ˜,UAาะ–ยTžเŠF˜ช:ฐ=qQa* Hภฦ\ 9KGั๑0บƒ”'|’AŠˆˆˆ(2 RDDษ๕$•๛ฺMใรหฆjžภ5=m๘7—๗รRœ-V๑ีGว๚ต& ๊5[‘55l'V6ไ„าบ†„ฆ ฉซH่๔ล0ิท๒U=™Š}มa*Bศฒ„ นZ,}UŽฃy"ฤ'ฟƒQDคˆˆ่<หฆชž๚:๐ฮํ}0Uc… พ๚่‘e…sŸXด=ŒฎRศ C ฅซีR ’บ]‘†/ฮ˜บะ0ๅ!tEF_6ู˜…ๅโิBkํำ€/B|‚AŠˆˆˆ(2 RDD๔ฒรT›ฉ๕ >/ปnX‡;ท๕ยPœ.T๐•GA•ๅุž“๎ด…pีfคW9H- ‚iSCRSai*Rz}๘9P฿"้Šืร\ีEี๕_6Ly"„ฉส่ษ&—žxชPมZ๛<เ!>q€AŠˆˆˆ(* RDD๔ช†† t™‰?๊L๏ฝ˜0Ut<ถนwl]CQp2_ฦW= -ฦAช/›ฤ{ฏุ„ดกข`ืg/Eฑโk1LYชŠคฎ ฅkKกฯ ๊aช๊๚˜ฏ9(ป>ิs่Šบ"/mู›ฏน8/ฏน๋šAŠˆˆˆ(Z RDDtA.6Lๅmo์มอะ'J๘ฺมฃ็ล‘ธhIแ=ป7"ฅซ(8.F็ส‘nAAˆดก!ีx"_RW๋[๙๐โŠฉšW~^i„)วPe›าศ™:ๆซŽฯ—–VZญ"๑๛ RDDDD‘a""ข‹RSๆ—;“ึฏฆๆซyY๖l๊†ฆศ8>_ฤ_>~ RŒ‡HmnMใ๎]Hh๊าำ้šแpD"ฉซhKฐTฆ&/ญDsƒŽภ๖|LWmŠ,aฐ=‹ฌฉaฎ๊เ๐L ]]Sื1ƒQดคˆˆ่uูป๏ภP[าบ”u๓ห…ฉูŠw๏ภ[6ฎƒ*ห86Wฤ฿ำ+ แ!สއช็ฃ๊๙(9d้า S!€“AŠˆˆˆ(* RDDดข\|T๎fr!๛็Oล๚Xฎํiวืว6H@อ๐ƒื๗vภิˆ €„ะd’xAˆŠ๋กโ๚จyEวฝdริ๏1HE†AŠˆˆVœ+‚“๓ป'G?–2ิT\ใ๚vฑu=4EA1ฆAส๖„ธฒป –ช`กๆเ่\ ฝฺูtEฉ‡) ๊๙จx5ฯGั๖pฉu))"""ข่0Hัชษ๕$•๛ฺMใรq S7๕wเถอKAjtฎปyKถ/ CยฮฎXช‚๙šƒcsEช‚Vห@สะj @—ธAˆช็ฃๆ๚(ป>สฎwษ\ RDDDDัa""ขU74<’๋ตค/ด&ฬฆ -6ฏ๛ๆ ุณฉš"วv…”ใ hฒŒห:s0A๊๘\ a}š,MEgาDBWa( tE†,KK+ฆชฎJcฦT1HE‡AŠˆˆ"34|` หLQgาxoยิ[ึแึM]Peน๑”ฝR์ๆ+น~CUฐญ#CQ0WupฆP9/ฌ-?oOšH6ย”ฆศ%ภBิ<ฟ>cส(ุnlฏ?)"""ข่0HQไโฆ†6uแ–u็ฉ2bถcฎะTlmKCWฬีœษW^vฅ—/B่ชŒ๖„คฎมT:LีŸสฯ0ล EDDD)""jw ?xe‹กืu)๋ๆf Sทm๎ฦอ:กศŠŽ‡ัน2d9^็ุ!R†ŠM-)hŠ‚๙ชำ…๊ซ†5_„P IIM…ฅีgLษง๒ีผ๚๙*ž’ํ!@<>[0HE‡AŠˆˆšฮ}†ฺ’ๆ็š-Lพe=n๊๏h)ฃsลุ 5๗ƒCร†\ š"ืท์ๅหt/ SฆฆยPd(S5O ๆืท๓A“ฦ`""""Šƒ5ญf Soฺƒ๚; H@ั๑p|ฎ%fAJ!r–Žพlช,cฎjใtพrQวแ‹Š,กณ1cสT่ชฅฑ•ฯ๖ลy3ฆš5L1HE‡AŠˆˆšฟxเ]YS{2V_”a๊g{p]_d%ืร๑น2”˜mู ‚- =™TYฦlลฦ™Bๅu…ตล0ีž4jฬ˜2Tฒ$A4ยTีจy>๒Mฆคˆˆˆˆขร EDDฑqว๏Mฺgฃ S?ปญื๔ถCF}…ิ‰๙R์ถ์!ะž0ะถ ศf+N*P฿ภq๘"„,)IMƒฅ)0^ฒbชๆ T=๙š43ฆคˆˆˆˆขร EDDฑณฆ6ตฆ๚๔U\ข๔ฮํ}ธบง €ผํโ๔+<ฎ™…!ะ‘4ฑ.eพค๒จส?_„e4fLฝฆTY‚ิgLูพ@ู๕0[q†X–Ÿ๛z1HE‡AŠˆˆb๋฿ํ๗>7I~$eจฉU๙y—๗ใส๎V@พๆโLฑ)†็ญ3iข#eA‘€™Šำ๙*ดe Cพ!I๕0•า5ชK“กศ2D ๆจy~=LU ข0ล EDDD)""Šตกแ‘\ORนฏ4>ผาa๊] ˜ฏ:/UcyฮบSZ“&dณUง๒eฌฤJ3_„€t$Lค ฆชยT๋Oๅ ย5/€x*฿tล^๕0ล EDDD)""บ$ฌF˜z๗Ž ุีี‚ภlลฦdนหsี“N %a@jว้Bฺ n}๔Eˆ!:“’บ KSai๕แ็Aย๖ุ^}+฿Lล^ตญ| RDDDDัa""ขKสะ๐.3๑GIใฝห9๘\๐๎ุน.‡ภLูฦt%~A* พ\9S€ฦS๖–wห+๑E3วโS๙,Mฅ)ญ|๕แ็Žb˜ ‚• S RDDDDัa""ขKาr‡)Y’๐ ;7`วบDL•j˜ญฺฑ;/A ไ’ศš๚าJฏฑb๕ =e๏b-nๅkOผฆLUชิร”๋ ิฮ~.‚pEยƒQtคˆˆ่’ถ\aJ‘$ฝk—uf!‚ฅๆkN์ฮG„ุุšFฦะ" R็}ณ $4–ชภิ๊Oๅ B,…ฉŠ๋cฆb/{˜b""""Šƒญ {๗jKšŸ[—ฒn~=aJSdยฮl๏ศ@!ฮซศn์ฮƒBljฉภ\ลฦูbŠํ๓eHศZ:,Uฉว)MY~๎๚l_ ์๚˜ญฺ๐ล๒„))"""ข่0Hัš๒zร”ก*๘…0ุž„8Sจ ไxฑ;ล •55ˆฐy‚ิ"Y’1๊๘’Zžป•ฯ*ฎ‡้สS RDDDDัa""ข5้bร”ฅฉx๗Ž ฺุž8ตPFล๓cw็ฉ ฤ\ีiช ตH–$ค ษฦjฉ„ฆBUdAWิWL-nๅ๓D๐บยƒQtคˆˆhMปcƒ๗ฆ ํณ=ซ๏ียTRฏฉอmx"ภษ…jพˆ๑พ\/V!7Y:Wึะ‘ะ๋QสิThฒ„0ฤyajถjร๕/.L1HE‡AŠˆˆ/†ฉMญฉ>]‘๊ž64ตc6ตฆแ‰ว็‹pEปใ|1H้๐ƒ๓UใฅšธGB9Cฏฏ–าUX0„€'8B ์๘˜ซฺp.0L1HE‡AŠˆˆ่๏๙๛‡฿฿•ฒ8eจฉs๛ฌฉใฎhIรGๆŠA†žค<`กๆ`ขTƒ$ล็า†KS‘ิU$ฯ SฎเŠท๒ฝV˜b""""ŠƒัK ไz’ส}ํฆ๑แล0ีb้๘๙0Kม/L)~ว„ภฦ–ิRšซ:˜*ว+H-J้Rzcฦ”ฎAWd„a}ฦ”ใ T=ำๅWS RDDDDัa"""z‹a*ญชฟ1ุžIฟkG?6ไRp„ภ๓S๙ฆ~!ฮRณUำ;Žm †@ฺP‘ะ5$รฯuEF,…ฉŠ็cฎโ ๆ‰๓ยƒQtคˆˆˆ^ระ๐HnCZ๛่พ๕Šฯ๔e“ฐ}g&`จr์Ž% –rฆWฬTฬVํุฟGA"ณ8cJS‘าUจr๑‚Ž โy˜ฏบ(9. Ua""""Šƒั:™/๔e’๗พธ็ฉ‰9Xšปcxišฎุ˜ซ:—ฬ{!าฆKUll้ำaXSฎPu}L•k๘o๓เฏฝm„W6ั๊c"""บHeืxุ่ฤฺฦ/&๕xEฉ๐œ-{Ž_R๓5็’{‚ Dาะั5˜šŒิโŒ)ิท๒aˆƒcณ{nุ5ย+šˆˆˆh๕1HฝNCร๎์๚U๋ฎK˜:7HูB`บdcมv.ู๗H!Rบ๚โv>]…กิท๒9"ุcชสฏd"""ขีว EDD๔ํw`่ฮํ}_บฎท}gณ‡ฉ06ถฆ5tุพภTน†ผํ^๒๏Q=LihM่ฐT†*C•ๅ=Š,๐ &"""Z} RDDDห$.aj %ฌกม๖&ห5ึ@Zดธb 5๔=๋3‰^นDDDDซAŠˆˆh™ํw`่๖มo๊๏ุุŒajqห^อ˜(ีPt5๗ซxr|nฯงzๅฏX"""ขีว EDDดB๎ุเฝC›บพxห@gg3…ฉล U๕&JU”oอฝ7๓Uyไ้=ฝ๏ํ#ผR‰ˆˆˆVƒั [ Sทo้๎Td)ฺ?6ถค‘15T]๑Rewํฉ…š‹/|AŠˆˆˆ(ฒฯฅ RDDDซใณ฿}๚‹ทm๎-KSดศ๐ุุšFฦะPu}œ-UQq5๗^lŸ.ƒQdŸKคˆˆˆVฯะ๐H๎žฝ_lหE˜’ acK SCล๓qถPEี[{Aชh{๘wŸb""""ŠƒQข Sฒ$a WReืวูB5_ฌน๓_r<ง๏0HE…AŠˆˆ(BCร#น_ฺัื;r๏Xม็Š$aC.‰Œฉฃ์๘+V`ฏม Uv}|๖ก'คˆˆˆˆ"ย EDDิ†† ฝ}ใ]น=+ฆTYBถฑBส๑qบP+ึ^ชธ>AŠˆˆˆ(2 RDDDMdฅร”&ห่ห&‘15”\g๒k3Hี<ฯ<๘ƒQDคˆˆˆšะะ๐Ÿา๗Oื๕ถ๏\ฎ0†€ก*่ษZศ:Jއำ๙ ผ Xs็ื๖>อ EDDD)""ข&ถw฿ก;ท๗}i9ยT–ฆ`}:ฌฉกh{8S(ร ึgว๘ิ RDDDDQa"""ЁๅS"‘ะT๔dศŠŽ‡ำ ๘แฺ[!ๅ๚เวคˆˆˆˆ"ย EDD#{โwฝus๗Wo่์ผุ0ๅ!š‚พliฃพB๊Tพ ฑ? ธ"ภ|›AŠˆˆˆ(* RDDD1tว๏ฺิ๕ล‹ S~ยR๔็RH*Jއ ek๐ณ€'|’AŠˆˆˆ(2 RDDD1ถฆn฿าฉศาซ~ญ'BชŒMญiคteวว่BiM)_„๘ฤทc""""Šƒั%เ“=๙ๅ;ท๖ผ฿าํ•พฦ4Yฦ`{ MEล๓1:WB€5ค‚Ÿ8ภ EDDD)""ขKฤะ๐H๎žฝ_lห๕raส๑ศฐฃ3KSQ๓}-!\ƒAJ!~ŸAŠˆˆˆ(2 RDDD—˜W SŽ/HุีีKU`๛‡g k๒aˆ๛พล EDDD)""ขKิะ๐H๎๎ํ฿ุ•“ิUุž\ั SUP๓Ž0H๐J!"""Z} RDDD—ธกแwo฿ธส๎–=๓Uoฝคฯ ญมs„ภ}฿:ศ EDDD)""ข5bh๘ภภo^w๙o\ก(จz>ŽฮืไนCเใ RDDDD‘a"""Zcชž?ค+สฝ๕Tพผ&ฯƒQดคˆˆˆึจ#ณลก๕“ำ|cวUI]]sว{฿d""""Š ƒัทw฿ก;ท๗}้บ๖k)L1HE‡AŠˆˆˆw์๐กM]_ผe ณs-„))"""ข่0HัyึJ˜b""""Šƒฝฌล0u๛–๎NE–.นใc""""Šƒฝขกแ‘อฝูฯ฿นต็–ฆh—าฑ1HE‡AŠˆˆˆ^ำะ๐H๎žฝ_lหuฉ„))"""ข่0Hั[ S[Zำ๏‰๛|))"""ข่0HัE>0p๗๖๛wwๅ๖ฤ5L1HE‡AŠˆˆˆ^ท8‡))"""ข่0Hั64|`เฎมฏ_ตพํ๚ธ„))"""ข่0Hัฒูป๏ภะ๛พt]o๛ฮfS RDDDDัa"""ขe‡0ล EDDD)"""Z1{๗บ}ฐw›๚;66[˜b""""Šƒญธ;๖?x๏ะฆฎ/2ะูู,aŠAŠˆˆˆ(: RDDDดjริํ[บ;YŠ๔ต0HE‡AŠˆˆˆV๙sŸผyC็g,Mัขz RDDDDัa"""ขH ไ๎ู๛•มถฬ]Q„))"""ข่0HQคข S RDDDDัa"""ขฆ04<’๛ฅฝฝ#๗Žี|ฮ EDDD)"""j*Cร๎พq๎ฎž• S RDDDDัa"""ขฆดาaŠAŠˆˆˆ(: RDDDิิ†† –พบฎท}็r†))"""ข่0HQ,์w`่ฮํ}_Zฎ0ล EDDD)"""Š•ๅ S RDDDDัa"""ขXฺป๏ภะ[ท๔|–ฮฮืฆคˆˆˆˆขร EDDDฑvว๏ฺิ๕ล‹ S RDDDDัa"""ขKยb˜บ}Kwง"Kฏ๙๕ RDDDDัa"""ขKส'z๒หwnํyฟฅ)ฺซ}ƒQtคˆˆˆ่’34<’ปgw๏W2wฝR˜b""""Šƒ]ฒ^-L1HE‡AŠˆˆˆ.yCร#นปทwcwWnฯโเs)"""ข่0Hัš14|`เ๎ํ๗฿ิ฿พ‡AŠˆˆˆ(: RDD;vL 0๐ฅ …ฑ2† Fค ฅ.ฒp'!c€฿9๏็˜™yฏ๕ฉะ3คHRค )R†)C €”!@ส eH2คHRค )Rj%ุ๚^fhIENDฎB`‚sparse-0.17.0/docs/assets/images/logo.svg000066400000000000000000000545651501262445000202720ustar00rootroot00000000000000 sparse-0.17.0/docs/assets/images/logo_with_text.svg000066400000000000000000001102611501262445000223530ustar00rootroot00000000000000 sparsesparse sparse-0.17.0/docs/assets/images/open-book-icon.png000066400000000000000000000111341501262445000221170ustar00rootroot00000000000000‰PNG  IHDR’gdgAMAฑ asRGBฎฮ้PLTE”Qโ tRNS,Qn•ยโํ9หสIDATxฺš=S@†w๘ฐฎ]I8i…C ฬa“คŒ‘D\"AIBlSj ุ*5 1๚ูบLdํ้๎œห๙)\kŸู“5 8ญN<šฮE‘EฑXL'ฃจ ี€`ํ๑8หก6Ÿ„†-์ท;ั่‰๑๓ฯ0jZtฦ)P8“ n'žฮ2๘ถ˜Žย@‡€ฮE‚,‡!ญxF=›ีœคP‹ๅ00P|%ฮต‚€Pvบ๖๊วิแAZ@คX†ธ>8ƒบJ psค1ภ๕๐ๆ$`\Nภศณjแ ู•‚พฃ:จยd€vTไ=h จ/ภ=8]Tมฝtอ – <~ ฆ๘ >สrƒฃเซ๑7)ภธฯ`V€i=0,ภฐsุ(ะWซ฿~จีoฟึEq.a๓@#ะ0ฬ `ลโ‰ฌ(ิพษ$ยXำงร๐•‹๊ฤใYโD!ย'`VmgfนฮหpjRผl)ฃฐL=o03จึ:ห?JHผ„a๓QtŒฅ่@โล™ŽA่^“ใj|hผq.8T“ธ9ฑ‡4$ัSจๆ Vrจด4/€ฮ๏'›ภ๙ฦ‘ภจZมŽสX@›€=y่žI‚ฝpณ@e;:บัGดA็lc9nJlฺ เZอค ๘‹ฃ5่HปQ๗ฐขE่™~[๏4บh•:ื`%s๐ใ๊๖ะVุžƒ>ฑ`ตK}pแ๐-@ุ€}ด\๖„’”ุฌZ,ฯZเH’ํ@่ %1มเฅT ๕[.ผบ<"Bต^6ซ[เ”q‡v  _๓xe< ๅ่Aธ]ี ด^๖ซภแ๖  วภีW@‚๖  ใgๅ;ภnž//ฯG_{ไ€อ๒BSbw`ณ๚&เุ$‚scš%๓Z…๒qฟUfem :ไ๘2@ฟw…`อ฿ํM_ธ๐Sl/{์@ปฤๅฅณC€.Bฺ.ะฅห[Xบรฐt)๐ฑ๏4ฝ{๓รไQ$YŠฉžEW… dู‘}N๏ุ1ี P4Vป—๗I€j%โ— ๐๓ฌใw์™เ”`š†ˆฎ~,’A€EB๙ะ 3 ภXๅiภg&~AFaฏA๚ฤฤƒx ๘ฑQ€ฆˆ เ)๐‰PB/ เฯ†>ัฒq€%0ŒฬQG ฌ๏mฐl2`eฏK,P&Pห~<ช)ื5ๅwญ j๗nFxMŸrQ€โK๚ปณ้ B}ง€่ํMัO‹Dbใoกb็˜?จ8_ฆ„l_ŽธIdŠ"๑38ั๑ƒ d `QนgHช)`๕์$— ๐œH‚7งhช Ÿ่™!€‘ฌL๗‡O4o ฃ9ฆ^๛ลY0€ฎeฯbูซม V~ ›๒„ฦ‘ห-kอ€๊Sฝ ™›ั"‹โ›:]‰cภg˜ญœFฑพ, žaมU |๛Qๆด?ฐ‚ก• >๚a>šกtฅG}ษำัM#60ฤK๛ผ] ‰QJ™gS5‚ม‡ม๘˜šp๚ใ4 ˆ—ทŸ &๖54ำนbsผ5ฬงX0€งz๘ร.›%ฌ๗๚๘| จ๔๋lm `”pซŸฃฅP/ม [hฌ๏KวOTขL Šฺ*๎u๋7ๅp/นQ gS่Cjฺ็Hิ&ึณ˜ู&ภ –ƒแ›€ว@ˆ:f6€Sปฤ๋ภวm‘งU9มO€๘๚๏w™g€รw { ภeวลlg5›ฐ[๐ฎ“ฟหl `< žเ=x๛ํ(l)๐Zj›ษ2พXvิปก{ข)ไŒู2€z‘bK Oฌเฯฑ๓b ๐ฉฺภK5;ลM๎1@Zิฯ.<—5Ejเ :ปx7๏ฃภุYฌo~๐์-ฯ.n๚Yเ%จกฏฏซฮ€ฎ6ฏซญ'\uQ9 \q•ฐ‹0ฟ]ฅ๋1€8W hDŸฤ^xู๐YC?ใฃ๛Iตp,ภK ‰T๕x1ฃเ”Ex;'Ÿซ€ณณ8‡ฬO@๒๖ธฐ๐8เ€pภ8เ€๘ล<7 pภ/kม๙ฦฌฌ์๕๚ƒ“๋?Kถัฟ_y{b FZIiภคˆšฦเ[ l]‚โ้ฒย‚ฆฤ4@TPฅXtJภฝt>X&ย๚p+ก9C PอV2@xLF:I-?๚ฆ๊ห˜Pก้Yภe-—ใฬ,^ 0>๘g€‚ำฒถsš €๎ฃค€Kฑl/‚ฯ %8ะฐ–ส3C‹„s๋ g฿็„ใ5uิRย ุภ๏$’}k$’/ฦยŒD๒—.€w$?6š;€šy1PIฺT˜เฐๆ‰:@”L0ˆH งzถI I‰ ฎิya\ƒ†3ฯๅฬสnp`เฑŸZ)%j_ขk“X DW]ทc Œ f ˜4n$ๆZz@€๑ƒ-›ธญŸศDh฿k ` e`๘ึ0ภQ†o ทะม ส@#)รjญŸฝxZ€0ซญเใ `€0ณ฿ €สถอt ฐูd@iใ๎j€ฐำdg+งขœุI.่2xฦ0๘ฎ) P—”6{่H”ฮ‚ึ3€'๑1€฿ม4ซx+๋ƒํ5{ L!p๘ฯ.สXฬ6T—ย€X์จ ๘"l€+:pY-Qธโ๊`@ ใ7a…5฿ ภy/๘›น3ฆ#{ฐภO Lๅz‰yqตpWAŸE8ซ0ฆ)๐๚แtฉ:ฏา€;เ๓ox;ฃ๋X <&”ซ ฟ^ภ8เ€p3๐ู8เ€๘ฅrเ€p r๑/ nถ.†797๗wƒ ญ๐Ÿ๓ฯเโโhฏต1;aจŸˆ@q‚แลa+ถ PGต‹๊€apิฒnw^ฮเpร<@Œ^3ž - Zำ$6Pภmด์เฆj๚V/‰ฌเvU๐๐ญเ๘]\”f €@ ~€ท9UศิRขช6ulU €่L+@๘ช@^S@-ฃ €Iภ๔ฏ$y‰€wคœฆ๋P์, ฅd %-9Uจeค!ฑ์wŒจ็[ฌฐ˜“†<=ภ2iŠ—(ฌ‘–,H,‘ฎ๘Mi€mา“9 €ด%ุ‡ๅŽŸ๖%ธA๚r ฐCšโณ ภiฬ (u๔E €ำ20ภ;า• –จ‘ฮœ"๕๗n8ผ่‡ร๛พ ฏ“ฮœ…๑ƒ๓รอฑท9\ู๋๕๓ฑ๊k ฎ]R0 {ล?๓jณพwิ๋฿Hน๑฿M๘ช—• uY gๅมM9” <๗$พใE'น;‡ 'eฦ?่‚ั๋ภ‰ฺ}Kะ o1่ฆY&พQI"๑€ZN(^—™อ`ƒvF8^ ขLdำู่ฎ ณ€ยT`๘๖ิo|ดัณUu‚“‰๏๑Vณe๕;8อ ;pŸู€TCมโ๘๑ฃKhgฅ}@ a1@-ป‹@eGน*3๐qฒ*x7:\ผกแวoภึr…๐–ๆC€upY)|S็d`๒๘นbx!:ใa พชคj๘2ฦ“ฮี`~/๐œช ปป€๑›ฐ$ฦo ภพภ)ฐ/pลO€฿K฿&€}ฟ๙ษpว๎๘ํ„ฉฤc-ี›hฝ๚เvบ…R}pร D›y€pณ>บ๘Dย๗|ฏ*=?ฺŒq‹eœG^q๏จ7๖‚G‡ญอX;ฎิ๔€@pwQ\IุPxคhฅ}q“ำcน=!a ฌ‹>18o=๘แŒฤ๒ศ>ฮะŠKXi๗๓)K c๐ิ<†/บก g`โว7z๊ธJ์Nท 4A…จ(B7–_,+0_Nฆx%ญ@๔๖†๔$8Kฏพxแœ๔ไ๎,‘ˆฺฃว฿’๚,€ขศเ0‘ุ๊“๖œว๐ \?แฐGฺ3่N๕r*#ม:๎ณโไว•ํ`ตOฅล๏N<ฎถ2Q<a๐'(็6y ๐. D•„o3*=g‹d-Y< เ6#๑RฒHpI...........zr?JNฦBvs7ธ8ฺˆวo]ต{๛า ็‡ใทย•อฝฃ‹๑กต6$ฝ‚ ๆฅ1(cE ๚p้’4ๅ5-ี๏ทเเ;๊ๅม๊๗ฐJnหQ;Sปฃ’pQ0‚ๆทพ—RN‰w1ฅึuM„เผลЉzethลฉ‹ผnGฏ#แดป(ีeเSŠ๏ง่!่M}I=๘—G ํ๚๊-aฏแ+kkD=O/ฉ0ƒ๋ฯjJ๑vY๊}*ฬgZิบจ7ฦ ฮD๋JผOœ=อPZขKxใ Dk‹Ÿ‰—ลฺ)ช Nนฤ๘œcโ _{ข”‡$H˜˜฿€แ๋ฯ;PhŒเ+31‡)พฌๅ :ฺ 8Jมฅ—ร@๔วฆฃ—ค$ฝร„อๆ˜ฦrณัDํ๓.๓ภVถAuœก[หZงภต ๗Fฉ>ฎข šซptœื‰ี?แ?พฎ™ค™ฒ1IENDฎB`‚sparse-0.17.0/docs/assets/images/repair-fix-repairing-icon.png000066400000000000000000000126051501262445000242560ustar00rootroot00000000000000‰PNG  IHDR๖‚ๆSgAMAฑ asRGBฎฮ้PLTEคtRNS%T†ต>‰ IDATxฺํIw8€’าxฆฌ„gZฒyึุฯrœเ,ฎ'Lง฿๔เฉIb+ฌ2q้ืŽžฤ(ิ†*`ๅf a~ํ๑๊หoร?cลศฃr ฃJS่ค5๘;2kด_eา_†๑๘๙%ะwรไ(ฎŸถฆGฏ]Ÿ‡ู๑q๕:Ž1๐u›tๆจฏ๛ŠW qบZ“ฮ๕štศV"ใๆๆ&œI/มqP˜ฯŠฯm่ี :Z‰จอยšuจฆ.^ใ“๗์๙ 1 6:†เ๏™Q๓ะC‚Gƒิh%๕Fผฤำัณฟ*(๘h จ์โ้—ๅฏถ/eู็Qช ดวีฬH†!,๘Tš,:ํtt8H%฿ํต*ฦj >แ ฟŽUBงb/Zškณ)แP’ ึ?vฆ%ขถ€ฏ่ืC0๐5๚FลtรwV๘ถฒ FหwYญpV๔Œโุ์N@อPšPx๗หณ&ตU๖ใr๏วq'แF3ีเ?<_7ฟbฺภ๊๐“ฯชฎห๘,฿ขืmษwฺ‘แ#_ฑu1Oั@ ์f0L:+าm‘ฐ/-ต.?)ตuู,ฤWOq๔ี็m,พห˜ณ—์hIณC…ฃG์ฒ‚ ๓ูใ‰>y/ wฮ\G3้hฺ9๐ต7n}`EฯH์06|p์h&• _‡มฮf’YM๖0๑ู ๗wx3^๎Kไฑฎใ‡=ฟ฿๖)ใ฿ 9๚ญO์'…pw่฿ถŒๅฮL eมุw๚ู1ร(P?{ไำถบv้+ZZb™ษg3กสGM/๓,ฅ๚V%ช๒ซกRJ<~ศgcฟ๖#K๕M2t๛Lไ\:ฒˆNG ›‰™ๅ[Yอ]ฟถŒวฎภ่EธŽYฬYั“j>3†ีXJ฿eาอ9ึŽ๎xb< ๘qะใVพ|ทณW-๛:๕ฬฤ๊ฑfโJ~ืBi5mYซผศ๙…๛๗v๗ืฦ฿3ๅโมฃภŸ หฝžOผ4 vน๓#ฎ‚ณนˆร]๎K1า๑ึL๎ฦ’ˆๅœิ ˆ+“[Ÿvฺต.lG–~*ธ๐•ํบ๖ุาOnหฦƒ๕างิฮOล~•ศ~/GฅKณภJขZœ[/๓D–๖Ÿ ท:ย)ค ศ„‹<7รrO•ะ[lฑณupR‘ณึฅศNฉ TFfตผฐtd  )?ธ๘5่น ใWชUนมฦฏ`ฑ‡ฟฎ™”งฦz wh฿ฤiK–ด ‰WD`–>";๑k4gั๚Iqุฮrว\ืุจ๊ฑฟ–“`ฑn์ิŽšŸ1คฑำS–r+ม;ฺโฟ๓ฟš๚†๏\ถไๆึต+ฺ=6ฉ๔ฌคฎปE๎ถฒq ๛ฏ่l๙š™{๘หฝhpm็^็ภำ)ุ๕กm'๛ฟKฺ-|็๐€ฤึ๑ตG—ว^gNฏS้œhชํ\ยึkี7MDฤลAน9งฌ ํ๏‡Glพขฅ†๎&พ™ผฌ๐ำธธyฤ๖- รฤ๏˜๕Ÿแxึฤ฿VWk52๑๔บk‡O&…~๗5ฎ#%cกฟญ‚ผ„žมhัK(—๐ข๓2 Ÿ&lvsผ๖ |๗ฏ–ฐ_{l๒ึฎฬ๓{’?ทๆ ฯศแ๔0๑‰ •Eิ\™?œzฟ…>โ‹้ซ™0์\•yภยˆ“ฯ๗‚ำัeL|๋;เฯ…ŽNGก_่žฃำ‘y็ืยท.b๔k;ฯ4<ผ>1๑pมร+[B_x!<fก?{vงผ;๑ญ}ิFฆU่;Dฆ๚wโมถU5จŽu๗ๆ^ v”S4'๕ั-ผˆ8V๔๑’X๕O“ŠๆไXโแRฯ—ซ7ฬธŒี'๗ฦbFL\9D€์\สฒ฿:ฤ๎v๕ิ๚"ืษX˜๖_๛*?ร\’’Pษ4€™ำ๏๑ๅ>Mฅƒะ ่žง Ž.ํ%Ÿˆr+๕‰G/3ฉ}ไ.š)ฅv พ้ >+๚H๒็ˆศNผฟ'™ๆฒhTฒT=œM;อE รรeu#อ ›ววว-6ฤtXEEdšชr ๑บ€๎มGŒ๔ซ$|ญI๖Nbงคย ืŒ ”๛^S“[-นEŠ๕p…jb๓จ'Œle๋บL—_๓กฌ#[=Šพ“฿6zาcเZภNฆGแŠ,48<‚<ะvJัLฉ๕’5ไ;์ฆฉD3ฑึ ดdcภฐฃ๘ ๊ำeภ†รัฬ=๓(˜ศ@bฬE3๚€$๓=†ๆ8 #ั \฿๑ญฦ œไฌMG3ˆ–<ร=๋เษํx4คžิะf๔•*E4gkMB/๑%XืแO:ถ5:“ั _๊?ๅ kษ}`ญั ณภŸ!าŠ๔”ฤฆ#5]_+นำ™ž\gฃ)šQUwXกฐฎีดตัkŠfTี]ญPPyิxˆ<šิmayxm™ณัh†?๑ทq{3kภE†>ฮู„Gเyw‡ฬF๎;R€W๏้Œ‰wDพดูU@+3†9Cชkw…‰ญ–h ํ:‡BฏAxPyrแฎร?ƒ๛ฦํ*กฯ5(ปtฤ†‰_kxํ๋ั’ C่#€ฒc\™ h\Yน^ฯMอ๗ฆwทเฑ๖ฤ‘{/ …ณGžด[E๒Š~E>๑ฤง‡ฟ๕  ษme แKi๖ิ~C53แ›ฺ๋[ธRv=ุR4ะงY!บยีฺ ฮIไษY9๒ร่›š๒ๅุข๎O฿UŠชŠxาX[ชjY๔ข,ฑ•'งไŠ~7L ™๗ํ>–(ลQQลX{ํศศ5ข:๖น'ว†ฤ๒์w่•ๅ๒Š่Oฟ…n๖HVfฃ’ฉงเ์งy7โ-3gเ;ุฑYJ{ห๚อ7ฌ^สฃผญtจฉRฎ;๛Eคแ๎ภŽŽ*๔ฐูRRJูฺํ{M'4œ:ไยŒc:Rฅึu (็G๛ฬˆc‡> มฮ(8๋ฃ5ยžฆฝ•7ซ|กึงคข์กูย:gแชลฬ2{)4ํ?%hพฆอฆถ6ข*Tl‘ฌแ<ข†ซb2ซ์‘ุใ’Zวมk˜ทoูXeล.o%|ง~เwu`›์‰˜Y"ร‡†sจบ‘„:~hฯ๋ญ๙์๔sNย’๐ั…™7hmฮ๛šƒNูkะrงh๔';<>ดปf‡VO #๑ๆr๚?c[ธgงgบQ๖d้ฤำ@a"ž:ปgฤ]h8Zด sŒง‚ภฦ5{ทD๋Zžช๚5ฅœฐ7n๖LŽunZ?4y๛‘ำ ™า76แฯ๓MS›อณŽU%ำ)‘|t‡ˆu๖‰๕u=N;5O_๕ๆ.๘์o˜*[mg‹น฿ัb›์–๛ฑFืัูzR <_—Š…Qkึีrน–“dง๔":;T่Sั$˜่KN๕ฒทข์-ธฦ ฮพถลื๘ษV# ,ภŒ๕ฒ7ณ์๔^!ี/š๔ŽDู#ป์TYŸ`œŠษ—^๖šรNWm 2๏–‚๋ส2{ฌช์6ขช2fฯญฒS๋฿[ฅCฝ>ด4l์ฒ_v๗$iใ~ถXOKPl›}ญ|jI/ŒึำXƒlณGส!-zฆเŒ‘‹5ฑฬŽLŸSŠ๛ฮนe๖1\†_‰๗llณ—†{O$zb์นู.C$๓ฝถูSณMG‘ฬb*-ณoฬvฺล2_ป๑‚ฝ6sฟ:ๆ|ุ2๛ฺpใM$ำ F์ฒ'Œ ฏvฉ็Žฆ–ูท—>ˆรวNุว?ใูHง])~~ถ๖†fฑดwฺ!" Ÿ[e)๛ล›่#ไรว็}…Œ๔า็ยw„‡์ซฤแHธ[>uษžะKฟิ๓แ#—์ศLKu.ฺฌU:cงws๓g`อ๑>ถฯ^ฯ˜™_Oz=ก๙•1v2ใฯำn`Eh&~”œaงหฉcฐ'ฐVJQฆฦ๐ฤืs6คaฐวฆ›R0)˜˜bOจ๑พ๑3หสtุภผู‰ฏgพธว vdฌซฌL ฦฆุc*n์์Xชฐ:๑น!๖Utศ&œศ›ฝต:๑H;;lvbkโ[†3`…=อA5ุZ๑ฤณ?๎*;์ƒ U_ ~ณ‚๘M {มc?iv๎€mQํ๘@}eŠฝต|]ร=›4Xป๚&ษ^R๖หฟ3ดŸGFjgโdšแธบƒณำoJอำ้HƒชมOi†ฏขJ=›๛Kฉ7k$ ธ๏…*…“‹W‰J{u๑ฤY๐7‡รa/f˜KDIท„ฃ๊(•อ\œฝโฐS?c๒Rz:{ พD‹ุ฿ฏ—๑b‰._ษZ;#kMมๅ/f'R–ํ฿cšfšฅ-์TฏOถึขgยji๔ห๑w‡รแ๑f”n˜r„๐j Y๏„ฒฯeญ#บq']œ๖บว์cuท–๖mVc๖ูฌ๕-ฃk๋งคSeซปตlืฑRนี…nฝฅห\ ~ H‡ั๕>'Keฝ8“]ŽD฿”แ‘rlLู›๙ —ˆฑำYอDสRVทTมAฎจ.ีl1egว}ต่ทœ˜–ฑI&า‰|ก?ฉฑทฃฟ+ฑ5›๛ ‘์ีSH„ะŸีJ—:nถฃx คแ =˜}œพY@ยœงม6๊ˆf#๊3์ๅ W๊#ภ#’ูท[icoA๙๒ŽmQฤjVGฦPvชI… ‹ูSลGd‡จน ๖๎๔xข~%Q:ŸM@บุ;7ฝ‡ิูืŒ@เม๛Z๛พHฉœ๚ŽYTฉŸ=tO|ชกX/-ฒ'|v๘ฤวู๊_fฌ’๋fงO W๕๐๒อ’ๅB โ๛pY๐๊~ฬxzเzฏม์ญAฏ07์W[j๐๋ฮ๐BฯŒOะช7ก4์ฎ[f„๓H [dR{Nฟฦ๓ N*ฯวซ5๘f๖ ์Cม%ศ๛ุต๖ี=]‡0๖šรCบฮ:์ซํJ {ฯ3'-h?ำฬ/่—9๓็ ชล?yฬฮ?ใ+U%ท^ณท์ู๋๕ ™vx0‡ชก–้ี^ณฬ้ฐhง๓•Q}žฬ;<ž—(ž ฒร ฺฑั›ธ[Ÿ็}(ฬ^ิ‰}ž๗ณู™Oณึ์žฝ1{|3๛ฐ'๗์๖h^่5Žื๛€อj๚bฦ๓้Vฐgf…พž‘าฃ์GณB฿O`ท+ุkรw๏Œƒk๖feW่้pฯพ‡ผ๓˜}X†•)5™9šนrษ๗่a™+ูฆ {ภ๙งi>‰fhๆ๊่œฝหh“‹Oฏ‰ณศฮฏ* žZ›์^TาV€xฦฤ๏๘>rnBuํ2’๙hSฮ˜kืปb็ืบญŒ bรตS)F•q๛›zฅh%2ฮžุˆgิท*‘ปvŠ%e7žQLฬDฆอ๏ฺB<ฃฃ>P๖`…ž(๎6"B๕Aจ๑ฬ 7ฉ%†ู7ๆใ๕{e–=2๏ฺฉง"cสf<ƒ้๗’ฎŠ ใ™‘J({ฉ๊ฒVQ๖ใ™’}OŽFูsรiมTKDีPˆฉ๊5xท-\ืneตเฺฬ๐•ฆชs†‰s>"ณB_26ส่+;ฃ๗›ฎ=`กงชS๎e'ืฯ”€๗ะใ@?L่ฉ๊ะzช:ัฺV้M h =U]rูOWฯภY&Aงช้*๚ ž 3UMU]–ีv๎Ko่"ZลvZ๗ฅ7๔nฆาซฏาตC”.L|๑L๏าะuช)‚ลoฅ๏3_Joฐๆพรพฤ3Xo“฿bo\;˜V๓k๘Žwย๛_็ˆช y฿๚‘ฑไ–๐ซLvไ’ผ฿๛ตEqึa, ึญ๔”{N}m8้y{xM๚฿๏{ํา—ืฦYƒ๛Pฎu–๏uŸป้ƒ๎ธcำญ‚เIซจ… mTปวำ(พ„&{;7 tฺู๋7ฐi+รy:qlฺ๛U€#‚x&จ Wไั๎p๘ว<FI˜>๛ ะซA๋hqH๖M๓hŠcด, —๎ซN|48 ๖ตNๆภ\œ๛๙ j๙ภ|b‚ฝ๙ย์ํยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ยพฐ/์ ๛ย๎i๋?อาํ๛HMฐC?๊ใ๚๛@ะ;g4๎}8…ย…;ํ๐q๎)'๐qGดฮบE๗_ะ™ƒ๐โ/นIENDฎB`‚sparse-0.17.0/docs/changelog.md000066400000000000000000000626341501262445000163070ustar00rootroot00000000000000# Changelog 0.15.1 / 2024-01-10 ------------------- * Fix regression where with XArray by supporting all API functions via the Array API standard. (PR [#622](https://github.com/pydata/sparse/pull/622) thanks [@hameerabbasi](https://github.com/hameerabbasi)) 0.15.0 / 2024-01-09 ------------------- * Fix regression where [`DeprecationWarning`][]s were being fired unexpectedly. (PR [#581](https://github.com/pydata/sparse/pull/581) thanks [@hameerabbasi](https://github.com/hameerabbasi)) * Extended [`sparse.einsum`][] support (PR [#579](https://github.com/pydata/sparse/pull/579) thanks [@HadrienNU](https://github.com/HadrienNU)) * General code clean-up (PR [#586](https://github.com/pydata/sparse/pull/586) thanks [@MHRasmy](https://github.com/MHRasmy), PR [#598](https://github.com/pydata/sparse/pull/598) thanks [@jamestwebber](https://github.com/jamestwebber)) * Bug fixes with respect to NumPy compatibility (PR [#598](https://github.com/pydata/sparse/pull/598) thanks [@hameerabbasi](https://github.com/hameerabbasi), PR [#609](https://github.com/pydata/sparse/pull/609) thanks [@Illviljan](https://github.com/Illviljan), PR [#620](https://github.com/pydata/sparse/pull/620) thanks [@mtsokol](https://github.com/mtsokol)) * Bug fixes with respect to GCXS (PR [#611](https://github.com/pydata/sparse/pull/611) thanks [@EuGig](https://github.com/EuGig), PR [#601](https://github.com/pydata/sparse/pull/601) thanks [@jamestwebber](https://github.com/jamestwebber)) * `Array API standard `_ support (PR [#612](https://github.com/pydata/sparse/pull/612), PR [#613](https://github.com/pydata/sparse/pull/613), PR [#614](https://github.com/pydata/sparse/pull/614), PR [#615](https://github.com/pydata/sparse/pull/615), PR [#619](https://github.com/pydata/sparse/pull/619), PR [#620](https://github.com/pydata/sparse/pull/620) thanks [@mtsokol](https://github.com/mtsokol)) * ``matrepr`` support for display of sparse data (PR [#605](https://github.com/pydata/sparse/pull/605), PR [#606](https://github.com/pydata/sparse/pull/606) thanks [@alugowski](https://github.com/alugowski)). * Larger code clean-up with Ruff formatter and linter (PR [#617](https://github.com/pydata/sparse/pull/617), PR [#621](https://github.com/pydata/sparse/pull/621) thanks [@hameerabbasi](https://github.com/hameerabbasi)) * Packaging and maintenance (PR [#616](https://github.com/pydata/sparse/pull/616), :commit:`b5954e68d3d6e35a62f7401d1d4fb84ea04414dd`, :commit:`dda93d3ea9521881c721c3ba875c769c9c5a79d4` thanks [@hameerabbasi](https://github.com/hameerabbasi)) 0.14.0 / 2023-02-24 ------------------- * [`sparse.einsum`][] support (PR [#564](https://github.com/pydata/sparse/pull/564) thanks [@jcmgray](https://github.com/jcmgray)) * Some bug-fixes (PR [#524](https://github.com/pydata/sparse/pull/524), PR [#527](https://github.com/pydata/sparse/pull/527), PR [#555](https://github.com/pydata/sparse/pull/555) thanks [@hameerabbasi](https://github.com/hameerabbasi), PR [#569](https://github.com/pydata/sparse/pull/569), thanks [@jamestwebber](https://github.com/jamestwebber), PR [#534](https://github.com/pydata/sparse/pull/534), thanks [@sarveshbhatnagar](https://github.com/sarveshbhatnagar)) * Some performance improvements (PR [#570](https://github.com/pydata/sparse/pull/570), thanks [@jamestwebber](https://github.com/jamestwebber), PR [#540](https://github.com/pydata/sparse/pull/540), thanks [@smldub](https://github.com/smldub)). * Miscellaneous maintenance fixes. 0.13.0 / 2021-08-28 ------------------- * [`sparse.GCXS`][] improvements and changes. (PR [#448](https://github.com/pydata/sparse/pull/448), PR [#450](https://github.com/pydata/sparse/pull/450), PR [#455](https://github.com/pydata/sparse/pull/455), thanks [@sayandip18](https://github.com/sayandip18)). * Maintainence fixes (PR [#462](https://github.com/pydata/sparse/pull/462), PR [#466](https://github.com/pydata/sparse/pull/466), :commit:`1ccb85da581be65a0345b399e00fd3c325700d95`, :commit:`5547b4e92dc8d61492e9dc10ba00175c1a6637fa` :commit:`00c0e5514de2aab8b9a0be16b5da470b091d9eb9`, :commit:`fcd3020dd08c7022a44f709173fe23969d3e8f7c`, thanks [@hameerabbasi](https://github.com/hameerabbasi)) * [`sparse.DOK.from_scipy_sparse`][] method (PR [#464](https://github.com/pydata/sparse/pull/464), Issue [#463](https://github.com/pydata/sparse/issues/463), thanks [@hameerabbasi](https://github.com/hameerabbasi)). * Black re-formatting (PR [#471](https://github.com/pydata/sparse/pull/471), PR [#484](https://github.com/pydata/sparse/pull/484), thanks [@GenevieveBuckley](https://github.com/GenevieveBuckley), [@sayandip18](https://github.com/sayandip18)) * Add [`sparse.pad`][] (PR [#474](https://github.com/pydata/sparse/pull/474), Issue [#438](https://github.com/pydata/sparse/issues/438), thanks [@H4R5H1T-007](https://github.com/H4R5H1T-007)) * Switch to GitHub Actions (:compare:`5547b4e92dc8d61492e9dc10ba00175c1a6637fa..a332f22c96a96e5ab9b4384342df67e8f3966f85`) * Fix a number of bugs in format conversion. (PR [#504](https://github.com/pydata/sparse/pull/504), Issue [#503](https://github.com/pydata/sparse/issues/503), thanks [@hameerabbasi](https://github.com/hameerabbasi)) * Fix bug in [`sparse.matmul`][] for higher-dimensional arrays. (PR [#508](https://github.com/pydata/sparse/pull/508), Issue [#506](https://github.com/pydata/sparse/issues/506), thanks [@sayandip18](https://github.com/sayandip18)). * Fix scalar conversion to COO (Issue [#510](https://github.com/pydata/sparse/issues/510), PR [#511](https://github.com/pydata/sparse/pull/511), thanks [@hameerabbasi](https://github.com/hameerabbasi)) * Fix OOB memory accesses (Issue [#515](https://github.com/pydata/sparse/issues/515), :commit:`1e24a7e29786e888dee4c02153309986ae4b5dde` thanks [@hameerabbasi](https://github.com/hameerabbasi)) * Fixes element-wise ops with scalar COO array. (Issue [#505](https://github.com/pydata/sparse/issues/505), :commit:`5211441ec685233657ab7156f99eb67e660cee86`, thanks [@hameerabbasi](https://github.com/hameerabbasi)) * Fix scalar broadcast_to with ``nnz==0``. (Issue [#513](https://github.com/pydata/sparse/issues/513), :commit:`bfabaa0805e811884e79c4bdbfd14316986d65e4`, thanks [@hameerabbasi](https://github.com/hameerabbasi)) * Add order parameter to ``{zero, ones, full}[_like]``. (Issue [#514](https://github.com/pydata/sparse/issues/514), :commit:`37de1d0141c4375962ecdf18337c2dd0f667b60c`, thanks [@hameerabbasi](https://github.com/hameerabbasi)) * Fix tensordot typing bugs. (Issue [#493](https://github.com/pydata/sparse/issues/493), Issue [#499](https://github.com/pydata/sparse/issues/499), :commit:`37de1d0141c4375962ecdf18337c2dd0f667b60c`, thanks [@hameerabbasi](https://github.com/hameerabbasi)). 0.12.0 / 2021-03-19 ------------------- There are a number of large changes in this release. For example, we have implemented the [`sparse.GCXS`][] type, and its specializations `CSR` and `CSC`. We plan on gradually improving the performance of these. * A number of [`sparse.GCXS`][] fixes and additions (PR [#409](https://github.com/pydata/sparse/pull/409), PR [#407](https://github.com/pydata/sparse/pull/407), PR [#414](https://github.com/pydata/sparse/pull/414), PR [#417](https://github.com/pydata/sparse/pull/417), PR [#419](https://github.com/pydata/sparse/pull/419) thanks [@daletovar](https://github.com/daletovar)) * Ability to change the index dtype for better storage characteristics. (PR [#441](https://github.com/pydata/sparse/pull/441), thanks [@daletovar](https://github.com/daletovar)) * Some work on [`sparse.DOK`][] arrays to bring them closer to the other formats (PR [#435](https://github.com/pydata/sparse/pull/435), PR [#437](https://github.com/pydata/sparse/pull/437), PR [#439](https://github.com/pydata/sparse/pull/439), PR [#440](https://github.com/pydata/sparse/pull/440), thanks [@DragaDoncila](https://github.com/DragaDoncila)) * `CSR` and `CSC` specializations of [`sparse.GCXS`][] (PR [#442](https://github.com/pydata/sparse/pull/442), thanks [@ivirshup](https://github.com/ivirshup)) For now, this is experimental undocumented API, and subject to change. * Fix a number of bugs (PR [#407](https://github.com/pydata/sparse/pull/407), Issue [#406](https://github.com/pydata/sparse/issues/406)) * Add `nnz` parameter to [`sparse.random`][] (PR [#410](https://github.com/pydata/sparse/pull/410), thanks [@emilmelnikov](https://github.com/emilmelnikov)) 0.11.2 / 2020-09-04 ------------------- * Fix `TypingError` on [`sparse.dot`][] with complex dtypes. (Issue [#403](https://github.com/pydata/sparse/issues/403), PR [#404](https://github.com/pydata/sparse/pull/404)) 0.11.1 / 2020-08-31 ------------------- * Fix [`ValueError`][] on [`sparse.dot`][] with extremely small values. (Issue [#398](https://github.com/pydata/sparse/issues/398), PR [#399](https://github.com/pydata/sparse/pull/399)) 0.11.0 / 2020-08-18 ------------------- * Improve the performance of [`sparse.dot`][]. (Issue [#331](https://github.com/pydata/sparse/issues/331), PR [#389](https://github.com/pydata/sparse/pull/389), thanks [@daletovar](https://github.com/daletovar)) * Added the [`sparse.COO.swapaxes`][] method. (PR [#344](https://github.com/pydata/sparse/pull/344), thanks [@lueckem](https://github.com/lueckem)) * Added multi-axis 1-D indexing support. (PR [#343](https://github.com/pydata/sparse/pull/343), thanks [@mikeymezher](https://github.com/mikeymezher)) * Fix `outer` for arrays that weren't one-dimensional. (Issue [#346](https://github.com/pydata/sparse/issues/346), PR [#347](https://github.com/pydata/sparse/pull/347)) * Add `casting` kwarg to [`sparse.COO.astype`][]. (Issue [#391](https://github.com/pydata/sparse/issues/391), PR [#392](https://github.com/pydata/sparse/pull/392)) * Fix for [`sparse.COO`][] constructor accepting invalid inputs. (Issue [#385](https://github.com/pydata/sparse/issues/385), PR [#386](https://github.com/pydata/sparse/pull/386)) 0.10.0 / 2020-05-13 ------------------- * Fixed a bug where converting an empty DOK array to COO leads to an incorrect dtype. (Issue [#314](https://github.com/pydata/sparse/issues/314), PR [#315](https://github.com/pydata/sparse/pull/315)) * Change code formatter to black. (PR [#284](https://github.com/pydata/sparse/pull/284)) * Add [`sparse.COO.flatten`] and `outer`. (Issue [#316](https://github.com/pydata/sparse/issues/316), PR [#317](https://github.com/pydata/sparse/pull/317)). * Remove broadcasting restriction between sparse arrays and dense arrays. (Issue [#306](https://github.com/pydata/sparse/issues/306), PR [#318](https://github.com/pydata/sparse/pull/318)) * Implement deterministic dask tokenization. (Issue [#300](https://github.com/pydata/sparse/issues/300), PR [#320](https://github.com/pydata/sparse/pull/320), thanks [@danielballan](https://github.com/danielballan)) * Improve testing around densification (PR [#321](https://github.com/pydata/sparse/pull/321), thanks [@danielballan](https://github.com/danielballan)) * Simplify Numba extension. (PR [#324](https://github.com/pydata/sparse/pull/324), thanks [@eric-wieser](https://github.com/eric-wieser)). * Respect ``copy=False`` in ``astype`` (PR [#328](https://github.com/pydata/sparse/pull/328), thanks [@eric-wieser](https://github.com/eric-wieser)). * Replace linear_loc with ravel_multi_index, which is 3x faster. (PR [#330](https://github.com/pydata/sparse/pull/330), thanks [@eric-wieser](https://github.com/eric-wieser)). * Add error msg to tensordot operation when ``ndim==0`` (Issue [#332](https://github.com/pydata/sparse/issues/332), PR [#333](https://github.com/pydata/sparse/pull/333), thanks [@guilhermeleobas](https://github.com/guilhermeleobas)). * Maintainence fixes for Sphinx 3.0 and Numba 0.49, and dropping support for Python 3.5. (PR [#337](https://github.com/pydata/sparse/pull/337)). * Fixed signature for [numpy.clip][]. 0.9.1 / 2020-01-23 ------------------ * Fixed a bug where indexing with an empty list could lead to issues. (Issue [#281](https://github.com/pydata/sparse/issues/281), PR [#282](https://github.com/pydata/sparse/pull/282)) * Change code formatter to black. (PR [#284](https://github.com/pydata/sparse/pull/284)) * Add the [`sparse.diagonal`][] and [`sparse.diagonalize`][] functions. (Issue [#288](https://github.com/pydata/sparse/issues/288), PR [#289](https://github.com/pydata/sparse/pull/289), thanks [@pettni](https://github.com/pettni)) * Add HTML repr for notebooks. (PR [#283](https://github.com/pydata/sparse/pull/283), thanks [@daletovar](https://github.com/daletovar)) * Avoid making copy of ``coords`` when making a new [`sparse.COO`][] array. * Add stack and concatenate for GCXS. (Issue [#301](https://github.com/pydata/sparse/issues/301), PR [#303](https://github.com/pydata/sparse/pull/303), thanks [@daletovar](https://github.com/daletovar)). * Fix issue where functions dispatching to an attribute access wouldn't work with ``__array_function__``. (Issue [#308](https://github.com/pydata/sparse/issues/308), PR [#309](https://github.com/pydata/sparse/pull/309)). * Add partial support for constructing and mirroring [`sparse.COO`][] objects to Numba. 0.8.0 / 2019-08-26 ------------------ This release switches to Numba's new typed lists, a lot of back-end work with the CI infrastructure, so Linux, macOS and Windows are officially tested. It also includes bug fixes. It also adds in-progress, not yet public support for the GCXS format, which is a generalisation of CSR/CSC. (huge thanks to [@daletovar](https://github.com/daletovar)) * Fixed a bug where an array with size == 1 and nnz == 0 could not be broadcast. (Issue [#242](https://github.com/pydata/sparse/issues/242), PR [#243](https://github.com/pydata/sparse/pull/243)) * Add ``std`` and ``var``. (PR [#244](https://github.com/pydata/sparse/pull/244)) * Move to Azure Pipelines with CI for Windows, macOS and Linux. (PR [#245](https://github.com/pydata/sparse/pull/245), PR [#246](https://github.com/pydata/sparse/pull/246), PR [#247](https://github.com/pydata/sparse/pull/247), PR [#248](https://github.com/pydata/sparse/pull/248)) * Add ``resize``, and change ``reshape`` so it raises a ``ValueError`` on shapes that don't correspond to the same size. (Issue [#241](https://github.com/pydata/sparse/issues/241), Issue [#250](https://github.com/pydata/sparse/issues/250), PR [#256](https://github.com/pydata/sparse/pull/256) thanks, [@daletovar](https://github.com/daletovar)) * Add ``isposinf`` and ``isneginf``. (Issue [#252](https://github.com/pydata/sparse/issues/252), PR [#253](https://github.com/pydata/sparse/pull/253)) * Fix ``tensordot`` when nnz = 0. (Issue [#255](https://github.com/pydata/sparse/issues/255), PR [#256](https://github.com/pydata/sparse/pull/256)) * Modifications to ``__array_function__`` to allow for sparse XArrays. (PR [#261](https://github.com/pydata/sparse/pull/261), thanks [@nvictus](https://github.com/nvictus)) * Add not-yet-public support for GCXS. (PR [#258](https://github.com/pydata/sparse/pull/258), thanks [@daletovar](https://github.com/daletovar)) * Improvements to ``__array_function__``. (PR [#267](https://github.com/pydata/sparse/pull/267), PR [#272](https://github.com/pydata/sparse/pull/272), thanks [@crusaderky](https://github.com/crusaderky)) * Convert all Numba lists to typed lists. (PR [#264](https://github.com/pydata/sparse/pull/264)) * Why write code when it exists elsewhere? (PR [#277](https://github.com/pydata/sparse/pull/277)) * Fix some element-wise operations with scalars. (PR [#278](https://github.com/pydata/sparse/pull/278)) * Private modules should be private, and tests should be in the package. (PR [#280](https://github.com/pydata/sparse/pull/280)) 0.7.0 / 2019-03-14 ------------------ This is a release that adds compatibility with NumPy's new ``__array_function__`` protocol, for details refer to `NEP-18 `_. The other big change is that we dropped compatibility with Python 2. Users on Python 2 should use version 0.6.0. There are also some bug-fixes relating to fill-values. This was mainly a contributor-driven release. The full list of changes can be found below: * Fixed a bug where going between [`sparse.DOK`][] and [`sparse.COO`][] caused fill-values to be lost. (Issue [#225](https://github.com/pydata/sparse/issues/225), PR [#226](https://github.com/pydata/sparse/pull/226)). * Fixed warning for a matrix that was incorrectly considered too dense. (Issue [#228](https://github.com/pydata/sparse/issues/228), PR [#229](https://github.com/pydata/sparse/pull/229)) * Fixed some warnings in Python 3.7, the fix was needed. in preparation for Python 3.8. (PR [#233](https://github.com/pydata/sparse/pull/233), thanks [@nils-werner](https://github.com/nils-werner)) * Drop support for Python 2.7 (Issue [#234](https://github.com/pydata/sparse/issues/234), PR [#235](https://github.com/pydata/sparse/pull/235), thanks [@hugovk](https://github.com/hugovk)) * Clearer error messages (Issue [#230](https://github.com/pydata/sparse/issues/230), Issue [#231](https://github.com/pydata/sparse/issues/231), PR [#232](https://github.com/pydata/sparse/pull/232)) * Restructure requirements.txt files. (PR [#236](https://github.com/pydata/sparse/pull/236)) * Support fill-value in reductions in specific cases. (Issue [#237](https://github.com/pydata/sparse/issues/237), PR [#238](https://github.com/pydata/sparse/pull/238)) * Add ``__array_function__`` support. (PR [#239](https://github.com/pydata/sparse/pull/239), thanks, [@pentschev](https://github.com/pentschev)) * Cleaner code! (PR [#240](https://github.com/pydata/sparse/pull/240)) 0.6.0 / 2018-12-19 ------------------ This release breaks backward-compatibility. Previously, if arrays were fed into NumPy functions, an attempt would be made to densify the array and apply the NumPy function. This was unintended behaviour in most cases, with the array filling up memory before raising a ``MemoryError`` if the array was too large. We have now changed this behaviour so that a ``RuntimeError`` is now raised if an attempt is made to automatically densify an array. To densify, use the explicit ``.todense()`` method. * Fixed a bug where ``np.matrix`` could sometimes fail to convert to a ``COO``. (Issue [#199](https://github.com/pydata/sparse/issues/199), PR [#200](https://github.com/pydata/sparse/pull/200)). * Make sure that ``sparse @ sparse`` returns a sparse array. (Issue [#201](https://github.com/pydata/sparse/issues/201), PR [#203](https://github.com/pydata/sparse/pull/203)) * Bring ``operator.matmul`` behaviour in line with NumPy for ``ndim > 2``. (Issue [#202](https://github.com/pydata/sparse/issues/202), PR [#204](https://github.com/pydata/sparse/pull/204), PR [#217](https://github.com/pydata/sparse/pull/217)) * Make sure ``dtype`` is preserved with the ``out`` kwarg. (Issue [#205](https://github.com/pydata/sparse/issues/205), PR [#206](https://github.com/pydata/sparse/pull/206)) * Fix integer overflow in ``reduce`` on Windows. (Issue [#207](https://github.com/pydata/sparse/issues/207), PR [#208](https://github.com/pydata/sparse/pull/208)) * Disallow auto-densification. (Issue [#218](https://github.com/pydata/sparse/issues/218), PR [#220](https://github.com/pydata/sparse/pull/220)) * Add auto-densification configuration, and a configurable warning for checking if the array is too dense. (PR [#210](https://github.com/pydata/sparse/pull/210), PR [#213](https://github.com/pydata/sparse/pull/213)) * Add pruning of fill-values to COO constructor. (PR [#221](https://github.com/pydata/sparse/pull/221)) 0.5.0 / 2018-10-12 ------------------ * Added `COO.real`, `COO.imag`, and `COO.conj` (PR [#196](https://github.com/pydata/sparse/pull/196)). * Added `sparse.kron` function (PR [#194](https://github.com/pydata/sparse/pull/194), PR [#195](https://github.com/pydata/sparse/pull/195)). * Added `order` parameter to `COO.reshape` to make it work with `np.reshape` (PR [#193](https://github.com/pydata/sparse/pull/193)). * Added `COO.mean` and `sparse.nanmean` (PR [#190](https://github.com/pydata/sparse/pull/190)). * Added `sparse.full` and `sparse.full_like` (PR [#189](https://github.com/pydata/sparse/pull/189)). * Added `COO.clip` method (PR [#185](https://github.com/pydata/sparse/pull/185)). * Added `COO.copy` method, and changed pickle of `COO` to not include its cache (PR [#184](https://github.com/pydata/sparse/pull/184)). * Added `sparse.eye`, `sparse.zeros`, `sparse.zeros_like`, `sparse.ones`, and `sparse.ones_like` (PR [#183](https://github.com/pydata/sparse/pull/183)). 0.4.1 / 2018-09-12 ------------------ * Allow mixed `ndarray`-`COO` operations if the result is sparse (Issue [#124](https://github.com/pydata/sparse/issues/124), via PR [#182](https://github.com/pydata/sparse/pull/182)). * Allow specifying a fill-value when converting from NumPy arrays (Issue [#179](https://github.com/pydata/sparse/issues/179), via PR [#180](https://github.com/pydata/sparse/pull/180)). * Added `COO.any` and `COO.all` methods (PR [#175](https://github.com/pydata/sparse/pull/175)). * Indexing for `COO` now accepts a single one-dimensional array index (PR [#172](https://github.com/pydata/sparse/pull/172)). * The fill-value can now be something other than zero or `False` (PR [#165](https://github.com/pydata/sparse/pull/165)). * Added a `sparse.roll` function (PR [#160](https://github.com/pydata/sparse/pull/160)). * Numba code now releases the GIL. This leads to better multi-threaded performance in Dask (PR [#159](https://github.com/pydata/sparse/pull/159)). * A number of bugs occurred, so to resolve them, `COO.coords.dtype` is always `np.int64`. `COO`, therefore, uses more memory than before (PR [#158](https://github.com/pydata/sparse/pull/158)). * Add support for saving and loading `COO` files from disk (Issue [#153](https://github.com/pydata/sparse/issues/153), via PR [#154](https://github.com/pydata/sparse/pull/154)). * Support `COO.nonzero` and `np.argwhere` (Issue [#145](https://github.com/pydata/sparse/issues/145), via PR [#148](https://github.com/pydata/sparse/pull/148)). * Allow faux in-place operations (Issue [#80](https://github.com/pydata/sparse/issues/80), via PR [#146](https://github.com/pydata/sparse/pull/146)). * `COO` is now always canonical (PR [#141](https://github.com/pydata/sparse/pull/141)). * Improve indexing performance (PR [#128](https://github.com/pydata/sparse/pull/128)). * Improve element-wise performance (PR [#127](https://github.com/pydata/sparse/pull/127)). * Reductions now support a negative axis (Issue [#117](https://github.com/pydata/sparse/issues/117), via PR [#118](https://github.com/pydata/sparse/pull/118)). * Match behaviour of `ufunc.reduce` from NumPy (Issue [#107](https://github.com/pydata/sparse/issues/107), via PR [#108](https://github.com/pydata/sparse/pull/108)). 0.3.1 / 2018-04-12 ------------------ * Fix packaging error (PR [#138](https://github.com/pydata/sparse/pull/138)). 0.3.0 / 2018-02-22 ------------------ * Add NaN-skipping aggregations (PR [#102](https://github.com/pydata/sparse/pull/102)). * Add equivalent to `np.where` (PR [#102](https://github.com/pydata/sparse/pull/102)). * N-input universal functions now work (PR [#98](https://github.com/pydata/sparse/pull/98)). * Make `dot` more consistent with NumPy (PR [#96](https://github.com/pydata/sparse/pull/96)). * Create a base class `SparseArray` (PR [#92](https://github.com/pydata/sparse/pull/92)). * Minimum NumPy version is now 1.13 (PR [#90](https://github.com/pydata/sparse/pull/90)). * Fix a bug where setting a `DOK` element to zero did nothing (Issue [#93](https://github.com/pydata/sparse/issues/93), via PR [#94](https://github.com/pydata/sparse/pull/94)). 0.2.0 / 2018-01-25 ------------------ * Support faster `np.array(COO)` (PR [#87](https://github.com/pydata/sparse/pull/87)). * Add `DOK` type (PR [#85](https://github.com/pydata/sparse/pull/85)). * Fix sum for large arrays (Issue [#82](https://github.com/pydata/sparse/issues/82), via PR [#83](https://github.com/pydata/sparse/pull/83)). * Support `.size` and `.density` (PR [#69](https://github.com/pydata/sparse/pull/69)). * Documentation added for the package (PR [#43](https://github.com/pydata/sparse/pull/43)). * Minimum required SciPy version is now 0.19 (PR [#70](https://github.com/pydata/sparse/pull/70)). * `len(COO)` now works (PR [#68](https://github.com/pydata/sparse/pull/68)). * `scalar op COO` now works for all operators (PR [#67](https://github.com/pydata/sparse/pull/67)). * Validate axes for `.transpose()` (PR [#61](https://github.com/pydata/sparse/pull/61)). * Extend indexing support (PR [#57](https://github.com/pydata/sparse/pull/57)). * Add `random` function for generating random sparse arrays (PR [#41](https://github.com/pydata/sparse/pull/41)). * `COO(COO)` now copies the original object (PR [#55](https://github.com/pydata/sparse/pull/55)). * NumPy universal functions and reductions now work on `COO` arrays (PR [#49](https://github.com/pydata/sparse/pull/49)). * Fix concatenate and stack for large arrays (Issue [#32](https://github.com/pydata/sparse/issues/32), via PR [#51](https://github.com/pydata/sparse/pull/51)). * Fix `nnz` for scalars (Issue [#47](https://github.com/pydata/sparse/issues/47), via PR [#48](https://github.com/pydata/sparse/pull/48)). * Support more operators and remove all special cases (PR [#46](https://github.com/pydata/sparse/pull/46)). * Add support for `triu` and `tril` (PR [#40](https://github.com/pydata/sparse/pull/40)). * Add support for Ellipsis (`...`) and `None` when indexing (PR [#37](https://github.com/pydata/sparse/pull/37)). * Add support for bitwise bindary operations like `&` and `|` (PR [#38](https://github.com/pydata/sparse/pull/38)). * Support broadcasting in element-wise operations (PR [#35](https://github.com/pydata/sparse/pull/35)). sparse-0.17.0/docs/completed-tasks.md000066400000000000000000000000561501262445000174450ustar00rootroot00000000000000[Completed tasks](roadmap.md#completed-tasks) sparse-0.17.0/docs/conduct.md000066400000000000000000000124371501262445000160130ustar00rootroot00000000000000# Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders 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, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [hameerabbasi@yahoo.com](mailto:hameerabbasi@yahoo.com). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code\_of\_conduct.html](https://www.contributor-covenant.org/version/2/0/code\_of\_conduct.html). Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/inclusion). For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). sparse-0.17.0/docs/construct.md000066400000000000000000000165601501262445000164010ustar00rootroot00000000000000# Construct Sparse Arrays ## From coordinates and data You can construct [`sparse.COO`][] arrays from coordinates and value data. The `cords` parameter contains the indices where the data is nonzero, and the `data` parameter contains the data corresponding to those indices. For example, the following code will generate a $5 \times 5$ diagonal matrix: ```python >>> import sparse >>> coords = [[0, 1, 2, 3, 4], ... [0, 1, 2, 3, 4]] >>> data = [10, 20, 30, 40, 50] >>> s = sparse.COO(coords, data, shape=(5, 5)) >>> s 0 1 2 3 4 โ”Œ โ” 0 โ”‚ 10 โ”‚ 1 โ”‚ 20 โ”‚ 2 โ”‚ 30 โ”‚ 3 โ”‚ 40 โ”‚ 4 โ”‚ 50 โ”‚ โ”” โ”˜ ``` In general `coords` should be a `(ndim, nnz)` shaped array. Each row of `coords` contains one dimension of the desired sparse array, and each column contains the index corresponding to that nonzero element. `data` contains the nonzero elements of the array corresponding to the indices in `coords`. Its shape should be `(nnz,)`. If `data` is the same across all the coordinates, it can be passed in as a scalar. For example, the following produces the $4 \times 4$ identity matrix: ```python >>> import sparse >>> coords = [[0, 1, 2, 3], ... [0, 1, 2, 3]] >>> data = 1 >>> s = sparse.COO(coords, data, shape=(4, 4)) >>> s 0 1 2 3 โ”Œ โ” 0 โ”‚ 1 โ”‚ 1 โ”‚ 1 โ”‚ 2 โ”‚ 1 โ”‚ 3 โ”‚ 1 โ”‚ โ”” โ”˜ ``` You can, and should, pass in [`numpy.ndarray`][] objects for `coords` and `data`. In this case, the shape of the resulting array was determined from the maximum index in each dimension. If the array extends beyond the maximum index in `coords`, you should supply a shape explicitly. For example, if we did the following without the `shape` keyword argument, it would result in a $4 \times 5$ matrix, but maybe we wanted one that was actually $5 \times 5$. ```python >>> coords = [[0, 3, 2, 1], [4, 1, 2, 0]] >>> data = [1, 4, 2, 1] >>> s = COO(coords, data, shape=(5, 5)) >>> s 0 1 2 3 4 โ”Œ โ” 0 โ”‚ 1 โ”‚ 1 โ”‚ 1 โ”‚ 2 โ”‚ 2 โ”‚ 3 โ”‚ 4 โ”‚ 4 โ”‚ โ”‚ โ”” โ”˜ ``` [`sparse.COO`][] arrays support arbitrary fill values. Fill values are the "default" value, or value to not store. This can be given a value other than zero. For example, the following builds a (bad) representation of a $2 \times 2$ identity matrix. Note that not all operations are supported for operations with nonzero fill values. ```python >>> coords = [[0, 1], [1, 0]] >>> data = [0, 0] >>> s = COO(coords, data, fill_value=1) >>> s 0 1 โ”Œ โ” 0 โ”‚ 0 โ”‚ 1 โ”‚ 0 โ”‚ โ”” โ”˜ ``` ## From [`scipy.sparse.spmatrix`][] To construct [`sparse.COO`][] array from [spmatrix][scipy.sparse.spmatrix] objects, you can use the [`sparse.COO.from_scipy_sparse`][] method. As an example, if `x` is a [scipy.sparse.spmatrix][], you can do the following to get an equivalent [`sparse.COO`][] array: ```python s = COO.from_scipy_sparse(x) ``` ## From [Numpy arrays][`numpy.ndarray`] To construct [`sparse.COO`][] arrays from [`numpy.ndarray`][] objects, you can use the [`sparse.COO.from_numpy`][] method. As an example, if `x` is a [`numpy.ndarray`][], you can do the following to get an equivalent [`sparse.COO`][] array: ```python s = COO.from_numpy(x) ``` ## Generating random [`sparse.COO`][] objects The [`sparse.random`][] method can be used to create random [`sparse.COO`][] arrays. For example, the following will generate a $10 \times 10$ matrix with $10$ nonzero entries, each in the interval $[0, 1)$. ```python s = sparse.random((10, 10), density=0.1) ``` Building [`sparse.COO`][] Arrays from [`sparse.DOK`][] Arrays It's possible to build [`sparse.COO`][] arrays from [`sparse.DOK`][] arrays, if it is not easy to construct the `coords` and `data` in a simple way. [`sparse.DOK`][] arrays provide a simple builder interface to build [`sparse.COO`][] arrays, but at this time, they can do little else. You can get started by defining the shape (and optionally, datatype) of the [`sparse.DOK`][] array. If you do not specify a dtype, it is inferred from the value dictionary or is set to `dtype('float64')` if that is not present. ```python s = DOK((6, 5, 2)) s2 = DOK((2, 3, 4), dtype=np.uint8) ``` After this, you can build the array by assigning arrays or scalars to elements or slices of the original array. Broadcasting rules are followed. ```python s[1:3, 3:1:-1] = [[6, 5]] ``` DOK arrays also support fancy indexing assignment if and only if all dimensions are indexed. ```python s[[0, 2], [2, 1], [0, 1]] = 5 s[[0, 3], [0, 4], [0, 1]] = [1, 5] ``` Alongside indexing assignment and retrieval, [`sparse.DOK`][] arrays support any arbitrary broadcasting function to any number of arguments where the arguments can be [`sparse.SparseArray`][] objects, [`scipy.sparse.spmatrix`][] objects, or [`numpy.ndarray`][]. ```python x = sparse.random((10, 10), 0.5, format="dok") y = sparse.random((10, 10), 0.5, format="dok") sparse.elemwise(np.add, x, y) ``` [`sparse.DOK`][] arrays also support standard ufuncs and operators, including comparison operators, in combination with other objects implementing the *numpy* *ndarray.\__array_ufunc\__* method. For example, the following code will perform elementwise equality comparison on the two arrays and return a new boolean [`sparse.DOK`][] array. ```python x = sparse.random((10, 10), 0.5, format="dok") y = np.random.random((10, 10)) x == y ``` [`sparse.DOK`][] arrays are returned from elemwise functions and standard ufuncs if and only if all [`sparse.SparseArray`][] objects are [`sparse.DOK`][] arrays. Otherwise, a [`sparse.COO`][] array or dense array are returned. At the end, you can convert the [`sparse.DOK`][] array to a [`sparse.COO`][] arrays. ```python s3 = COO(s) ``` In addition, it is possible to access single elements and slices of the [`sparse.DOK`][] array using normal Numpy indexing, as well as fancy indexing if and only if all dimensions are indexed. Slicing and fancy indexing will always return a new DOK array. ```python s[1, 2, 1] # 5 s[5, 1, 1] # 0 s[[0, 3], [0, 4], [0, 1]] # ``` ## Converting [`sparse.COO`][] objects to other Formats [`sparse.COO`][] arrays can be converted to [Numpy arrays][numpy.ndarray], or to some [spmatrix][scipy.sparse.spmatrix] subclasses via the following methods: * [`sparse.COO.todense`][]: Converts to a [`numpy.ndarray`][] unconditionally. * [`sparse.COO.maybe_densify`][]: Converts to a [`numpy.ndarray`][] based on certain constraints. * [`sparse.COO.to_scipy_sparse`][]: Converts to a [`scipy.sparse.coo_matrix`][] if the array is two dimensional. * [`sparse.COO.tocsr`][]: Converts to a [`scipy.sparse.csr_matrix`][] if the array is two dimensional. * [`sparse.COO.tocsc`][]: Converts to a [`scipy.sparse.csc_matrix`][] if the array is two dimensional. sparse-0.17.0/docs/contributing.md000066400000000000000000000124711501262445000170610ustar00rootroot00000000000000## Contributing ## General Guidelines sparse is a community-driven project on GitHub. You can find our [repository on GitHub](https://github.com/pydata/sparse). Feel free to open issues for new features or bugs, or open a pull request to fix a bug or add a new feature. If you haven't contributed to open-source before, we recommend you read [this excellent guide by GitHub on how to contribute to open source](https://opensource.guide/how-to-contribute). The guide is long, so you can gloss over things you're familiar with. If you're not already familiar with it, we follow the [fork and pull model](https://help.github.com/articles/about-collaborative-development-models) on GitHub. ## Filing Issues If you find a bug or would like a new feature, you might want to *consider filing a new issue* on [GitHub](https://github.com/pydata/sparse/issues). Before you open a new issue, please make sure of the following: * This should go without saying, but make sure what you are requesting is within the scope of this project. * The bug/feature is still present/missing on the `main` branch on GitHub. * A similar issue or pull request isn't already open. If one already is, it's better to contribute to the discussion there. ## Contributing Code This project has a number of requirements for all code contributed. * We use `pre-commit` to automatically lint the code and maintain code style. * We use Numpy-style docstrings. * It's ideal if user-facing API changes or new features have documentation added. * 100% code coverage is recommended for all new code in any submitted PR. Doctests count toward coverage. * Performance optimizations should have benchmarks added in `benchmarks`. ## Setting up Your Development Environment The following bash script is all you need to set up your development environment, after forking and cloning the repository: ```bash pip install -e .[all] ``` ## Pull requests Please adhere to the following guidelines: 1. Start your pull request title with a [conventional commit](https://www.conventionalcommits.org/) tag. This helps us add your contribution to the right section of the changelog. We use "Type" from the [Angular convention](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type).
TLDR:
The PR title should start with any of these abbreviations: `build`, `chore`, `ci`, `depr`, `docs`, `feat`, `fix`, `perf`, `refactor`, `release`, `test`. Add a `!`at the end, if it is a breaking change. For example `refactor!`. 2. This text will end up in the changelog. 3. Please follow the instructions in the pull request form and submit. ## Running/Adding Unit Tests It is best if all new functionality and/or bug fixes have unit tests added with each use-case. We use [pytest](https://docs.pytest.org/en/latest) as our unit testing framework, with the `pytest-cov` extension to check code coverage and `pytest-flake8` to check code style. You don't need to configure these extensions yourself. Once you've configured your environment, you can just `cd` to the root of your repository and run ```bash pytest --pyargs sparse ``` This automatically checks code style and functionality, and prints code coverage, even though it doesn't fail on low coverage. Unit tests are automatically run on Travis CI for pull requests. ### Advanced To run the complete set of unit tests run in CI for your platform, run the following in the repository root: ```bash ci/setup_env.sh ACTIVATE_VENV=1 ci/test_all.sh ``` ## Coverage The `pytest` script automatically reports coverage, both on the terminal for missing line numbers, and in annotated HTML form in `htmlcov/index.html`. Coverage is automatically checked on CodeCov for pull requests. ## Adding/Building the Documentation If a feature is stable and relatively finalized, it is time to add it to the documentation. If you are adding any private/public functions, it is best to add docstrings, to aid in reviewing code and also for the API reference. We use [Numpy style docstrings](https://numpydoc.readthedocs.io/en/latest/format.html) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material) to document this library. MkDocs, in turn, uses [Markdown](https://www.markdownguide.org) as its markup language for adding code. We use [mkdoctrings](https://mkdocstrings.github.io/recipes) with the [mkdocs-gen-files plugin](https://oprypin.github.io/mkdocs-gen-files) to generate API references. To build the documentation, you can run ```bash mkdocs build mkdocs serve ``` After this, you can see a version of the documentation on your local server. Documentation for each pull requests is automatically built on `Read the Docs`. It is rebuilt with every new commit to your PR. There will be a link to preview it from your PR checks area on `GitHub` when ready. ## Adding and Running Benchmarks We use [`CodSpeed`](https://docs.codspeed.io/) to run benchmarks. They are run in the CI environment when a pull request is opened. Then the results of the run are sent to `CodSpeed` servers to be analyzed. When the analysis is done, a report is generated and posted automatically as a comment to the PR. The report includes a link to `CodSpeed`cloud where you can see the all the results. If you add benchmarks, they should be written as regular tests to be used with pytest, and use the fixture `benchmark`. Please see the `CodSpeed`documentation for more details. sparse-0.17.0/docs/css/000077500000000000000000000000001501262445000146135ustar00rootroot00000000000000sparse-0.17.0/docs/css/mkdocstrings.css000066400000000000000000000003741501262445000200400ustar00rootroot00000000000000:root { --md-primary-fg-color: #c96c08; --md-primary-fg-color--light: #94f2f7; --md-primary-fg-color--dark: #335365; } .md-tabs__item { font-weight: bolder; } .grid { font-weight: bolder; font-size: 160%; font-family: Georgia, serif; } sparse-0.17.0/docs/examples.md000066400000000000000000000000001501262445000161510ustar00rootroot00000000000000sparse-0.17.0/docs/examples/000077500000000000000000000000001501262445000156415ustar00rootroot00000000000000sparse-0.17.0/docs/examples/dask_example.py000066400000000000000000000017211501262445000206510ustar00rootroot00000000000000# --- # jupyter: # jupytext: # text_representation: # extension: .py # format_name: light # format_version: '1.5' # jupytext_version: 1.16.4 # kernelspec: # display_name: sparse # language: python # name: python3 # --- # # Using with Dask # ## Import # + import sparse import dask.array as da import numpy as np # - # ## Create Arrays # # Here, we create two random sparse arrays and move them to Dask. # + rng = np.random.default_rng(42) M, N = 10_000, 10_000 DENSITY = 0.0001 a = sparse.random((M, N), density=DENSITY) b = sparse.random((M, N), density=DENSITY) a_dask = da.from_array(a, chunks=1000) b_dask = da.from_array(b, chunks=1000) # - # As we can see in the "data type" section, each chunk of the Dask array is still sparse. a_dask # noqa: B018 # # Compute and check results # As we can see, what we get out of Dask matches what we get out of `sparse`. assert sparse.all(a + b == (a_dask + b_dask).compute()) sparse-0.17.0/docs/examples/formats_example.py000066400000000000000000000017511501262445000214050ustar00rootroot00000000000000# --- # jupyter: # jupytext: # text_representation: # extension: .py # format_name: light # format_version: '1.5' # jupytext_version: 1.16.4 # kernelspec: # display_name: sparse # language: python # name: python3 # --- # # Multiple Formats # ## Import # Let's set the backend and import `sparse`. # + import sparse import numpy as np # - # ## Perform Operations # Let's create two arrays. rng = np.random.default_rng(42) # Seed for reproducibility a = sparse.random((3, 3), density=1 / 6, random_state=rng) b = sparse.random((3, 3), density=1 / 6, random_state=rng) # Now let's matrix multiply them. c = a @ b # And view the result as a (dense) NumPy array. c_dense = c.todense() # Now let's do the same for other formats, and compare the results. for format in ["coo", "csr", "csc"]: af = sparse.asarray(a, format=format) bf = sparse.asarray(b, format=format) cf = af @ bf np.testing.assert_array_equal(c_dense, cf.todense()) sparse-0.17.0/docs/examples/formats_example_finch.py000066400000000000000000000020601501262445000225460ustar00rootroot00000000000000# --- # jupyter: # jupytext: # text_representation: # extension: .py # format_name: light # format_version: '1.5' # jupytext_version: 1.16.4 # kernelspec: # display_name: sparse # language: python # name: python3 # --- # # Multiple Formats with Finch # ## Import # Let's set the backend and import `sparse`. # + import os os.environ["SPARSE_BACKEND"] = "Finch" import sparse import numpy as np # - # ## Perform Operations # Let's create two arrays. rng = np.random.default_rng(42) # Seed for reproducibility a = sparse.random((3, 3), density=1 / 6, random_state=rng) b = sparse.random((3, 3), density=1 / 6, random_state=rng) # Now let's matrix multiply them. c = a @ b # And view the result as a (dense) NumPy array. c_dense = c.todense() # Now let's do the same for other formats, and compare the results. for format in ["coo", "csr", "csc", "dense"]: af = sparse.asarray(a, format=format) bf = sparse.asarray(b, format=format) cf = af @ bf np.testing.assert_array_equal(c_dense, cf.todense()) sparse-0.17.0/docs/examples/scipy_example.py000066400000000000000000000016521501262445000210610ustar00rootroot00000000000000# --- # jupyter: # jupytext: # text_representation: # extension: .py # format_name: light # format_version: '1.5' # jupytext_version: 1.16.4 # kernelspec: # display_name: sparse # language: python # name: python3 # --- # # Using with SciPy # ## Import # + import sparse import numpy as np import scipy.sparse as sps # - # ## Create Arrays rng = np.random.default_rng(42) M = 1_000 DENSITY = 0.01 a = sparse.random((M, M), density=DENSITY, format="csc") identity = sparse.eye(M, format="csc") # ## Invert and verify matrix # This showcases the `scipy.sparse.linalg` integration. a_inv = sps.linalg.spsolve(a, identity) np.testing.assert_array_almost_equal((a_inv @ a).todense(), identity.todense()) # ## Calculate the graph distances # This showcases the `scipy.sparse.csgraph` integration. sps.csgraph.bellman_ford(sparse.eye(5, k=1) + sparse.eye(5, k=-1), return_predecessors=False) sparse-0.17.0/docs/gen_logo.py000066400000000000000000000062751501262445000162000ustar00rootroot00000000000000import xml.etree.ElementTree as ET import numpy as np def transform(a, b, c, d, e, f): return f"matrix({a},{b},{c},{d},{e},{f})" def fill(rs): """Generates opacity at random, weighted a bit toward 0 and 1""" x = rs.choice(np.arange(5), p=[0.3, 0.2, 0.0, 0.2, 0.3]) / 4 return f"fill-opacity:{x:.1f}" rs = np.random.RandomState(1) colors = { "orange": "fill:rgb(241,141,59)", "blue": "fill:rgb(69,155,181)", "grey": "fill:rgb(103,124,131)", } s = 10 # face size offset_x = 10 # x margin offset_y = 10 # y margin b = np.tan(np.deg2rad(30)) # constant for transformations # reused attributes for small squares kwargs = {"x": "0", "y": "0", "width": f"{s}", "height": f"{s}", "stroke": "white"} # large white squares for background bg_kwargs = {**kwargs, "width": f"{5 * s}", "height": f"{5 * s}", "style": "fill:white;"} root = ET.Element( "svg", **{ "width": f"{s * 10 + 2 * offset_x}", "height": f"{s * 20 + 2 * offset_y}", "viewbox": f"0 0 {s * 10 + 2 * offset_x} {s * 20 + 2 * offset_y}", "version": "1.1", "style": "fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;", "xmlns": "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", "xml:space": "preserve", "xmlns:serif": "http://www.serif.com/", "class": "align-center", }, ) # face 1 (left, orange) ET.SubElement( root, "rect", transform=transform(1, b, 0, 1, 5 * s + offset_x, offset_y), **bg_kwargs, ) for i, j in np.ndindex(5, 5): ET.SubElement( root, "rect", style=f"{colors['orange']};{fill(rs)};", transform=transform(1, b, 0, 1, (i + 5) * s + offset_x, (i * b + j) * s + offset_y), **kwargs, ) # face 2 (top, orange) ET.SubElement( root, "rect", transform=transform(1, b, -1, b, 5 * s + offset_x, 5 * s + offset_y), **bg_kwargs, ) for i, j in np.ndindex(5, 5): ET.SubElement( root, "rect", style=f"{colors['orange']};{fill(rs)};", transform=transform( 1, b, -1, b, (i - j + 5) * s + offset_x, (i * b + j * b + 5) * s + offset_y, ), **kwargs, ) # face 3 (left, blue) for y2 in (5 + b * 5, 10 + b * 5): ET.SubElement( root, "rect", transform=transform(1, b, 0, 1, offset_x, y2 * s + offset_y), **bg_kwargs, ) for i, j in np.ndindex(5, 5): ET.SubElement( root, "rect", style=f"{colors['blue']};{fill(rs)};", transform=transform(1, b, 0, 1, i * s + offset_x, (i * b + j + y2) * s + offset_y), **kwargs, ) # face 4 (right, grey) ET.SubElement( root, "rect", transform=transform(1, -b, 0, 1, 5 * s + offset_x, (10 * b + 5) * s + offset_y), **bg_kwargs, ) for i, j in np.ndindex(5, 5): ET.SubElement( root, "rect", style=f"{colors['grey']};{fill(rs)};", transform=transform(1, -b, 0, 1, (i + 5) * s + offset_x, ((10 - i) * b + j + 5) * s + offset_y), **kwargs, ) ET.ElementTree(root).write("logo.svg", encoding="UTF-8") sparse-0.17.0/docs/how-to-guides.md000066400000000000000000000000201501262445000170300ustar00rootroot00000000000000# How to guides sparse-0.17.0/docs/index.md000066400000000000000000000024321501262445000154550ustar00rootroot00000000000000--- hide: - navigation - toc --- # Sparse This project implements sparse arrays of arbitrary dimension on top of [`numpy`][] and [`scipy.sparse`][]. It generalizes the [`scipy.sparse.coo_matrix`][] and [`scipy.sparse.dok_matrix`][] layouts, but extends beyond just rows and columns to an arbitrary number of dimensions.

![Sparse](./assets/images/logo.png){width=20%, align=left}
![Sparse](./assets/images/conference-room-icon.png){width=10%, align=left} Introduction { .card } ![Sparse](./assets/images/install-software-download-icon.png){width=10%, align=left} Install { .card } ![Sparse](./assets/images/open-book-icon.png){width=10%, align=left} Tutorials { .card } ![Sparse](./assets/images/check-list-icon.png){width=10%, align=left} How-to guides { .card } ![Sparse](./assets/images/repair-fix-repairing-icon.png){width=10%, align=left} API { .card } ![Sparse](./assets/images/group-discussion-icon.png){width=10%, align=left} Contributing { .card }
sparse-0.17.0/docs/install.md000066400000000000000000000007141501262445000160150ustar00rootroot00000000000000# Install You can install this library with ``pip``: ```bash pip install sparse ``` You can also install from source from GitHub, either by pip installing directly:: ```bash pip install git+https://github.com/pydata/sparse ``` Or by cloning the repository and installing locally: ```bash git clone https://github.com/pydata/sparse.git cd sparse/ pip install . ``` Note that this library is under active development and so some API churn should be expected. sparse-0.17.0/docs/introduction.md000066400000000000000000000074071501262445000170760ustar00rootroot00000000000000# Sparse This implements sparse arrays of arbitrary dimension on top of [numpy][] and [`scipy.sparse`][]. It generalizes the [`scipy.sparse.coo_matrix`][] and [`scipy.sparse.dok_matrix`][] layouts, but extends beyond just rows and columns to an arbitrary number of dimensions. Additionally, this project maintains compatibility with the [`numpy.ndarray`][] interface rather than the [`numpy.matrix`][] interface used in [`scipy.sparse`][] These differences make this project useful in certain situations where scipy.sparse matrices are not well suited, but it should not be considered a full replacement. The data structures in pydata/sparse complement and can be used in conjunction with the fast linear algebra routines inside [`scipy.sparse`][]. A format conversion or copy may be required. ## Motivation Sparse arrays, or arrays that are mostly empty or filled with zeros, are common in many scientific applications. To save space we often avoid storing these arrays in traditional dense formats, and instead choose different data structures. Our choice of data structure can significantly affect our storage and computational costs when working with these arrays. ## Design The main data structure in this library follows the [Coordinate List (COO)](https://en.wikipedia.org/wiki/Sparse_matrix#Coordinate_list_(COO)) layout for sparse matrices, but extends it to multiple dimensions. The COO layout, which stores the row index, column index, and value of every element: | row | col | data | |-----|-----|------| | 0 | 0 | 10 | | 0 | 2 | 13 | | 1 | 3 | 9 | | 3 | 8 | 21 | It is straightforward to extend the COO layout to an arbitrary number of dimensions: | dim1 | dim2 | dim3 | \... | data | |------|------|------|------|------| | 0 | 0 | 0 | . | 10 | | 0 | 0 | 3 | . | 13 | | 0 | 2 | 2 | . | 9 | | 3 | 1 | 4 | . | 21 | This makes it easy to *store* a multidimensional sparse array, but we still need to reimplement all of the array operations like transpose, reshape, slicing, tensordot, reductions, etc., which can be challenging in general. This library also includes several other data structures. Similar to COO, the [Dictionary of Keys (DOK)](https://en.wikipedia.org/wiki/Sparse_matrix#Dictionary_of_keys_(DOK)) format for sparse matrices generalizes well to an arbitrary number of dimensions. DOK is well-suited for writing and mutating. Most other operations are not supported for DOK. A common workflow may involve writing an array with DOK and then converting to another format for other operations. The [Compressed Sparse Row/Column (CSR/CSC)](https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_column_(CSC_or_CCS)) formats are widely used in scientific computing are now supported by pydata/sparse. The CSR/CSC formats excel at compression and mathematical operations. While these formats are restricted to two dimensions, pydata/sparse supports the GCXS sparse array format, based on [GCRS/GCCS from](https://ieeexplore.ieee.org/abstract/document/7237032/similar#similar) which generalizes CSR/CSC to n-dimensional arrays. Like their two-dimensional CSR/CSC counterparts, GCXS arrays compress well. Whereas the storage cost of COO depends heavily on the number of dimensions of the array, the number of dimensions only minimally affects the storage cost of GCXS arrays, which results in favorable compression ratios across many use cases. Together these formats cover a wide array of applications of sparsity. Additionally, with each format complying with the [`numpy.ndarray`][] interface and following the appropriate dispatching protocols, pydata/sparse arrays can interact with other array libraries and seamlessly take part in pydata-ecosystem-based workflows. ## LICENSE This library is licensed under BSD-3. sparse-0.17.0/docs/js/000077500000000000000000000000001501262445000144375ustar00rootroot00000000000000sparse-0.17.0/docs/js/katex.js000066400000000000000000000004611501262445000161120ustar00rootroot00000000000000document$.subscribe(({ body }) => { renderMathInElement(body, { delimiters: [ { left: "$$", right: "$$", display: true }, { left: "$", right: "$", display: false }, { left: "\\(", right: "\\)", display: false }, { left: "\\[", right: "\\]", display: true }, ], }); }); sparse-0.17.0/docs/logo.png000066400000000000000000006724451501262445000155130ustar00rootroot00000000000000‰PNG  IHDRคค]#าะ pHYs  า~tEXtSoftwareCelsys Studio Toolมงแ| IDATxฺ์y|wa๗๑ฯof๖”dห๗}฿ฑใ8N์ุ‰ฯฤนI8 ”ถด ”ปจZ่Cฎ–(NG ”คO -<Bศ}XพวN๑}HŽ๏SฒคฝๆzXIถ›\๖Xใ|฿Jซ™อj~๛yอึ„aˆˆˆˆˆˆˆˆˆHTŒ‚”ˆˆˆˆˆˆˆˆDIAJDDDDDDDD"ฅ %""""""""‘R‘H)H‰ˆˆˆˆˆˆˆHคคDDDDDDDD$R R""""""""))‰”‚”ˆˆˆˆˆˆˆˆDJAJDDDDDDDD"ฅ %""""""""‘R‘H)H‰ˆˆˆˆˆˆˆHคคDDDDDDDD$R R""""""""))‰”‚”ˆˆˆHDŽ๗๊ฦะx -ฌ…ี5uzFDDDไํJAJDDD$"ว๏บบ<๑ h0ฦซญฎYrฏžy;R‰HGjง0%"""oS R""""yUjะ`Œ[S]ณ๔ทz–DDDไํ@AJDDD$"g RํSชญฎYVงgKDDD.d R""""yอ ีNaJDDD.p R""""yAช]ภbc‚š๊šบตz๖DDDไBข %"""‘7คฺ}ฦ˜ฺ๊šบz=‹"""r!P‰ศ›RํฆDDDไก %"""‘ทคฺ•รTMuM]ฃžU‰#)‘ˆœต ะoก…ตPaJDDDโFAJDDD$"g5HตS˜‘R‰ศ9 RํšŒ๑jซk–,ิ3-"""]‚”ˆˆˆHDฮijะะฆ๎ี3."""]•‚”ˆˆˆHD" RํฆDDDค S‰HคAช]@ƒ1ฅชk–ีiDDDคซP‰ศy RํSชU˜‘ฎ@AJDDD$"็5HตS˜‘.@AJDDD$"]"Hต XlLp[uM]ฝFFDDDขฆ %"""‘.คฺ}ฦ˜Z…)‰’‚”ˆˆˆHDบdjง0%"""R‰H—Rํฎถ0ีจ‘sEAJDDD$"ฑRMก๑ZX ฆDDDไ\P‰Hl‚T;…)9GคDDDD"ป ี. ษฏฆบfษฝE9คDDDD" `ูT\๓น†ไ๘ตX๖ฝMy+คDDDD"็ eœูŸ#9fXvƒปkEmbฤ•๗jTEDDไMอ-คDDDDข๋ •ฬ’]๐y’ฃgƒฑpท/%ท่{/†ญ๛jชk–ีitEDDไ อ-คDDDDข๋ •ฎขโฺฟ#1b&‹า–งษ/๙A๎,6ฆTซ0%"""ฏ{nก %"""8)+ƒ์ตObุๅๅ ต๑Qrห0ส—๏)L‰ˆˆศ๋ค %"""‘Xฉส>dฏ;Cฆ‚1ื?H~ลO '^}ใภภSS]SWฏQ‘ำQ‰HฌƒTทT\๛yœA—€โฺ!ฬ}„ล–3Qเ฿gŒฉU˜‘WR‰Hœƒ”c0ู‡3`"Rx—VOXสฝ๖+L‰ˆˆศ+(H‰ˆˆˆD$ึAชืฒ >‡ำo<„…็๎งฐๆ—„nu฿GzwZX ซk๊u4ˆˆˆผฝ)H‰ˆˆˆD$ึAช๏*ฎ๙vŸั๘ไŸ๙ลตC่฿ุ4…ฦ[จ0%""๒๖ฆ %"""‘8)gภEdฏ์^# ๙?กด๗o พ๛ึ๎8 มฏถบfษฝ:BDDD>คDDDD"็ •6ฬOcW"t๓ไ฿Ciห“เ{g็ฆDDDVคDDDD"๋ 5๒*ฒs>…ีญ?aฑ…\]”ถ-ภ;ปค0%""๒ถ  %"""‘8ฉไุ๙df} ซฒ/aก‰ึงฟ‹ปsนyภ€ลฦ”jซk–ี้ศน๐(H‰ˆˆˆD$ึA๊ขศฬ0VE/‚1rO}ท~„มน}`…)‘ ’‚”ˆˆˆHDโคR“฿I๚Š?วสTดก๕‰ƒท็ˆj.ฉ0%""rAQ‰HœƒTz๊๛H_ง˜T%A๓!Zgผ}๋ฃ฿ภฟฯS[]SWฏ#JDD$พคDDDD"฿ eHO3าS฿‡Id๑›๖‘{์xทœฟMR˜‰5)‘ˆฤ6H‹ฬŒH]๒nL"|7ญ~ศฮ๓ฟmๅ0US]Sืจ#LDD$Fำ )‘hฤ6HY™ซ>B๊โ[0vh=ญ|๘ห]c๛šBใ-ดฐ*L‰ˆˆฤƒ‚”ˆˆˆHDbค์ููŸ$9๑FŒeใA๋รwœ8ะตถSaJDD$6คDDDD"ื eœ™นท“špƒw`s9Hตํšฆzึ,ฉีQ'""าE็ R""""ัˆmJdศ^]Crฬ|ผhyจ–ฐpขkox@ƒ1^muอ’{u๔‰ˆˆtฑ๙…‚”ˆˆˆH4bค’Yฒ >OrิU†ธ{_ค๕กZยR.; 0%""า๕ๆ R""""ัˆmJUQq฿“1ƒ0๐๐vฏก๕ก; Rผv$ มทฆบf้ou4Šˆˆœ็๙…‚”ˆˆˆH4bค2ฉธ‹$†]N่ปxปžฅๅกZ ฆ๓ศ€ลNŸ!ตUผฏNGฅˆˆศyš_(H‰ˆˆˆD#ฎAสส๖ โฦ3x ก_ยฑŒึGพ๋ฑp๚ฃโ_[lU๖ช๊ttŠˆˆDKAJDDD$"ฑ Rฝจธ้qN"๔Šธำ๚๘?วy Œ3h•7‰ษVด]\|—5™ูŸ\ซฃTDD$ขณฑ‚”ˆˆˆH4bค*๛Pq๓8วบJ[ž"๗ิwb<6$†Lฅโฦ/cาU-‡)ฌล~}Ÿ1ฆถบฆฎ^Gซˆˆศ9>+H‰ˆˆˆDใ๘]ื„q\wษ๊ึŸสw|ปฯ่rฺ๘(นบปc<6$†ฯ โ๚/bRอ)ฌบŸโ†‡สฟ|…)‘s}:V‰ฦษ+ค q SV๗T๒5์^ร <ล๕ฟ'ฟ๔‡1ž[$F]Eลu_ภ$2M๛ษ?s/ฅอOtพ]เ฿ีฆu๔Šˆˆœๅำฑ‚”ˆˆˆH4š๛ฏCศNBฏุ>#aส๎1˜Š[พc0a)Gqไ—8พaู$วฬ#ปเs'…|…•Ni[ซoะoก…ตPaJDDไ์Q‰ˆtW่n_Bi๛Rฃป cึตร”s•ท~ซ๛ยb+ลตฟ&ฬฝ๑ห!9~WlX๙ๅ?ฦนโฬฃ0%""rV)H‰ˆˆˆD'$๐๐์คดๅ)J;W4๎ๅdŒ๊šaส๎=ฒคช๚[(NŸQ`':~[๐๖ญงด้ =/ๆ›บฬv;/ฆ๒ึฏaRU„นF๒ซƒโ‹ฟ๏8‘&5๕df€ทoนล๗เฺ๚ๆ๏4 ม˜าmี5ห๊tค‹ˆˆผŽ๓ฑ‚”ˆˆˆH4Ž฿uuเ๔ฟgุ4’ฃฎย๎5,ปใ6aก๗ๅ5”^z๏ภFยbหy฿ngศT*oพ“ช ศฃฐ๒g7<฿ p2K๚ฒžAผ=kษีดญ฿yภbcJต S"""ฏq>V‰ฦ๑ปฎ O]สt ‰‘W’>ปzpง0ไŽแีฏขธ๑1C[ y๎ฤ๐+จธ๑c’Y‚ึฃไ—„าฆวโ;NUž็คงพw๗r‹๎"hs๖DaJDDไŸคDDDDขั~…ิ+-OŒ˜Ab๘ C/รช๊ถำ๑ป ๙๎Že7?Apดžะ+Fพ‰QณจธแK'Eะ|ˆ๒Sฺ๒T|'ภ้*23n#uษปpw=Cฎ๎n‚ฮƒฦ˜š๊šบzฝDDDN9+H‰ˆˆˆDใd๊˜Šัฆ,›ไจู$†Oว<ซฒฯษ+ฆย iฅmu”6?‰฿ด|7ฒํNŽฝš์ตŸo Rษ/ฅm‹ใ;ฮT“น๒รค&ฝwวฒrj9r๎4๐๏3ฦิ*L‰ˆˆดคDDDDข๑๊ ี1%ฃ=L™d–ฤจY$Fฬฤ0ซขSพYเใ7๎กด๙ J[4‚ภ?็œp=ื-ุAำ>rK„ปcYlวมส๖$3๋ฃ$'\@ib๒uwไŽŸ๛W˜)ฯyคDDDDขqๆ ušIZฆ;ษ1sI ฟป฿8ฌlสแ Bฟ„dฅMใn_B;็pN—št3ู๙w€ๅ4๎!ทไธปž‰ํ8X•}ศฬ8ษฑWPฺ$นล๗NDถ a่ia-ฌฎฉkิ+CDDŽคDDDD"๒F‚T;ซขษqืเ ›†ำ{&ำ Œ@Xlล?ผโฆวqw,o๛Fพณ?ทK]๒.ฒsocแk ฟ๘๛ธปWวvฌnศฮ๙$‰Qณ(m|”’๏[ฃ€ฆะx ฆDDไํHAJDDD$"o&Hตณบ$5แZœ!Sฑ{วค*สa* ‹อxทP๐{]+ม๗ฮ๊vงงพฬฌO€1๘Gv’[|žตฑซzูนท“~ล ‘_๒ƒ๓๗M† S""๒6ค %"""‘7ค:#Ÿg4ษqW“|)V๕`L2[^c*๐ '๐๖ญงธ๎ผ—_8kž๖งdฎ+ภเFฎ๎ผ}๋c;vฯadๆNb่e_-๙ฅ?"๔K็wรšŒ๑jชk–ซW‹ˆˆ\่คDDDD"๒Vฎze˜r]Lr‚๒ย็๚—ร€๏ไŽแ๎~žโ๚฿แ๒g‹†๔๔ฟ 3ใ/๐n!_w7Mฑป๗Hฒ๓>ƒ3h2…็E~๙!๐บฦ4ใี*L‰ˆศ…LAJDDD$"o-HuL฿85L%†^Nrยต8'`*zaœ4ฝม‰xปืP\ ฑ†7๗p–Mfฦmคงไ๊๎ฦ?ด5ถใ`๗Cv8. ๐ฯษฏ้9]MQ˜‘ ˜‚”ˆˆˆHDฮN๊˜ฦัฆŒ!9z‰1๓p๚รd{`์Dy})ท@ะดท~ล 4|๕฿!ถCๆสž๚~ผฝ๋สA๊ศฮุŽƒำูyŸม๎7)<๛ไŸนฏ๋npภ‹ฦ”jชk–ี้U$"" )‘ˆœ ี1ฃ=,;Ib์|’c็c๗…•้– _Fพฦ=ธ;WP๐{ยB๓ซด๗๎คศ\๕RS๏ๅ็ษี}๏อ_qี8'‘v๏Q†ไW”ยs?๏๚ฐุ˜Rญย”ˆˆ\คDDDD"rn‚TวดŽŽ0•ช 9แ’cๆ`๗†IU–ฟ‘/๐๒M๘G๋qท-ฆธ้ัSพ‘๏๔aส$2df}œิไ[p๋W‘_r๑=ฑg๐”r๊9 ย€ฒฃ๐/ใณ S""rP‰ศน Rำ;ฺร’Uู›ไฤ›IŽบซภถoไ| w๏เfํK)m}๚”๕“:‡)“ช 3๋ค& €ปs9น%? hฺqH ฝŒฬผฯ`๗Onษ๗)พ๘๘ํHเ฿—ž๒žฺฬ;๊๕๊‘ธQ‰H4A๊ี์žรHNผ‰ฤ๐้ๅoไs’B่—[ใํ฿Hi๛RหO&!&UEv๎งHNธ€าถลไ— ๙Plว!1bูนทcuพGฎ๎.ŠŠๅพค/}/™ซ>zvขจืซLDDโBAJDDD$"็+Hตณ๛Ž!u๑-$†\†Uูlh[๘ผ๙พ ”ถ>๗๒ ''‹™jฒsšไธk(m~’ฒ%h=qHŒšEvฮงฐบ๕'๔KไžฅอOฤr_า— ™ม?ถ๛พงถ๒๏ฉืซMDDบ:)‘ˆœ๏ ี~ี“3`"ฉK3d Vฆบผพ–rM{๑๖ฌฃดu'E๖บ/3€โK_๑cย\clว!9f.™9Ÿยช์C่ษ=MJ๊bน/้้NๆŠ?หม?ZO๎๑o5นื/ดฐVืิ5๊U'""]•‚”ˆˆˆHDฮ๊˜‚e“v9ฉ‹oม0 “ฎ,ส฿ศwผwวrC[INzวษ ตA๒+~JX8qHŽ_@fึวฑ*zบyZฦ+>ฎ™™&}๙สA๊๐vZŸ6กญะOaJDDบ,)‘ˆt ี>ด0NŠฤจซHMผ ปฯLชขํ—!aฎ ๘ห˜l์ƒ(<+ ซƒฐุqH^t™ซ>Š•ํAXสั๚P-๎๎ีฑ—ฬฌ‘พ๔}`ู๘‡ถ–ƒิแํ'o 0%""]”‚”ˆˆˆHDบ\jgู˜TษัณIŽฟปฯhL"]]เ—ฟฯv(ฎr‹๏‰๕8ค&ฝƒฬ•…ษt',4ำ๒๛/ใํ}1–๛’™๓)าSฦย;ฐ‘“฿ม?บ๋ี7 h0ฦซญฎYrฏ^‰""า(H‰ˆˆˆDคหฉv–ƒUั‹ไ˜9ๅ0ีk8XNง›๘วwS\๗;J›Ÿ ,4วrR—ผ‹ฬŒaาU„๙ฦrฺท!–๛’๗R“o-ฉ}๋ษ=๕/๘วฮ S""าE(H‰ˆˆˆDคหฉvvป[cๆ’šx#Vท~ดลณwืJ]ฯ–rฑ‡๔ฅ๏%}ล_`R•ญGi—๑lŠแL"{u ฉ‰7•ƒิžตไžใ{^๛oŒqkชk–VฏL9/ง1)‘hฤ&H"5๑&าWVU฿ฮฟ‚ๆCธปืเ6ฌยน/๛”พ์คง“ฬ4ขๅ๗_./7ถCลีŸ%9แz0w๗jrO/$hฺ๗๚๏#`ฑ1ฅฺ๊šeuz…ŠˆH”คDDDD"ว ๅ œDvงฑ๛Œ฿%๔]L"ฆ|>ม‰ธ๕ฯโึฏยmxฎห๏Sz๚I_๖r:ฑŸ–ฟŒdG&๒NŠ์5Ÿ%9nA9HํZIฎ๎{'ผ๑;S˜‘จฯc R""""ัˆcJ ฝŒฬ์O`๗E่)ฝ๔&U3d*VEฏ“7 ใ/ใ๎X†[ฟ o฿๚.ปO™™&u้{1‰4๑=ด>๔ง_ผซOไ“Yฒื-ษ1๓สAjว2r‹๏!h>๔ๆ๏4`ฑ1AMuMZฝbEDไœžวคDDDDขห 5b™ซ>V^เ๗ศ-๚.~ใ^ฌส$Fฯ!1ไRLช๊ไ„‘”ถ-มฝ๏เ–.ทO™Y'uษป0N ่.Zบ๘๎๘MไSUT,๘‰QณภJ[‘_๒‚ึฃoฮ>cLmuM]ฝ^น""rNฮc R""""ัˆe5‹ฬUล๎1|—ึงพƒxVe/0Ve#fโ ผ“ช8๙‡พ‹wxฅอOแํ]ืฅ>—{;ษI7—ƒิแmดx๖ฉโบฟ'1v~9Hํ]G๋c฿xk๋.'VU฿r:0ื=@~ๅฯ 'ฮอ4…ฦ[ha-T˜‘ทJAJDDD$"q RฉI7“žAฌช~„ลVZ๛:๋ิ)%aชะ –ƒ3pษฑ๓ฐ{;ัqหฐ”รmxŽาๆ'๐n9;๋ฝฑ้/7|‰ไู่`'pwฏ!๗ฤ7 ZŽฤ๎xฒบ bม็q_ ฯŠยช๛ ‹อ็๖ฆDDไlœ‘คDDDDขห uษปI_ฌส>„…fZZ[Œ:ฎœฆŠญ'…3d*‰‘WbW้ธeX8ปke๙Šฉ#;su กโ๚/‘yุ๎ฎgศ=๙ํณป๎RD์Cศ^๓9œA“ „ยšขฐ๚็„ลึh6 -L๕ฌYRซWทˆˆผQ R""""‰cJO}?ฉฉ๏รช่E˜kค๕ัฏท…ฅ?ด+ง„)ท€IfH ›Ž3๔2์๎ม:ฆ‚–รธปžกดๅI#๕็๊“ศPqร?6ญคถ/ฅ๕้๏ž›u—ฮ1ปื๐r0ยยชKแ๙_–rัnH@ƒ1^muอ’{๕*‘ื}NV‰F,ƒิด?#5ๅ=XูญGi}์Ÿ0ง\้๔SM:ย”๏a’Yรง“zVUฟN๛ š๖Sฺฑ w"ฃ „nL~ำUT๐%Cฆ‚ฑ)m]Dฎ๎๎sท๎า9d๗C๖šฯโ๔ O~ๅฯ(พ๘Bทp~6HaJDDศ9YAJDDD$q R™ท‘š|+&SMะ|ˆึ'พ‰9eMจื9ๅค#LVถ‰ก—แ นซข7SพYโ7๎ฅด๙ ห ๗zลณบ?Vถ'7| gะล`,J›'ทไ„ล–ุON dฏ์>ฃภ๗ศ/1ล๕ž๕็์ h0ฆt[uอฒ:ฝ๊EDไŒณ)‘hฤ2H]๕QR“nฦคซš{๊)ƒSONจŸฉ่Mrฤ ์ae{ด<ฃ๕ๅ0ตk%‰ƒเปgeฌชพT\ฅ๒ว€โ†‡ศ/q๔s; œA“ษฮฟปืpBฏD~้(ฝ๔ก_๊ฐุ˜Rญย”ˆˆœvV  %"""Xฉ9Ÿ"uั๕˜d%~ใrOใ$ฯส}‡^‰ะอใ๔KbไUุ}ว`eบw|”/๔Š๘‡ทSฺ๘(nร๊๒7๒[zLซz0ืง๏ุ๒บK๋ ฐ๒฿ฯ฿ว‚ฤะหศฬ๛4v๕Bฏ@~๑๗)n~โฌลปณFaJDDNCAJDDD$"q Rู๙ww &Yฌพค’ูณ๚aก,ง#f`๗Iw๋๘(_XสแุD้ฅ‡๑๖ฎ#ศ7Bเฟฉวฒ{ งโบ/`๗Ex_๘V?ๆ๖&$Fฬ$;็SX–rไ๊๎ฆดm๘^ืเ€ลฦทUืิี๋ฟˆˆ(H‰ˆˆˆD$–Ajม็HŽ™‡Iค๑์ ท่.Lช๒œcจธ๎๏;>ๆVXs kป๋]U๔:$วฬ%3๋cXU ‹-ไž.ฅห๒Ud็\เ฿gŒฉU˜y{S‰HƒTล๕‹ฤจY;‰wh+๙บป1™๎็๔1รB3&S3๘ƒง`U>yUVดร๓ล ใVFพื9งuL$ปเsุ=†บ ฯGแล฿tซŠ€ไ๘dฎVeย|ญO ๎ฎ•o๚๊ฑศ)L‰ˆผญ)H‰ˆˆˆD$–A๊ฆ/“>c;x๛7’[๚ร๒:O อX๚ใ ž‚3๐bฌชพ˜d0เ{อpw?Oqร๏ ŽฟL่•8uั๔ำqOกโšฟล๊>€ฐุJ~ลO(พ๔p|"ฮ)Ro"=๓CXูžนcไžnร๊7|ีุy๘w%Nฌญใ4๊ฟ„ˆศ‡‚”ˆˆˆHDโค*o๙*ฮะห1ฦยปŽสŸbฅปEบ aกปืœA“ฑ๛_„Uูใคภ@่Nภญ–โ‹ผๆย็‰แWฝบฆ|UQแนฅ?ขด๙‰ื}…UW’บไ]คง+SMะz”ึวฟ‰ท็…X๎Kๆส6%'^ฟะุ‰…&Uฅ0%"๒6  %"""‘Xฉw}g๐%xปŸงฐ๊bาU็e[Bฏ„ำ{ฮ )ุฦbฒ=0vยฐ”รo‹ป} ล—!ฬ7qบซฅฃg“{;VE/‚|#๙บ๏Qฺถ„ืบฒช+JO}?้ห?€IUดข๕ฑฦท!–๛RyหืH ›˜&w๗๊…ญ~ia๕gžR˜น€)H‰ˆˆˆD$vAสXTฝ็8&†-Vd IDAT~๙*ค5ฟฤค+ฯ๛ฆ9}วbšŒkVบ;ุaฑh=ฅ-OSฺ๒$ก[8u‡ส๋.อVฆ;Aหr‹๎*ฏปC้้$}้๛0ษ,ม‰ด>๖ ผ›cน/U๏๙6ฮภ‹pwฏกๅท_j2ฦญฉฎYrฏsˆˆ\˜คDDDD"ท e์$•๔œ~ใ}w็ Š๋~{r๑๓อNb๗Cb๐ฅุีƒห฿ศgูxนใ๘ทPฺฑ”าๆ'vศšx™+?‚IWœ8H๎้มฝ&†ณxCfฦmค.yO๙๗ะ๚ศW๑์Œ฿พX6U๏๙N‹สแsื3ด>|g๙w ฦxต S"")‘ˆฤ.H%ณTฝ็;ุ}Fzล๒วแ6>‚Idบฬ6†^ ‚€ฤศ™$†\Šี}@๖BฟDุzoFJ—เ๎\Aj๒;ษ\๙aLฒฟqนงพ…ทw}&ห&sๅGHMพc'๐ํฆ๕แฏเ฿ฟ7$‰4•๏v[๘tqw,ฅ๕ฑ๊|#…)‘ Ž‚”ˆˆˆHDbคฒีTฝ๛ุฝ†บJ[žขดuฦIvนm อ˜T%‰QณpMฦช๊W^๘< w’„nŽะ+•ืœ2ศNZ๛ฑ†๘Mโ™นMrยucแB๋ร_!h9ป}ฑ*z•ณ^ร KyJ[ž$ท่ฎ?G‹)ี*L‰ˆฤ—‚”ˆˆˆHDbคชS๕ฮยชHXhก๘าCxปื”ืi๊โยRป็pฃga๗•๊ถ aฦ@AหarOทู๖ฉ1q๙†:“H“๗’ใเุHหรต„น๘}1ีญ?•๏๚g์๊!ๅ๐น๑qrKพ๚XaJD$ถคDDDD"ฏ eฐ{ ง๒ึฏcu๋G˜?Aqƒx๛6”ปML„พ‹ำo‰aำฑ{j๛†ภถ๐]ผ›)m[„ปsA๓กŽ}๏๊aส$ณdฏ,ษ1s o฿:Zช%,ถฤํํv!ๅใฌz aก™โ†‡ศ/๑ป›ภภSS]SWฏ4""19(H‰ˆˆˆD#vAช๏*o๙*Veo‚\#ลƒhk Ÿy‹0๐pN"=ๅ˜TUวoส Ÿร?ผwื3”ถ/%,6w<]5L™T%ูŸ#9j>๎ห/ะ๚p-ก[ˆ์#ฉผ๕kXU} ๓MืŽ3๗พนป ๛Œ1ต S""18(H‰ˆˆˆD#nAส8‘Š›๏ฤส๖ h=Jqํเู็?,ถ’บ๔ฝ$วฬm๛Aว๗h S-G๐๖oฤ๕ ๎Že„~ฉใน่jaสคซจธ๎ $†ฯ \ผ†ีดโฒVงใlภ$*nGฌlฯr๘|Wžๅู๓ะปำยZX]Sืจ@""]์  %"""ฦ๏฿†^ฑ}ึตใฑH ›Fล _ยค* šQX๓฿M{ใ;v’ิE7’9|w๗ ซAjโ8รฆae{žผbสอ4๎ลณw็ ผ=kO}rฮ๛ุYูžT๘8ƒงzEหi}๔k1|7bH ™Jล ะ>SX Š/๖์=F@Shผ… S""]์  %"""ยณ–v,ว?Vพ>ฃK†)c‘y%ื}“ฬœุOน_ถŠ๏ฤ7ไ๘kI ›พKiว2Z:&™ล๎;–ิไ[I šŒษTwMXlลoƒท{5๎ฮๅx6Ÿz็m์ฌŠ^T๔8'•ƒิถ:Zf ล"1|:ืฑ-|คฐ๊?)n๘ู,…)‘ฎu P‰ˆ๏…๑Jใ๎\|7๘ํktฑ0eู$Gฯ!ปเ๓˜Dš q/…ี?'h=฿‰o"Cj๒;หW๙%m‹i}์Ÿ:žซข'v ค&Œ3h2&‘้๘ฐุŠd'nรณธปž}ลZZัUี—Š›jq๚'๔Š”6?I๎ฉ๏ฤoP,ป>ฏ๛r๘lฺOู๛(mz=f@ƒ1^muอ’{๕OIDไ<ž—คDDDD"Sžx๙ฑ]”ถ<ปsA๓!N~”ฏซ„‡ไธซษ^๓YŒฤ?พ›ยs?'ฬว๘ยc“ž๖ง8&๚ฅrฤy๒ฏธมช์‹3h2ฉI7แ๔Ÿถsr‹-๘‡ถRฺนฏa๑=็gxบ๕ง๒wb๗C่(m|”\๑ห!9ฆ-|:)‚ฦ=ไWŒาึE็ฑฆDDฮ๏iYAJDDD$a)šd๖ไ|่ฎ๒S;–4๎:aสvHMธž์ผ;ภv๐ึSx๎~ยbK|Ÿภ#{ๅGฑ๛+_Uด๑Qr‹๎:ร,ูย๊>€ฤเ)$'ฝงฯจŽ…ฯย\#มMธ;–ใ๎^Cะ|0า}ฑชQ๙Žฏ`๗QR"ทไ๛๑!9๖ฒืMG๘ฬ/ ๎Žeัmƒย”ˆศyก %"""‘โ{ยไ่9ุ}วt๚8XG˜ฺZGiว2ยๆC„~้<‡‚ฉI7“{; ศ ซ๎'t๓ฑ}C฿#;็SุฝGzEŠ๋$ฟไฏ1[ถฐ{ '1lษ๑ืb๗–~ญว๐๖mภพoฯฺ๒7๗E1<=†Pqหืฐ{ !,ๅ(ฎ{€๒วoPN>๓หทู่ท%`ฑ1ฅฺ๊šeu๚o%"r๎)H‰ˆˆˆDไ๘]W‡vŸ18ƒ'“5ป฿8Œ“:yƒถ0U๎Žๅๅ๕š:?x’h'IMพ•ฬœOะV๒ซ๎‡๓สŠภ'3๗v์žรล๓๚#Žภ้=’ฤศ+IŽU=ธ๓]ทลฝšาถ%x๛6›ฯ้ฎุ=‡Qyหืฐช•ƒิฺ_“_๙ณ๘ฝฑ“$'ิ)|ๆ—w๗š๓xœ(L‰ˆDrP‰ฦ๑ปฎ๎˜xู}F“6ฤˆุ}ฦ`้“7๔]ผร)mzwืJย\cไWL'Ejส{ศ\๕Qผ›(ฌ๚Oฝ๘@วษˆ๓ยฏศ?s฿{^i์>cHŒšErฬ\ฌชพข๙๎ฎgpw,ล;ฐ‰ฐ”kKฮๆย็v๏‘T๚uฌช~„ลV ฯ7…U๗ว๏อˆ“*‡ฯูŸ =|ๆ–|o๏๚.pผ(L‰ˆœำs€‚”ˆˆˆH4สAชs˜p๚ร6ฤ๐+ฐ{ ็ิ5ฆBฏˆxฅ-Oโ๎XFoŠ์Š)“ศš๚>23nภทยsI—๚&ภ7( B*-VU_ยRŽย๊_ดํำ›x~’YœHŒžCbฤLฌสง๓‹๏ม;ฐฉ๋ldเ฿gŒฉญฎฉซื1‘ณxP‰ฦฉWHฝ*L๕ฟgุ4รงa๗ŠIUv.t๓'ริ๖ฅลf๐ฯํ•J&™%}ูHO`9์YKa๕ฯมXฑ}ร0คโฺฟรช่UŽ8ซ๎ง๐/฿ฺ๓”ฎย8‰ไ˜y8C/วส๖8๙หภวoƒปcฅmK๐ํ:eZ˜ฒ๛+ฉlOยbsพ*~oFN>su฿ร?ผญ๋mฌย”ˆศู=(H‰ˆˆˆDฃs๊˜Žั)L ธˆฤ๐+p†^ŽsX็+ฆ<แํ”6>Jiว๒๒วม‚sฆLช’๔ด?#}ูเ๎^Ca๕/0ถ฿กโบ/`ฒี„…f๒ฯGqํœ•ปถฒ=q]Lbฬ<C/๋ |c ”ถ>ป}๑—O๓7ฆœQy๋ื1™๎g}_"}3rš๐™ซปh}ืh…)‘ณsP‰ฦ้ƒTวดŒW^1•= gะ์žC;‡ฉRะVŠล๕ aฑยเ์NำUdfF๊’wเึ?Kaอ/1N"ฦ3_Cลu_ฤคซ๓MไW;ล๕žี‡ฐ*๛เ ™Jrฬ\œม—t6ลภร;ดาฆวp๋Ÿ%8qเŒใZœSyหืฺ๖ฅ‘3๗R\๗ป๘ Iช‚๔ดv Ÿ๙บป๐๏ฺ้ะoก…ตฐบฆฎQDDฤ9@AJDDD$8HuLฯ่ฆ^Lr๔œAcu„IUด&$,ดเฺzr๑๓b+gk'“ฉ&3๓Cค.พฅ v.งธ๖7ใ+คŒ• {0ฉ ย\#๙?ก๘ารg๋;=๗V๗$†_Ar๔์~ใ;-Z๚%›(n| ฏแน๒ท)žแ~ฮฤr)•7ๅไพฌ)ล ลoLาUdฎ๘KRSS>ฮ๊Ÿ%ท่n‚๛ใฑ S""o  %"""ืค:ฆit Sƒง5ซฆบ ,_1e „!aพฉฆ6?Žปc๙) hฟyVถ™ซ>J๒ข(m[Liƒ๑RNš์ต‡If Z’_cJ›?าi์^รI ›Nb๔์#;‡ฉRo†rP€์œฟ&1๒JŠ/=oโ;w4ฝจ˜7`;M๛ษ-นง|%Y4ฮซ‚โศซp†^†}@ง0ดลน‚าฦว๐์8ํG0“ใฏํืwวฒุษซย็ๆ'สAชุ|aผ่Œ)V]ณฌNED^qfT‰ฦู Rำ8:Ž3IŽปง฿xLEOŒS^ซ(๔Š-G๐lคด๙ k^ืฝ=“™s;‰แำ(ฎท™7uตUWau๋Ov๎ํ`9๘ว๗_|nรชจง฿'วอ#ฏ"1b‰ม—bU๖9นFWœ8@i๛J›Ÿภ?พ“มLMผ‰์š“qm๑=ธปVฦpL๛)#ฏ*g/=B~ษ๗เีaq†Z[JDไ4gD)‘hœ ี1ใd˜2$F]ีฆฦaฒ=1v Aหแ๒S›ว๛โ๎ฃฬ๎9Œ์ผOใ ™Z/ฆํ*ซ žO~`๕Fv๖'มX๘วศี}๏ๅ็ฯื4ผใ97v’ฤู่$†_3h2VE/ฐ์ถํ๖๑›๖Rฺ๒4ฅ-O4๎BR฿Bvงมr๗ซ๛nรsฑซz0ูนMb๘ๅใlƒไ—ะ-\0ฏ{)‘3œ คDDDDขqn‚ิ้ๅ;$G]u๒ฃ|™`ๅoไ+ๅ šโํ‰า–'๑๖ฎ?ujH{$ฑ{$;๏38ƒ&Px—ธ๕ฯal;žQภ๗p๚#sๅGภ#;สAj๏บ๓=๏xฮMช’ไ˜น$†_Vถg๙›|๘nŠฅดๅ)’cฏ&;๛mW{ํ&ท่๎๓ื<ป็02so'1๔2 >๓K•ะ/]0ฏ{)‘3œคDDDDขYjŸ่9)ฃg“ฟปฯฌtUZE!a1Gpb?พ๕”ถ.ยทกs(่;†์ผ;p\@แนŸใึฏย$ำ๑Œ^ gะd230๘‡ถ–#ฮ]n[ญlOใๆ—รT๏QX™๊Ž0–rx6_ย<“ศเU—NWฝลC9|~gะ%ๅใ์๙_‘_๑ใNOŒ;)‘3ฬSคDDDDขu๊˜๐%ณ$Fอ"5แ:์>ฃ1ฉJ0VSญ{q๗ฎรถ๏ภfœ~ใศฮฏม๎7)<๛ๅ •ฎŠgp $†^Nz๚๐l"Ww7ม-]v›ญชพ$'\Gb่ๅุ=‡bาสใ๘„‡ฑๅE๎้…x๛ึวn\์>cศฮ ฮ€‰V‚ส๕Zeฏ:๖คDDN??Q‰ฦ๙ RฟT%ษัณINธป๗HLฒฒ|ๅM[๑๗เํ}‘โบ฿aeบ“™๗œ€ŠŸโึฏยช่ฯ(เHŒผ’๔ิ๗เํ]_RGvt๙mท{#9แ:œม—`๗‚IdOฎ1Eyแ๚าQ\๗@์ฦล้?žฬผ;p๚ ๐์ๆ>โmŽฏ:๖คDDN?/Q‰ฦ๙RภLu9L_€{&™m็LXlม;ด•0w ป฿x์C ๐ษ/7†็ฐช๚ฦ๓ษw $Fฯ!5ๅ=x/ฟPRวบ๒TSรŒำ<ษฑืเ นด<.vขใwA๎๎5ื=€ทcl†ล0‘์ผฯ`๗„ไWŒยช๛/จืฝ‚”ˆศฮr R""""ั่*Aช< 4XฝHŒšu2L9ฉถwะa)ถำ๑ณ“฿ยทซ๛@N๗ญ|]> ธ’ใฎ!u๑-ธ๕ซศ/น๘ž8Lู;=฿‰‘3ษฬ+์#;๏ฃW$h>Œท{5ล๕โีๅ๗ฬt ูyŸ.๏K’_c k๋‚z+H‰ˆœแ์ฆ %""".คฺYvG˜JฟถNน๒ฆป})๙ีฟภsส:R๑ Sก[ 9แ:Ro*๏ฯฮๅไ–€ iœฆ๎ฯwfฮงH_๚๒พyล๒oํdพๆ š๖โ6ฌฆธ๗'๖wู๑J ฝŒฬฑ{ƒ0 ทไ‡ื๚‚z+H‰ˆœแฌฆ %""".คฺู ฌlฃf“บ่:์ž#ภvNพฉv๓๘ทเ6พOPS‰F,‚T›ไ„๋ษฮ๚8&[ _Žฦ:๙&ปุ‚h๋ษ0]:L…n๔”?"1z6ล ‘_๙Sย\c&๐ษ,ูนท“ผ่+(ผ๐kŒ“ฏ„=h2ษ3ฑช—ฌ7| w ๐J๊(m~โิ{UIvgHŽ_”ฏ*ฎฆmํฏะ+บy’cๆ’1ซช&‘ ก_"l9Šw`#๎Že”ถ->๕ž#ฏไุ๙df}ซช/กW$๗๘?ฟb›โOAJDไ ็3)‘hฤ)Hฅ&Lzๆ‡ฑฒ= '(พ๔Vท'‰ีc(v๗ๅ+oฺ฿t[;"‡xะต?}๔eLbุ4 ฯ’ยช๛ ‹-๑›ภgบ—ƒT๛วทBฏ„ำ{8‰ัsฐ๛Œฦ$2ํฃEXhฦ;ดฏ~ฅํK šž›ใl๊๛HO๛ณ๒•xนFš๗ฟ๐nน ^๗ R""g8)H‰ˆˆˆD#VA๊Šฟ$=๕}˜d–เฤ~ร;Kญฏcvicา0ษ4ฆข7vU_ฐNฎm[๑๖ญรพ” ุY˜2v‚๔Œa๗ a@n๑=7<พปใศ๎1”์ี5ึร๒๖พุ้สดือฒ! ฑz#9fv๏ปฃ•!Aฎภ&๚g)ํXJ˜k<ปวูๅB๚๒t\‰ื๒ภลฟp(H‰ˆœแฌ %"""8ฉฬ•E๊’wc’Yฦ}GwพŽ uสZC–ƒIU•รTฆGyฉNaชo฿๚r˜*4แซฏฮฦคืI“ž๙!์žร ๐ศ-บ‹โฆวภ๗bwูฝG’wGวzX๙g๎ร?ธ™ทดฮS๐ูป๓๘ชฎ๛๎๗Ÿ=žIณ„„˜gฬŒcใyสิดiฆฆอmฺไ6M“fhฺคฉดฝiโไIœฆMำด้“ฆ7O๛คNว‰ ƒ ๐Œ™ัŒ„@๓pฆ=ญ๛วBƒ ^ฟ๗๖ห>G็œฝืึพึ๚ญ ;„Uท {ฅำวซ’=xํฏโ6๏รmฺNฟผขk?@tๅ{FGโ ๔ฯ๐ปฏซ๓^)!„˜เ,”B!DnไU ต๑ฃD–>ˆfล๐๛Oโ๗4รฅŒ ป˜Œ ฆ0L๐\Œฉ๓/ ฆœษ—qŽ๏De‡A๙Wงำkฝ้Caะโป$Ÿ:ฮัง!๐๓๎82*็ฟ๕˜ี‹Ez๗๗ร็ ์;ๅf@)ฌkฑ็†^\=าžก`่ ^KธMฯใถ์ ฿VŽณ›~›ศŠw‡#๑:้๐{[ฎซ๓^)!„˜เ,”B!DnไS ฿๔q์ล๗กYQ‚พ6ผžfpS—ีd4˜า๔p*Ÿ ‹ŸT\Lน-๛qO์@นู+Liั"b76zq สห’ฺ๒Uœใ;@yw™ี‹ˆo๚รฐ–RคŸ๛nX| ๎3ๅป ๖์ Xณ7 Vž{1๐ ;ร๖jูผ๗M๏วุ๚นแํแHผพ6’?๛โU/คžkH !ฤ๗f ค„B!r#ฏฉอŸย^xš!่m )/fปœœ1e วJมŽฃลK0`X็3Cธm/แฟB‘/#vใั‹ชQNŠไ“_ฦmฺ yุ6k—ฟๅcSๆBเ“z๖;จกำaˆt…)฿Gณ"XณึcีฏA/จ8๗bเใดใ6์ยm}ฏํๅหุฦY๚@8ฏง‰แŸผ6๏ IDAT9ม@วuuK %„›%B!„ศ| คw~kmhfฟท%œฒ็eโงŽŸสงวKมŠกลKะๅcŠiC๊รk ๗ุ6”๗ Ÿ>zษ4ขซ฿‡^X‰ส“ล_แถ๎ฯห@สช[ElใวยํพGj็ทQร=จซX‡K)ะฃa05}%Zฌxฬ‹~Oฮ๑xญ/เuพไฯ฿๚ ์E๗ YQฎใ ?ƒืีy/”BLะ+@J!„"7๒*บ็ Xs6ข6~O3~o3xWjEผ‹Sv-V‚ž(Lฉt?n๋K8GŸF๙.šnpนลป•็`Tฬ&บ๊=่จ๔รO<Œw๒UR!๐kฤšนŽุ†฿ร(ซGyYา;Ž ู A ดkzผkึzฬiKั"็^ |ผ3วqoวm{ๅ’Vห‹฿i์w ™ผฮ#$Ÿxˆ`่ฬuuK %„R$B!„ศ ค4๗} kึบ0๊n M๛ฮžqมTขอŽกลŠะโc‚)ฅาx-๛pŽ?ƒ๒=4ไRร$ๅf0ซY๑k่๑2‚T/ษŸ๙eไ™Lฌ9‰ŒาZ”›!ตใoQ้h7l๔xึœ ˜U‹ะ์๘นํe๑Oล9ฒ๏ิมptw ึ[ร@ชใษ'Aฒ็บ:๏%Bˆ zH !„BไFR†Eม}Yฟอฐ๐ป๑๛ZภฟZSยฮ ฆ ฆ„มTด-VŠfFFž์‚แnึp?ƒ 4รโ‚)•Mbึญ"บ์hฑ‚แฎp5ทฎyyู๓7[๗แฐ@ป“"ฝ[ูกซถBแ„๛ีsPnณj๖[1*็ขYัsฏ;)ผฮร8‡Ÿฤ๋VŒถ•rRh‘ฌ้ซ0gฌม(ชLษ†]8'v[๛Aฬ๊ล ้8 ฯ‘z๚kจl๒บ:๏%Bˆ ๎H !„BไFR…SI๗็˜•๓P* ่n GH]“80>˜Š W…#ฆฮ†S† Jกœมเ)–ธ ฯA3m‚มำDVผ{mhVฟง‰ก๓)Tv8/ฃ่ช๗Y๙๋่๑R‚แž0 <&วЁc‚)฿Eaึญยช]Ž^T๚ู`J๔w€E• ็ศึp๚แ[อqr@J!&ธcH %„B‘yH•ิRp๏1*fกฟง™ ฏT0y~คล(ช‚HAX=R€ฆ[ ”“ย๏kรk๏ไ+ƒงˆฎ๙ึœแjnง1_ŸFน้ผ<Žขk?@tูปยzXรgH=๓(ŠษH>fŒฅ@aีฏฦฌ]Ž^Pš>๒>พW)œ†gIm๛*=p]๗H !ฤw ค„B!r#_)ฃ|&‰{พ€QV‡๒‚พ6Vฎํ”ฐ X1Œข*ดH!DโhV M7!Pูaพ6ฦ]˜ตห1k—ฃ&ฉร ไsy;'ถ๎รDnxZค`๐ฉํ฿mฬ(ฒษ๕ธq๎wi:zมฬ้+1k– วหF~wHe‡ศ์wœ†็PC](฿น.ฮ{ ค„b‚;„RB!„น‘7Tๅ<w)FI-สอเ๗ท๔ถ29Vฃธ:œฦgวั" จภEฅะ ;(ถ—๏ฯ‡Cw๒Plร๏Yršวh'ฬทA7&w๛Œ ฆt ฝh*๖œ[ย:ec Ÿป๎FœCฟฤmูึ๙๒ฝผ>๏%Bˆ ๎ H !„BไFพRf๕bw zq *›$่ภ๏mar#[ป•ย(Ÿ‰^\ฎบงแtCM~_+ร๕‚dO^G๑Mว^xwXซท…ิŽoฃ™‘ผhŸณŒŠ™ฤozขltๆศ?PูaผำGqฏํe‚ฬ ~^ถ—RB1ม=[)!„Bˆศ›@ชv9‰;?‡^X‰ส โœ kHๅSเนhv ฃb6zมดHœัฐรอเตพ€ฒ็๘Tf0ฏถ-พ๙Sุ๓oG3m๎FR;อŠๆี6ƒงA$๎8œ๊Œdณใแ ‰#SUzฏูCฟภ๋8ˆr’“ซ–ูฅ‹H !ฤEI %„B‘#๙HiX๕kˆ฿๑i๔D9AบŸ`เAษ|์๊‚C•bTฮE Pn•๊ร;sฏe?ฮ‰จl2/ถ)qื็ฐๆl ๋a9Fz็?Œn๙#๎Fณใ$๎๚ยŒ YQ์waV-ผpฤT_nห~ฦ๘gŽ_ี`J/จ เ`”ีฃฒIœ#Oแ6๎]%0šลฤจ˜M์ฦ‚fเ9Fj๋#๘o3 {ึอุ‹๏ งhF‹รขTfฟทท๕Eœ#[;ฏZO)!„˜ ฯ!”B!Dn ๓{”9ํฌk1งSš>๚z๊ ‹gŽื๑:AบTฮaดุอฟKd้ ภ๏mม๏m/“ง{]G/šŠQ9อŠ๔Ÿ$ฝ๛๛จห ุ”็€—A/™Žฝ๐"ม”๏๔ถเ6๏รmืu+?•O/ฌค๐]_C/ฉEe‡ษ|ฏ๕EะดอN„๛ว๗ 2๘ a;5‚sl;nร.”—พโ#ฆ๔ยJโท}kๆM็ฉฎ9ฏeuๅ6ศ@/ชมฌZš†ืy„ฬuลฟFe†F uฯ ฆๆ^ธ*_O๓h0ๅtŒuv้ก‡Q9‚ท=^ŠJ๕“y้?๐{๓ฏ]T€ฝเN์w‚ p›๗’๒UTzเ๊>X1๔ฒ:"Kฤšน=^2ฒG‚ฉŽpๅห†]x'_๛—ผู`J)!„˜เš,”B!Dnผa 5ๆแืžณs๚ ฬiหะ‹ฆŽ 7*3Œ฿u็่Vœฯข๔ซ1ฅN%~วgฐ๊VRm##p๒ดฯจ›่EU˜ี‹๐N$ณ๗_ฏZ๐0˜ฒ0ฆฬยž{+ฦิ๙Nๅ๋i ๋ƒ5ํฦ่ผฌัgfอ +ดhAชฬ Nะื–ํ๘ุKยัJจทaษญ_Ee“นyŠbL™Mไ†ทcี.G‹ŸkC'ฎœุ๚nำผS‡ฦ›—{.H %„\‹%B!„ศKคฮฑfญว™สงWL ŠUvฟซ็ศœฦจฬเ˜ฉFoŽ^\MโŽฯbึ.Hอ฿ฎ›่Eี˜ี‹๐NพBfฟƒฎ_ีฏ=L™U๓ฑfm‹ชŸLy a}ฐฆฝจdฯ%Mร4งฏ เ‡ั"ษ2๛xๅ ็’๏YฌY๋A8วwฺ๚สอํjŽzผc๊|"K฿†Yณ-’8ื†ู$~o๓่ส‰~wรุว(.5˜’@J!.N)!„Bˆy3ิYVZฌู๋1ซกUฌฆ ย็๎œcqžEฅ๚฿๔o4Jk‰฿๑9ฬš%แhžvำG๒wง๋ึศฉ0r[_$๓ย‡6ฆFืีค2C`F0ซbอบฃbึธฉ|๘.^ื œ#[รขฺฉ_LY๕kI๗%4;N0Mf๏†ป๒ฎY”๏]๙X3ึ†ิัm$ท>rอŠ็๋…•˜ีKˆ,นฃf šaŸ๛ญูpDขธทy~_ุ๋ว)(˜’@J!.N)!„Bˆysิ๘^ซn5ึœa0U8อއ+๓ฉ•ฤ๏nยix็ุ3c–ฒฟtFY=๑ป>9u~X{ #ฟ)รฉช…ธอ๛ศผ๘#4ำฮEW{ดํTfอŽcL]€5sF๙ŒฐํF(/Žv;๚tX<ัpฦšตžฤ=_@ณขCgH๏๙Tบ/๏šE๙.ฑ5๏วœพ็ศ’[ฟvmk•izQ5ึ๔ุ‹๎ญ;6๚›GpO<‡ฒŸ`่๔ฅmซRBq๑หฎRB!„น๑VFH]LญยšฝณfqLYฑpZเ‡5ฆzšpžร9พ}ฬˆฉ7อa”ฯ$q๗ŸbL™Rํ๙=eฯˆ WaN]€ธ‹์ห?†œRถ]L%0ช`อผ ฃ|ๆ๘โ็gƒฉรOโ6๎&ศ Œ+\oฯDฎฯฃ™‚มSคw•สปfQพG์ฦ฿ยœv>ูƒOฺ๖I๒„คc”Lรฌ[Edษๅ3ระ๗์oO๕ใuภixฏํฅ7\P)!„˜เr+”B!Dnผต@jด๛ฦุPษœถ {m#มTUXcJำ!๐ฒรแสnปqŽ>=fฤิฤม”1eNH•ฯ ฉพ6ฎใ๙ปำอFq5Fๅ|œ;ษพ๒šน]oฮฆฬš%˜๕ซ1สgิ )/‹๚ูCฟภm~• Wฆ›;‰;?†E0ะAzื๗ยข๖๙ฦ๗ˆฎ๛p8•2๐ศxœิ3฿šdOJ:ฦ”ูX3nฤž;FiธSAฒ๏ไ+8วwเตฟึpป ค„b‚หฌRB!„นqeฉ‹3k–bฯ฿ŒYณtd*_, ฆ|— 3Hะ‚ำด็๐Sa]ฃฐ+ศ๙ม”1u>‰ป ฃt:สห๔ตWฬ9ฯ˜Q๔โฬสน8Gท‘=๐ำqแฯ5่‚sA05}ๆ๔•ฅำวOๅsR๘gŽ‘=S๖์ฤo๛#0,พ“คw#\B1๔I'๐‰ปS็ƒ๏‘y๕1าฯ~gr>06Fๅ\ฌY7cฯฝฝธzฆ w……ฯ๏ภ๋<„rRใ^—@J!&ธพJ %„B‘W3:หฌZ„ฝ๐ฎpUพยส‘S๘Azฟทฏe?ฮ‘ญฉณต‡ฮ$fี"๗zqMH๕ถเ๗4ๅ๏Nทโแฉ)sp?I๖เ/ฎq ล๛]e†ะbล˜ตหฑj—ฃOC‹ฤGพ๎:ˆrำX๕kะ"๘ฝ-คŸ.uตทษD>๑ลจ˜R/'้]฿›NV ฃj๖์XณืฃL๗z0ุ‰ธ็ฤNำGQ^6Vๅ?\๖ษษP!ฮปฎJ %„B‘นคฮ2ฆฬ%ฒ๔ฬฺe่‰Š๑มTfฟปฏ๕%œใฯ ;ณf)‰{พ€^XN๋i"่mษ฿n'0Šk0*f}็8GถL’@jดKฮธ`*^Š9mYL•L;ทขbเฃ|7,ศฎ้#ิw๒ฒY”Rฤ7~ ฃ|๘.™๔๓?ศจHณf)ึ์X3ึข'สวn~_;๎‰ธ ฯโu7ขผดŒBˆ‹]O%B!„ศ\RgSๆY2LU.gฏ•ฤ๏mมk{ ็ุv‚มNฬช$๘K๔D9สMใw7๔ทๅogืN —ิ†ม}๕'8วถ„<“๎ืr~0eีญฦœถฝธf$Dณ๊››!๓โ๐Z๖ฃE ๓ฎmbท|ฃดๅ;d๖ฟd๖0ฟŽญhVํrฌ9ท`ีญD‹•œ{1๐๑๛ZqŽ๏ภ9พแโเ!น !ฤyืQ ค„B!rใZRgณย`ชnU8•ฯ)๊ํ{™žfใฯ]>๔โ๊ฐ~QOAษํ์F ัKฆa”ีŠ์ห…sb็$ คF5cƒ)ฝ ณ~ f๕Œ’i`Xฃ๏T™Aผ๖ืpN~wใรFๅ‡ไ („็ $B!„ศkH 9Œาุ้Kภžu3Zแ”ัSจ€`ธ •ซH!*3Rƒช•๙&ug7R„^Z‹Q:Pd^๘n๓๓hV>7็˜*ช"ถ๊=•๓ฦผG…กbชฏ๓0nรshั‚Iพi๑>^8ๅeI?๗]ฒฏ$฿Žฎq็„^8ซ~ ึœ˜5KฦcษP!ฮปŠJ %„B‘ื6๓ญieuุ‹๎มžu3zQ่f๘ฒ Fฆƒ p›๗ก2>„O๚ฮnดฃt:zษ4P™?ฤm{9ฌร”?[(‚ฌyทป้CaSyYPA*j:สwPษ‘`๊ฤ‚แ๎ษ;•O7‰฿๖I๔‚ ”›!ฝ๓๏ษพxพ>R;'๔’Z์Y๋ฐfmภจœ‹fE%Bˆ‹]=%B!„ศษHyˆ6 Œา:์๙ทcอบ9œv6˜แw7เถพ„fวั sย‡๐Iู•`”ึกWCเ“๛ผS‡ฮ– าXsn!บ๚ฝa๔4 œB/ฎF/ฎ>Wฬห ๗เwฤ9๑,*ี7&˜šํฆ้ฑ?…/C9)Rฯ-ฮแ'๓ัjพ5*faอ\‡5๋ๆ‡อช…ษP!ฮปjJ %„B‘“+ำ!4l๔’์น›ฐŽ˜ฅP™ažfผ๎tำ˜๐!|าm[ผฃฌฝp*้]„฿p6ไๅคฑๆl$บโธMฯ“๓}๔X1‘Žฦ‰•ŒิS('M0t๏ิAฦจtไ ฆฬ‰อŸB‹• ฒIRฯ<Šsd๋๕๒ˆ5n฿S็=\๔›๐\…โผซฅRB!„น1Yฉฑขซ฿KlGFK1nUท‘"็~ื‰p4ŽฎO๘>i:ป๑rŒฒ:๔ยJ๐=Rฯ~‡ ฟ}์fๅ ภšตŽศฒwเถ์#ฝใ๘}'ม0ฑgoฤ^pFๅ<๔h&(…r3ƒง๐ฺเ6?ึฃบึม”%q๛ฃE‹PูaRพsl๛๕๖จ(”๒.๛ไฮ‡ไ („็]%%B!„ศ|ค์E๗ฟํะฬ*r3h๑าsซ๒ึ.๒ป๑NEทข i<„Ozขฝฌ>ฌUไ;คŸ๙[‚แฎ<€ ซ~-‘ฅเ6๎!ต๓๏:ฮํ}3‚5g#๖‚;1ฆฬA†ฃม‚ๅฆ :๐:เ6ํE9ษkLiv‚๘ๆKe2 IDATOกE P™A’O๗ฤณืๅyฏ”pู'w<$W@!„8๏^ ”B!DnไC นแmฤ6~ อŒ t๗oD—ฟฃ|ๆธSสwz[๐NYQl๒’า ฆ„TขๅeIm•๊อฯŽปวฌ_Cd๑ฝ8'v’๙‚กำพืŽcอน…ศ‚;1ฆฬF‹ŒชWู$@^๛ซธM{มหไ<˜า์โท1šGฅ๚I>5ฦืๅy/#ค„b‚{RB!„น‘S๖Vผ;œฒgX'I>๙7x‡0Jk1๋oฤž;ฦ”Yฃดa$˜๊;‰ืyอฐC`Rl^85 คโฅ(7C๊้ฏฃฒC๙y™Q์Y7c/ผ ็ุ6าฯ~๗WŽ๘าb%ุs6†ํV1-gt*Yfฟฟ ฏ5ๆน ฆhฑbโ›?‰fลRฝคถ<‚ผ๗บ<๏%Bˆ ๎QH !„BไFิบ้wภ0๑๛ZIm^ว๋แ‹†‰^8ซn๖;ย"ฺcฆ๒กพถpฤTด,(šn^X…^>=VŒrR$ท>n:?;๎บ‰5o3๖อ8‡Ÿ"?ผแˆ/ =Q62•๏Œ๒™##ฺ*;Œ฿ำ‚ื๖n๋KธW7˜ |๔ข*b›>Žฤ๎"ตๅซธญ/^—็ฝRB1มI)!„Bˆศ‹@๊ฦ฿"ถ๖ ›๘=Ma ีy๘ผคŽ^4ซvyXซจjแ๘` ๐๛;ยฉq^– wMถG/ฎม(ซ?W<{หWQพ“ง=w {แ=ุs7=๘้]„J๗_โฮ0ะc%a0ต๐nŒ๒็ฺM ฆšq^ฤk{๏ชSส๗0ส๊ˆm๘0:M๒ฉฏเ|ๅบ<๏%Bˆ nkH !„BไF>Rฑu&บ๚ฝ ๘]'Hn™ccปŒ &๔โjฌฺX๓nลฌZ„fวว}^0E0ิnš =›ำํั‹ง…มหH๑์ิ–GP9 WLนแmXณ7}ํงค๗3*s™Su=Q†=็์๙ทฃ_L แ๗4แถผ€ืฺฆ”็`Vฮ%บ๎รฃตส’[พ‚ื~เบ<๏%Bˆ‹“@J!„"G๒"ฺ๐๛DW:h:้ฃคถ~ ฟปแbH.Lอ€YฝxL€ ’=CgภMค!Gฃ”๔’้แฉH‚ ีG๊้ฏAเ็ๅ๑ฃ|่ส_วšq#ูW#ฝ็_Pูแ7๗#S0ํY๋รSๅ3มSด>3ˆ฿ˆฒฏUP๊ŠSสMcV/!zำ‡ะ ฟฏิ–ฏเ:t]ž๗H !ฤลI %„B‘#๙Hล7‘e๏ ฉSI=užๆ_ีd\0UT9m)๖์ ˜5Kะb%ใค๚Pร]จ์๐H0•ฝชฃ—ึ…”'๎&ตํ‚ ๒๓ |"ซƒUท€ฬKAf๏ฟขœิ[๛\รย(šŠ5{ค๘yY=่ฦ่ห*;Œw๚(^ห~ผŽpำ[ ฆ”“ยœพŠุš๗‡ตสz[H=๕7xง^—็ฝRB1AB)!„Bˆ˜๔”ฆฟ๕D–พ 4 ๏ไ+คถ}ฟฏ๕R˜qมTa%ๆดeXณnฦœถ=^6!=3D0ิ๛*SzษtŒŠYhV”`่4ฉm฿ไชญwต)Et๕{1k—ู๏d๖u…Šดkfฝฐk๖†ฐ๘yู ะดs_?LนM{๐G๊Šฝ™`Je†ฑfDtีo†TwษงพŠ฿uบ<๏%Bˆ ๎;H !„Bไฦคคt“๘mDd๑} iธญ/’ฺM‚๖ห้^rA0UณkๆM˜ำ–กTŒXw’ƒจT?Avผ+L้%ตSๆŒึ*J=๓ญ|๎บ]>ฬiKศ์W2/ๅfฎ์ท˜‘p5ลน›ฐ็ŠQ:๔1S๙ฒCa0ีฐ ฬQ@ปฌ`Jeฑๆl"บโืยโ๙ง’๚~wใuyK %„o$B!„ศษHif„๘ๆOa/ธ3 คš๗’ฺ-‚มSoๆำL%ส1k–`อ\‡Yป ฝ` h๚น‡v'I0ิ…J๕dมsธ#™ฦR~_+ฉ‡6fิO^ัuขซ?€Yฝ€๔ž๏“}้?Q^๖ชzษ4์9ฑๆlย(ซ฿fู!ผฮ#ธป๐NEณbhฆ}ั๖+H๕Yx'‘บ‰ืyˆิ–G๐{[ฎห๓^)!„˜เ>#”B!DnL๚@สŽ฿วุ๓o4†็Hํ๘vXŒอ*cƒ -V‚Uป kฦ˜ำn@/จ_H;›$H๕ ’ฝแช|^–ทL้%ำ1*็„ลณปI?๗ใB•ผbุDืผs๊|าปพG๖ๅฃฎrxอŠข—Lวž}3ึ[1J๋ฦOๅsRxฏใ6ํ!H๕ž7สํย`*H๖Y๒‘ฅ„ตสฺ_ k•๕ต]—็ฝRB1มE)!„Bˆ˜๔Tค๘ํŒ=wฮฑํคŸมp๗•ฎhๆดฐfˆUป ฝp*ึน‡x7C์B ๗ค๛มsห/Fn”ึaL™†…ๆ8้]8ฎ`w^1"ฤึ~ฃr. Hํ{ฒฏ|77ว‡A/ซรžฝk๎-%ำ/ ฆฺ_ลmฺK๎›p๚e0Cdู;ˆ,พ7 คฺ^"นํ—954H %„W$B!„ศIHลKHฌY7เ~Š๔sŽzนฒ฿ฤูQ3šวฌ]5cm8•ฏจ อฐฯ=ฬ{‚แTฒ‡ ีพsYซไ้ฅu˜#”ืyˆฬž๏ซ‡”Ww+Ftํ1*f Hํ๘6ูื฿หํ๏0#3ฑfoฤšฝฃxฺ…ซ๒uผŽธ› 3pA0${‰ฎx7๖‚;@ำq[๖‘ฺ๖่›œ:๙I %„O$B!„ศษH้ฤo4ึŒศพ8้฿Gฅ๛ฏVW”ั้\บŽ–šฑณf zQ5š=๗P๏fย๚Rษ^‚dฯHฉ7ฆ๔า:ฬสนaญข๖ืH๏๛Wด| ค"Dื|ฃ|ฉํ’=๔$ต๙=vฃlึœX3oย(ฉ=/˜ย๋8S้ั•Uฒ—ศ๊฿ฤž{kH5๎&๕ฬทโิะษK)!„˜เ>"”B!DnL๚@ชp*๑;>ƒUท €์ซ‘~จฬเU๎‘j ฦSkรU๙ช†ม”# ฏยโ็*=H์ ง๚ฟj*ŸNูซœบ‰๚"ู—๓์Gๅ_ว=Zސ*พKr7pŽlภฟถฟหŽc”ฯฤšฝkึzŒ’i็ฌOแ|ทแ9Tv˜่š๗Rฮ๑คw|; ฏCH !ฤ๗ ค„B!rcาR%ำHiฬฺๅd^฿d๖•ฮUื”s#ฆ ฌ๚5a๑๓๊ลแT>;JกTLฅ๚†NS็ํ^M™ฒ7; คšž'๛๚ฯ!p๓๒๘ัใeDืผฝคๅeIm}็ุ3—5…๑jถ™fF0ฆ.ภšu3ึฬuลีใGL9)ฆ็q›vc/บk๚ส0:ฒ…๔ณ฿ฝ SC' ค„b‚;ˆRB!„น1ู)ฃฌž๘ํŸฦฌY(2๛ฬ‹ๅคrEelธdีŒ˜ชY<2b๊l0 œ4*;Sƒงฦ๘ึอp„Tล,ะ ฯโ†rำ๙ูqOT[๓~๔โj”›!๕ิ฿เ4<{ntูตuŒ ฆชcฯ€Yท ฃจzJŠ้~@C‹‚ฆ“}็คŸ๛nƒฯ’@J!&ธsH %„B‘“>š2—๘ๆObV-ฅH๏๙>ูW๋8็Skฐfฎ GLWกู‰0˜ |”“Be†Rฝแˆฉภรย(ŽQRฮัm8'vLธ๚dงV]๓^๔ยฉ('E๒—$ ค.l3อŽcV-ยšฝs๚Š‘`สบเ/ฦ=คw~OำuyK %„1$B!„ศษH™U‹ˆ฿๖ Œสy๘คw}์kบๆฮ๘`สœพ2}Sณฝpd*Ÿ>Le† †Nใ๗ตขY1๔ยฉSf‡Sร็๘3y{่%ำˆฎ|za%*;D๒‰ฟฤm}‘ ฆ*NŽวŒั฿ฅE˜SŽSซ0Šชฦ˜"๐p๗=๐3๎†p5ล๋ˆRB1มB)!„Bˆ˜๔ิดˆ฿๚‡แ7฿'๕์฿ใล$ค&๘ฝำWbฯูŽ˜*œŠfลFƒฉ =€฿‚Q\ƒ^4ะศ๘)ฮฑํใV๏ห'zi=ั•ฟŽ^PJ0๓‡๐ฺ_crRฃฃฟOณb˜ำn๋Gอธ๑‚vP™Aๆ}d>฿ำŒJL๒mป4H !ฤw ค„B!rcฒRV*bท฿ๅ3PžCzว฿แ~ ๅ;“zฟšำW`ฯZ9ํ†ฐ๘นMC๙.šf€aูW#{เg่…•yyๅณˆฌ๘5๔D9Aช—ไใ_ย;u(_;8.™ี‹‰฿๙YŒาบ‘้†jช|Aชฏe?ูƒ?ว๏iAe†ศ็`J)!„˜เฮ ”B!DnL๚@jๆ:b?ŠQZอ-œcฦ Ÿฤฬšฅุ๓n ƒฉยฉhvl|ะ1ุI๖๕'๐ปŽ“‡Q9่ฒwกลK’= ๔ฯ๐ฯฯฟํ(ŸIŽฯ`N]€๒‚พV4+ŽVPfFFฅ†ฮเตฝL๖ภใ๘}m#Eฯ๓ฏ$Bˆ‹“@J!„"G๚ฝ]Mๆj{ฮ-ฤ6^ธŠ›“&๕๔ฤiุ พ—W๛ูฌZˆฝ๐nฌ๚5แh(_|‚T/~O~วAผ๎†ฐ๘y>P ณf ‘ฅoC‹ a๘ฟ4/ ›ี‹‰o$Fล,”“&๓โ†บฐ๊VbV-LAฒท๕Eฒฏ” $สI“Oม”RBqqH !„BไHz็฿+ทีI;ชล^p'ฑuฟƒ^4•&น๕ฆ็๓'ด9Oไ†ทป้Chฑโ‘d€pๆ˜๏ค๛๑ป›๐;แ>AึŸšฌs๚J"K๎G‹ œb่'Bะ2๏ฺลฌ]NึO`”ีฃ|‡๔3฿"{๐h‘BฌY๋ฐfฌลœบญ อฐร?๒]‚d7nำ^ฒAะืŠ๒๒!˜’@J!.N)!„Bˆ\=˜บiๅwภ9ถฏ~wรค๚}‘%๗ฝ๑ทยขูฉ’[&\ล-๐๓r›ำW„มGi]8า&;Œfวัฬ0ไPพ‹J๕ใ๗4ใ|ทใu4 E '฿ฑใปX3ืYt7š]€฿฿ฦ๐cB0t:๏ฺลช_Klำ`”ิข4ฉ฿ฦ9๚๔่ิP-Z„=็ฌ๚5•sะโeฃ#ฆ”—%๊ยkK๖ะ/๑๛ZGF๐Mg ค„โโ$B!„ศแณ)€r3๘gŽแู‚ื๑z๘P= ๚d‘e๏ บๆ่๑ฒฐh๖“_ฦ;๙ จ /wถ5c-๑MG/ž†r†ษพ๒Zผ๔\๑๓ณำย| ‡฿ำ„ื๖2^๛ซ€6ฉ‚)ๅ9ุs7aฯฟอŽใ๗63_Ÿ%H๕ๆ_ปฬ@|ใGั‹ชQN’ิ๖oโ฿92๏\๑s=^Š5wVŒŠ™h๑า0LT ผ ม@n๋‹8ฏ?฿rางH !ฤลI %„B‘ณP!ซฮmG{๘งแ†ื@ว5]๙DVฝ=VL0M๒—w๊๕I–ฝึ์ ฤo๙za*3ˆsl{2ฉ-V‚^:#Q†Aเค๐ป๐NพŒืZุažม”rำุ ๎ยžwšล๏n`่วŸAe‡๒ฎ]์y›‰m๘zA%*3Hj7pwMx์๋…•Xณ7`ีฏล(Ÿ+ง๒ฉๅฆ ๚รS~J0tf๒๗H !ฤEI %„B‘#™ฝ?Pึฬu่e๕Œ ฆ4ฉC8GŸฦ๋8@0p๊šŒ๖ˆฎ ัๅ๏B‹ v’ล_โ>F>ฎl`ฯ฿Ll๏กTคpwซIคแ๔ฝH!zแ๔x9ๆHƒ#มิqผ–๐ฮ฿นฆม”rRD–>ˆ5{šaใ9ฮ๐?…๒ฒ๙ื.‹๎ k•%*าคถ>‚ผ๏ yฝฐ{๎ญ˜๕k0สg G‹@7!๐Qn ฟ$nำ^œCฟ$๎š4+”B\œRB!„9า๗ญ•9mV*ฌ๚ต่euใƒฉ์0^็!œ#[๑:^G{ไ0˜Šญ๛0‘ŽIเ๗ท“|โa๎&๒6Ztฑ›/๔x)Aฒฏํๅ‘m9ป=่&šE‹กTŒ1ฅTzฏปฏy/~w#สM_“`Je†‰ฌ|7ึŒ›ะ ๏ิa†๛tญ€Y๚`Xซ,^F์!ตๅ+ธm/]๒H<ฝจ{มํXuซัKงฃG ร0ั๗ฒƒ}mธอ๛pŽlม”RBqqH !„BไH฿ฃ›รŽ—ฆaึฎภช_ƒUท ฝคอŠŽ †๐Nฤ9ฉƒษžœSฑ%ฒ๘pJXo ษ'ย๏;™ท๛;ฒ์ฤึZฌˆ`่ ้ฃเeนhภfXhf-Zˆ/ร(œ:fฤ”Be๐ป›p›๗โwศy0ฅ2ƒDืผณn5šfเuผฦะcŸหห๚^ั๏&บ๚}aป w…ตสฺpนมงQZ‹ฝเ.ฬบUล5h‘่FLe๑{ย๖rn#H๕]ณํ•@J!.N)!„Bˆ คF{b:Vjฬ๚ีXำWข—L?b*3„ื๑ูCOโŸ:Dธz5ฆ4๘ฆ?ฤ^t7šaใw72๘—๒rท‘ "บ๊7ˆฎ~/Zค€`เ~_*3๐ซฬฐะฬš@+šŠQ0%œvถMฒC๘] ็‚)'•“`*Hป้w0ง/ภk{…แ|^ถLtอ๛ˆฎ 4;N }๒ฏ๑:ผ้ฯ3สgb/ธ#1U\…fลAำยUำ๘]'p[_ภ9ฒ•ฮ๙๖J %„ฉ%B!„ศ ฉั'jซn Vฬฺe่ล5c‚)…JเuผN๖ะ/๑N ชฏt0ฅฤ7 {ํแœฎใ$๖ลk:ฒไญ๕r๕ฐ&ึŠw‡#พ:๚Oข2ƒ—ธ?ฌp*ŸE+ชย(ฌL9)ฎใธอ๛๐ฯG9ษซL้โ๋?‚YsJ๙xญ/0ณ?ฯว†!ถ๎ท‰,{'šร๏?I๒—฿uโ-ฒ1e‘E๗`ึ.WQuจ<•๊ร๏ƒD็ุv”“สูK %„$B!„ศ ฉณtkๆMa0Uณฝธ๚\0ฅ‚T^๛ซ8‡žย๋<„rำW.˜2Lw|k๎-h่x‡ูฏษˆ’+B7‰ญ๛ฐ&–iใœ"่mE9ร—9šGณ"hS0Šซ/ ฆบรSงขZคเŠoN๎'พ๑0ซขทiษ'G>}่ฤึ.‘ฅo ฅท•ไ/ฟทๅ ตปY9{ัa0UX9z)7ƒJvใuภm‡{bgN‚) ค„b‚[‚RB!„น๑†ิhMรžณ ณ~5fอ๔‚J4+h๘ษž0˜ฉ1ฅ<็-ืาฬ๑ป>=k= ๐:0ำ/ไๅ*n๑ ฟฝไ~4ภ๏oว๏m๗Mบ‰f'ภฐะ่%ี็V์#\)ั๏iฦmฺƒ฿yๅ9hv๔ŠmŽJป๕˜•๓Pพƒ๐ษ'ฟœํข›ฤ7~{๑ฝh†N }โแpeษ+๖„ฃก6ฦิyDScŠี+/K0ิ…๚pL5๎รซD)!„˜เr-”B!Dn\r u–abฯฝซnFีb๔ย)hฆ haแๆd7ษWศŽS%ฏTvAงะŠ‘ธ็‹X3ึ Tึ(๚ูฎ^อชซษ5#ฤ6๖ยปะ4= คzšมหผต>L้zAลyำ+รฐร๏iฦk~ฏใ สwฦฌณTf๘mŸย˜2ๅfpŽ๏ ต๕‘kร&v๋b/ธM7๐ฮ#๙๓‡†ปฏย—้hV4œสทไ0˜Š—ŽŒpS(7K0|ิก0˜jฺsUX ค„b‚หดRB!„นqูิู›ลšฝkๆ˜S %สGBึวI๖เถฝ„s๐—x‡ฯ>_wD$๎๛ ฌ้+Qพ‹ืฒŸแวฟ”ฟ\+Fถ?ยžทเ$~w๘ฮ๚-RšSฅำว˜ยw๑{[pwใu@ธเ๊r)'EถOb”ฯD9)œฃO“ฺอk3ึ*iฏ๓ร?•๎ฟŠ_ชฃู Œส9D–>ˆYฝ=QšžCn†`๐4ฉƒธMฯใตพpEƒ) ค„b‚หณRB!„น๑fฉัŽ›วž{+ๆŒต˜Sๆข”„ cชOพL๖ะ/๑ฯฟผฯŽRpร˜ำ–…Sย๒ตFั˜}ฟำุsnเ๗ฤ๏i G‘]QzXฬ\=V†^^?>x <พ6†็p^ รรธ์oQ^–๘ๆOa”LG9ร8‡Ÿ"ตใ๏๒ธ]6‚Rธํฏ‘๙_ไฆศธฆฃลŠFjLƒ9ํ๔x  …๛ุอ žย๋xทy/nห>๐ฯ/—๒ŽถRBq๑หฒRB!„น๑Vฉัœล^x7VŒ)ณัb%ฃฃs”›&่ภm}็่ำ—ผz™+ฆเมฟ‹fปY†g๓ณFัูํ‰’ธ๋sX3oF.Ao[HฝลZ[ฟ๒;ฃE่๑ฒpฤ”?๗bเใ๗Ÿฤmzทe?่šฆ]๒g+7CโฮฯกWฃ2Cd‚๔s฿อรvIธ๓OฐfฎเถฝL๒๑/กฎิศตKก่ฑŒฉ ˆ,บณvY8ฺํ์พ๖ฒxm/ใถ์รm{้-SH !ฤ๗ ค„B!rใJRฃ9;Žฝ๘^ฌบีณัbEhบสG9iผึpŽnร๏i๚ีฯ่๑R ๖`Lrำ8Gท‘z๚๋๙ษ‘ธ็ฯฐ๊ึŒRญ๘ผูQ.—๛(…/E/ฉ GP๘๘ง๐š๗โ6?BCำ๕7L•M‘ธ๗‹่…•จ๔ู?%ฝ็_๒ฐ] IgX๕kQม˜ฉกืโ™DำัๅS็?_6.DT^– ฟท๕Eผ–}ธํฏพฉ`J)!„˜เ2,”B!Dn\้@jดC/!ฒ๐nฌ๚ต่ๅ3ะ#…`เ๛(gฟฟฏ๕ฒ‡Ÿณšู๘j=QNม;พ‚Q1+ฌQtd ฉํๆo'7^Bมฝ_ยฌ]Ž๒‚žๆ7 ๅฎ๘oˆ๒ัโ%e๕ใFแ ม`'nำnๆฝa0๕+FL™! ๎ฝ ‚ ีG๖ีวศ์๛ทk—XqX<ฟn%ส›$SC5 ฝ`JL-นณj1Z$qฎฉ An๋KธMฯใw3ข๋ƒ) ค„b‚หฏRB!„นqตฉณ๔D9๖ขปฑfฎร(™ บฎศ—$่mลmูsd+มpืธjฝฐ2 คส๊๙ูป๏(9ฎ๛ภ๗฿[U{ฆ{ยƒH3)f‚$DQYTX{u,ู–d’`ฏณฝึP–ร๓yปฦฺฒฌณ๖ฃd[ฯkหส –@`ŽˆŒม Ožžะฑช๎}žˆ ™๎ม๏s%Q“บBwU}ฯญ[ฆœงฒ๗! ปถnืต“j!๕ึNผ•Wู‘.C๖){‹qยKƒ1จXทuรไญ}€ Sใ}๖Vพ๎'1กrfฮ1eยฉ๛'ู„. Q~๎‹”žื๚.ษ&Ro๙ชk1A๐#ไฟ๓gต๓๚2ํx+ฎ บๅx+ฎœฆ‚2z๘8๑g๑>Npf฿ด9ษๆSค„bžใฃ)!„Bˆ…qฑƒิไEuช…ุ5๗Yw+NถI€r1ฺว”ฦฺI›?‚๋ซ^ˆฏคแ‰“]…)S๓ Š~ถnืตำฐŒ๔[จ‚X":†๊^MกชO็sšVแ$›˜˜Pcะcฝ๘วžถaส/ฃ\0˜าธQาo*‘EPz๎P~?๊oปคZHฝํผWฺ u๐‡ไฟ๗—ตv‰„“]…ทb ั-ฏวkฟfฦD๕&ฌุธ{๔ ๎'z€'vv˜’ %„๓|ฺJB!„X ค&/Wป๖ํDึ„“iGEโ€ฒaช#่ย?๖•ƒ;!(ั๘๓/Nรr;i๖พJ๑๑ฌuํ4ฎ }ง์-ˆ~=tŒp่Xmผธh '‘EE“จdNผัŽd0=Opโy*]ป1…aงค฿ฺ‰Š5 ว๛)=/”๔ต๚. หHฟํOp—mย๘%{k่ชv_ov5ส+‰^๑zผUืL><&ยิ1*Gvใw?I8pdฮ9ฆ$H !ฤ$H !„B,…RฦNvีด0ตrrด‡ ส˜bŽ ๗:wŠ่ๅฏลi\)RzแK”ž|ฎk'ำN๚ํjoAฌ‡ฃk%HMˆ$pRญจH•hฤIdภ๑์ืŒA็Ž?Kๅเ!'u฿๏ b)๔X/ฅ'‰๒K฿ชฟํ2+V^๚6…‡?]๓ฏmZƒื~5ั-ฏว]~ขcพ IDATลู#ฆปm˜:๚8z่๘Œ9ฆŒ๑%H !ฤ\g(ค„B!ฦโฉ‰+j7ณŠุีo#ฒแTบerด‡ ส˜J†‘HSศQ|๊๓”_ (ต8O@{ีกƒ๔;'ำnƒิะq๔๐ฑฺ|ฑ^ 'ฝlž0ฅัใ่ nำj;Bjไ ล'คฒ{uท]œL;้wnำLฅ@๙ว_ง๘่฿ืฯ~ีฒพ:b๊>e—aซLPถaชk7~ืc„ร' ๔e„”BฬC‚”B!ฤYิ eOPnงฉรฮ1ตVœTหTภ BŸสกvnŸ—™งฆรA๓Zา๏ { bู>ipั็๚‰/:Šำฐ IขโiœDธSaสn=ึgƒิ‡๊๎=0;–_2ลวก–ฃuสซˆnพwล–ท๒๚„ƒGฉzฟ๋1‚แc4ฺ:ๅP!f•HB!„X‹ค&OQ‘nห:ขW฿Odอkpาญ œษ๏0…ฑ'๑ปŸย?ŒXป๚ณ๕ฆึ ค฿๙แคZ0ๅ1๔ศยมฃ๕ฑฃ8Nใrฆb จD#ส‹25บ&8๓ฅง๘3v{ิษ9ฝฒฮฉj(,=ลบพ5ิm€ท๊Zข›ถแฎธbV˜ ป(๏}่ไถ_๋”O@!„˜u6"AJ!„baิNš<DEธ-๋ˆ฿๒๓Dึ:ใซ&(cฦ๛ z๖แw=ŽiL9?๙ณตฆๅ›ix๛Ÿฃ’YLq=ฺCX๋#คฮฺ^Ÿ;ŽใแคZ๑V_g}โ] ํƒž}T๖่c5ฆe›Hฟใ/p’Mv๒งฟ@้น[2๏qw๙f"ซฏ'ฒq+n๋†‰9ฆ:ๅP!f’ %„Bฑ@j9HE.ป‹ิๅละ๙A๔hNบ งaูิ7้=ฺƒโ9ฃOœ|ใ—&N+ฉฅ0ๅu@๚ญŸดOฅ+ฃsgะน๕zสF๒Pฆ<ŽฉPฑ4*j'ี6AS&่=€๐‡TŽ>aPsKโญุbCa"ƒ)ๆ(>๕ฯ”_๘า’{ฏ{ห7ใญพศ๚[!ภ[ถนS>…bึัM‚”B!ฤยจ้RWG๊พ฿ืC็NQ|๚_Žฟ้ฝD:n@ลงพY‡่ั3๘วŸล๏zœเฬLฅ0qzI-„ฉศบ[IฝๅQ‘:?„99Uงg์ส‹น์.ป๚sง1~PvR๚hข:วฟ\ S๛๑๏ขาต{Z˜Zmใญบ–๔ŸBลา๖iŽO>H๙G_[J—W3ึฑปl* แงWง| !ฤฌOL RB!„ ฃ–ƒT์ช7“ผ๗7ภ๑‡OP๕ท๘Oก’Yผe›‰^๙&"ซฏ?kฃpด‡เฤณ๖ibง\3#ฆ"ท’zำN๘99]งg์.*09วW˜;‰:(T"ƒ“jEล’เฦPีง๒™ ‚ษ๔ย?(•#ปj"LEึผ†ิ[@E“่ ฅ'คผ็›K๑2kr<ะผ}Wง| !ฤฌOJ RB!„ ฃฆƒิตo'นํื@9„Cว(>๒w๘OM~I6ใ.ฟœ่–7Y๓T,=๕รFฃGNใ{šส‘„g^ยๅE]ž้sb้๑~t๎4zฌง>wวลIถเu@8|; އrLขขITฒ ๅล์S๙œ`์ฤ๔๙!ฆบร?ดkQ็Šฌฟิ›ศŽ\๏ง๘ุ?Pู๗%พ— %„s“ %„Bฑ@j6H)E์บB๒žŠp หฉใฯž๕ญNชwลbWผoอkPัไ๔Kot๎•ฎว๑>NุณoัยT๔ส7‘บ๗7ํ-ˆc}v„ิXo}๎8އำฐฏ*๔๐qยi”™๑m& Q‘„ Rฑ4N<ฎŒ_ชŽ˜:‚฿๕•C;!๔'ทBM„ฝ์n’o};rmฌโ๎ฯR9๐ƒ%พ— %„๓œ~HB!„Xตคโ7W[P„‡(<‚S/ฮ๛#NชoีตD7฿‹ท๊zT,5œp๘~ืฃ๘GŸ ์;8ํVพ…ปๆ~;โหqัฃ=่‘S่๑๚qงฉoูๅ„Cวะcฝ(ว๓Q^•ศขโ 8ฑ†j˜ยŽ˜ ่;ˆ฿กำขแล15cไฺ่Š~–สก‡—๔๛^‚”Bฬs๚!AJ!„baิlr<โ7 ‰?@ะ{€โรŸ&8๓า\งLNร2ฆ6mรkฟo˜v%ฎ s'๑ปว๏ฺMุwhมFLลฎ)’w ”šœ?J็๋sวq"ธญ๋q[ึvฃวPŽzู3:DนQ{+_2‹M3-Lๅ‡z๗แ{ฟk7ฆ46็6พfŒ\9Maื฿แwํ^า๏{ RB17 RB!„ คfƒ”๋‘ธ๙็ˆ฿๚>‚3/Qx๘ำ„ฝ^๎4’™ajy5Lƒท๊Tlz˜2„นS๘‡wแw=J8p๔"‡)E5๏ž๑ฅsงGNa ร๕นใธถMธM„ƒGซAส9งuat0ฆœx#(๛ณ&(c Cฝ Ž?CๅศnL17๏v~ตฆ\ณ“็ฟ๛ษ%พ— %„๓ก$H !„B,ŒZ Rสฟํฤozมฉ)์4แภ‘s๙iฮ SซฏทaชšYท๒ู นC;๑ป#์พ8aสq‰฿๔žษ_แ๐I๔ศฉYกฅŽธQถธMk์๒ ta๒ƒpNAjj; œศT˜Jfซ}taˆ ๗มฑง๑ปG†ๆฮฏT์บw‘ผ็ใS“็?้9็*[J$H !ฤ9k2๘Wฆโ7พ›ฤึ_e'ฯ/M8|๒•6f„ฉL;‘ต7ูp;๒-gฯ15tœJuฤ”:Ž +ฏ~yf๘ ‡บันำ˜๒X}๎8^ทe-n๓:ป<Gะ๙กWคฆถ“ั!J9จD•nลIdง} ๓C„ฝ๛ํSวžBœ™w;Ÿซ๘อ?KโŽูๅ่;Dแแฟ!8ฝgIฟ๏%H !ฤOุฃiœl;nหzป<9HM0Z9ฆRอี05kฤ”_"์;€d7‰็f…ฅณร”๒b$๎0ฑ๋ํญ”ฏl๒๚#AJ!ๆ9พIB!„X5ค–“ผ็ใD6n ผ็woLi๔bž†2#Lตฌ#ฒแN"k^ƒบมŽ˜ช”ํˆฉ}฿ล๏ฺฮ‚็_žT ‰{>>ุ๔๐ ‚#–๋rฟQฑค&&5๏?‚)^ 5มhBกRMจd NrฺS๙&ถO*‡~Hp๒Eย#ฬ5JJE$ถฑk฿ผฺษ๓๋‡)!„˜็๘&AJ!„baิlสด“ผ็Wˆฌฟ €๒‹_ก๘๘?bส 1 ๘ฌ0ีบศฦญD:^ƒฒ๖์05ะEe๏C๖ฉ|…a0๚์ๅiXf—งุยIยพƒ็u_Mฐวqฒซp›ึุๅ้?‚.ๆชGM2:Nฒษฮ3•ศ€ใN}=ฌล?ผหŽ˜๊;4c๛จXŠฤึ_žผ•า๏ฺMaืgf=ฝo้‘ %„๓฿$H !„B,ŒZ Rnำ~…ศš›(=๗๏”žฆRXศำRf„ฉถMD/Šืq#n๓T,=๙=ฦ/ู0ต๏ป๖ฉ|…ŒŸุยมฃ„GAืiJdp2ํำ‚ิaLqd๚ ฅeย”ยIdQษ&œDธSaŠ0 >Fๅะรวž&่‚ะGEโ$_๛๋Dทผธะ“็ื. RB1ฯ๑M‚”B!ฤยจู ีบไ=ฟ‚ท๚zJฯ”ž๚gฬขL>3Lyห7ูtชkqณ6L)ฦ`*ใ๖Vฑ฿งrไัษ[ ฆ~u2ฐฝะน“็4TMžฐ'špณํ8ูี„}ซฃืww2a(œD•lฒธ‘้฿aoต<ด ศ#˜ržฤœœซฒ๛{{ ๆ&AJ!ๆ9พIB!„X5ค–mฒAชJOฅงฟ€ sฮฅ9ยิๅฏ%ฒ๊:œL;*šดท‹)๔กฒ๏ปT๖ทi ษื:^ว ง~„๏ฏ฿ •lถA*ณ 0„ฝ1•<‹คฆถ }ภฑa*‘A%›Q^tฦw‡]๘วŸลkปlrป”๗|“โใ€)ไ–๔๛^‚”Bฬs$‘ %„Bฑ0j5Hy+ถธ็ใx+ฎ ๘๘?R~๖฿0aฅNW™ฆV^E๔๒ืโต_ƒ“Y‰Š$ภq ัๅQ‚“/๖!ฒvผ๖ซ๐=moq3a]๎7Nช'ำŽ“ic๚ยขฉณทฯD˜RัสโdWUoตฌ Œ Q^ €๒K฿ฆ๘ศg0ๅ’~฿KBˆyŽ ค„B!ฦ๐_รฺZ{]ชkH๕1ๅ›(>๚YJ/|ยZQ4+LญบŽ่ๅl˜j\aร”ใ—1ๅqT$f'DวฮUDX™s๔zเคpWโdV‚ัฝภ/ิุซœฆ*–Fลา8ฉ–™aช*์;D๑‰ Nฝธภ๓•-, RB1ฯ‘C‚”B!ฤยฺqwง2v2ต๒šผŽI๕หธm—†ยรŸกฃฏึ่-nณยTวD7฿‹ื~5NชMT+“O็๓?‹)ƒฎำR้eจL;nใrะA๏AŠ5๚j็1•h@ES๖้|ีH8A๕แ}‚สม$8บ@Ov\Xค„bž#†)!„Bˆ…•ฑ-ซัk%LEึBb๋/แถฌฃ)์kส{พYW'ฒๆ&ข[^ทโJTCสšรศ”FGz0ลฆฆN…ฉ^žำฐ'ณงa„A๏~ห5ชงญ[Sืh7ณ?{—ื๙AฃSู๛ยกcK*LIBˆyŽค„B!Gญ„ฉศ†;HlEฆ5  ?๘+ส{‚:@8|โฌ๙คj;Lู๕Vเค—aย a๏{ b}\nLฎ[S)โดฌรอฎbยŠ ‡ ะ…!‚๎ง(๏๙แะq;y{_ฏHBˆyŽค„B!Wnวถฌ1fŽ๛ล๘๛ัM๗ฟCธMซ!๔ษฆฒ๛ิร่ก9—็Š๛H๙œt›F‰ฯมT ่ฑ>ยแใ๚ณๆ•ชอ0ๅdVแ4ฎภIทb‚ฒ Rฺฏปํbทyœ= WŸศCESเz฿‰$8๕"ๅ=฿$์?\ืaJ‚”BฬM‚”B!Dศํุถฮำนะa*บ๙u$nœL;&(S๘๎_ุ‰ภ๋Tt๓ฝ$n ]žr=ึ‹Jdpp<ภ`ส๔่iยมใ`fฯ•U[aสษฎฦi\Ž“jล๘%ยพ5:ฟืห3aˆบ' ส{ฒ“ž'ฒ8้6T,^ๅFช?`ะลa‚S?ขฃฏ๖ฤ๘ฅบ›œ^‚”BฬM‚”B!DY่0ฝ๒M$n}Nใ Œ_$ะŸโw=Vท๋/บๅ$n{ฟ]žJยPั+฿„ทl“}โ›r˜ Sน“ƒ](ิไD่ีSej!L9MvฉT3ฆR ์;8๋–ร๚`ดถ๋?‘c๐ปŸุ‰่ต†jœRฑDSaJ‡่า(มฉฉ์๙A๏>๛Tพ:นŽ‘ %„s“ %„BQฃr;ถn3&ฺ‰ร=๓๏ฤฎนŸ๘-?‡“nร”๓ไฟIcOืํz‹]6ป< ห0•๙o}@Eโx+ถูp'nFT4m”1˜Jžp๘$aa”๋ีT˜ršืโ4,รI6ืูwจ.ƒ€z*‘œz”Qด ่ตถฃฆ’YT4Y Sีฏ๋]!8ฝ‡สพ๏ฺŸฏj~™%H !ฤ$H !„Bิธ‹ฆbืฝ‹๘M๏ฑs•ฦๆ'NพPท๋+v;‰฿๔^ป<ๅ1ฦฟ๑ };๒ฦq1Aฏm#‘uทแถmฒฟM˜า8a๎a฿a;Bวq}yๆu8 หQษฌ}}ฉฯ๙ฝœึ๕8‰,„>้=Vlด—&“หeรT 'ูlฃT$†rc6๊"่oริ๑g1Aํ>uP‚”BฬM‚”B!DธXa*~ใป‰ฝๆvN1ว๘ืˆเฬบ]Og/ฯทฃmฆzr<ะ!nห:"ทโถnDEโSกAแsฦhงs:I๖โ nหzข—…ดฆอ#eสc„Cว1ลภTGN]n๓Zœl*–B็ะƒณžX'ั4ชqN"3m.ฌW๗C๚จh'†Š%มูษ่™ฆz๖โŽส๏-สญ|ค„bžใ‚)!„Bˆฅใ•„ฉไถ_#zล๋Qฑแ`7c_๚ML!Wท๋ นํืˆny*š$PฟA*žมiXnoฅ,ๆ๛T'4ฟPื#ช:b*…Š7ุIะใี๙ร ฦ/ฃว๛โw?๘aป]/2 RB1ฯงถ)!„Bˆฅ+ทc:cL'Ž๛๙พ'๕†฿%rู(/Nะ{€๑ฏฮEฝํbKฝ๑๗‰lผ ๅลz๖QฟQ‘่๙&๔งยิ๚qณซgสW%์F็‡PฎwAFLน-๋pš์$๋:?€>ม…‹8 ฤ€J6ู[๋โvr๖p ๋"…ตj˜ŠฤQ‰,*€kœN&(cฦ๛ zœx–ส‘G1ฅ‹7˜)!„˜็ำZ‚”B!ฤา๗ra*๕ๆ?"ฒ”%8๓ใ_}LฅPทหšzห'ˆฌปๅFฮ์ก๘ฤƒฏz„ิl& ˆฌฝฉฆVอผ•ฏ4F8pฤฦ +_ŸNหฆ”E็๋4HTช'ŠŠฅ0๙ก˜œ]ู[๙"qT"ƒŠ7ฺSี‘m&ฌ`ฦz๖œxŽJื๎๊œ`zั%H !ฤœŸาค„B!.ี0ตว}วฤ—~'๑ึŒr\‚S?f๋ธ ท2],้๛oอM(วร?๙<ๅงฟ^๔ข-(วล๋ธศ†ญธ™•3oๅซŒฃว0ลtiโy ทu#NSส๑ะ…!๔๐I@ืี61Zใ4,วI7ฃ")tapเ(สqโ’ฃC”CลQ‰ N"สฉnร f| wม‰็๑>ฮ^ธe— %„s:KB!„ธ๔ไvlfLด‡{า๏๘sผŽP8๘'žcBฟ^OoIฟ๓ฯ๑V_o—็ุำ”Ÿ๗‹ค&ุ[ฤDV฿@dญ8+ฯšcJ๕bJฃ่า่y…)ทํ2คpะลat๎ไข<-๎Uญญq2+q’อv.ฌ|5Hน๎‚๎Fk”ฑ“ž'28‰์d˜"๔ั๙{+฿๑g๐=๋{๕ห.AJ!ๆT– %„Bq้สฏปทฅ๕W‘Uืc0O1ญO‚๊๓ไึ‹‘~วŸใญผƒฦ๏zœ๒ฟŠrฃ ๒๗MเฤRx7โuX15๓V>=)ๆะฅ1~าH4งคVกPvR๓ั๊o„Tˆ›]J6ก\{๋a8ุ…r#‹ฑ—`ŒA9ž<™ลI4UŸ๘่$ู่W Sฯ วz_๙ฒKBˆน?%H !„BB›1z‡฿๕ุu๙‡>uzŽจbiค–mฦ˜๐.ส{Z๐๐aดฦ‰ง๑V฿€ท๚ฬŠaJ—F0ใ˜B]Ÿ?L9nFฬjŒ ํ(ซ๑๊m)ธMkชAสณOป์Fy‘ล[0”rQฑTบ'ž™ Sฦ  Cง๗{ ิํ่ด๓]v RB1๗งฐ)!„B1กฒ๗กไฟ๓—8ฌญวื๏$›IฝO๑ฺ6bBŸสP9ธso ›bŒม‰7เญพHว 8 หf…ฉQฬh/บ˜ร”๓–g7‚บa*HๅNV'ฎำ •jF)ฏ… 5ใฒศgผ•jž9bสtq˜๐ฬ^*Gv๖ ์>}@‚”Bฬ๙ษ+AJ!„Bฬ–q๗Œ๑๊.L9+H฿)ๆต˜ Le฿w๑ปƒ™<{~F‡ธ้6๖kˆt\“^6mŽ)ƒ.Ž GํSฆ<aล~ษ‹แ6uT—งB8ิŠ&F_l&๐q›ืขา-๖ึรแเฑ R“—G๖?c จdNชe*Laะ…aย๘Gv๔์#<๚“—]‚”Bฬ‰+AJ!„โไvlหj๔vgGv๛ฮา_ๆปทWรTฆNmq›:HฝํฆL%OyฯทN>_3ฏะh›Y‰ื~ สซpฺฆFLUoำ#gl˜ ส C;Bชy-ฦ/๖ึ~dทeNชwฯ's IDATc4&?D8|l‘ๆ:ท} *Zฝ•/ู<-L)๔์ว?๒ม™ฝ/ฆ$H !ฤ<Ÿดค„B!^DˆRฦ>f4#F;.…05็๒ืhDp6’~ห'p2+1ๅqส/~• ็ฅš|ญNใrผUืแญธ'ีn5L้=ฺK0pSชNŽŽ๑ g๖ข\ฏ๎๖!Tp[6เค[0:ฤไ ‡Oิอฒจd3Nชkœฆสy‚žฝ๘]ป N๏!่:{ู%H !ฤŸญค„B!ๆvN!FยT ู*ผๅ[Hฝๅqาm่bŽ๒‹_&์;Xรgใnฆทผ[์Hœ‰H่โ0*GES˜๒8มษPฑTํ;&๐q[7ุRaS&>นhs{๗๋7ฅTช'™Eล@MjสyยพTŽn๋Fคาไญ‰j‘็๖:๏ๅ0ีง๒%›P‰ Nผqฺ\`ี0ีˆสแ]6L ว% RB1ืกO‚”B!ฤ”W=™ทๆ˜RA็%ฆึUริ๛kโ9.‘ŽIฝแ๗Qษ zฌŸาs†ฮฌฃณsทuส+q.ณ#ฆฆO~>>ˆ>Ž }Pn5่ิ๙ผ รฉR•"บ0ˆํฉป 5น<ฺ •ศ Yฆฆ~h"a฿aฎGฉt?๑@ๆ็?ื)ŸฎB1๋'AJ!„โ"ฬn 0aˆr#จD#*ˆJdง?A๐ S>m…b& RB!„ธ$]๔5›ๆEฅ*ณน๔ืํึmฦD;…bึัA‚”B!–ขšQg]น†ŸSJufท๏์^๚ๆโ„)MปแงI๚~PŠ g/ฅ'>_ฟgœฆ’๗*ธzฌ฿)ข\w๒L่ƒ๒p—ฃb จhๅFํื‚2„๛dพ  S“?ปPaJrซA*)ๆGฮ@Jy^o๋ย0ใ฿ค)!„˜๋่ AJ!„KInวถ๋qvิMˆ:๋ ๖า SC;๎๎TฦŽCๆ‚œุฦRฤoz/๑›~Pง~L้้ฆฆ&S:ฟ}งi ษป?Ž‹๋#:†Rjฮeš˜PiXŽŠ7‚ณ#ฆ”0~*taSฮ/`˜Rเxธห/GyqLq˜0w‚โ’<า…ใ฿~@‚”Bฬut %„Bˆฅ ทc:cL'Ž๛ฅq%{i„ฉŽmY~!ย”Š7ธๅ}ฤn๘)‚ฯQ|๚ (ืซ๕ใ6ฏ#ฑ๕—@)๔hฏ RำFGอล„!ส๕P้V๛DพX#ธ`0-E%oรTฅ0m\ฌ0ฅภเ.lo5, Žœ‚ ผไ?—tq”๑o}B‚”Bฬut %„BˆzถไBิYWดฆฮ๙ฤ6‘!q/ป๖ํ๘วžฆฟูงะี#'‚ฒŽฤฒปยhแ๐qิ9-ย่ๅxจT *‘ม‰ฅม๑ภLPฦ๘EL%ษaโE S (๎๒อ(7‚. ฃs'!ฌ,๙ฯ']c›,AJ!ๆ::HB!D=Z๒!jฦU-#F;œูํ;sK|ปf1;^ษvu’Mฤ๏๘0ฑซ €๔qส/|&'๓ฎ3nฏํ2โทฺUกGฮๆNN›œœN๗1Z[๙Rอhฤ‰72Œ_ฦT 6L†0~้"„)^ oูf;Va;กฟ๔฿บๅ<ใ฿๘# RB1ืัA‚”B!๊ษ%ขฮบบฝคยิyog'ีBโฎ_&บ๙u€ม?(ๅ=฿จ฿9คoลUฤo~ฏนS่‘ำฏ0ฐ)Œ16L%›Qษ&T,5๙U”0ๅผง0Œ Š(72๙ณฏ.L)ˆ$๐–oๅ  C่ก`‚%–5ๅc฿๘C RB1ืัA‚”B!๊$Pผโ‘3KŽ„ฉ99้6wŒ่ฆ{CๅเN*๛ฟ u{พ๋เu\OฦwๆNกGฮ œWุชqษฺ0•jAEโ“_5ASว”ฦl˜ ห L)T4‰ป|3 0๙!ยแใ`ย%V5•"c_ RB1ืัA‚”B!jๆf—wฆ฿ท.b๋6cขs…)'ำN๒ํDึฦP๛mรป๊vY1D7n%vอ่แ“„ฃ*HM^ุ๒b8ฉT";kฤT Sทqช0„ ๓Sฺ@,=ค ร่มฃ\œ'๚ีุ6๔หŒ}ํ๗$H !ฤ\G RB!„จญเ !๊œOไœ†ๅxํืYs#nำšc*‘ํTฑิƒK?9;LนM$๏ ผีืƒั”๗|“สมขผh}.คึD7฿K๔ส7ู>‰=s‘&iWLL>๎คšQฉf”;ตLPถฃฅJฃvฤ”>0ฅu5Hmฑฟซ0D8ptฒ…-e&จ0๖ี฿• %„sy$H !„ขŒ}แ#Yฟ๏€„จŸx๖ฆPัNชล†จwโ6u โ”ล ~๑˜๏๏t6=ธิWGnว0ฦ๋ฤaญผ–ไ}ฟ…ท๒*ค^2•?Dลำ๕นpa@๔ส7ฝโ>๛ฏร'ะฃ=x„ิY;(<;ว”“jืช +˜โฆ8‚.ไภ?1LญQัค.€ฐท์IBˆK๛”F‚”B!ขอ/nืนSฑง3ๅ=฿ดOณฮฺส‹ฃ’Mxซฎ%บinv•}Zšฦ/ข๓ƒ่แ๘'Ÿว๏z๒˜=ั™พ๋มฅพzr;๎€ำryg๒ ฟฝึ[พtH้๙/R9๘œdS}พ/BŸุ5oฏNาแะq๔X๏ER“;œ Sn'Š“l™ฆ|L1‡) ฃ‹#`ยyริYAjด‡0wฅ–~‘2aภุW~[‚”Bฬuค‘ %„BˆEผ\๛จN`-ฦ`*ยม.ฎว(๏๛ฆ“U„ืรI6แuHtใV–uvฎŸ‰”ํผ<น“๘'žล?๒˜ฝตk‚ๆaฅ*ูํ๎\๊k+์?ทํฒNtฐถ๔ฬฟR9๘Cœ†ถ๚|w>๑ ‘ wฺe[ะ U} ZุkฤIทฺ:žช#ฆ 9L1gGLaPฎ7ต๏b0aˆŠ%๑V^mwวI๔X—ฤRค„b3 RB!„Xh๙ฏม"๏้ŒฌฟmญJdf_cสใฝ๛๑ปง๒าท1aๅR=UรI5ใญฝ™่†;lˆJ6W็DR“ฃTยI‚๎งจ…๋ŸBา SYB{๑ษฺ๎™q2ํuน&๔‰฿๘n"๋n'HM-€ S m8‰&˜ OajSฎ†)=ฎŒQัิไฉpฐSฬ —;ุ่ฑ/–)!„˜๋,G‚”B!สไ|?žทึm]›YEdำ=DึˆŠ5ฬfขK#„=๛จIๅภธFTLp’Mx๋n%ฒ๎V–u8ษfT$Jat€)ขGNใูmCTaย๐ึั%ฆŠ}ถด็kฟๅ5m๘uOี฿DRกO์ฆ๗Y๓๛ฏCว1c=เธ‹๖’l˜RจxNช'ี2๓๕„!บ0„) ก‹9ะ6:ฉDf*H๕ฦT  %>6Z3๖ๅ&AJ!ๆ AJ!„๔‰งg|ม๑๐–]†ำด–่๗แญุ‚Š&gฐะ๙!‚ำ{จ์6๑g—๔บrR-DึŠท๚z–ต8้6T4eCT่cJc6Du=Fๅ่ใ๖‰g~^ษ9%ฆr;ถeษฌ๘=7ู๖๑บ Sa@–ŸณO ยกc่ฑพE!Uฝt`"xN„)'‘E%›ๆSล!๔h/ฆR@ล&Ÿฒ๔์ฟ่%ู'AJ!^ๆจ"AJ!„/lfLtืฝ์7:ฒMธห6ป๚m8M(/6ฒย=โ9*/}^ิ.!“#ขV]‹ำผงq*šB)ฃงBTp๊E*‡A๕c*ใฏ,Dอฆรฯ)ฅ:ณwv/ํq:•j3งq๙{Tผกๆ_ฏ Cทฝฏภ๊ฆว๛1HM^B03L98ษ,*ั„JfPntj‚2ฆ0 Jแ4,มษ0ก_หฑะฦพ๔›ค„bฎฃ‰)!„B\๘ ญŒ‰vโpฯy`uฤ”ท๒jbืฝ'ฝlฦ<5`0A=ึKpYJ/|;Y฿'cฑ4‘ wโญผทe=Nร2Tผๅxำnอ;Cpz~๗่ฑ>t~ฬE]"aชฆ’ธƒี‘E†pเ(:?XC!gV˜R.N"c'Ofg„)Œ๊S๕‚ำ{0•๑“ฃ/Uค„โeŽ"ค„Bqแ.๔_aˆšอ๕p[7Y{3ฑ๋…“ศุGะOปธ5~ับึ$ฅ็รN’\?ง`6Dญฟ oูๅธห.รi\‰ŠฅQnคขช#ขฮผDp๒Eย)๔x฿ไœ<•„ฉฺ`4‰?„ป|36Huก๓C58ฒhพ0ี„“lš•A็‡ะ#ง œ˜Cj้^HBˆ—9zHB!ฤซฟฐฟ@!jฦ.6L5uดุu๏Bล’S_ณW{˜Jžp่8แ]”u;ŸRอžy)T"Cdีuธห.ว[พ'ปzฺˆจSC๔๔ผDpfฏฝMkฌwฺล๛ฒaj{v๛ฮRวพ๐มm&t>ฅโษ;ฯšรl‘%๎๘0ne` แภaLqdrคQ ๎เฬ SN2ƒJ69ฆฆ t~]ฟ\นฅw]b Œ}้7$H !ฤ\G RB!„xฅ.|ˆš็"ืuq[ึป๊-Dทผ‰ฯบ๊ำ˜๒8A฿!*๛ฟOe฿wj์Œห†(oูๅx+ฎฤ]ฑทy *ึ`GDU_ฟ9Mะณฐ๗a๔h&ฌ,๎kืŒ์ppv,๕05๒ู๛ทaขŸrฒ+๎ฌSŠฤึ_ฤmYFงำ•ฦf4ูฝฤ`z˜Rั4Nถ'ู|ึwšฐ‚) ฃว!(MฟKfŸ’ %„/sด %„Bˆ๓•ฑm1ฆว}‚žธDธ+ฎ vๅ›‰lธcึ๙ h.ž~‰๒๏แy๔ฌ ไ…พ0w’Yึ vDิสซp›ืข’M3Cิh/a๏>‚พC„ฝะ#g?Dอv …ฉŽญH,๛7ป๒๊E Sส!ฑ๕—p›ื‚ ๚A%_O—€AWŠธMkp›ืุงf–ฦPฎ‡ŠMญ[ใ—0ลœ-(.โ{๖ย’ %„/s” %„BˆsฟP_œuึ L2k'>ฟ๊อxํืฮบ•ฯ€ั๙!‚S/R๓M‚S?Z๐ q•ฬเ6ญล[พฏj๛ไผdผ่ไญ†fฌŸ ๗a!‚ำ{GN/ฮญy็ใ’ Sw€xห'ฆ๖ŽE SŽKb๋Gq›Vƒ๚_จป๕hย`rย~BŸเฬKฟˆ“iวiX>cฤฃ J“#ฆLPFฉ๚Sค„โeฮ–$H !„โ'_˜ืFˆ:๋z=ณฏbWฝทํ2T4มŒ0†่๑~ฯR~๑+„]ไ*ˆ›]…ป ผีืแถl˜ข ˜๑‚C„‡ Ž?gC”๊kงจ†ฉKแB{2Lตญ๏P^tแฐ!yืGq2ํ†}ชฃ‡๊‹ั!ฒหQ‰,มฉaJฃจHยŽŒงQ๑ ส‹MŒ_ย†ะ๙ALPฉ๋05๚ค„bฮs& RB!„˜Bผ6CิYืํ-๋๐V_O์๊ทแdWอธฐƒ }๔ศ๎')ฟ๘U๔่™ RoภI/ณ#ขึ„บ'ู„๒โฃม/ขว๖ฦ๏~Šp่8u[’ๆ˜RAgv๛ฎ—๚๛aไณ๏๘ˆ“Y๓?U<•^?่ลlj\ }ย–๋nฝญ๑VlAลา` ~๗ใ`@EbvŽ)/ŽJ6ฃb)๛7mฤT%oGLๅ‡ํˆ)วฉป๗Œ)!„˜็I‚”B!fหํุ–5ฦ์จ๕5›ป|3‘57ป๊-จT3v4Kuฤ”1˜ Œ>Nฅ๋qส/|S~๕๓๑จX 'ูŒป๒*"๋nฑท&ฅšmˆย@ฅˆฮŸ ์=€฿$แเัฅทำ\"a*ทc[–ฬŠ฿s“mฟุaJE$๎๚(Nร2LXฉฉJญ4๎ฒMจx#่ิ‹เ—Q‘ฉัf6L%์๛6šBE“จHŒ‰นคLiฮ1Uฒ#ฆ—z Sค„bžใœ)!„BLฟุึ่ํสxqศิ๋rx+ฎ$ฒ6ขWพ '‘ืe*LiŒ_"์?Bๅะ)๏๙ๆ+˜ทIก"1TผoีuD6ฒ'ี ฃณ2บ0„>Aะณฯ†จธepัI˜บ@๋Qฃ6HฅZ0Aู)ํืแฺRธญ์-{: 8c;ฺษL~}ฦS๙"‰๊ˆฉด฿^ ”ฒsร•ว 8Š. bๅบ5ฟ๔ค„bžฃƒ)!„B,•5ƒใโญุBdใVข›_‡“lๅL}ฝ๚„ป g•—พMๅ๐ฎsบฐฦ๕pโxํW฿ผ'ู\ฝMPM>ฦ>ฬ$8cฎวป/ฝJsL){v๛#_Y๊๏ฆZ~๗BN|nย'Jโฮุ ๅ—{๗ƒ ๋๐Šร|บไไคๆAๅzgฟฟf„ฉ$Nชbฉj˜ชŽx ty Sฮcฦ๛1aeŽ฿U;$H !ฤ<‡ RB!ฤฅkI†จg: Iเถฌ'zล}D/ป•ศุั“W]ฬŽ๒K฿"8๙ย<ฟหAลาxซฏ#บ๙uvDTฒลVคL่cŠ#่‘ำ๘วŸกrh:wRv2อรJU:ณนฤ฿K๋Tช๕ฯœฦๅ๏นaสL;‰;>ŒJf1~ัސชว ๅx6H%ฒvคWฯ>œgพ7.Sa*@ES8ฉ๛4M/>9Gœ +”1ฅ1๔h5ฆ$H !ฤ<Ÿ๖ค„BˆKฯ’Qg๑(œtNฆุีo#ฒ๖&l4˜ฆtˆฮโw=Fy๏C„}ง~<ึ`Cิฆ{์dๅ้6๛จ๚‰UG๖เw?‰hW๕ฉy!u?a๙…$a๊ผฟˆผŽฤํฟ`CNฅ`GHีใ>ๅFq›:์rLŒ๔ามฬ0<๗—a*–ฦIตขขIpฃL<๑ะh฿>ฝฒ<Žํลhฺํ€‹O‚”Bฬ๓)/AJ!„ธด ํธป๓’ Qg๙8ธูี8™•ฤฎ};^๛5๖ษ_Sย=ึKๅะรTํฤอฎ"ฒ๎ึjˆZัสq1:œ Qวžม?๒ˆฅแ—ภhูัๆs)…ฉDห?;ูwพ’0eสye—“ธ๕}จDSฮฃ๊peธ1ๆ5จx#ฆR$์{พo\ฆยThรT2kฃTผa2>™ bรT%ํึD˜’ %„๓|บKB!. นwภฏ‡ตrคp›ืโ4uฟ๎]ธหฏฐ#žฆr„!ฦ/`"8ž}๒—๋M…จ‘3vฉƒ?ฐ!ช<^%ฮ‰ๆaฅ๔๖์๖/,ํ๗ึm*ฑS็ฆLiฏ*โท*ึ€)๖ฌฯ ๅ%pšVใฤ1•Aฯ^”z% 2-L้kฐŸวR8ฑp=ะ”0~SฮฃG{ฝจaJ‚”Bฬ๓ฉ.AJ!„Xฺ$Dฝฎิป๒Jโ74nSฬธp5SฅCti=ฺCpf/~ืn๔h:?do?ฏŒ?ง”๊ฬn฿ูฝด฿‡็ฆLqฏใFโ7ฝKcŠฃ‡P๕ค"I์jTผSฮ 5๏Q็วhƒo€Xoภ‰&ม๑์C *LฅhGL๕ฑXaJ‚”Bฬs&AJ!„Xชภขฮ๙„(–ถ“žoผ ทช๊E๋๔+Zฃ C๘]Sz‹่ฑ%D]0—P˜"๖yทฉฝใๅย”. Yw+๑ื *šฤs„‡ฉว"ฅขiœ์*ึสc=๛/Xš|‡pโ๖พXI€ใ‚ํhฉาจ S๙!ภ,h˜’ %„๓$H !„Kํ‚WBิ9Ÿy1ผuทเตmย]~นล‘ศฺงxM^0ฯ|"Ÿ)๔๎งฒ?ฉ์žฌฤ น=Yผๅ›?็ต_ฟ๙gป—๚๛”xห'็ Sz|่ฆป‰]S6H† ๛€S‡A*ึ€“Y…ŠฅะฅQย(วฝXอFฉxKู0ฅ” Sฅq๔xŸ T•"8๎cs‘ %„๓|bKB!–ส๎ึmฦD”uฌั๎๒อx+ฏย[q%n“ QxQ0ฦ>ญ+?สฑ#;"qฮz"_1Gpz•=฿ภ?ฌฌึW,าx+ถุmฒ| ns*ั๔9ศvๅEsK๛}[ Sm๋;&ž ว๚‰^qฑk฿Š&ัลบ๏p)*‘มษฌDES˜โA฿มA ผ8*วi\aŸฬ ti 3ฺcoศ ส`.๎oค„bžOj RB!Dฝ_ะnfLด‡{dmผชŠ%q[6ุ๘ฑ๒JผึีQQฬฤœ3c}} z๖ู[ค‚๑›Kdใ]จX’™a*@็๑=C๙G_#์?$ซ๙|ถHฤ†Aทm#สซq[ืใ$›ภ‹M„มณ๗Av^็ๆsฮ๎ฝo็t#gฬ”DRเHVฒdำดฦvูซ‘ึตฎ™ู0ฦlีฎ]ต;ณPีxw๕ฯjผฎ๕ฬ์he{g'ุ–-K'ˆ”(RL`‘นsบ}ำฮู?ฮEฃ@“ ˆะแ}J*ชิ$๎ํ๏๛}็<|฿฿;{๙P:๎กฬ_\ีbjๆ_}แะ๙มฎฒ๕ fv„pื็ศ์๙*ศE‘s›ฑUื‚n๊ฉตN‘Žž\Px_ฺ0)*ศบ๗ะุuux5˜ส ถZ„ธŠญ๎˜˜!%‚ฐฤ>@„” ‚ ฌLDD}€ Oฆฏu=^็Vž]x[ัu-(?ƒลBTยฬ‘Œž =A:yขข#Qย:ฒ2~^T˜ใช˜r๙La„x่*/{Li๒ส+3Œ.\ล๓๑;ถโตoภ๏‹ืพะ>ฝ็ลเุI’หo“ŸI&Oา–C๙‡งW๏wzžๆ๎฿ึAใฉ6f๖>Cธ๓35!5E2vzลEHYcะ ํ่ฦnT˜ร–&๏q+ฝlšิก๊[ั(/ผ๒1ี9lyโ 6*v1%BJa‰™)AAXYค“g๗W_ำƒั้็>nห3rAkฃ“iD็{๑;6ใ๗WQญdP(l\ฦฬ“ŽŸ"=N:zชvpอ๘`kAY2{Ÿฦ๋z˜ฒiŒ™นL|โ0•W•dโxญx๙>‚มG๑:6ก: ฌsทถฆ0ŠฉPJ ›FFคS—ส้ไ้›ž{ๅŸญv1e0๊?๑?wLณ๒3๎šฬ\บใํeทk บฑะแZ็ฦI'†๎ชบ๚^Rฐึ;IV฿žๅ‡˜j[šคโ*ง0ทๅuEH ‚ ,ฑO!%‚ +†ภAเใ6HวNQy๑I.ฟญฮษีYธž๎ ฒ IDATมษ4 ;๑:ทฌปฏ}“ซภ ฒ5๑Qม”&I'†HวN’ uญ=a๎ๆ^@๛่ฦN2;>n]ฟ8cสZla&ฯR=๚ทT_3๗šk/@ืทใตo xฟ{;บฑฎN'ULq S‡š4ฐ•ฅ[จLaขฟ๛ืข๑แ๏ฌๆหฝ๓ื๙`๋ƒสฯf:u[ž†4ZQฟƒ5)บฉะŽ r˜นฑšา๗๐=ฐฦIฒ\kv๙ภๅรU ุา4$ีšTpbJ„” ย๛5R‚ ‚ฐผ)‡<˜ศฏ๒{wตแฺ6ฉ’\|ƒส ุ้ilณ–[ฤTX‡ฎoลฺ๋Nฐ!ผŽอW[ม”‚ธŠ)Mb&ฯ‘Œ น๔6ค*Sk/่gะอฝdv,บฑJฝ@L%าั“DGš๊ต๖ฤ๓ัูftห:‚uเฏิ… ๋Pสรฆ‘ Š&˜๒”Q&yฉ&ฤงžIฮ<๗๙ฯ~c•_ษมไ๒;›ญฬŠ๒ƒ๕ฦmš ›๛jB*‹™รL • ฉ๗}oฦ‰&ะแ2ฆฒ ๔๗ื”งœSทvo!%‚ฐฤพM„” ‚ ,OฆํดึD๛_Vู2;>Mธ๋ณn๊›ฐHLU็ˆ‡^ค๒โ’ฮ\rU&kiCใgPนfผฎm„?vUDYืš—D˜าf๚<้่ โ๓G ญขฒP›DT%๕9tฎ<๏๊๚XƒŠ$—฿ก๚๖&>๙๊_ํฃย:ผ–~uฌ‹ส6ขด‡5ฉห์IcฌI0…Qle๖ฆDิuื?ฎŸ๙้šS3๚ƒญO}=่฿๗ิ-KิปŒMผ|?ชก๙ึรษณหBHอฟGcQJกฺั๕ญจฐ…a]ฆ<-Maใ ฤe>จ˜!%‚ฐฤM„” ‚ ,/ฎŠ(๏หื๓›zศ๗ยMO ปฎๆŸ€๛7๚ฅIโS?ฆ๒าฟรฬญ‹ๅจฐฟk;แึงๆ3‰Tญฬ&ถด_ธ๋๓lxd๗rS6‰g`^HNž[–ำญต(ช.๏Z๙๊[Y˜gส3ุาดซ่Kชฌ˜!%‚ฐฤถA„” ‚ ,›Cๆ’"๊Zผฮ-d๖ม†วn >LaŒุ่๗จพ๑-Lqb๎`4*ศโw๏ ๑ฉ๙Œจy•&ุส,fv„ไ๒[$็^ม&Tฆแฮj+PŠp๛งท<‰ส6]m—QsE๙&f๚ยjXะบกpำใ๘9 ‘หปj>k]€|a SwํOwจฝ๔ช˜๚แฏไ่๐๊พg<พ?๕๔๏.g1e“ฏe=ชฑฅLa”tjy ฉ๙๗l†Rู&7•ฏฎuAล”ล”ฆฐฅ™Zซi๔พŸcR‚ K์DH ‚ ยฝ>Tผˆบฟู}ฯเ๗๏ฝฆอค6๑m๊<ีทšุ่๗\สjุผ๘ผždv|บึšืแBล•ฎUเ13—H.พA|๎ืšํรบย&U@‘ู9‚Gœ [ะ„I0ณรDวพOๅ๕?c%OLTนfยญOฌฏu=ชฎๅgjBข๊๐qชELa„ป‘sfใ ษน#oฤ'๎7EL; )S#:Oฆ์ฺ‡[ฃย'ฆUL)N`หWฤิา‚U„” ยทXR‚ ‚pฏ‘nดปฒ4อๆฯ 6~„์พ/โ๕์Dyแb1WHGŽQ}๛ฏˆ‡^p๓ฏย-Ot.1eำฏe=บก”ยฬปช@ฅVะ•U51U๏rฆ๊Zิ”ฆฐล lTยš˜k3R‚ K]EH ‚ ย>4>u-™]Ÿ#๓๖ต|ฉbชZ$นp„สkJ:zUฌ\9l-c1ฅ2๕๘=ป uQM=ตpl—IDต„™#น๑นWฐi‚าmษ%บญZํฃณM„;?…ืฑๆX,ฆ’๑ำDoล๒Ÿศ็„>Aะw^๛FtC'„นซ“๓ส3.จ<*บ5JขZะฝ๛œญ-1๕ไW‚mŸฺrS6Mฎ )lMH]ญVเ•uญฉ*l@5ดนv้˜าvnUL-ธˆAXโฎ*BJA๎ึ!๑ฮ‰จkeAv๏/๎๚,^พฏ–/ต`โ[e–่๔Oจพ๖วค“็ภšซ‡ญe$ฆTX‡฿ปxํ๑๒ฝจl3ส ฐึ@Tv"jไ]ขำ?F้Zภ๛-Ljปซ่/฿Kธ“่–๕๓ญmตยฦUาัT_3ข‡—ืฦ1ศโ๗๏ซษมอ่ฦฎZ•š็ชิสณ˜าค›˜g“žiฒฌ>WWลิ~>เ๐ะ๊พ็ิฤิŽOvช…๎ๆ๕NSผึ๕่†vฌ5NTฮฏฐ ฉ๋พ n’dถUื‚ฮ5s]+_e[šํ1๛วญ)A„MEH ‚ ย>%u บฎ•ฬƒฟDธe?บพํบ‰oฆ<ํฺฤŽ|sYŸ+?ƒ฿ฟฟo/^๛&ผ|*ืŒ๒3XkฐQษ#$>๙ถ:ก“"+kฆ๑;ทl๛$^s7่ลm4Gr้-ชo~‡๘ฬO๎ํ[ ๋๐บถ >โึคฅ฿ษม+“๓*H*˜jัฤซW2uฬฒฝ6ฎ\~็oใฃ๓Wป˜*ลo^vื~C…ูเฎ_gc๐Zะ๕ญXc0…Late๛จซwY๐|Tฆ•kชต๒-จJ-ฯb*ณฟ๛ตฏ6๓ < Aฎู_ˆA„;วไก'mu-^็V|>๐k1™Ÿ๘ๆฺฤLqœ๊๋฿ข๚๖_aซ…{ท)๑3๘}๗แu๏ภ๏ŠnY‡ฎ…c_QvnŒd๔8ษนW0ลI@กย์ŠŒX“nมฆ'œ8\8‘ฯLy†ไยkT฿ษ…ืน›ีF*Sืพฟ~็ผถ NzกQQ37ๆฺ๑ข2ฆ:็ฆŽYณrฎSำ‡๖็3~้๗ฮํฯM1eลk@ืตธA ณ#ุโจUtq•็*ฆ2 จ๚tvแํbf‡ฟช›zสQแšง)AAธ‡ฟ'ฟbญอภryOมเ#d๎{ฟg*SฟHLู$ยฬSy๙฿?|Wƒต•โ๕๎ฦ๏ฺ†฿ตบMณ๒3n๚•ึผฑ“$_วF{MซสวV‹„;?Cธ๙ Tฆ๑š‰|)ฆ4E|ๆ'T฿๚้่I๎ค˜RAฏc~ฯ.ผฮญ.'ฺ๊5)ŒLกณMุ4คฒขDิuื_ฤิมXt RI3;Œ-Mญ.!u๕f^ˆสิปVพlำ•Ÿ|8(OFA„kn›"คAแv๖–Ÿˆบ–p็gศ์YWํไฎŠkฑq™trˆ๒5ษล7๎์&ฤฯธ๖ฏ๎ํN|ดot-/AึIธŒ™';Ir๙wต)ส V๕gศZKf๛ฯlxฬ‰ร…'๗4มฬ๘!ี7ฟ™แถŠ)/ภk฿่ไ`๗—U฿Aฮ<*cๆF1…๏…ุจฤrnอ๛ภื?ฎŸ{๙?$'Ÿ๛ว๙‡งW๏ฝj`ธ๏™ฏ๛žบ“ม็ึ‚ืถkฦฦeฬ์e—1ถชOXNL้l#dะนWA”'ค ย5ทKR‚ ‚p;wห_D]Kๆ_$ณ๋s่ๆ^”r5๘b+ณ$ร๏P~฿’Nœqงสต๙rxญ๋๑:ถเ๗ํY =ˆจโ้๘i’แw1ำ็ฑf๕‹จE‡๘$‚คL๖แ/แ๗ํEe๊ธv"_:}่๘จพ๚วุค๚แ^P๛nMฺ7แ๗๎v-“]ึDT\ม'0ณรฎ ส ฐๅ–๓tฦ‹)LTโ ฏ฿้นW™ˆฉ[ถเตo@e›]‹็ฬๅ{ฺ|ืพรฦธI{:€ด๚ี`รGส“Rแšง„)AA๘0‡น•'ขmย:ฒ*แึงXvoแD>‹)OŸy๒›T>”˜RA๏รุ๋Lะw^็VtCปQ0/=า๑3คฃวI'ฮ`MฒฆDิu‡ฺธ‚ ๋ษ>๘หx›QaŽEกษI„™๕ฃใ•kยk๊ม_w?มบั๙>Tถ้ชˆŠJฎ0ฑฅILiL,—พ†)LTโw๚_4ƒ?ี~ฏ๛๐bJาฎB*ำเ„ิ๔€ฟ†!%‚ฐฤSB„” ‚ แl5‹จk 6?Af็g๑{wฃย๚ล฿า„tโ4ๅ—๑ษ็ฎ฿\๘Tฎูตๆ <‚฿ตm>h^DUfHg.“ŽŸ"96*ขย:๙ ๔ฮ#x`ใใ.๓น*ฆ ถZ$พ๐ีื„ไา[ND…u่ฆ๛รkY‡ส6ขด5)6.A9)Uœภ”ฆ\›žp6MˆO=?’œy๎ท๓žฦjฟ๗บ˜R <ผฮMจ [™&บฐๆ>W"คA–xJˆA„๗>Œญ%ตx— ษ๗s„[žย๋ฒธM ‹+ครG)๔%น๐:x>*ฌw<?Šืต ุ‰ ๋œˆชต™้K$c'I.พŽ-Mกฒ๒AปU มภร>‚ฮ5ปvส+˜Sž&9๗2ัฑ ;6=ŽืฒU—w-‘ึbฃ’“O liSšu“ุธB|ๆงkDL=๙•`งฟnyข๓‰)/tRAฎVy~อU‰AXbซ)BJAnt๘Zร"ฺ๊อ‚Ÿ!๛๐ฏlx ฏuเšjœฺDพหoŸ1^๗Nฮ-่†NWฅ=W}Sรฬ\&=N|๎elyFDิmย&˜”ฬฎฯl๘ˆปฎืดZฺ4†$rาฐ–อeใ*ฆ2ใชขชฬคาšwKk b๊ฦืลZ”Ÿq~SžฦLžuำ็ึ"คA–ุcАA„…‡ญƒึ๊oˆˆบมฆ!—'๗่—A7u_|ŽIฑ6E)ดv:ี9la”d๘โ3/ธึผLƒ\ฬท*€ถ2 aŽ์}Oใ๗๏ปพีท็ปา’gM ี"้ฬEQท Wˆ‡^9•œ•?๐ฃรซ๛^YS;>ูฉ<ฦ“1เ‡x]Q~ˆ-อNœฬš๚\ˆAXb#BJAฎˆ({ํ}Yฎฦ{ฃ›{ษ=๖e‚‡Qนfฎถ๑X๗ŸคŠ™ฝLr๑Mขำ?†4F๙ู5wฝ‹[:ภbฃ บกฬ๎ฯ9 ไjหณ`ฌล'HGaซ%ทfศ~๐vbใ ษน#oฤ'๎7Wณ˜š>ด?๏m|์wฒปพ๐*ฬ^7ำšิๅ๕ํฯร–gHวOญนฯƒ)A„%v/"คA„ตŒˆจธq๐3x›ถ์ว๏ƒื6X›๔‹ค‡I1ณรT฿๘ษิ9W5ตฦฺt๎&ถZD7uโ๗์ม๋Šn๊BePฺฏUIฉkฤ”uแ๔ใงIg/CR]ฐ†ฒ7ผm๋ฒ†ฤTๆั/พ฿น™…bสฆ)˜”`๐ะ[ž%;นุaฏDH ‚ ,ฑฏ!%‚ ฌEDD}ภ Cล๋J0๘(^็ผๆ>Tฎๅ…W—J/ฮ.lT";E๕อ?วฦPaR%u๛ฐ•*ˆ฿ฟห๎ส๗กฒอnržMฑ•Z6T\F5tข;jqA8}‘ŽภฬŽ€I!uึiญ‰ฉŽ-ฟฌ2๕.ป,M6~”r-{ใ'ฏi%]ˆAXb)BJAXKˆˆ๚€… ‹ืต ^วtK?บฎๅgฐˆ+˜โคkŠJ๘๋@7uฃฬยใ8ถZ$~‡๊ซโrค$ะCaฃ"สฯโ๗๎ฦุ๋Šืบ•หปœk\ˆ์ษๅทH.4 xˆ`ใวะ]ตp๚+ ›ถ— ฟƒ-Mอฏ›p›ื-ฎ\|ใ๙๘ุ๗~-เ๐ะjพฯ†๛ž๙z0๐ศSfv˜ฬŸwฟiŠd์J๋5ต๎"คA–ุgАAึำ‡๖็ ๆ€R"Wใ&6~ฏ{~ฯ.Wี:€ฎk… &šโ fnœt๑้ŸNวฬ\kศ์}šฬ๎ŸญIฑ2ึ`หณฤ^ฃ๚ฺ?ท OธlTๅแ๗์ภ๋Šื6ˆฎoE๙Yฌ5•I #คร๏_8‚)ŒขณM‹+R”Gฐ๑cƒป5]No0ๅi’Koน๊+/@ฤิXวธB|๒น?9˜yเ—†V๋๏9๓๛?7˜}๔ห3๗โ—มb‹“คCาฒ'‚ ธ-‰)Aa53/ขฌMณ\‘๗ม๓๑{vใwn]$<r๎็qลbŸ&>*frˆtbW1บกฬฟDธ๙I'=Vใ˜Sœ >๗ ั‰gืZ๗ฮ-aฃ `๑:ทเwnqีj ํ.ดฐq37Fr๙’‹ฏป๖;/xoแง4แ–ง๐B็๒‹ฅ•I1ลqโ๓ฏBšึู3~x4์์^วfผ๖ ิุƒสฯ ญโ_|์กt๚ยฯ™ะาฒ'‚ ˆAV)3ฟ๗ู|—(๋‰ˆบฉsฒ‡ืพ ฟ{~ฯNผ๖่๚ถ๋Eิฤ’Ko‘ŽŸ"?J๏๙วzํษ>ซ๘๗ปJ๙j‹Mcฬฬe’ก‰†~ส’ฃใื06*ƒM๑Z๐:ทแwms•g™:สM3,NŽž น๘:้ิP๊ฆ+ฯlAšyเ‹๗ฃฒ ,*_Iาูaโกะูfd"฿ญฟTถ ะ ภo฿ ™:ื*Yš$น|”ไยkŽ;ุ๘ซzhต^†๘ฬ๓๛ฃc‡p๓ใ๗ซLšY~R‚ 7F„” ‚ฐบ๐ีBฬ ˆ‡^:\<าŸ{ูm[ า^๊ ์ตฌว๏แZ๔บถกฺˆR โ*ฆ4I:q†๔๒;$ฃ'œˆช>ะห๋"๓เ/ใwmuมๆjqจถ™ขz๔oHวOฃ‚ฌ|Žำโบฉฏk~๗ts*ฌGi›Dุ๒้ฤษล7Hง/b“Jญล๎^ฏR@e๊ษ<๐ห๘;PaŽลb*&น„™นŒM*Dˆ˜บ™๏—ส4ๅ…่ถAฎˆ[)` ฃุธŒ2kฬฆ0ซ/ั?ส8<ฝZ/ห๔กว๗‡ปž`ร#ปื‚˜!%‚pcDH ‚ ซSฯ๋บึX{ภฆQ3&มฦˆฯผ@rแ5โsฏˆ˜Zˆเ5๗บชจ]๘ฝ{ะ๕ํๆPJปส›า$fโ,ษ่1’แwIวN`+…๕ฒแถฟGf฿๑ฺjโi˜Šสค#วจผ๒ฐฦ ๕pำG7ฌf1%BJa‰])AaฅSหฏsวAฟ{๛€nDืๅม2_ษaJSDว@r๑ษล7ืด˜R~ุ…n]Oะฟฟw7บก๓5™:G:v’ไา›$ฃว?ดˆZ๐@A๖ฟOธใS่|_ญฝL-"ณ$—฿ก๚๖_AZ]3kcฅ^ว=x-จlณห฿Jlต€™พH2r 3užด0rหQ๏๛^โ ~ฯNยญOก[ึ_ณFWฑลqาูa7•ODฏC๛.ืหฆเg๐ZPu-๎ชE%lตI[žมฆษ๛Jืต#ฆžJฐํำ_ ท<ันล”)A„%v…"คA„•|ˆฑึ?ˆงts~็6ฝxmƒx๙uจบผ;ิ%Ulu3;B|ๆ'$็_!>ฦZj9R~U฿Šื6Hฐ๎ผ๎]่ฆ.TX็DTcห3คำ็IGO’\|d๘šw๓oHฃยzฒ มฆว๑šบOไ|~Uข7ฎ6>›ฆ`ท6เตฎGีต ผkาฺg๗2้่ าษณ˜ยศไเ๗๎"ฒีุY`51e 6ฎŽŸr!๊6]ป7#ํปI‡iŒ ฒxํPนฺ'.CTr๗กา6>ฐH\sbjว';WSฆœ)A„%v"คA„•xhฑึ?ˆf`๑กะCืทใwo'ุ๐ผŽM่†ฮซญ2IีŸ:Orแ5’ GHฦOCฏฝขrอxm๐ย๏ูn๊tYDJcMโฒlฆ/Œ"9๛ษ๐;๏V~๛ ฦkYGๆ_$xุฉ๋+Q (ฬ์0๑™็‰Zฎ‚•qฟaeฒžpำใ่ึAt] สฑึ`ฃ"ฆ0†™8C:v‚tf๘žH›DTw|š`ใวD>ฯc‘˜ชIFŽb็ฦืฏด^ฦ &k๑:6:Uห_ณq •ฑฅ 'ซ>dE›*q|๒_ญ๛๔;ซ๙ฒ๒Ÿ}-™ชยlฐ~R‚ K์†DH ‚ +…%Eิ ‰บพ ฟwมฆว๑;ทบ žL;Dง1้์0้่q’หo“\z3}WVฯล๒|Tุเชn6<ๆZ๓ปP™Š}ฅlf˜trˆ่ฤa’Ko;9ง4~๏ฒ๗"~ฯnTฎษตVบc8XK:yŽ๘ฤaโณ/ืฆมญศญส๓ฑT˜%ุ๘8^ บพ ๅgฐXˆสต‰†CคฃวœˆZญ‹ถR?Cf็gqk ˆ)“bสำตฯQดบ๘”/@g›PูfTฎ•mD)ฯ‰๏คโZ๔ๆฦnฉ"๊=ื!MˆŽ~w4>๖7ฟ•?๐์7V๏~>๓่—~฿๏ฬJS"คA–xœŠA–มไ‰งญ ฝฏˆบ/pbชแๆ'๐:ทึ„Œ๏าiB:u–ไ๒;ค#๏บlž™หฎลfฅข=”Ÿq"jำND5u_QWZภ ฃคใง‰O>K|๖e–O๛ข"ุ๔1ฒ{ฏs๓๕๙า˜tไ๑‰’ŒžจMƒ[[ฎZvYŠ ๋]_็–y๊jไYโKoaf/Cฒ2ดlฅ€ชk!ณ๓3๘}{ิธyyˆSลqฬ์ฆ4น,‡๑s฿ฑlบ.jhG‡๎๗ทILŒฉฐ…‘R๕ž๋P-xNฤิ @„” ยป#R‚ ย๒=ˆ<พ฿ฺ๐ š ‚,*ืL0๐0มๆ'๑7Aฦe'ก<ฌ‰Iวฯ^zห…xŸฦวWXล”ญ๑ฺ6n~๗NtsฯUe D%ฬุ้)ข“ฯŸ} ฬ๒อษ์Yย]?‹ื6xดทจLrแัฑ๏aๆ&–yฦ”ยฆ1:S?๘~ื6tC‡“iสรฆถ<ƒ™พH|แ5โณ/ก‚\-H|๙b+ts/แฮOใwํpSๅิีpzLJ:uŽt๔*˜ฦงœˆส4ข๋๒่ฆ7%๒Jkmrž-Mบฐrญ๏:ฌ%1๕เฏ‘฿ณ๓๓+-๘\„” ยOWR‚ ย๒;xuƒ‡^ถ k"ุ๐‚มGX๛ฐมy ึฆnฒๅทI.ผN:1ไ˜หพยCแตฎ'๕Y๎ตŠจฦ๙\—1sฃ$#ว‰O=Gr5lมŠุ(ฒ|‘pื็๐š{Ÿ[‹ญฬ~ž๘๘a๗EpSฒ ช 2๕ใu๏ภk์rEฺs"ฃZฤฬ^&9๑้็๔๋Vฤบ\ฉชณ•^็VยmŸภุ๋ไDอ‚|ฉt๊<6*aๆฦ!.ญผ’๒P9ืšง›บœ,ิ|จป)Œbซณุ4ญ‰จ{๓ฝฒี"ัฉ็ฯฤGฟ๓๋๙?:ผzŸ๛ร}ฯ|=่฿๗ิJS"คA–xฤАA–ฯAใฮˆจ๋~uyt};แฆแ๗‡n@g›k็็ฤUvŒŸ"=N|ๆา‰!ฬrmฏ}'ขถŒซบi๊vYY~ๆชˆ*Ž“Žž >๓๑ล#ุ๊ค+ญZEก2๕d๘%ยŸชิO๒œ IDATŸ/ฮถ37Nt๒Y’‹oบjœ{<๑อV‹จl~฿^ฎํ่ๆ^ท6ฺวฺิ šย(้๐;Dวƒ ผ0+nmๆลT๔Gฐ๑#่–๕ตVDภคุธˆญ–ฐี้ิ0+`€๖\>TึUEฉฐัU๊Yฺ๋+ #ุJkSิ‚๊ฝ{ญ‰ฯ๔ญ่ํ?๏DL-DH ‚ ,ฑ‹!%‚ ๛ƒลQื7๋ะ๙>|พ๏ฏษํฺ๕’ˆtrˆtbˆ่๘๗]ลTฅp๏^ˆn ๘1ผฎํx-จ\ตxY๋ช6Šคc'ˆ‡^$น|[žยฦU–OVิญ ห“}์+Nzไ๒ืd%ค3—ˆO>G|๑อEcwMT เe๐๛๏ร๏‰—๏ซญM‹j S!=N|ๆyWฅฆผ{.ะn๏:๙๘;๐ื=ˆ—๏Eu๎;e๔]žu-Š…แๅ|~EDe๊]‹^. sn๒a\ฦงฐ•ูZโ!ทพWkJLํ๚ทƒ ์^ฎbJ„” ย{ZR‚ ย=tCปห‡‰Jุ๊้ิyฬ๔Eขc฿%8ƒญ๏๕ผถ ƒโwnAทฌCืทข|—Ÿd“ ฆ8้ยสO˜t์fnฬ…ดฏฆ็ฝาxmƒไ๛ ~฿}จLใขเsา„t๒,ัปG2rฌึBv‡•(…฿ปวIยถม๙ตq“๓\~W2rœ๘K`ฌ“P+>[i‰๋‘DุธLธ๑ฃ๘,SI-3kๆฆ<ฝฌBฯUฆ‚:—Gืตข2๕X,ถ:็DZuŒqŸณ2Epํˆฉว๗‡ปžๅ(ฆDH ‚ ,๑!%‚ ƒรAkํAด๗ๅๅ๔พtC;^๛FยŸuีSM่บ—๗•0ล ฬิ9ฬ์0ีwฟ‹™<{w‚ฯ=ฏ}Ax[๐Z\%W˜C]™ฮVœ "z‘t4fๆ"ถZbEWDมเ#d๚UผŽอต‰{ ‚ฯใ*ุ้Iขท’tๆโษfบ"^๎๘][๑ฺ7ก: ศ:IW0sใคc'‰ฯ3;์ควมV  }ยญ๛๑๛๗ก:็ืษ&Ulq‚t๒ฌk%ฝ—ŸUkมZts7:ฟ•krี†ี9๗฿ส,6*ก<ๅฎEตH|แศข#฿๕รCซ๗๙ฒฤ”)A„#BJAธ‹…ๅ)ขฎE7uใun!ณใS๎ะŽฮ4:1—k“๊Nb ฃDวพ™พpg‚ฯ=ฟ}^๗Nฎmxํœ์ธ"V’*ฆ4M:q†ไ์หคgแ>*ฎฎŠจ› ณ๛๓d๖=ใrดNไณอ‘\>J๕ญ๏`K“ทIiะะ‰ืพฏs+บฑึ;•T]ตฺุ)’ G\n’ึ+Zh|(R) ๊Z6~ ฟwOญzฬeLน<ฆQาฑS คw1_สฯbJSxอฝxํPู&๗žH(•…าjuฌลSมถฯวpห๗ZL‰Aธ1"คA„ปp0X"๊jŒ๛ซืา๏ฆ‡m{ฎZชฑรตวi๏๊!zไ]า‰ณD'่ฆˆŽรด๖๐Zึใ๗์ฤ๋฿พ ีุ้*|ฎศŽา4f๒,๑๙ืœˆ?…ญใ*“{ฝz^Hๆ_$ณ็ จ๚V7epมด7Sš"น๘:ี#ๆBฤ…ก7น8 ”ซ k้ว๏ู…n์BePฺร&ฆ4‰™TฺUวY ~ˆื:เ*ข”r•P•Y๗ญฮaญA)ต6ึbญ‰ฉŸ์ผ•Š"คA–x4‹Anฦพ&ข~sี<0ณx๙~ผ๖๘ใตmภk๊ฉy ‹5‰ ญพ๐:ษฅ7‰ŽฎUฬฤaZiผ|^๛&]x];ะM]‹D”-O“Nž'=F:rŒdไถ<-ถ๗ภk@๖ก_!xคVณ J)MHง/๛ษลื฿#_ช&น :฿฿wŸนf”๖ไตส,f๚"ษๅทI'ฯaซ…Zหค์ฑW†$~๗v‚มGั-๋]kฌ็AšbขDeW 8s้ึ'*ฯญฏIA๛x›Qู&”า˜๘Šˆชb+ฌIWmEิ๛ฎET‰“ัwฟY}๑QภแU{s)ลo^vื~C…ูเnฝฆ)A„%ั"คA„ล๔กyƒ9 ฌM๓ju}บuฝ›vืทฏcบฉ่าุUx”ฆˆฯฝB|๎eโS?r•้ ฆฉi่&๙}๗แ๗๎พฺฅjำศ*3คS็]5ิศQาแw1ฅ)๙ฐ}}dež]W3žŠAšŒžpม็“CN\]๙ฉ1(@ทฎ'่฿‡ื6ˆสต ะ‰จ๊fv˜t๘]าฉsฎeำZ@ชข>ฐ ฑฟw7ม๚๑๒W'๒™ฤU,•ฆฐ•ฆ0สM‹>ํฃ‚6‰Pž็‚๏๋Zka๓Ulต€+ุส 6Mคญ๒สZฌ15}h>๓่—~฿๏ฬS"คAnŒ)Aแถl๎Wปˆบ๎ฌเD’ืตฟk‡ o๊rบ$‚4ม–ง‰‡^tbj่งW,x>บฎีๅD๕๏ล_wๅaMญkุ๊้I’Ko‘ ฟ-IEิ-ใ๙„[?Afื็\…ŒŸ]$ฆl‘^~›ส‘oขผSœtญy๋จMฮk?S I/a ฃ$c'1S็HงฮKEิํ!II•`หว ึ=€n๊Aีฆฺ$ย–ฆIg.ธษ}๏ีซ}”Ÿqž็ใwlAี/Qqษ‰จาิผฌnฐ"ฆn"คAnŒ)AแCmๆืšˆบๆ1Šฎoฏp๒บwเตฎG7ดสUKE.'>๛ษลืIงฮฃ๋ ึํร_๗ › —mtํ}i‚ญ03—IวOŸ•ไา›ุ๒Œ|ุnืŠ…ud๎๛yย-๛ัญ๋›ใdSR%?-Mข›{]^XเDqSœ ?M2~ 3}้๖ๅ ๓ุJึl๙ธซl่@9ภbใ ฆ8N:1Qqqฆ๒ภ]+eใwlB5t8ษ›VฑQ—ฑฅIl\ฎ… ๏ปQ%ฎผํ“ž~แZอb*๗ฬ7ƒ}O‰เsR‚ K์หDH ‚ ทฒy_"๊ิl~ื6‚G๐zwฃ;ัu-ต›๐fK“ุจ‚สิกฒอตษlพkKŠŠ˜ย้๘’ณ/Ÿอe wะAf฿/nzฺุ้*Yะสgบa›จXœภL'>Jrแ๘™šฬ๎ถR@7vl~ฟk‡›ศ็g‹ส˜ูห$c'ฐฅits P™tSบกํชˆŠซ—1ล l\u+k‘&DGฟ;๛›฿สx๖ซ๘ู6๎{ๆ๋ท[L‰AXb,BJA๘`๖'ฟbญHDิ ซจL~๏}?Jะฟw^<ก”ซฒY8uอššˆ%8C|ๆโกฑqE.ๅ@{๘ศ>๒k๘๛\{ ญ-ƒ™ฝLt๊วD๏สฯ,ส™๎<ถ:‡ืพ‰`๐ผŽ-.P ๗'.cๆ&ฐIkBีตึDT&q๛ ฃุD*ขnฯZ‰N<'b๊"BJa‰ณ)Aแๆ6่O~ลZ šน๏ƒ๖Qaแ๖Ÿ!ณ๋3่–u(ิ\‡›ฺv%‹ศฬ\&:q˜๊YZ๓๎๖ๅ‡่|™=_ภ๋†—๏Cež/SiB:uŽ๊฿ยฬ-–Šย]รFผฮ-51ต ir๙ๆภค˜๊,ถZฤ'jaๅJ.ํ^‹ต#ฆ๖…ป>Gม†Gv1%BJa‰™)Aแฝ7ไ"ข>(^K?แถOโ๗‡n๎Eีๅ)d_[7Ao๘(ษๅทIวO“N_tc่…;ด๓ัx๙^ย]Ÿร๏ฝ ๋]๕ŒI]6ธึ0ฯcก˜ฒI•t๔8ี7พญส lTๅ‘{7๐;ถิZ-<‰ๆง๑ูjuอฯ…ผี"ั๑gฯ‡Ÿ๚็มฦ}c๕>฿๎z๚woUL‰AXb[&BJAธ๑\DิE7๕๎๘~ฯNผๆ^—yใ…\™โf“ ˜ผึกดW‹+2˜ฉ คฃว‰ฯฟJ:vาจฃ’\ิถใQx๙~”ทืUDๅ๒N<ูิeอ’N]ภฮCX‡ืฑฏu}-ทhŽส$฿ ๚ฦŸcำXZม๎6*ƒ๒6>Fธ้๑ซk7฿ kAk@aS'ฅlRล'ฑqQึ่vฏGฃผะMBฬิป{šะš๔ ๒3‡W๏s๑ึฤ”)A„%ถg"คA„E็๛ลฏ˜j๕ ‹"ขn๊Iชั ํ„;?ƒ฿น า็ยฬ 5ˆNž%น๐:้๘I=๘๐:6กฺ]ๆI\ปั๔—'u๚'$ฃ'0…‘-bตv?แก›z6 ๆฎŠยา$fv[™Ee๊km•ื_+กูมภC่†ะ ฤ“I1ฅIHๆฐฉ\๘!>*๗|>tc:ไ~WฐๅLyส}—ŒA…9tC'*^ๅNึชmฅ€™ฅPZหE ๋‘ฆ บฑไๆk1;้n+3& 5fnœ๘ฬKozm}$๛ศ—ฏึ๋rณbJ„” ย!%‚ฐฦ™—฿๏๕ูyฃJIR‚ 7F„” ยe๚ะใ๛ญ ข๙ธืพ‘์_$๖ PkSˆซคำ็I‡฿%:๑Cฬ๔ELir [ฏe~^—-tEt„uNtค1ถ†ืฝm„๙C f๚ษุIโฯ’NŸ_[2D{xอฝx;Wd‹จฉ $็_ลฬ^ฦฤU”า\[๕พ‡@@y!แๆ'ะM=่ฆn๗:~าุeQŸมฬk'ฉชsฒ™ษ6โ๗ํล๏Šืฝร–g›\ฅYc+3nšแฤYผๆ'‘>๔=†4Aๅ๒x[\xฝบ:FัFeา๑ST฿ฆ8.ูEK]วZkžืนฟw7~็VT] hJ๎็Qษ $k?ธ@4]ืโZ2รWง=ืjฑU๗็›าŒศCภZ &Aeš\^Ce๊jmฎั|ušญฮaญฉUพฯYBiืๆ๊๙ฤD' :๒อ_ฯ8<ด:Ÿฑ๛๓™Gฟ๔๛~็๖gฎˆ)R‚ K<"DH ‚ ฌ ฆํ฿gญ>ดPD-z ๘ผ๎dv}ถึ>ถiฯวฦฬ์0fๆ"้๘W15sัฏฺ'คB7๗โwnร๏ืyตโญ!Mฐๅา้๓$฿ฤฬ^r-`ท”๏tอ?ฃ4ส 6~ฒฏฅผะI “:15z‚t๊ั‰g1ณร๎`ฝถถ0จ0็ฺบ:6นฟถฌsกไพปNถRภฬ\"พˆ๒3่๚Vnw๖–)McŠS}{๐ฺ7บC7๊ส้[-\z›สซษตŽI9Žป4•9'{ˆ๊‰ชou"<*9ั•ˆ๛Š๚j่yX 2ฎ4k%ULi [[ปแ๔สsำ๒rอ.#*S๏dRฯW”ูJk’›Qืโ…nสจMฑq…๘+kBL๋|f๖Oษ*BJแR‚ ซ›้C๛ญตั—o๊มiฤ๏ใฤTCGญ-อ๕ถq3uŽtbˆt๊<๑ษ็œ IฃUuอtS7^วๆZ๛ะ.tsฯbU™!พH:|ิU)Ufo“่XpศS^Mn@ธแ#xํั-๋ฎๆๆX‹)M‘Ž'>Jt์๛๓‡ฦUพuA…9ผฮญxmƒตษy่บึšˆ2ฎk๖2fๆ2x>*ฌๅ ๋ซ๋•N_Biฏg^พฏ–/uELLy†ไ์หT๏ฆฺ8W+6*๏#่฿‹฿ทwพ"Šธ๊๒โrM|คที฿YcภZtS—kๅ rNLีชHซุjSม&•5TีฆA{ฎช0เฆ^นืล%lตไ๎qit๎s‹E–ญ‰/๙Atไ›ฯไ^•yำ‡๖B2˜?๐ฃรฒ#Aธๆฉ BJau๒AEิuG”๚6unคซ˜ชoE๙ูy1•Nœ&=I:~Š๘์Kซ"๘\7uปœจž๘}{ัอฝฎeN๛ตJŠf๚"ษ่qฬไ9ฬ] FVุjะF0๘(^วft๋zืhมbฐๅ’แฃ$ŽŸx[;XฏบMKE7๗แwmร_๗^ว&t}สฯ`ฑP-a #ค“g][ž๊.ดb]9d+าย(สฯ8‘Y฿ถ ๘œซ๙N&:~•m\;7$cฐับฉ๋@ีทฮอ๊\MDอ`ำไŽ’ู4FyชฑปzžqีkเK\qblvk“U+ฆ, ”v"*ำ่*ขผšิหฎRญ<ํ๎๋w๘>gฃJœŒพ๛อ๊‹๔Vซ˜Anฐƒ!%‚ฐบ๘ฐ"๊Zผ–๕๘๋๎'ุ๔8^พUืZ›Jๆปœœ‰ำ$ร๏’\z“ไโn ปYAีJฃ๋\{^>‚%Dิ์0ุ้Iา๑!๎~—+\ฌ1(kP nYื6ผึืRcฌ5ุส,ษๅwˆฯผ@|ๆyH"lR]๙›• ‹n่ภk฿X›žท ะ๎ฆจกฑq37F:~ 37†n๊qmYw=ศ‰)k fvุฝ็ฎm่\๓ขเsาS&:๖}โกWต˜R:ภj—!๔๎ม_บฑ ฅผ๙๏–+ND%ีป:ะMไซG7ดฃ2 .|ป6ตo^’U็0…QWAt+์๎ ึX”RNฦๅš]lcฑIโฒk_,ฯ๕฿[ฤ” ยฺB„” ย*แv‹จE ?ใd@฿}๘๋ฤkpทึบส”ธB:1Dr๑ โs/‘\~ฌkYถhmD็๛๑๛๗บึฏ…"สค๓ญ_้๘iาฑSND™ไ&S ๏›ฯO๒Zึปlข4มš[ž&น๔&๑ะ‹ฤg_ยฦeH“๗™V~UืŠื>Hฐ๎A'ขšบ ผฒ\qNCตสคะษŽeNmำ•QนFŽ-ฎํsa๐yc&ฯQ}๋;คฃ'V•˜R:ภb]ตX๗N๕บึWฅkืฅXหmšฤฦ•{*{lš 2่บผ“œ™7‘ฯคุจ8m\bๅ&€)ฌIมZTฆก๔๎Bฬ.ธ฿ท<)MโžV†‰˜AXˆAXแLฺŸทึบ"๊บ‡F]ฟsซ ๙๎ูๅr๊Z Mฑi„J˜้ ฤ็^&><้ฤRศ๗D{จL^s~฿^‚มGk"ช ๅ๙๎pV-บŠจ‰!า‘c˜นQ'ุ–6M\fQ็žN6vบ1kฑัฆ4Er้-โก]๕Zฅฐ์~โ๙่lณ ผxฟgท ”๋ฏถx•งIง/`fG฿_฿ถlรจMySม๏qม็*ศ]SึUฅค#วจ๙Slqre‹)ํปฯ˜๖๐{๗ <์*ึด็$o\คŒ)Œaใ๒ฒZ3k :ื„ส4A˜C‡ .ว-M0Qข ถ:‹)ฯฎ jฉZี^ฃย:t};ชฎูM…Daโ"ฤleฮต]รฒš6hฃJ\y๛ฆ๑็๗Fž๖‚ ซR‚ +”้C๛๓s@Yšๆปzๆl์ย๏…ืนอUNulBๅ๒ฎB'q“ฑฬฬ0ั้็‰O3;ผ žxฺต~5uฌเaผ–šˆ œˆห˜ย(้ไ9’ ฏนช›e>อ&Qญ eบ๛ัM.เป–]dหำค3—HGŽŸ{…t๘จซ€X–2รCux๙^‚MOเ๗ํqฒ0ำเZผL์&็M_$- ฃPจบŠษ๘Iง/5๘}๗ก๓}ต๗ฝ@LEsฤg_ฆ๚ๆทGถ–kด2v”ž๛,zžหˆบ"ข<›&ฦn(ย(ถZXkิน๛E]ซkiำตฯ_uโ+๙f—๑๏ก˜—สCีๅkykYPสU|%Ule37ๆ*ง–‘ˆZtKขฃ฿อoๅ<๛ y๚ ‚ ฌDH ‚ ฌ0๎ฅˆZQ.wฉ{'~฿ผ–ผถWa 6.aๆฦ1…1ขใ฿'9๛ฒkyป๏ํใ5u >VQ๋\vส•T0ลIฬิy'mฆ/ ดฯฒช์zฟC[TFe๐๛๖ธชฏบVRฌ]Š)Nธึรัใ$—";9_ฑฯส ะM„?฿ปว ›l“kŸด้|ีš™น&นบ~+tvd^"ฮWต-œศWš">๕๑ู—kylห๙sจ\K(š`๐a‚E7uืฺ^๗ณธŠ)N`ฃJฏŒฆ7k ˜ิMญMžS~ต–Cาชซ*šวฦลe๖YTXkQตjPุAฆV]ƒI0•์ฬ0ึše+ขฎ[“j‘่ฤs"ฆAV"คAVหFD]‹๖๐jเ~^ผ:—“ฉฏUUH'ฯbf.ฝ๛]’‘w]X๎]:˜้ฆ.ยอOบ์ซ–~Tถู,-ฎอฐ4I:užไห$ใงQ~fJ4ถ2‹nhฏ‰ฉวPน&๐ย๙j37N:z‚d๘า‘cคS็1…ฑ{'=”Bๅšษ์tพ฿ษ&ํ_ญZ›รฬ\rmŠaŠ9@ฟfnย:ฮญ.ค]-h3)ฆ0๚ณ๗f1–]ื™ๆฟ‡s๎ำyž3rN&Gq‡”Yถ6บห ฬ •^ ไS€Tไj๔[็C?4ะ‚ ~่๎j@ๅjฯ’lฆIQโ˜ฬ$™๓๓<w>็์ฝ๛aˆL’I2‡ˆศึA$ษˆg฿sฯ_ฎ๕/W แิyz_บ5,ภU ผมoย9YืJกเ.ฏฌQ%QX†ุฅแKฮ„€ะ๕ํ4‰ฮK~v"_Tฅ๐๓<œ ป˜ข … ขถขฆยKQuก345ฏZ ้6ฺฑ-ฎ_๛;ฒ˜b†ู3ฐb†ูแ์X๕๙ŠNP;ภ7 {Ÿ†j๊#๙ใง6of๎2ฬาMืฮยฌNPฆั!ฺ๋เœ"Iึุ ‘jคCฝˆ’ฅU˜•qD“็ญLฤีRfoผiค‚+e!:ก;C๗= YำOฬา€ฐ๙y˜ล›ˆf/"šฟ ป:นอS"Y‡ฤฑ฿†๎Žื(g€ชึ Kิ๎iBภKลน={i฿BAำ6ฟY ~"ีmB˜์‚‹‡hแ:ต=ๆ๗ฉซแl๘ex^กj<Œ…[ถœขสีฎ^)gB™B0 IDATคะD  žKำ*\XŽงqฮวSHๅถฟ>M])สˆ"๑naรP^ƒ-ฎภ™๊ฎซช๗!‰ฉ๘w™ำo๏†avแNˆ…ร0ฬฮ%{ๆีำฮ้ั,ขพ๐`I5@ึ4รz ^฿3Pอƒโ,U€Xƒpโ พ๙ j)็6m"฿บˆRGใi€Mqˆ4๎„aฏŒ#œ๚vmยัดณฝ%:ึ/†‚ซไก2ฝP‡ใ)uwe9สฬZพp๒ฬ˜์L, –พGผ็แ พHkTำ แ%Iะ˜ถธ ป6U!ต๑{รํอ5ฺ ฎฒQำvˆคว_+0หท|ื0นi?ฝฏ1(ึภ๘ผแ—iต๔ต(„+ฏย–ฒ€ hญ[3gยx"_# ”oฆHธฐฉ ฉœ`ทๅ๕’ชำตTฅ“จ:ีU๓q•ZBศmyMพ&ี"ย๏}\ซ?อœลY90 ร์XH1 ร์@ฒg^}=Qปึ…ิถ@ค›เt๗4]L๙t@. ฝ‡h๎‚๋oาA.(ฦ™9๚4$มF^ƒn?ีL’~ฮ„pๅฬสขษอ_…Hึึ๎iษqg14`#ศ†.จๆAส.สtC(ฮ™๖0ป:I๙ๆฏา„Dm๎ฆ#ีฏ็I่o@ต AีทSลO~X`GXก6ร–ขณWฑๅ5ุ4tฯำPญรŸk!upีข๙kจ^๘ฏp•]ฟญ>๐[„e +xž‚๓“ BฤUฒฐลีฯ…•๏ํ5sฦ@ฆฟ†DŸฆส#kฉZ*(P๎YiuKชฅœต€3ฉFศD ห(D!‰ 9ˆฝS๙Uื„ลร0ฬฎƒ…ร0ฬb/ˆจฯ=f 3]ษ๘G บ๓(TำUKYg# ฌ ั%„ท~WYƒ‹ช4:kฟฝ„ฌi‚72t๛!ชฦชmฅรกฑˆสยฌN!š<ณxNi์อJฏEj@(จฦ^จฦจึ$ฆtฮ†@ทษๅ็\{ัุ์ิgึ๓aDƒHe ปŽร๋y’~fCDฒ๎3“๓ฬ๊$ฬสJิฦำ ๗๏ลdgบ๓Tc/pw6‘ณฐๅ,ฬ์%T>/€า[3‘ฯZ@๙P-ƒ๐zŸขuซiบ#yื3ขชjSr฿ญ™s€Le 5~ ต(ฏฟฏใฬ&T ฐๅตMส>ฃ้y"Q gCศtsœนฆเL[Zซไแชส๓ฺ‡๗‹)†a˜]tR`!ล0 ๓๘ู{"๊‹จฆ~ศบv๘Gพ ี:L‡l!mด–„็`ๆ.#œ๘ถธ๒ๅ:BBึตQK`A’umฑศP”cSษQX๙ิ˜•๑Xp‰}Q)p?งhVเ๕?™้‚j V>/E๋Q-Rxiมฅ 1•Ÿ๐MFฒบ๓t็qจถ‘;ก๒สฃ l•ใ๘>๚ฬบT‹งฮฟœษw3งฯŽ๑a†ู9ฐb†y dฯผ|ส9 $N๎ืk ฺม๋y’ฦล'jจีN'ย–Vฉrf๎2ฬU„3C(ŸไF๛!่๖ริj–ฌƒšชmชุ์4ข้ 0ซำpa‰EิุL!ผม ๛H %๊๎ˆฉา*์สlq ีO–&$–ฒ_T่T๛a่ถจฮcPอ4O'จ*ซZ€อฯรฎNRำืx}HxุตyจL7TศFึบ„pQณtแอทM‡Hึ%5พ|ฟG"J@5๕“ˆ๊8B"JiQqพI}’ป๖H๗SกตqีฆNlศ'gBส˜ชไ7ชฟ,cส9ะดผT…•'๋จ"สYj\QCx]พr]XL1 ร์8XH1 รl#ฑˆ…ฤk|5uภ ๐_ ฟ๙O6P;Š1ฐ…E˜ีIธJ€€ฌm‰ซ๊้ฐli2™ออ"šป ป:[ษลaๅ–/๎ !$ผม šกZ†ฉฉ•ฯYุJf้6์ส8‚๋gasณTม&tAจ–ก8ภ~˜ฺ๏ผ$ชƒ2l~fm€ุ_Šฐ็"<\P†ฌmฅเ๓xBaUšศ7wมๅŸมฌN%ฆ>๗}ข.,Ceบก{ž„๎<YืF๗VภU‹pa‰ค‡Xx<ฤ: ฟ"DR*YOไ3pQ.(“ค-.ึ@(gHะ ํoHa‘ส@( ็ \Xฅ ตrŽD ย_S ร0;R ร0‹จฏAjxฝOรz บ็$U=N}เฅ4*ฏ็ญอ!Zธป:gN…|-yg!;ฏYจึจถ@ช8ดZภU๓0+ใˆf.ย,^‡Hิฦ•Q)kศKั๗ +ฐล%˜• :`๋œต›๎ฬุRถฐฏ๗ศฆพ8ุ.1U-!š๚ีO?ช‚>—’ฎ4ผŽฃะ}OCึตSถWP5T\ลCSฺxฏ๘HฤD$—R๕~ dข–ฎฟเ‚2ตฦๆ็แ*kดFส‡ฌiฆ*5/X˜.(ภ–ฒ$่…bก๛จ๋R-"šฝ๔7ี๏“9}6หW„aๆ1l;YH1 รl,ข๐ก”j€?๔Mx#ฏBท„๐kปๅ…U—อ^D8uฎZL•/fฏ…๒แ„Ls’ฺ%;C๗‰ธสฃ8Kํ–^’Zศย*lify 6ฟU฿xษx๊ณู˜์ „าPวก๊;พ0‘ฯU๒n แ๕ณ€s$[ภ|ฒพฺ^MtGD•s,ขถB€XK๙’u€Ÿ†๔R”Ÿg#ธ [ฮRฅTชŒspA+TIUฮฮททR†t‚Z_ฅฆ`์  „eชพ‰สเฌ๒ญ_3‘สPีT"กSภ๚=โ่(ณซUh‹จ-‡ลร0ฬcx"ฒb†ู<ฒgN 8็F!ี๓ีธŸง„Hิ@ึถQ–ิะKP=”#ฅ|8XภDิฎ'$UแH VฉฌดŒh๚cDณ—`๓๓ยอlม:)’†^ ๐หะ]ว!“ ฑ”Š…ˆM9,,#ธ๕6dฒžฆ็ฑุ6ษ8 >ฯฯCึตQ–Wบ๑ฎ‰|wํ๙ฌญจ[X† J’k{ˆืรKBึwฦ-|wW‚: -/.รW`"ณฐ˜b†ูฦ )†a˜G‡Eิƒ>}$„—„ฌm…๎9 o๐Eจฦ>jUYQAถธ ››ก|kกš๚ก2]u€าthซๆaKYDS!š๙ฎด๚ฅ!ฮฬCH!เ‚2t๛!่พgh_*CdO๖Š…œƒ หฐล˜ี ภY:ls๐๒ถฏ›ญไอ|‚ไS ฒถ-ฎภนซิ0n3หcpA™sฝถ๋~‚ )เผฆ‰BฯeปgโŠOVโถฺุ<œ YLm#.จ„ม•๘_k~๗GฦWƒaf‹žŒ,ค†až์™S็Q๗๛ิาƒจkฅ๓มกš๚7D@Qถด ป2pโCjหห/Bึ4Qpv็Q่Ž#um4L’˜ฒลธย"ยษsˆf/Q•ลิ#œ…ิpBBึwภ๋}๊3๋DยฐD™6a…ยๆ๔ฦ41”yณ6KmdฮR๕šs\1ต ยรdgเ*yx}ฯ@6๖าšA_๋wคGqถฐ,ฆถl]X ‘ฌ…จ๏€๐RqvWH๗J~.ฮสP๕ก—‚ะ$})hพ@ƒ ‹$๔น v[p&Bp๙็ แีณฬ้7ฬW„af“Ÿ,ค†a•3ฏŽ งOCขฏฦW=qdm+ผoภZQ™รฒ‹ช”?”Bxใ-7฿‚+ๅ๐๙สY ีq^๗I่ฎ้ dบ *SK0หcˆฦ฿‡YพM!ฮ,ฆ่๐์ฌn„๎>I๋”n˜ะๆยj<9oั๘๛จ^๚GจๆAxฯมz™2ฃ4„Ÿข๏Uษo„›C ฟ€`๑ฑ๋f‹+ฐี<ผฎใ™žธŠ pQ™ึ ธ ี2D๕bqธq๐ฎ`๓ ฐ…@jมๆp3ึ„!ฉjP>ez…eช๐,ฦ-“Jร…U’Rฉจฅuด.,ัŸชpี"x‰ถW-ข๐ืฃใBุS™ำgว๘Š0 รlา“’…ร0ฬฃ“=s*caOณ˜บ7ฒฆเ‹ะฯB5๕Cึด@x & ฟ๙ฯอ ธ๑&‚๋gแ*ภF_=3ิFึ๓ผž“q@p ‰ฉ‚€ฃ™O~ถฐH c๏™{บ‚ Tหtฯ“Pอƒ5M^,–ข*\ifeแ๘{ฎ \em#฿ ฮาz๔?ภ+4ฒ^'bแ(แJY˜ๅ[0k๓ฺ'1%5็mถฐ Vจrฐก3^3ช64K7\{แญ_ยW 4’O+๘ ฒฎP๚๎w›—VaK+๗สžb`—ญ ’๕•ƒT๑‡วะ…•ธฺ้ณืุYCRสฏ…HึBz๔ูมE ,ำ็f9วีR[นX-ข๒มy฿ฬŸ2งq–ฏร0ฬ&>*YH1 รlbJ่ฯW้ แW {Nnˆ(๘)ศXDญมๆfN]@pๅgpฅUธ่>วอ  ีุีv^฿3ะ=OB$j คGbส†ฐk๓o แ๘{:๙ตขk_ธ*yศ†.x}ฯPv]+เง!„ค–ขrfeัฤn ฎฐt๏uZSฝOQKf฿ณ ]€๖)'G(ุ๒*์๒ฬ๊„Ÿฆเs8–~wม–`+y่ฮcP๕@"ฝqo™ล›ฎŸE4๑‰ษ)’*ำฤฟ๏ภซฉฬFnQlCH˜—a๓‹\อ๖ภKฃจฒ)Q ๘iศuฉkชโ,,ภ…eศ๛x๏ j•Mิ‘T๗S4=ั„ฐีMใ+็เชˆฯศEๆ‘?ซE„ท฿๛4ธ๘Wส"Šaf‹™,ค†a6Ÿr.’๕๐ผ uœZพ๊ZฉeE$ขึผiDำ#ผ๕6la ฎšฟ+ุ๗A~˜„๐S ]ะmก๛ž+ฆjฑส kaV'\? 3wๅฎ€็ taฒฆ‰Z๓Z†ฉบ&Yv6‚ซฌมฌN!š<‡pุตYธ ‡”ีšPภน๎}^๏ำTqีุ x‰๘Sซ“ˆ–nCฆ๊)เ™ืไ~๐ิšWสAw‚ฌk#aหCณxมอทMž‡อฯมๅ/_/!กปŽ!๑ฤภ๋{Ž๎qW๐นตฐๅ,eL•VYL}-’DTฒPปTp&"มWZ‚R|yอ.ปข็dm\ํ™„ฺจ0uQ…„~Pไเ๓G\dล0 ณ}ปR ร0[วS"Y๘eจŽ#P-ƒ”U“จฝ#ขึวฬ'ˆ&ฯมๆfaหYภšM๘๑"Yีุqบ9่๖รฑ˜`"8ภฎอ"ธ3D๓Wฉโรzบ(€๐ำะO@ท€ฬtSk‘๔เœกสต์4ยษM‡ษNWๅ=฿‰4t๏ำะGก;’ ๔า๔:œ+ญภฎอ#šฟ™j€H7ว•ผ?๙<ถฐ[ฮCต B5tR&ั]Qแญทอ| “›…ซ่พ๒†^Bโ‰?€๎:N•„w‰)pฅ,liฎ’g1uฏ๗นŸ‚H5A๘I@%(ฐ’ˆrๅ\9K"๋d๎Wทึ$!“qลT,Ÿœ ใlฉ<\~ฮTYL=่๕ญ\k!ผ๚๗ฤ"Šaf›ž ,ค†aถž์™SO:'ฯ@โตฝ๗$ฉฬ†Rญ ๋;โ๊‹จp™B'โujŽƒๅ•Or((ยฎอ!š8‡h๚c˜ี ุา*`ยG>ฐหฺๆxชbT๛aจฦ^’…ฮRๆW) W\B4{‘™—ไ๕๚บ˜์d]_um$yM@™^ท‰h๊lv ถฒ๖H‚ื?๚$N>T๋P|c1ๅ\X‚+Rพ” +๛^L9€L7าT ไ5ช Jิmด๒นสLnf๑ข™Oaณ“ Vณšเ Jส‡l๊ƒn?ี2Dำ๏โ)l+ฐ…E˜ล'ฯม,\‡ษMoYๅฺกPm๏ภ+P}๑๋j!1%W)ภๆ็c` ‹q%๖‹๗ŠD‚ ก{ ๋ปํQfuัไ9DณŸฦ๗ีา– แ%‘|แu๘#ง(|๎เsk(_ชดJำ้ทท†5สงชB/Imsส‡s.ฎผฬรWเย Vุฆ๗ซ32UฤA๊ยOCHMb*,SตVต[ฮBžj้L„เ๒ฯYD1 ร์„- )†a˜วG๖ฬซฏ;งG!ัฟ^ฏL7Bต€j? q„๒€Rw‰จ Hy@ณaฎ!šฟ›_xl"๊ ฏฟก^stภn์ฅรeไlKqศ๖์E3kณqxฐฟ๛\ี"U#ีwPฐ|Aศ8XUa K0หc”ต2†hสึ‹จฯฃผXLฝ ี<6หย@ศธตršฺ*k๑๋ท{ท•O(ธ๒œฉ@5๖Cfบํ“ˆสฮ š˜Z^ฎั}ต]/+•A๚ๅ๏มx"UOำื1UL•V` Kwๅํ€zg-„Twฆๆฅ›่3Žฺ๒‚ณZ ๖_ฉำ๋t้ Ul%(RR–U\-…jถœทํ–.จ„แ7C๚ท~๘yย0 ณถ>,ค†a?;]Lษt#d๓t๋t๗ จๆAˆdUm8Gํo๙E˜๙หˆฎSฅัฺC„`o๕SB…Uหผ็เ ฟีะh?žZ%a‹K0+ˆ&ฯมdงHา์’vpaฒถ^๗IจŽ#wrf„ UZ…]G4{ัโu˜ูKpQ๕๑.‹—„l่‚7 t๋0T็1j…๒RT1U-ยฎอยฎัิ8‘ฌง@๚=ำส'aหซpQบe2~O"ขh๖2ษจนKt_=–—จ šz๑฿Bw๘ยD>g" ๏..Q%ฮ†4ฝbŠBฤqง๊(xW]ฎธ˜ถผภ>6๕ลJั‚DD"‡ิS๘ฝซ่s ธ๖MN› *aดpๅ'ีw๒๛™ำgณผ๋`†ู!,R ร0;‡•3ฏŽ งOCขaG“ำ ]Pํ‡แ๕œ„l€Lg่ฐ์UDๅฉๅห,\C8y67Cู);๚้'็ {Ÿ‚ืt๓P ”ร"่PiKห0ซ“oพ ››‰ม{ศ ฮYxว ปOP^Vข.;ย•VaV'อ_…™ฟ‚h๖"URํ'ฃILีถย;๐ Mๅ๋:‹'M๊ฐ ››Yพg T}็–f๔l๑‚ั๛ฌธ–๏dฐ้œ )\~j\๕1lnv‡ผอ|่g‘|ๆโaฉ;bส9bP\อ/Pฺ๋.ฮZภ9’Pฉ๚Š#„UุJˆชpฅlฦงv์๏ฐ1`"]\ฆEย<\ดw'๒ฑˆb†ูแป!R ร0;‹์™S {๚qŠ)‘j€ฌk‡๎< ฏ็)ศๆ~jSั ๎ Šp๙ลธี๋*ขษ`Vฦwฅ~ ภ๓Pญเ๕=Y฿ แงโรต+.รไf\{6;M‚D'ฐ3ฺ’$,„sP-ร๐๚Ÿ%ฉ‘ฌง๐kยUr0ู™8#‹ฆบJ~็.‰๒ Sศ:สา]วกปžˆฬŽฤ[Xฆฉs—!”Y฿พ~๏9ฐ…EุjบใMิ4อๆ็iญฆฮ#œ8ป6ป3 /่o!qฟ‹Ešปƒฯ]Xฅjฉย"I›Pอๆโ p/Q ™ฌt’~ญฐJSใ฿‹&้+™Z“ฺ๋ใะ๓KBlTTบJ6?O๋บGชYD1 ร์’8 )†a˜ษ†˜๚฿o›œIค)ผ๓ผ็!›๚iคy\…ฐ [\†Yบh ตต-ํง!ต{#ง 2ะ]วจฒภฏูถฅุตyWN-nŸisn1%แL8Cํ‡ฯ“ิH5™pX-ภๆfa–n"šพ€h๚cšฤถ[P2ีีิu‚ฆ9vฅ–)ภธฐ ณ:‰h๖„—ข5;ตELP๘u~ถฒFฟSห „๔เœอ/ยฎŒ!แํwhภnธuา$ŸC๘ฟY( 1e l5Oฟs9ทƒe‡ธ๓XำLUQ^ B(8Sฅ๖ผฐW\†‹*;ง5๏Aอ๚Dพšˆd- $ิœ…‹ชฑ˜ZŸธ{'๒นjัโ๕‡Eร0ฬ.ู‚ณb†ููdฯœpฮBช?*#tขฆบใ0‘oA6๖R8ฎNppae#[ษฬ]Bx๛]˜ี‰ฝwฑฅ‚j่„w๐ืก๊ใ0๐v ูถle .ฟ[XDpํŸแยJ2ถฝgWอC5 ภf,Ÿกi_ฐ@P‚Y›‡Yบ…h๒CD“็aK+ปxM4dM3Tt,ฅT๋j่0]-ภfงจb*QYืถƒไ‡€ณ–Zฃชy่ฎ' š6ึห•ฒฐksฎฝเฺwMฌeหิะ…ไ3o๐Ej๋•๋ีCิฺ๋*ุx"฿ฮฉ,"I(€XD5P›žˆ\M%ุโ2\XปB3!DขžขมKาKฅkเ‚๒iๅๅr\ตˆp๊ม๙Ÿ|7s๚์๏†av,ค†av ›/ฆ„๒ าะ]วแผFQ5MYI.ŒCฐณSˆฆฮ#ธ๕Kุ์ิฟุสƒj์ƒไ7กบ Z†H๚๘iSลeุ์1eถบ%IPๅ“ษ:x฿€jZUhrส8ย[o#{oืส{_Amค]วก{ž„nl์‰CฆM+ญภไfaฎวZ้วXอB•Zfu.(ฦQCด^ฐี5ุ"ยซ„เ๚Yุโ๒ฮฯ^ปtวa$žCx}ฯศฏฟ‹ย8ทจDแ๔Q๙ฑ็9Aค2ต-‰ก)+(Q๐w9K!๚b๏}ฤ9k!S ษ:ภKA๚ฑ˜2lXชล;ำ-wp‹(†a˜]พฝc!ล0 ณปศž9๕คs๒ $^{่o"dฒบ็)x^j„L7วกท๔ท่ฎœƒออ œ๘มท`sำ{โภ@I/ ูิไ5จ–a ^Š2ฆฌู@feแญ_ลืoณฏmWตญะฝOAต ๔๚ดฏ(€+็`ฒSˆ&>D๕สOแJ9์ึษf๗uEฺ๊ {Ÿ†?๔U๓ี4“,\>_›ƒอ/ึฤ•Hnี๋aๅหฐ๙E่ฎใTั‹จ๕ื\{มๅŸ๎๎๊ตฏ@w?ฤ๑฿…๎:‘lุ˜bนัW-ภญอรูp›ลUฌ ?Y็*ลCย2l%T๓$ฅฐ๗?๏œU…y)^๔™ใช (ร–spีŽสฬbล0 ณG๖ฺ,ค†av'ู3/ŸŒ IDATrฮ}P1%’๕๐๚ž72TหP\e!6;p}ืyใ`ฟฏ–~ชํ ผม Aต าฃCถ5ฐฅ˜…kˆๆฎภฌN@ Uะ”!ฺ๋แ๕=M"ชฎmcช™3\e 67ƒh๚*็+ ทต…๐๑"๋;่ฝ|เสะ๒k(๗K >ทล%z*ช$j1e KฐฅU +oŒ๏-‡6วฎฦ๗ี>^฿ณ๐:t็qj‹‹ร้]ย…8P{!žฆธตkใฌƒLีCิถQธ๗zuก จjซ’lดง๗อ}D๙ dmDฒยฏขˆ+z.๛ไDQ ร0{lอBŠafwsฟbJคเ๕= oเPอƒTeใื|VDๅfM_@pๅ็ใภมฯ BiศDTผ็ :C5๕RB8 [ฮ"š๙ัิ˜ ค—คš>tจ จ๛$THœcU !–W๒ฐนiDำŸ ธ๒3ุrว~[+j‹“™nzoฟ ีิแ%II W-ยฌNยๆ็)๐Kp›สg๓‹V~ฒฑ๗ฎ,ขfuแ7๚%‰์ป๛ส9ะฏAต„Hg คGนEa ถธBูEAiKฺใึซ€dM3ๅ&y้X๊†ฐฅธต9ธ(ุ˜>ท๎ฃ๘:™Bz๕ํ$ฅผไฦDQq^[ฑTถนjแํ๗> .ีŸfNโ,?†a๖ศNŽ…ร0ฬ {ๆีืำฃ่ฬ}ช^s$7Z†!๋จ’DjQ•5ุตyD3Ÿ ธ๖Fพ\W•6๔เ๔R฿:LYF'ก{6ไˆsฎธ‚p๒ข‰`หk^ ฮ†_่ชไ!4t฿ณะํ‡ :ฉI}vr^4{แท`ฒ“ผVwก{ก๛ž…7๔U'yiฉฉbชš‡ษNรfg(ศ?‘„~ฤ jณ6WษBท‚ฬ๔ฤ“)›Z9)ฯหๆฆ)?W*‰#฿ฆ‰|อ‰๚;นEA*๔yTษmŠ0t.nปญmกV4ๅวR7ข บ•q’๎bฝ:‹๗ฤ@,ฆt2SP~\Mๆเย*\Xขฯขตyบ—ถฐฒEร0ฬ฿Wณb†ู[ฌ‹)‘ชํ๗_€๎<ี"ฒฎย่›H6tSปž”pA™‚ตซDำ็k ’ P™Nุโ*U่x)ศฦ^จLตRšvmŽฒยฆ.4,,qฅแ#เ๕=Cbช0DMๅYKaฺAฎœงjNg!ผ\X…P>Dบ๐๑”ล4K•<ฅWึโ๛g๓ƒ์๗๏วœ"๙—จฅJ4ค๛( ่บGUชไผ‰|,ข†a๖๙#……ร0ฬ!๗ฟ}gภ†ๅQH๕ว|5ถ๒้*!แฝo๘e่ž'!5สงV็(ซ+ž่ๅLWฮย,D8}fแฬโuQ[€l่„7๐x)ชd‹ซืเจ๊ฦ,฿B4uมต7`–o๓ๅฎ๛จถ‰ใฟo่%สําษฯ>ฐ&7ป6O๕pœ๕p&„๐า4ะม‹'๒ญท\%jo{/Wน๘7g2๖/ืๅ{?๘ั๋NˆQ ๔เ[็_๐,ฟ[†a๎ฺฑb†!!ตqพ.ภฺำ?“๏์›c๖ฬหงœ๓GYLmยƒ5Yีะีq^฿ณPM”ทข}š๎–)Hู8๓&š,QัโM˜๙ซวYผVมีR[ถZ$›๚เ๕?o๘จ๚vเ3ฬ4-ัWŽฟp์]ุ์lv†*ฉ˜ญCiศtT๓ ‘ืเ ฝ‘จ๛‚ฒๅl~ฎšาเsf{pฦP _ชžฺ๘’uสหมš3ฎZ<#R ูpไ‡qJZ7*คุxฆฒb†นวNŒ…ร0ฬg…ิ]‹ตv”ลs_T? Y ี~Pf”n„ะ 8W ไ(zeถธ yฒฑชyถgเT 0+ใˆๆฎ ผ๕6ฬโ พภ›บX"Q Yื‘Wกšh2@aุ&ค*ฉ) €3\aแํwN|ณ:Iแอ< `s‘2YOขฐ๗ixฯA6๕Q๋คณ€ กฅใŠCภ…UZทjvmžึ˜ณฃ. M^ศyร/žQ}gDบq฿ŠจuXH1 รcKฦBŠaๆK…ิ:๛PLฝ๚บsz๎๘สว(„๖!j[ Z†เœ‚j‚ฌi‚ะI88 (ร—`–วอ|Šp}ุ ป๋; S;Rm dS?ltฐ.ฎภฎN"šป„เๆฐู)พได\ยOAึถAw‡?๒T๛ajั`หซฐซSˆf/ย,„ฌkƒ๎~บ๛ษะ์๕ #[XD8๖ข‰s0หทaหYภZp5ฃฌ‰Bีะ๗,ผก—จยPyp6‚+ea–วxเ <q”Za•Gํฏa.,มU๓pฅ,ฏวcภ•ฐr๑ฏs๋dNŸ"๊{?๘ŸŸ„3g๎%ขึa!ล0 sG? )†a˜ฏRดษ†๛oกtงฯพฑr]XL}้ใ“๒‡’ $ขT๋dm3e@ภEุา*์ส8ย้ ˆฦ…YฝทPRฝะ]วแ พQ U฿‘ฌง๏W|˜ี D3Ÿ ผ๙6lq๙ฮ๋เ๗‰€จiืผ‘ืHd่[อร–‚‹ทq}Eชบuช๓(tื šส—จ'‰Uฉšญฐ„๐๖/N ณt ฎœใuy˜%๒’ต-ะฯร?๐T๓ „WV๒tM~„hๆภั„7WอC๗<oเจึaศd=ต[juA‘r–ซฅถW-"œ:Fp'ร~Q฿๓ัง๕(€ฏยBŠaๆฯR ร0๗'คึฑ9RvtŸ‰ฉำฑ˜jเ'ง„๐ำPญเ๚5่Ž#ตm^ŠD…เส9ุ์ย้O^? ณ2q_‚Bต A๗ณ%ขพ›9}v_<DDญรBŠaๆ;5R ร0&คึูob*{ๆTฦยžNŸทbJyะmแ<uฒฎยฏ”L[-ภฎอ!š:เสฯaV'๛€“๒ค‚j=ฏ็I่พg ๛ R Rpa f้ข™‹ว฿…™ปสแฺ_ถษIeเ |‰#฿†j;HQBา8๚โ2‚ko ๚้฿ยๆp?ยP$๋ {ž„?๘"T๋0Dบ 2ู(Mm|๙ุข้ฉํo๑\eโหฏ(ผแo"๙๔ฟŽ3ผR€ ‰mvmแท„—ข‰m_๛ํtื1x}ฯBึwาภ!แLWษรEธโ \Xโเ๓M`?ŠจืGG3‰HŸมˆจuXH1 รใัอBŠaๆแ„8‹œ‘8cฅๆ์๋ง๖EIฤSบใ0ผแ—ก;B6tำ๔(ฉแœ‚l~ั์ET/=lv’‚ฎ๖gฉฦ>่งแ๕> ี2HU9ฮัฯŒ˜ล๋'>Dp,l~๑มๅื^]ญd=ผมเ5จถˆD „P$:๒๓oม•ŸมdงแA๋Dฒžยถผ ีz€ฆ%&๋ ”Objm6;…hๆ"ขนKิสW-๐ยฌฃ4ผgx๊)#*Y!Mก,.#ธ/oฟCืฺO?ะ๚ธ(ฌ…๐5่ž'I๋ฤฦืœ (_*ฟgS๓ง"JW๔i%๑ะฯ=R ร0๗ุSฑb†yx!ตฑA฿Ÿbjภ97 ฉx/žชeะKะวก{ R Y†‹Eิข…๋.LvŠฤร=ฺพ Iํfฒฑ^๏3ะ]' Z†่n œ ลa้ ผ~ถ”#Œ๖฿๓]$๋แ๕=ะฏ‘(JึCH ฤถksวAxใM˜ี)ธฐศ๋$k[(รh๐๙8_ช๐SŸSf๙6UKอ]Yบ V๖๏ฆำKBw‡์ทกAค›๎–W๒oแํ_ยYKษ=๚ธJ"•?๒t็1ˆšๆ๘{ฺ;๙_•5ุย" `ฮ˜๚๚kZ-"ผงมลฟ๙=Q )†a˜{์ XH1 ร<บฺุฐณ˜ฺ3จฦ>xƒ/@wƒl๊ƒL7Qห”` ‹0‹7xf้li0แึผฉกบ ›๚(วจํ ‰)/ุ.ฌ6B8๕ข‰zฎ”[W๎‹{ุ?๘-๘#งhญjZ t‚DTnแ๘๛ว…]‡ญๆใชจM\žšf่็เ <~"QO9HRมeุฬย5D3Ÿ šฟณ2ถu๏•ธู๔RPm#๐GNAwวญฎ^’หซE„ท฿Ap,ตฅ ฑyB$ฆdฆศซPญ#ิช}ภZธ [\!9–มA๔_r 7Dิ_iๆ๔/๖P๙w?ัiXฑiู‰,ค†a๎ฑG`!ล0 ณyBj๋0.…้w๓ว๛ๅ’˜’?†ฤkป๙๗ ๐_„n?ี2YำBU/paถธณxแํw`ฏร–ถฏ๊Eiจฦ>จ–axฯB5๕C6๖‘(s.(ภU T15qแญ_ฦญb{๔Yฏ<๘C฿„7๔"Tห0d}U'9›F8๑ข‰a–วเสน-ฯฺR}ะ='ก{Ÿฆ  Tf#D…eุย2ขนKˆฆ. švmnOWฒ /ี:L๗S๗ ‰:8g Œpโ}„ท~ ณ6‘จsาถHช่ŽCะ฿€jุhน…ตฐAลx"ŸณEิ๗~๐ฃืฃ›;]–…ร0ฬ=๖ ,ค†a6_Hญณ?ลิหงœ๓Gw•˜ฒฆ™ฦวทP๛U]+– Dli…&MžC4w6?๗๘ฆ)ชiบผ‘W!k[Iฦฌgๅ”s0ks0ณŸ"šฝŒp}ธj~๏l^5”ญี๗,t๛!ศLฯต6‡h๒ย‰s”T\†ะwu้'tชuชใtื ่ึˆšธ5อ„@X†ญฌ‘”š๘แิ๙=|.t‚&F๖?Gmฆฝ้ Iำuaz๛ุ์ t<๕p{๖ฃฮZ่ฮฃ๐ึ๘5€Œƒฯซ…๘^_ื๙XDmฎˆZ‡…ร0ฬ=๖ ,ค†aถNHญรbjง>Dช^฿s$ขฺCึทC$โๅ๘pjWวM‚h๖S˜์4\9ท3^พ๒กZ‡กปNภ?๘-ˆšfศT <ฎœCดxั๔ว0KทM}”v๏ฆ%YyŒ*:ŽB5๕Qkžณ*?๕ขษ`–nยๆ(XqพึŽ#P-ระGกš)วH๙qฐv\อ6yแ๘๛ˆ&ฮํ‰i‰ชe^฿3ะ' š‡ jš!เช˜…ว฿ƒYเ[ ธ‹ *ร~•&๒5tฦแ้‚พfธ ป6U๖M๐นซ\k!ผ๚๗ดŸDิŸ๐/N)ธ3€8น•?‡…ร0ฬ=๖K,ค†aถ^HลฟXkG'฿ู7›า์™W_wNBnอ฿:?ำO@ฆก:Bท@w€l่ŠCฐU<&>ณ2Ij*ฬ๒mุย2vV๛› )ชm„ฝ|›2ŒtPŠชR*kˆฆ?A8๙!lvัๅ]%ฆD*vบ๋8tืq’;~zc*[8๕ข้O`ฎมไfvT>“L7Bw?ี2 ี~*ำY (ยตKซpA ั๔„ท฿E8ฎผฯUct๗ ่๎“ะG jš)j fe แd\ฑUwLKœซไ!ผผƒง่3 ถ5ฮ‰ด6a…&๒ญอรมํูเ๓;"๊,s๚อ๏—gำŸ๐/NI๋F…๒—&,ค†a๎ฑวc!ล0 ณญBjSSnดCตคvขๆ~ส๚‘šG5Oาf๖"ข๙ซ0 ื` K€ณ;h้Au7๐ ๘‡B'!”H8 [ZฅŠœฉ๓ฐ+c0หใqีฮ ศt#TหTวQ่๎' [†DD8๑!ขษsปโพ–๕ะGHD๕œ$ู&V`Vจฒะ,„ Š;๖wp•vบ๗)ˆdd*Cm–6‚-็`ณ3'ง฿qๆใ|gฎฉถ่ฎ'เ |ฒฎP0ฬ๊$ฬ%˜๙kฐ•ฮ—น1ฎR€j=@ฺัDพ8๘…eธฤ”-ฎBHฑk?ƒ‰\๙พQ฿๓ัฃๅ ๙ใ็ณb†นว–‚…ร0ฬcR๋๐Ÿ#eGฯพฑpฝณgNe,์iแ๔้ญS"QYื~บ็Iจึ49ฯKฤ“้Jp…ED ืaๆฏ"ถธด๛Dิsศบ6xฯB๗< y,ฎHPVQP†-ญPธ๖๔Ds—a๓ ธ; {_qฒชก ช๋8ๅzตC$๋hJซ0K7ฉ๕p๊#˜…๋ปtYญM*C-nวแ ฟแื@x)SฮมUrH?๑>ฬโ ˜…๋5‹^ป„L5@6PnูW ๋;)ฐุตY’นณ—Hๆ๎าj"Ui`ภภ M‰:j5lP‚\e ถผกvืD>Tย๐ฦ![?๛ๅ๙?pZ๘ใว๙:XH1 รckมBŠaๆ๑ ฉuXLmโฮKBึถAต€ื๗,TตyIช8 +ฐล%˜ฅ[ˆฎลSฟfฐSืีzเ ะํGจEฑฆ™ฤTT…+็`‹+0ณ)‹iๆำmŸ๚Fาฐบใ(ผกกฺQๅ,ฬ๒8ขูOŽฝป{Eิ~iHMSิ๒ๆx…ชูคŽ,l9‹hโ„“ม,‚อNm—‰ZจLtืqx^…j €vXธ"ฬส8ยษsฐนูฝำึ&$T๛!xฯCeบ(๘|#c.OŸ!ฅธ ธใƒฯ]P ฃ…+?ฉพ๛—฿ฯœ>›ฯ—"ขึa!ล0 sG- )†a˜#คึ!1ๅŸ>๛๚ฉ}qpศž95เœ…T|pส‡จi‚j L˜ถCuญ€Ÿ†ˆรŠmqfy fแยฟŠง~ํqค†๎<o่E่ถƒเ\Bb*ฌภ—aึๆ`ฎ!šั์Eธฐผ•[?Eญ_Gเœ‚n?LQ‰ฒ ขฉ๓nผต๋Z๓่:x ˆtt๛!่งแฟ ‘จฅPp็เlW^C8๖.ขฉ`oยi}ถr'„Ÿ&Yุu๐หะํG่= WสยfงŽฟ‡p๊ƒ"Tห0ผกoB5Bค๊ฉ]ฯธ  I„—s2‘ฯU‹งฮฟœษw3งฯŽํ—u๘~๔บbT;ตฒb†นว๎ƒ…ร0ฬ๎R'tต7 IDAT๋X‡q)ฤ่Oฟ๛›?/๋๓E1% :แผง!3wFดรQ๋Y~‘*ขฎฟ‰h๎\PฺผŒาะG ๛ ๛žn;Q !$เ@UAมีฆ)wใ๏Q(๗}\Oj< ๐ฏSE”_!œ5pๅย[oฃz๑`W'(ฏผF_ผˆบ็$ผ็ก;Cฆ3w‚้MWX†-,!šฟ 3wัฬ'ฐลๅ๚บ๋'~*ขาRื•s”ใuํŸแชลธ5ืhgBxวก๛žj์#‰'Iผx‚‰ฉ-ศืbต๓Eิ:,ค†a๎ฑลa!ล0 ณป„ิ:ึaผ)•/ใk?/๋”ู{๓๘บช๓P๛ูำ4อ๓dู–-’lใl,6† ‰Œ N.%้Fํmo“@sO’ๆถ๗~UšฆIšถHฐ€<ฒe[’5ฯณt$s๖ฐพ?ถl“ข$<ฮz~?ƒฮฐืz๗็๏zW†[•ค‚zฃ|m‰Qฒ5ญu๖มQaทa๙` ัฆุ}8แqฐm๙}>I๎Eฯ_Š(ภ(]ƒ–YŽš”y๖ม›ูสHใ3X]‡ฑzŽ๒ป*ฅo"za ž…ตn#l*Š๎qEิิ0fkDO>=า†0รเุ2อภ(ฌฦ(]^P…โKF๑ฅบMถmห]f9ํJฉ็ตcข–ฝ๐\…aRถ#แ "!ฌฮƒ˜-/แDBณ’dŒ~'ชŽžW‰^ธ-ญลH8WžDXaฤ๔่‘/^Eิ฿~๋ทŽ„&๋cIDA )‰D"™#๗”BJ"‘HbSHd$x๙๛jฺ็ฅ'ื้ช๒xผฤหlฺฆ&็1|% ชปs๔(ฮHัS/œk๖,+n^rเMD/จFKอC/]ƒ–QŠš nŸœH™"r๔ ฌž์มฆ฿zฟQถฯยMn๏ฃฤ ทถc#Bƒ˜0[^ฤjADBry‰แC/ZQr•lŸ๊๖า=`8SƒุรํX}ว>`}ˆ่๔™wƒชขฅใฉŠQฐ5%็lSrv—ๆ5๏+‚ขhว”“~+ BเYฐฝฐ 59ว•…ณป{ +‘ฮDย1฿–˜ŠWeNmw@๐{>ฒqdl<6ว …”D"‘ผ9ง‘BJ"‘HbWHe'๙^_CiZฆm๏zถฉ'xKeq$ผ"2ตอ™ญwFSอึWฑzํืฯ๖9’ผฃ$ม—ŒQ|Zๆ<๔ยjิิ\WL1+ฆฆ†qฆFˆžุŽ5p5!อ…,g!jrฎ+ข„ใ.Ÿ์>„ู๚*๖`3bf}8—ผณ๘๘…U่ลซะ๓– &คกxฮ-›ลlvซู๚ŽcตžmศmUป†ฮTZฝว0[^ฤiG๑&ษ ~ป๗ฅ๐$JBš+ฆr+ฯ๎b ฎด‘ป#_h๕ผvไ‘)ฬึืขวB ๎ฅธนว๏xๅ๕ฺ๕+ช‚รุุ30ศ๗|”ก‘ั˜‹R‰D2G.#…”D"‘ฤฎสK๖๓ฟฏฏฆ$DฤถyๆT฿{ํฤ.วq‚;๎ุ‰๏ฬฮ ˜=G๊œ๑ก:H•g๓…GMHC/]ƒžปุSพd* ธฅfฦA8วAM€๎!ŠจฎรX๛ฐ[SC๎๒<ษฬไิ”\๔ผฅ่…Uฎ˜Jฬœญฬg›ฦ‹H่lฯ/ล“เJ’ู๊Bซๆ้—qF:(ณฝŽd~๘NแIด๔RŒฒตh9 Qท*J8๎†ก!„่๔๏œ๏xQw๕อj„]‘›7o\ฟชฏว wpˆ๔Q๚‡FbrLRHI$ษiŒR‰DปBช05ฟปฎšโิDย–อำงบ๘๗ืO๙๓ฎจ๊lนmk[<ฤpฌพ6เเิ)BฟขทŽYT51mฌ]P…^Tb๘ฯ.IrปŸยม™ร๊iภ<ฝ{เฮdฟQ;กำ<จi…่9‹ะ –ฃe/t—โ้>(ช&fCeEฐ›0[_ล8((ช†Qven๑Jดฌ๙จdPup,œ่”›-2้6ขW4๗=‘VวธQŸRฐT่zธเ[ฎcใ๊•x=}Cร๛C?งw`(&ว&…”D"‘ฬ‘ฟH!%‘H$ฑ+คJIฝi9…ณB๊‰ืพ฿๎็ใ๎ณ4'ObJQช.ฯ์ 5จจzษUxnBฯYˆโK=':P‡์‰>ฬ๎ฃ˜M;ฑGปp&๛])"น๘!๒$ ฅ—ขฏpำงฃx’]ฯโVN9Cญฬ์}1=‚โMFสจ‹‹ŒœE่%Wก ฯํศwฆŠmf13AไะใGฬๆํ_Œgu†[nจฅvํ*|ร#วรฟ ซo &ว(…”D"‘ฬ‘ทH!%‘H$ฑ+คๆฅ'๓•ฺๅค$0cู๊x๗hž๓ตq(ฆJ…A)ฆ.PยเMFห,Cฯ[‚^X5[้‘2[% 8+>Dt {  ณ๓ V็~œ‰œ™ัณหฦ$5) -งฃhz๑*ิ”lU”Yq๘1%Dd ซ๋แƒ‚ชนั%‹žทฃจ5%wvG>aGลิpšš7Tl S* _œ๋๏๏ด๋ฏ^ƒฯ๋apd”๘ูct๔๔ลไXฅ’H$’9๒K)ค$‰$v…ิŒพRปŒผไfL‹_๋เ'‡Z~›„๘vT๓wnซ‹‡ุฮŠฉzTํyฆฟDม›Œ–Vˆ–[‰QT–]โOq{DอŒcฦ™่C๑๘ัาKQ3หPอSŽ™tลTว>ฌฮC8ใr๙ลˆSB=ณฝ`9zัJดดขsป†'ำฃn฿(_สฌtzร๚=gv™eวข-/‚•z‘qBC(šงr+zAuปšœT<‰๗ฦห๘ทƒ=ฌืi*ฟw‰๕Mฏa๓๚ต๘ฝ^†Fว๘ฯGงญซ'&ว,…”D"‘ฬ‘ฟH!%‘H$ฑ+ค*ฒR๙าฦeไ&๙™6-ihใกรญ๐}ยaVฉwTO}ˆฉ๕ตBx‚จl”gy$$ิ@zNza z๎โูFๆธ"jฐซ๏8V็ฌพFิ„tด Œฒu่น‹Q…(š‡3;ธ‰ฐ๛ณ}/VO๖H›ฌ–บ qJFK/v+nสึขe”ขxฯ๎nh6a๕ล๊kD๑$bVใYผ5! ดูส)7ช`8กAฬฮD›_ํU~’ ŠOb๕4ถ+Š ิํพ7^ฦ}พ"๊์o๓๚ตlฝ๖>/รcใ่ั_าาั“c—BJ"‘Hๆศcค’H$’ุR•ูืฦฅไ$๚™2->าส#GฮกHŠ)ษO < ๎ฎm9๎ฮz9‹ฮ‰จHgคซ๗˜ ปฏ๑M๏W“2ั‹V`”ญCห(EMสB๑๘Fฺแqœฉa์แvฬŽ}ุฝวฑG;ไคฟ๕(กxจฉ่น‹0สฏAฯZทrmz{คซ๋ f๋ซุ#ํœi<ฏ๘’ัณb”ญรSq;JีxW@XQœ๑ฌถืˆv’b๊"ฌ(๖`k'แแฏฦ“ˆ๘_ฏรQ‚oeำ‰๋ืญๆๆM๋I๐๙ใ_ž…ืaฏœ•H*":…3ัูuณ้์กV„9ฯ4ฯฬ(มSyF้รหo5>G "Sุงˆžุ=๛aห˜œ'"<ฒงฟหx฿ทu;วโe๏DDaรชnู\Kข?ั‰ ~๘“46ŸŽษ๙BJ"‘HๆศDฅ’H$’ุR+ 2๘หk*ษJ๔11นw3O|็5โXLีฟ•ๅ$1๓?[%“<[isFษ*๐$ธ"สœม™ล๊€–W๙฿Ÿใ. O`๕Ÿ r๔ Dtล๐K1๕{ˆWuว๗ิjˆzPช้gญซYฮoผžค„ฦ''๙ษ/ŸคแTKLฮ‹R‰D2GF*…”D"‘ฤฎบช0“บk*ษH๐26๙}งุtแv rเ0ŽSทใŽญq‘Dีืœ:E่u๏Z1ฅจ(žD๔œ…x*ทbฏ:WiใXˆ๐ๆ้Wˆž|{ฐ้ยํˆง๊(šŽ^ดฃฐ-wZz1hwW>ฮ‰)ณ๓f๛^Dh่ํ –wEœP5ดŒyx+ทb”ฏw+ื !lฤฬ8V_#‘Cฟภ๊oบ _ซeฮCฯY„wู๛PำK[ล”pโฬVO‘#ฟ;โ.ลDๆ“gˆgฅ:"จจส๋ฯwี๒%v๓f’M๒เฏžๆศ‰ฆ˜œ)ค$‰dŽtG )‰D"‰]!ตถ8‹ฟXท˜๔/cแ(?xํ$/œ๎ป_ตหqœ S1ฃo๘ะrใญŠ^ผ ล›ˆขเุs๓๔"วžยlF˜‘‹"‹โKA/XŽžท-gZF Šf „ใVโ„†ฐ‡Nmู๖"2wยCห,วปdv]bบปcกˆHkเ‘ƒbv˜ฉP\”๏7สึโYtƒปิ๒ท*ฆ86ฮ๔(f๛>ข ฟ;๛š๘E„'q&ฟ/B_–"๊ยฐr้bn{ฯR“˜…x่ืฯp๐๘‰˜œ')ค$‰dŽผP )‰D"‰]!ตพ4‡?]ปˆtฟ‡ั™(฿{ํป[๛/ๆWฦ˜Bิฃjทว๔ฝแGห]Œงโ:Œข_*Š๎แเD&ฑ:ixา]๒eNƒs๑—aอ/o Zึ|๔œEจgฤ”m‚ล™รj&ฺ๐$fวธธiฅnฃ๑’ีจฉนณหโ@˜ำุƒอDžt%วบ๘ว“ฝO๙<‹7ฃ๘oSถ…ฤ์ุ‡ูพ์๘ซhแIœ‰ŸŠฉกฏ๊vถลหธ?ฅ`ฉญซ๕*๊-๋;ชW๐๑๗m%%)‰ษฉ)~๖ิณ์;z<&็K )‰D"™#”BJ"‘HbWHm,หๅOึT๐{™‰๒ฏฏ4ฒง}เโ?€!~iชขn็ถญq๑๐5V_[*„ฦข˜า๓*๑,พ=jbŠแCว]๒ี}ุmT=ุŒ m^๚Dฤ“€šZ€Qผ-ณ -{!Z Tl'2‰˜ว๎?IคแษทีX=PS๓๐,จล(]šV„โMBAAXa์มข'ถปห#ก ทŒ๒ญœGน•x*oฤ˜w5ช/47Š)aE;๖avqf7>…wku[<‹(ก๋Aเข฿—Uฬ็“ทLjR2ก้)}zฏnˆษy“BJ"‘Hๆศฅ’H$’ุRื•็q็๊…|Fฆ#|{O#ฏu^ฒ๏w๗YšŒ31Uชrฅซ^P…gมต๎’ธ”<oข+ขฆGฑzŽmy ปbf์Š่ำคx“ัา‹1สึขe–ฃe” &eƒชนb*<39เ๛ษ็ฐš‰}ัก &ga”ฏว(Y–^์V!ฉยŠ`6=2V๗œ๑žูฅ‹—UGฯ_Šw้{c$PUฮŠ)!\6†ู๒V wGพูฑพ[ฤ”Q\21ฟdA9Ÿบๅf))„ฆงy์ู็ูsเpLฮŸR‰D2G&$…”D"‘ฤฎบqAŸ]ต€TŸมะT„o๏9ฮฎกK~๑'ฆึื แ ขฒ๑J;6ฝฐฃlญป.ญี›4+ขFฐzŽaถฟŽ'4„0gฎผฤฤ—์๖/šw5z๎bิ”\T_ h†ปฤpj{คซ๋0f๋+8cฑื๘\QPา1สึนQ้%จI™(ชŽฐM์กฬถืฑบ`อ๎8่\9ปู)D๔)^ƒมฉ0าq๔ _ถใฑเ๏ีSฟs[m\4๔ฝbฤ”ข ็/ร(^‰žท5ฃิญ^13†ี{ซcVIœ‰>Dt๚สOP|)ู่ 1สฏq—&็€nธอฝgขซฏซ๛(Vฯัุ—ข ๚ำะ‹W`”\…–555฿9ฯ1qF;1๗buฦnล™ฟ$}ขXTิฤt๔ผฅx–พ=wฑ๏JQฮฝๆL๖๎รDOlว ก๘’c๎:?'ข๚๏ิฝ7BแrŠจ3,,+แ๖พ๔ิTฆffx๒…yแีฝ19ŸRHI$ษ้„R‰DปB๊–สb>U=dฏม@(ฬ{้‡{G.๏ร›รธญR_bjรญB๕จ”\า/ึ ๔ฌ๙่ล+ั๓–ขe•ป" Q}X]]5ึˆ„bkb5ี›„–ปุญ&*ฌrล”ขธ=ฆ„ภ๋ฤ๊iภj฿‡=ิŒ3=z๙—ถอ•p๙R\iXฒ -ปย]žงyยฦ™์วj฿‹ูuุํ็55ถ#1าัRr%ขKnFห(E1|œ๋/ๅDxณ}‘ฃฟUu›๊_แK๘คˆโฒ๏2:ฟคˆฯ|่คL‡gxz็ห์ุ๓ZLฮซR‰D2G~$…”D"‘ฤฎ๚เ’>^5dฏN(ฬ?ํ>JC•แ€โSL]ปM=xฑล”b๘ะ2สะ๒*ั๓—ก็-9[y"f&ฐ๛1ป`๗7bด_‘‚ๆ-ื›ˆโID/ฌม(น =)jRึ์‰f#„ƒ3าีำ€y๚e์ัNฤฬ๘•ัห—‚žฝฝ`9z2ดฌ๙ {ภqSร˜]]กึ{gฒ/vDิง๎EMอว([‹งโ:ิ@แ›wไsœ๐8ๆ้=˜ํ{มŠp%๖–ŠWp็]_฿&%จr‰ๅ๚๏`^q!Ÿ๙ะ๛ษL 0Ž๐ฬ‹{xๆลWbrnฅ’H$’9๒)ค$‰$v…ิmหJนmy‰†Nh†ภ๑+ห‡qEU๊ž์ๆ{ใๅ|šS๕บบ@๑$ ฅกๅ,B/ฌBฯญDIธŒLc๕Ÿภ๊9๊๖:}E๖ˆzGใ๗Pา0JVกผaB ์(86๖p+V๏1ฬฆ]ุใ=ˆ๐\†\G๑%ฃe”ข็-uc•ตผ ฎˆ ปช๛ˆป‰“—•Iิ4yi฿!yz{Lฦ@ )‰D"y3RHI$ ฑ+ค>ทบ‚›* ๐jใS|ํนCtOLวjvi มง?ณ%.๖Y1Uช‚ชป$™๓๐.‚^Tใส UC˜๗!ฝทhำ.ฌžฃ—ฌRฌขฆๆฃ,ว[นีญ–JHCQuPUDt {จซง{่4VืAœ๐ไ๙๗/RToZ ฯย๋0ึขx“Xู&bzซ๗ัฆX ˆศค ศsจ๚่…Ux—ฝ-ฃ Ÿ#Eu๛€M c๕6m~1=๚Žล”O…์้ม๏2๗ญ@ฮฑx™๊/~๕›ฅaว ทวโ๑gฆ๘“O~„์,ขฆษžGx๘ษgb2RHI$ษ)R‰DปB๊Oื.bห‚|ผšF๛Xˆเs‡่›Œี†ฦ7ถฌผฏ"+%hจj[<œ{ใถฅVM[๔,ปyฃQถล—Œข๊ฎ˜qwc‹žี}DŠจ?œึณ—ฒฆฃg–ฃ็/ร˜w5jjž+ฆPˆบฯอ๎#X=Gฑ{Ž๕{vปS@ำQ“s๐.‚g๑*Šๆวย O`ถ=4fว)ขฮ7b$๔œ Œ…›ะ๓—ก&eบs +‚3ูีuˆhำ‹ ๐–๛Kลซˆฒmปดฝง/๘๛z๛๔L์.MKMแO?y9ู˜–ลk‡๒เฏžŽอ˜H!%‘H$oฮค’H$’ุR_ธz17”็ahmฃ“๏‡œŠ‡$ฏม76ฏ`Af –ใทซฝ/xcyA[|ฌใเ˜๐ณโUDฝx๐``๕’%A]ำฟุูวฟ๘!&งb๗‘šœฤŸ}๊ฃๆๆ`ูฏ9ฦO26o๑RHI$ษ›BJ"‘Hˆ]!UwM%›ๆๅah*งG&๙๊ŽƒŒLGb6ฉ>๗lฎก<#ำvฺุว?ฝx์>Ks‚;ทmm‹‡sั<๕|mค๑น{ญฦ™ว–่@  ็,Bหฉ@ฯšš^โŠ)วBDฆ\15าy๚eฬ๖ฝ8}๎๛’ณ1ๆoภณ -P่6ใFAุQœ‰^ยŽy๚eDtๆ—I~OfชเYxžล›ั2ๆก๚SA3fใยํฤj฿‹ูwEU฿๔๖xQ‚ม€ึ๋>ม›๋ึT/Mี5ฎ~ํว?cl2vซ๕’๘ยง?FQ^.–mณ๏่1๎์ื19)ค$‰dŽŸ})ค$‰$v…ิ\ฟ„๓rัU•–แ ๎~€๑ฐณqศH๐๒ตj(KOฦดžo้ๅ{Ž#ฦm•zG๕ิ๏V™c๕ท กQ)‘W่K{ะา‹ัrฃ็-Aห(CK+D๑ฅ€c#"“83ใ8ฃ˜]‡Q4ฝจ-ฃล—‚ขhณ"ช่๑฿=๕":‡;็]‚Hy็'#Œ IDAT๐,น O๙ดดboจฺo-ดฺ_ว์>‚š˜ŽBLTL }%Pทณ-^ๆ้ŒˆาT๊PI๘๛ถฒถz9†ฎำ30ศ๗๘รcใ1;พŸ/qJ๒๓ฐl›ƒว๙ัฯ“c‘BJ"‘Hๆ๘ฝ—BJ"‘HbWHอตKูPšƒฎช4 Mpืณ๛ EcทJ#'ษO๐†jJI˜ถอ๖ๆ^๕•ฦณO1uํ6!t)ฆ.h๖ฃขe”ข็/E/ฌA  ฆไบาC8+โV;)šป3œช!,ท"สly™่้=8ฝˆฐ์uฑQ3๐.?z๑ชู 5ฟ๘L๙“˜'ŸฟฯlŒ'๐นปฟ^‡ฃQI=๓฿nปy ืฌฌยc๔ ๑apd4fว่๕x๘โํงค0วq8t$๙ศใ19)ค$‰dŽ”L )‰D"‰]!๕ฅหธฆ$MU898มWžฯŒปKผ๒“๘๊๕U”’ˆุ6ฟ9ีอ๗_;๙ฆืSฯvK0ฮฯฑ๚ฺ€ƒSงฝ๎Ÿ’wˆf ฅ— Va”ฎEหœ‡๊KU็l)!แIฬŽ}DO= gz๔๗4?—\”Peฮร[yzQ+ ภ}ยŠร฿Osq็]_฿&%จ๒fIกฏgรU+๐z ๚‡๙มCา78ณc5t/n๛eE8Žร‘M๐แ_ฤไXค’H$’7#…”D"‘ปB๊ฎMหY[”…ฆ*4Œ๓ๅg๖ตc๗Aน(5‘ฟปฎŠขิDย–อS'ป๘แSฟ๓๕Ž ]U”เณŸ|o<œงRL]„Dศ“€Qrฦ‚Z๔ลจ‰้๎าฐ3މwณc/V๛^์‰>ฤฬธœผห€^ฐ oๅอ๗้๙K‚j ฐ-žฦ๛Dิnผ‰kVโ๓x่แ฿9=ƒ1;fUU๘หฯ|Šyล…Gp๔T3?๘้ฃ19)ค$‰dŽ&…”D"‘ฤฎ ^_อสยLT กŒฟอพ˜ŽรŒdพRปœผไfL‹วŽw๐ใƒ-็ผ*h๊v-วรy;V_[:+ฆn—W๑y&>†-k>zมr๗_๎b‹@ fฦฑzŽbท!ฆFฮพNM+Dั wวC!ๆ fkX]‡0; fฦถ)—๑],โXDฉŽ*ชฒ๑|฿sำฦkุผ~-~ฏ—กั1๓gำึำ๓๐ลmŸ`ai Acs+฿ษรฑy๏ัดM฿๛/๏”ดD"‘ผแ(…”D"‘ฤฎ๚ฺ 5ฌ(ศ@Ž๔๒ๅg๖วt*2S๙Rํ2r“L›6ด๑ำรญo็ฃv9ŽqวึธHฅ˜:„ว๐ฃฆข็-ล(ชAหซD๑&ก  ขSXฝวฐบb๕มnGXดŒRิ”<<๓ืฃๅ.FKษMwwไ3รsณ๕UฌžฃXGpฆ†])%sซ ƒQ฿๊{ทlXวึ Wใ๗นB๊ŸŠ–Žฎ˜ž/|๚ใTฬsW)ž<ฦw๎(ฆŽ_UUV-ซคfษโM+*+vส ["‘HŸI!%‘H$๐ญ]Gลฎึ>l'ถ๎‰๗lYAu^:‡{Gธู๋1‡ส์ฺ๋ฅไ$๙™2->าส#GษGฦ™˜Z_+„'ˆสFyUฯ&:†5%-งฃจ=JB( ˜aฌ“ุ}ว1;`๗Ÿrwุ๛ญงI ={ZFฦผkะาKPSrฦ็Žw›žทฝŽืˆีsิS’ทOœŠจฯ)Xj๋jฝŠzหŒ๋ืญๆๆM๋I๐๙ใ_&#nแฑcไajชR๗์ถอ‡โแ|–bสํฅ$gฃg/@/ชA/จBMฮvETt{ด{ฐ‰hหKX]‡ฯkนžท=o ฦ‚จ iจ‰™nล”mแL แL cuฤ8…ีsT6>ซ8ด+ŠYจ{๑๑x๖็ฟ,บq…ใฦี+y฿๕I๔๛็ว~อ‰ำm1=?๚ษจœ?EQhj๏ G\๑วผta9kซ—SRGjr2CG6ฉชฒS^่‰D๒†|M )‰D"!„ˆุ#ำNMฐฃน‡ฝ]CWqk๋J–ๅคแ๛ป‡๎ˆm฿R—ฮฐ„ฌD““๛ด๐ฤ‰ฮ ๗ผ+ธฯาœเฮm[โแผžS๗ข๎]นuจ:jb:ZF)F้๔ขจษ9(ช†0ร8“}Xง0[_ม๊:„ˆNฟๅฏะ‹j0 k0ๆo@1(T๋Šฉะ๖X7V็A์กฌco๋;โ ‡vEฑ‚บ๗ฦำฐ/คˆ:ร5+ซนu๓&’›˜เ'ฟ|ŠcM-ฑ=OŸ๘0KฬGQZ::๙็=ภ•๚ฒdA9ซ–UR^\D % Cื0-‹‰ะิฆฌ๔ด๒‚—H$’sH!%‘H$ภŒi ฆต†ง#ewk+ฆEแทฎค2;€์ํไkฯŽ้8ฌ*ศ ๎š%d&z›hฟ9ี}แŸใNL]ปM=๘ฎSŠŠโKAK+ฤ([‡Qบ55E5v'4ˆ=ุŒู๚ f^Dd๒ฅQถฃไ*Œาี๎Ž|žWL9๖xŸ+คบbตbฬฑ0‰Sต- ่aฝNSฉC%๕B~๖š๊e|x๋๕$%$2>9ษฟzŠฃ'›czพ๘ฃd๙ขจชJkg7๕?zหถฏจc\4ฏ”ซ–/กผคˆด”d<†(XถลDhŠถฎ4žt็mุ)/|‰D"yC๚&…”D"‘ภฎึ>Q‘™Jš฿ƒกฉfh*ยฑ1^h้ๅP๏ศuฬบช๒ถฎdqv*ถ#xตsoผp$ฆใฐฆ(‹ฟธz1 ^ฦยQ~๘๚)žk้ฝx_(ฤทฃš7ธs[ํX<œ็๏V1ฅxPSr1สึโYxjJ.ŠๆA8&bf {่4ๆ้=˜ญฏโL^ุ/ื <๓7`ฏB/ฌrฅ๋>Pu„cโŒ๗b๗5bฮVLู#เX๑}ร•"๊‚‹จ3ฌZVษm7o!91‘‰PˆŸ>๑5žŒ้yปใถPตx!ชชาึีรท๏}จi^ว6ฏธซk–3ฟค˜@J2^ข(X–อไิฝ์o8ฮ๋Gฐ›ใžปwสŒK"‘HรI!%‘H$p๗ณDfขีE™ฬOO!ี๏มฃ*8ย–อะt˜cc<ิร๑+ร]xuธq%‹ฒRฐมžŽAagl ฉซKฒ๙๓u‹I๗{›‰๒ฝืNฒซต๏ข~งpทU๊ีSbjฌพ6เเิ)Bฟhล—(…Aั ๐๘๑,„w๙-จIYฎˆ6":…=ิŠูผณ๕œะEฌtTTิ„4Œฒu่…ี่y•๎2>อB ฌœษAฌžฃ˜๛ฑ๛O\ในRqŠUŸ^ท;Oรพ"๊ 5•|์ฝ[IIJbb*ฤ#Omg฿ัใ1=Ÿ๙๐-ิT.BำT:บ{๙—๛สL๘๒V–ไณถf JKศคฮŠ(หฒMOำ?ศ# ์;zl5—R‰D๒ฆJ )‰D"ปž= ฆM›ฒด$ึgQš–DŠฯƒGSqAุฒ้อpดo”อฝœบผ ‹=:฿ุฒ‚…™)XŽเฅถ~qwCLวacY.Ÿ_SAš฿รศL”๏พาศหํ—ฆษผSฑ”ฝ((ž$< 6โญ€#J๗ ยŠเŒv9ฑณy7Nh๐า–๎E ธีRyKะฒ $คกhย6‘bjณ๋Vว~ฌ†๘่/5+ขTิ๚@ฮธจF<รw}}›P” สฅฉJ\^ฑ€OrฉIษ„ฆงx๔้ผv8ถnเ๛Xนด]ำ่่ํๅ;๗?ฤิ๔ฬe9–‚œlฎ^QEลผ2าx Šข`6S33๔ r่๘I^=t„H๔ทซธค’H$’9r')ค$‰ไœ:CฤฒฉศLeEA%iI${ Œ7TL๕Mฮpธw„g›บi ]–cN๑ณe๓3Rฐ‡ญ฿c๛มใบ๒<๎\ฝ€ฯรศt„o๏iไตฮมKz ยa•เ๖ฯnฉ‡sฌพ6 „ขj_Œ‰ฤล—ŒQบ๏า›ัาKQผ‰ จฎ๐ i|†่ษ็p&ภq€KŸ็($ดฌ๙…UhY ั2Kฯ๎ศ'ฬ0bfgzซc?fว>ฌ๎ฃ—ๅ8/:RD]2u†% ส๙ิ-7HI!4=อcฯ>ฯžฑ[๐n}W-_‚ฎ้t๖๕๑?ฬDh๊’CNfืฌจbั22ำx=TEลvlฆgยt๗ะpช™—๗"‰ฮ๙RHI$ษ9“R‰D๒f!u†จํP™`yneiI$y tUม‚ำSz†yฆฉ‡ฎ๑K› ง๙=|ms ๅ้)˜ถรฮึ>๙ฅc1‡๐ูU H๕ ME๘๖žใ—ญฉผ#hW%๘์g7฿ืภX}m้ฌ˜บJ<>ล—‚QXQqzฮ"wIœช!ž zข'ถใL #ฬธ๒5! -)zn%z๖ิิ|ิฤ ะ Dt'4ˆ˜วl}v)฿ษwวษว"๊s_น็VGฅR‹จ3,*/ใำทพ‡ดิTฆfฆ๙ีŽ]์{ ฆ็๔“๏ฟ‰ีUห0t๎พ๛“‡›˜ผ$฿‘ฮบšๅT.˜GfZŸื‹ฆjุถอt8L๏ภ GN6๑ฺก&ง~ …”D"‘ฬ‘฿I!%‘H$ฟ[HEภ‚ฌj๒า)$‘ไัั5q˜Šฺ๔…ฆู฿5ฬณอ=๔L\š%8™ ^พถน†าดdLๆน–^eOcLวแ=‹ น}ล|RผƒSaฟ—Žqฐ็๒6“—b๊2'*พd๔ๅxๆฏGหY„š˜ข๛>QำฃD›važ~{ฌ 13ยนโๆTMHs{Kๅ/Cฯ]Œ’†š๎VLEงq&๚p&๚1[_ม๊>Œ=ฺป'c[Q”`ผ‰จ;๎พงVuDPQ•—๓8–•p๛฿Gzj*ำแž|แEžeoLฯํว{#๋jช0tžAพ๗ภฯปธKๆ3ำฌฉ^ฦาๅdgฆใ๗๚ะ4หถ™ ‡แศษ&^=t๔ผๅ˜R‰D2Gž'…”D"‘œ‡šES*ฒRฉฮKง 5มSชŠๅฆขS์ํโน–^๚&/n‹œ$?มช) $aฺ6ฯ6๕๐WOฤtnญ,ๆ“ีๅ${uBa๏‹ ้ฝ"Žอด#œm;๎ุc๕๋k…๐Qน,ุŠ'=)Fูี่น‹Q“ณQ<‰ณ"jณ}fหKุรญ83c`_๙;ืฉ)น่U่๙Kั ชP}I($P5Dtgข{ฐ™hำ.์กำ๎ฒรXมฑ๏›Qm๑๔qฅˆจ3”—๑™ฝŸŒ@€™p˜งwฟฬ๖—^้9พํๆ-\ณฒ aะ30ฤ๗|„ม‘‹๓ปHNfŠๅTฮŸG^v&~ŸMUฑm‡™H„ม‘QN5ณgaF'&าgK!%‘H$sไ{RHI$ษ๙ ฉ3hชBev€ชt๒S$:บช` A(jั16ลพ๎!ถ7๕0<}qvสONเซืWQH"bๆT7฿-ถ—|xi)ซ*#ษฃำšแŸv7pฌŠ+ดุๅ8NPŠฉ‹”˜hดผJŒฒตฎˆ ขz“ยALb๕4`žƒ5ุŒ3ูถ;“ฉ(๎#P€^XใVLี ~ท)ปข!ฌ0ฮx/V๗"วƒ˜ฦ™พrวง"๊ฮปพYฐ๋ฏu†าย|วGn%3-ภL$ย๖—^ๅ้]/ว๔\่ฦู๋pี ผƒพมa~๐ะฃ๔ ^ุk"ม๏cช–-œOnVฦฌˆาp„C$ฅx„-ญผดรฃo๏7I )‰D"™#5’BJ"‘Hบ:ƒกฉ,ฮNeyŽ[1•`hจŠ‚ๅBQ“ฮฑ)^๏โ้S„"๖มน(5‘ฟปฎŠขิDย–อS'ป๘แS1‡U•๑‘ฅฅ$ฎ๚ึฎฃœฟR7ลิฝจฏ7ŽžSQถ-o‰ฐ— €˜ว๊kฤj฿‹ีgผa†cw2Uอ‘/ตฝ`z2Œโ•(†o6;SŽ…3ฺ…yzั–ก!œ้1ฎ˜ๆ็q*ข>ฅ`ฉะ๕ pE๖Z+ฮฯๅŽ>@VzแH”็^y_?ฟ;ฆ็ึอ›ุธf%>‡กแŸำำa6ป๐yฝฌซYNueyY™$๘}่šๆ๎ฌu+ขNดด๑๊ก#๔ผณ~†RHI$ษ›‘BJ"‘Hx๛B ฦ็ฆํฐถ8‹št๒“๐:šฆ#EL:ฦงxน}€ํM=L›fiQiZwmZNaJ"3–อฏ;๙ฏM1‡?ช)็•ล๘ ษiพน๓(อรW๚a๏Šชฮถถฦลƒ๙Xต„ะƒLL) ZFzQ zRทว’? a๗Ÿฤ์:ˆ{{ด™z๗Lฆชกx“ัา‹ฦ็หั‹V h@€pVgข—hใvฬ๖ฝn#๔่ิๅkฺ๎ฐKQœmRD]™ไd๓ว๛ 9้„ฃQvพบ_๎ˆm๒พ๋7rบซ๐y< ๒ร‡AW_;๚LฏวรUห—ฐr้b๒ณณHL๐ฃk:B8„#Q†วฦ9ีฺฮ+ำูAฦ!…”D"‘ฬ‘J!%‘H$๏LHa2bโัTฎ.ษฆ*/œD~CGULa"bา>bwk;[๛˜1ํw๔}๓า“นkำr๒“˜ฑl?ฮZb:Vฮ็‹‹๑๋ำ|ใ…รดކbโุม}–ๆฅ˜:OT]บ–ฟิWXใŠ(E่ ึ`3V๗aฌ๎ฃุรงแษw๏d**ช?-{!z๔ฅhน‹Qt/ยŠ "!œฑnขง^ภ๊:„3ูiซฤv)J4จ{)ฎจทƒฉ…/ฦย๑ๆeer็ว?Dnf‘จษ๎ฝ๘ล3ฯลt ณi7\ฝŸืรเศ๙ณวh๏้}[Ÿฅkkช—Qฝธ‚ข’ ่บ[1ฃŒŽOธ"๊ภ‘ทฟ )ค$‰dŽH )‰D"น0B๊ “ŸกqMq6ห๓าษJ๔แำ54ขŽ`2ฅyx’=์h๎ลrฎ` 2S๘๒ฦๅไ%๛™1-~~ฌŽ้8qีBSQˆOื่Ÿโ๋/ฆc,ถ*bม}ŠAp๛งทดฝฏ›ฑ๚ฺ€ƒSงฝ•ิ๓z“f &gฃgW VนKี3P aGฑ‡Nc๕6`uย๊?OฤัHAMส@ฯ_Ž^Tž]š^|NLEฆpฆ†pฦบ1OฟŒี{{ผ็โ๖ัŠSu็7พp&Eฆr็๖@vF:Ÿ๛๘‡ษฯฮ$jšผด<ต=ฆcฑ๕ฺซูฒa~ฏ—กั1๋‘_าฺี–>CำTV/_JueEyน$'& kB@ิ4Ÿ ฉฝƒื7ะ~qvน”BJ"‘Hๆศ|ค’H$’ +คฮ01I๑ฌ)ฮbYNY‰>ผบ†XŽรX8Jห๐$/w ๐BK/–๓ึaQV*ปqนI~ฆM‹GŽถ๑ะ‘ึ˜Žร็ืTฐua^Mฃ},ฤืž?Lฯฤtฬรซk|๓ฦ•ฟ(+ฅPีฑw๛๕s^bJำQาัฒสั ช1JVกฆไกจยฑqF;ฐšฐ:๖avxwWDjr6FษjŒาีhe(I™oS“ุฃ]8ฃ˜๛ฐ๛O`๕€p.ฤฉˆฒl;ะี[๗/๗>X6ฃฉฑviไ!?; ำฒxๅเa๚๕31“อ๋ืrำตืเ๗๙ใG?-็)EaๅาลฌZถ„โผ’1t˜Q4ทw๒๚แNตถ_ิqH!%‘H$sงฅ’H$’‹#คฮ01 ๘<ฌ-ฮbiNY‰^ผบๆ&ฤถร๘lลิฮำ}์n๋็|๏หKrอตKษM๒ŠZรŠ‚˜วnล8…ี{ {ฐg.3r8ฌ(ับxQ‘จนMืด๚๖žิ๏Sฆgbฏq~ %™?๛ิG)ศษฦด,^?|”~๕tLวๅบuW๑žMH๐๙็พ_ม๗ฏZVYขk:ฝ}๋br*๖ช3“๙๓O”ข\,b฿ัใุฏc:>ืฎ^ษ๛ฏ฿Hข฿ฯ่๘8๗?ไ๏•HKฮ็ชๅK˜WT@jr†n`Z&ใ“!ฺป{ypGN^ฺM@ค’H$’9า)ค$‰ไาฉ3LFLr“\]œอยฬา^ผgฤ”ๅŠฉฦ1vž๎ใ๕ฎ฿PY“Ÿฮ_ญ_BVขษˆษ}Z๘๕‰ฮ˜Žร_ญ_ยฦฒ\ Mฅed‚ฟ{๖ cแhฬ#ี็แžอ5”gค`ฺป[๛๘/C8Œ+ชR๗์g7วลCฬฮ๏” ฎ j™ๅท+บ!Dx{คซํ5ขMปpBƒ๒t>จ:8z2Œาี่ลซฮ‰)UsลTh{จณ}Ÿ+ฆF฿ฐ+กฬq›‹s%%จBษงn}ซ—/Aืtบ๚๚๙ื?ฬD(scJ๔๛๙๓OŒ’<,ๆภฑF๎๙ฏb:Nืฌฌๆ[6‘่O`lb‚?$ว›฿/qษ‚rV.]Lyq”ไณQฆe3 ัึหc8ึxYทR‰D๒fค’H$.ญ:C(bRœ–ศช‚Lfฆ’ๆ๗`จฎ˜Šฺ6CำŽ๗ฑซต}รozส‚ ๒š%d&z™ˆ˜ืพ&~sช;ฆใ๐7–ฒก,]Uišเ๎ํ˜Œ˜17Žtฟ—ฏmฎa^z2ฆํ๐ย้^๊_>~ฮฺUE ฦ‹˜ย6kEt*huo4๗=๙<ฮX—ผ๑ผญฬMEั Œา5h๙K1 ชPS๓Q /แJฟฉa์แvข-/b๗5bvผก๚lVLลฑˆบใ๎{j5D=(Ug'kช–a่:O~ฦ่D์5ิ๗y=ลํŸ ค วq8x$๕ศใ1ฏตีห๘ะึ๋IJHd|r’Ÿ๒)N5Ÿ๛‚าbึV/ฃผคˆด”d<†ข(˜–ลdhŠ๖ž^;มพฃว.kฆR‰D2GZ#…”D"‘\!u†Pฤค,=‰•™,ฬL!เ๓เัTfh*ยฑ1v4๗pดo๔์๛Vf๒ลk*ษH๐2Ž๒{›ุัำq๘าฦe\S’ฆ*œšเหฯ`ฦดbn™‰>พvC5ฅiษ˜ถอŽ–^พณง๑Mฏ‹715yงjญแž *ๅ]็fp Š/ฯยZดŒ2๔ลณbสŽ…ฐขgwไ‹žzซฏัํ/็"JuDPQ•7{๏ฌซฉยะuz๙?cxl<ๆฦh่:_๖ สŠ p‡#'š๘แรฟˆ้ธญZVษm7o!91‘‰PˆŸxšรง˜WTภบšๅฬ/)&-5ฏวƒข(X–ลไิ]ผ~ธ วqœหผ#…”D"‘ฬ‘ฮH!%‘H$—WHa*jQš–ศบโlสา’I๕ปbJมŒi38ฆก”ํอ=4Œsuq6~๕bาฦfข|๏ต“์jํ‹ํ8lZฮฺข,4Uแฤเ8_๚อ~ขถsใศI๒ผกš’@ฆm๓lS฿}๕ฤ๏|ฝ‡qœบwl‹‡•ฑ๚๕ตBx๊Qฉ’wŸwšษ)จIูx—„šš‡–ต5%วํ/e[8‘ฮไฮx฿x๔ไ3๕N฿ษ๚”?๙XลผโB„#8zช™๔ั˜Ž฿Š%‹๘่{o$%1‰‰ฉฯ๏yฏ๚U: IDATิ”d*สJHคโ;+ขlB3ำ๔๔์w|ี™ฐŸ™u๏•ฎzต%Yฝหใ ๔†B ‰๗๛๖๐ )ฺ…dูvฟษฆgC ฝwlllใnฒeษV๏ฝ]้๖2๓!ษ@ฬn7}ž?m™๓ž๗ฬ™็พ็๖Wื๒QU5มะิ๙AC)@ ๘œว!ค`jฉI|ม0Yฑvๆฆล‘mรn6b”%ผก0}./Uร z|\S”Nดลศฐ7ภo๖c{Kฏฆ๓๐“ๅeฬ›N‚ฃฝ#๐ฝ(žJฑG๐ำeค;l๘รaซ๋ไw{‘nSฅโยS‹ืฉชพ™tq:UกG†ฑ่2tQษศŽ4d[<’1ยI8Xฉx†+e{ย%ข๎{ "Cี๋+€;ึ฿^ฟv%‹ๆฬฤd4ะ?ภ๏Ÿ}‰!MฦฝฎศIŸŽชชิ44๑›ง_ะtห๒sน๕สK‰ฒวซŸ"ฬๆQแpทืKW_?‡ีณ๋@ภิ[{P)@ 8!ค€ฉ%ค& „รไว;(OŽ!=ฺ†dภ0!ฆ|มฎ@‡ลˆI'3่ ๐ซ]5์nำ๖ั+ห™‡ ้ๆ๗๖k2Žดจ~ฒผœ้QV|ก0๏ึu๐‡๊พฬ!„˜|ต;ƒ]l:†œๅN]\Fฅฮ1ญRŽL"๊opํ๊ๅ,ž7“ั@ฯภ x๎eบ๛4๗๏ผ…ผฬ TTj›๙๕Sฯk:Ÿ‹ๆฬไ†KWb2šUU'D”‚ว็ฅณง๊บFvฌยใ๕Mู8„‚ฯynBJ ฆฆ:๑ซชไลEQ–MFŒ ปั€~BLษ Ixƒ!~ฟ๗8๏j|Q๓‡VอdfJ,Pี3ฬ6hSHฅ;lxY)iB๊ญcํืพ/ฟลธข๒DHงTl]ทถๅBธ‡*WHช~=2Qโฎ๔QpชRจRgŠฌŒ๚๖[BD}AฎZน”ฅ ๆ`6้โฯฝBgoŸ&๛แ;n&oFuอญ๒‰g5GBl สK˜SRHBl ’$qEมๅ๑า;0Hีฑ:๖ชึฤŽˆBH มษ!%Lm!uโ†DN\$ณRbH‹ฒi2 “วะUUฅsิรถๆ^o่ขkิฃษ<|๕,ส’c8ิ=ฤ7ะd™ั6\VFjdP˜7jx|รWw ˜ฉ\๊PPึ 1๕UJ๘ I’*๋ทถ\Haฏซจp่}๚๕:™ฏล[ถk./BH มษ!%hKHtzX:#‰›K31๊uจชzb _XQqCดปุ฿9ศปu8})“$Iหฺู%8P€}ใๆCšOนq‘<ฐค”dปo0ฤKG[yๆPำi9ถชเ หT*ฒฑr๋บฅ็”ฌ‘สฅชชV ๋๎D๐Y.PpฯWœ5ษฺลฑ๚’…XL&†G๘๓‹ฏำกอ)ะ๗|ฅy9HฒDS[> กpxสถ7ยlๆขูe”ๅ็’‹ลlF'ฏ๙~F]nข์6ฬ&^Ÿw?ษ๛;๖h./BH ม็< !%ฺR=c^ๆO‹็ๆฒLlF=ž@ˆAฏŸh‹‰ƒY‚ะDลT๛ˆ›=ํlจ๋ฤMู˜๔ฒฬ#kgS˜EX…ฝm<ผฅJ“ใ)?>Š.)!ษfม ๑โ‘ž;|Zฯ!ฤิฬ,ข๎}๐กuช$Uศœ๐W/Zภฺลc1›q๒ุKฏำุึกษ>บ็ฦk(+ศE–eZ:บ๘ๅฯเง\;อ&๓หŠ™U”ORBV‹ฝNช*๘๚‡‡9ุB{O/W,_L\ดฏ฿ฯฦํปy๏ร]šห‹R@p2BH ฺR}. งวsCI==./Ol$+ฦNyr ษv ƒY–†ฦAฺF์h้esc7žเิSfฝŽ_ฌ™M~|$aEeW[?ผ๕ฐ&วSa‚ƒXRLขี‚;โ๙รอผxไฬธƒ PL•ซช\‰ฬ’ ๎FฅฐM’”๕Ž๕[]hกŸ)5ษŠ…๓ธlู""ฬ†FFx•7ฉoiำd_uUฬ,ฬGง“i๋์ๆ—O>‹ื็Ÿ2ํ3 ฬ/+ฆผ0ŸดคฌŸQCN'ว›[ู} Š๖๎^าS“๙ฦ ืใภ็ฐiื^1eO ฮ„ด'ค>.JOเบขt,=c^ูv˜บQฒb์ฌฮIฅ,9šซ๙3bjิคyhŒญ}ln์ฦš:ำ8ฌF=?_=‹ธHBŠสŽ–^๕รjMŽง’คh’bmfฦ!žฉjไีฃg๖ๅVQi•%ฉbใซฟฎู‘สEKUีXqAˆ)…m’จpฌ฿qมฝฬ~๋G_ญศTž)5ษา๙sธ|๙bฌ CN'Oพ๚ว›Z4ูgw^{ณ‹ ั๋tดw๗๐ซ'Ÿรๅ9๗]่๕:ๆ—•PšŸCzJ2ถˆtบ๑ฉy`€a็(uอญ์=T™้’ำ’น็ฦkIˆฦ๐ม๎ys๓6อๅE)@ 8!คํ ฉAŸE \]˜ŽEฏฃkฬร/ถฆq่“‡๒ใฃX‘•Lir ๑V3fฝY‚ ข2๊ P?0สž๖~67v +็<ฆ(ณ‡Wอ"+6’ขฐญน‡฿~T“ใฉ,9†ฝจˆ›™1ง6๒Fm๛ูqฆ˜ชDฆ์ผ ๎Q๗๘แฅฒขVHฒtV„ใ%sgqีส%X- Ž๒ิซoQุฌษพปชห˜WVŒ^งงฃท—_?๕<ฮ1ื9k,หฬ--bVQ>ำ’“ฐ[#ะ๋๔€J dศ9JCK>๚นUi)‰๑{ำu$ฦลเ ุถw?ฏฝฟEsyBJ NF)@ ~ดaฟ:น[๖Xœ‘ศๅำฐ่utŒบ๙๙–รด ŸาQœอ๒ฌ$Jฃ‰ท™1้tHร #พƒclo้eksaๅ}'ฤD˜xhๅL2c์ร [šบฉYฃษ๑4+%–ฟ[THผีฬจ?ศใ๛x็๘ู]FQiึo๚ฦ๊ื.„kxคr๑:UีW ŸูJšณ“šรทnนž”„8ม ;๗W๑ย;5—!คเd„เ๗๖ซ:Y;Bสฒ$3‰ตนฉ˜t:ZG\ำUtํuBfฅฦฒ:;…ผ๘(b,&Lz๙„˜๒๘ฉS{๛ฯjLษv ?[QNบร†?fC]ฟ{L“ใi^Z?ธธุ#พ๚ธžM ]็บY”˜ช\\!ฉ๚๕ศDM๙ฦ*ดJRจยฑรว/ด{๏}Tdจz}pNwOœ[Zฤ —ฎยnตโt๑์๏QuฌN“}zฺ•\2g&&ฃžA~๗์K๔ ž•s็f3งคำาpDฺ0่ €J0bิๅฆฅฃ‹}Gj8T{ 36ฺมทoฝ”„x‚กปV๑[4—!คเd„เ‡๏ํS๕ฒฌ™๖บ!–อHbuN † !Uฑ้ฝ.๏>ฦE้ ,อL"7>’hณ ฃNF’ภR๔๘ฉํa[su œ•˜าขฌtyำขฌ๘Ba9ม?ึๆ แย้๑ฟฐ€˜#ฟ๛่8[›zฆJ๓ถ!K๋7ฎ[u๏ิ6Rนิก ฌŸฒbJˆจ ฮฑˆšdVQ7]พšHซQท‹็ภมm ๑ซW-cษู˜Fz†๘ร๓/ำี{f`(ศสdniYำงแˆดa4‰`(ฤ˜Ms{'‡jณฟบ†/๛๊แˆด๓o"51`(ฤGUGx๚w5—!คเd„เผปO5๊ด#คผม0หณ’X‘•ŒAงฃexŒŸพฯ—ฺ[g&qqz๙๑Q8,FŒ:U8ฬ วOM๏[šz8ะufaฯˆถ๑เฒRา"ญxCaชm็ฯ๛๋59že$๒๙D[Œ {fฯ1ถท๔N-ข๒DHงTl]ทถๅ|ฟพOˆ)Iณฉั๙ฎˆZWQแ0๕’ฤฆRปส ๒ธๅŠตDฺlŒนผ๐ฮF๖ัๆvWฌXย๒s1›Œ๔ ๓ง^กฝ๛ฬฒฆงqัฌฒ eวd4 IกP˜Qท›๖๎๖ฉแใร_}ƒŠH›•๏}fา’ …C||๘(Oฝ๖ถๆ๒"„”@ |ฮปˆR@ =!ๅ…Y‘ยาฬ$ :™ฆก1~๒†ฝฏ|ฬe3’X’™DVฌ‡ล„Q–PT๐…ย x|ิ๔9ูPืIM฿ศ™yฑ‰ฑ๓ฃeฅคุ#๐CผVฦ“ดนจ๐’ฬ$พ=?‡ลศ7ภ๎ฎeื9Z›๋oบ‘ KLeจชZฌ;7U9 ฮ UyกcืUT8๔>zฬ”ฌV+อหแึซ.%สfวๅq๓โป›๘จชZ“}}ูาEฌผx>“‰ก๋ลWiํ์>ญ็ศHKaแฬRr2ฆ…ษhœQ!\ฝ}|TUอพ๊ยงธ‹ซ5ยยwฬ๔ไdBแ0๛ซkxโ•75—!คเd„เ๏฿๙X5้ušio0ฌฐ2;…ล™‰่e™†มQ~ผ๑ฃเ)W/K,ฮLbyV2™ั6"อ“S*`˜>ท๊ža66tqผ๔๎ฺ”ษKJIถ[๐Cผ\ส3UMšOหณ’นw^.ณ‘!ŸGwีž๕5นพด+Qy"ค3ฎ฿บn้ศ๙~ฝŸu1ฅเTฅPฅŒ\้Xฟ๕ผ๏฿O3ีEิ$ลนY~ีืˆฒqy<ผฒa3ปึdŸฏนd!k.น‹ูฤภ๐ฝMmงg—ฯดคD.šUFnf:ฑัQ˜F@"ใ๒z่๊ํ็Pอq๖:B <-็ด˜M|Ž[˜žšL8ฌpฐๆฝ๔บๆ๒"„”@ œŒR@ฏท?V-ํฉฐชฒ2+™E‰่e‰บQxw tZŽoา๋X™ฬล้‰d8ฌุอF ฒ„ช‚7ฆืๅฅช{ˆ๕]4 –s&8๘‡ลล$ฺ,ธƒ!^8ฬ GZ49žVๅคpฯœ\ขฬ=~*wึ๐๑YZ‹๋TPœa™JE6V^ bช\UๅJdฮฬฮnฐˆธ๗ม‡ึIชTฉ……ๅ ณg๐๕ซฟ†#2ทืรk๏oeวพƒš์๗อใฒฅ‹ˆ0[แ๑Wคพฅํ”Ž™œวย™ede=^%K2a%Œ๋ฅณง๊บv๎ฏยœึxLF฿ฟ๓V2าRP…ชฺ:๔ยซšห‹R@p2BH ๐ฟHต๔šiฏชชฌสIๅข้๑่d‰cฃhร~|ก๐i=ล cMN*๓ฆล“mรn2 —%”‰Šฉ^——]Cผ_฿E๋ˆ๋”ฮUœอY\Lขอฬ˜?ฤณUMผrดU“ใ้าT๎š“Cคษภ€ฯ์<สฮAํŒฏ NL-ZชชฦŠำ&ฆ„ˆZงJR… ้Zis~V&w\5ขฃขp{ฝผฑy~ด_“ฟt._พซลยฐำษฏพล๑ฆ–ฏtฌฤธฮ,ฃ0{qัฬ&ฒ,‡๑๘ผt๕ p๘X{Uใ๒xฮH—^อแžaMฦrSi&หณ’[Šฬhใย`คr๑:UีW  ฑข„Ÿ˜Q-ฺำ็,•$*ึ7–ู—5Cv๚4๎บ๎Jb<>/๏nษฆ]{5ห‚๒ฎ[ป[„็ุyช๋พะg‘v.š5^•‡ลlB'ห„ร ^ฟŸม!Žิ5ฐk#ccg-ฆ๕wFN๚tTUฅฆก‰฿<‚ๆ๒"„”@ œŒR@เญฝชอhะฮƒญชreม4fงฦ!Gz‡yเฝณ7ฝ$6ย41•/Žิศ" z๔ฒDXSํN7๛:ูX฿ษ€ว…Ž9/-Ž\\Hl„‰_€?}\ฯฆ†.MŽงk‹านฅlv“ž^—๛แช{ตY0๓“ๅeฬITvท ด>ฒํHลฦปW=~!พ˜บ€Eิ=?~x้ทXQ˜3c‰„D}K+•?ฃษXfLOใฎ๋ฎ$.ฺื็gร๖]lุพ[“ฑฬ))ไฦหVcทZuนx๖อ๗8T{ŒีbaัœrŠsณIŠ%ยlA–eUม็๗ำ70Dmc3;๖bhฤyึc๚ท—™ŠJmc3ฟ~๊yอๅE)@ 8!คธอฝjคI;B*Vธฎ8™)ฑH@Uฯ0?ฺp๖ื;Iฐ™น47™)1คFZฑuศ’DXQ ้pz๘จฝŸw๋:๛;^”žภ๗c12โ ๐ฝวูึฃษ๑tcI7–fb5่้uy๙ืซฉ้ำฆzdอlJ’ขQƒlำA•VTeฆ{ึ^/WC•‹+$Uู]โ.p%+j…$KK๎ป๕Šsฒ$‰†ถv*๛ Z|ดฬHKแ7\=.ค~฿ฑ‡wทํิd~fๆs๓kˆดฺuปxแํ์ฏฎฟ0›™_^ยฬย<’โใˆฐ˜ั๋t(ŠŠ/เงp˜ฺฦf๖:LOน[๏;n&oFว›Z๙ี“ฯj./BH มษ!%ภo์U#อฺRP˜›J3)KŽเP๗?xเœตgZ”••ู)ฬJ‰!%2‹AN‚ ขโ๒iwบฺููวฆ†.\อN€‹3๙๖|ข-F†ฝ~ฝ็;Zz59žn)หไ๚โ " zz\^ูv„ใNอล!r้Š(ภพŽq๓กOษ6EQ*.15Rนิก ฌ—T)ใBQ๗=P‘ก๊๕ภ“๖ญ[ฎฃ$7I–hj๋เ?{ๅ ญew&™ž’ฤ=7^C|L4>€อป๖๒ึ–ํšฬSi~ท^y)Q6;.›—ฤช๊ฯูdbniณ‹๒INŒวjฑ`ะ๋QŸ?ภเศว›Zู}ฐŠŽžพsำwoฟ‰‚์L$$๊ZZyTƒ•xBH ม็ฒ- +๋ถฎ["๎*ฺ็Q“}รีฬ,ฬC–eZ;บx๔‰g๑š‹5%1ž{oบŽฤธ|๖๎็ต๗ทh2oY™|šห‰ŽŒฤํ๕๐๚ฆm์>Pลผฒbส ๒ฦE”u\Dฉ*‚A†œN๊[ฺ๘จชšฦถŽ)ำทnนž’์OUโEัึ;ŒR@p2BH ๐ืwซั“fฺ๋„ธkv๖ฉ\9ง&8Xž•LIR4 V3&ฝ ) N_†กQvท๖ณนฑ›ี9)ฌ›Mคษ@ฟว์จแ@ื &วำณsธผ`ฝŽŽQ7?฿r˜–a—ๆโฐtb๕l๒โ# +*;[๛xd‘ฟ๙9Eๅ‰NฉbJ›ฌซจp่}๚๕:™ฯฎ›๕9u•ฬ,*@ฏำัฺีอฏžxฯงน˜“โc๙ึอื“‹?d๛วxyรfMๆ/73;ฏฝ‚˜จ(^/GŽืa13-)›ีŠA?พฃl dุ9JCk{ซชฉoi›ฒ1u%^ๅใO …5•!คเd„เพืvซฑฺRž`ˆปg็P๐…+Wฮ)ล‰ัฌฬNฆ(ัqBLย N_€ฦม1\๓งลa7่s๙๘๗Gฉ๊าไx๚ๆ\.หKรฌืั๎t๓ะUด;š‹รfิ๓๓ีณษ‰‹$ค(lo้ๅ~X…?/ฤ”ถ๘2"j’;ฎนœ9%…่uzฺป{๘ฯงžcฬํั\์ ฑ1w๋๕$ววูฑ๏/พ๛พ&๓˜>u;†ฟ?€^ฏวhะŸจˆrŽัษ๎ƒ‡9ึ4๕/ฯop5ๅฏฤBJ NF)@ @{Bส ๑9น_บrๅ\Sžรชœ โฃˆ0จ˜๒‡a…ƒ$ัํ๒๒oVSซอ้พ5/K๓R1้tด9ำๆCtŽj๏%=สlไแU3ษŠ$V๘ฐน‡฿q๔หHU ่L[ื-w›ฉษ=<\๑eDิ$ท_5ๆ•กื้้่้ๅืy็˜๖ชใข|๛ถHIˆ' ฒ๋ภaž{ƒ&sนjั._ถ“qrบzBDบ\ดttฑทชš๊บอฤ๔™Jผฮ.~๕ไsšซฤBJ NF)@ พ๕๊.5ฮjึL{ฝม๗ฬอ%7.๊+Uฎœkๆฅลฑ<+™๘(ข-ฦb Iภ ๑ิม&^ชnัไx๚ฮ‚|V็ค`า้hqQฑ๙=c^อลaโŸVฮdFŒ`XaKS7•;kพาฑTgXฆR‘•BLM๎}๐กuช$UศU>๋•—2ฟฌƒ^Ogoฟy๚†ฺ[ภ?ฦลwnป”„‚ก{ๆู7฿ำT ลนYฬ..$oF1Q‘H๗ำP8ฬฐs”–Ž.ญๅภัcšหฯx%^zŽ๖๎~๕ไsธ<ฺ’BH มษ!%ภ7_ฅ&hHH๙B!๎›GV์๘TชmM_ฑrๅsIF"‹2ษ‹$มjF/หใ๒BU้s๛8ิ=ฤ‡อฝš[{ X•ŒAงฃeุEลๆƒ๔นดทฎNผีฬ?ฎ,'#ฺN0fSc7ฟฺU{Jวbjjpช"j’›/_รย™e๔zบ๚๚๙ํ3/28ฌฝด:์vพ๓๕IKL$ฑทชšง_GmฯอLgAy Yำงแˆดa4?u/ํไญ>d฿‘อŽืฟฎฤ๛ฯงžgิฅญJ3‹iQ'~ี+*p˜AŸš65vkfMฉ\Tศ๒ฌd :™ๆก1~บ้ ƒฟๆ๒’hณPฑฒœt‡`8ฬ†๚.~ณ็๔TWจ NI–ึoผ{ีใโt๖๘ึพZ‘ฉVO]s+฿ธ๑j’ใใ †B์>Xลsomะ๔8พแาU,š3^‰ื?ภ๏žั^%žR@p2BH p๗ห;ิd{„fฺR๎›Ÿฯ๔(+p˜ww๒๛Žk:฿Yฯšœ :m#.yJ“ขY0-žฬ;v“รค˜ †่qyฉ๊ๆ†.š†ฦฆT,ฐธ˜E‰่e‰บq!ๅ „4—“Œh.+%-าŠ7ๆอฺv_Fฯฉ@Šฒ~ำ=kล‹)p฿Šฌ{L‰จIฎXฑ„ๅ ๆb6้ๆO/ผB{wฏๆ๚หd4๒ƒ;o!=-EQ8Tsœz๑ต)ัถฤธX.žUFA๖ bฃฃ0่d™pX9!ขช7ฐ๋`^ŸŸ่ศHพs๛ค&Ž/ะพท๊ฯผ๑ฎฆว๓ตkVฐxฒo`?<๛2ฺZ[P)@ 8!คธ๋ฅjJคv„TXQ๘๖|า&*W>มŸฮPๅสูโ‹ X™5น๘o๋ 8มAเ(1FrcฃศŒถa7ะหaUลฏ˜ฺ฿9ศ†๚.:œ๎)หKJธ8=,qผ”m7ึ\Nฒb์hY))๖ผมฏีถ๑ไฦณu๚mŠขT1๕ๅธ๏Š Uฏฏ๎<็ปl้"V]ผณษHะ๕โซดvvkฎ฿ z=?Xw+™ำRQ…รว๊๙ใ๓ฏœำ6%ฦลฐ ผ”ยœฤGGc6™ะ้dBก0Ÿžชjุ๋[Uอ˜๛“{_คอส๗พ~3iI‰šY๋oqีสฅ,]0gผopˆ?>๗ ฝ}ฺxู’`fa>ๅ…๙หๆ—‹๛™@ |๚)„”@ ภ?o=ข๖Œyฐ™ šhoXQ๘ฮ‚R##๐†ยผQฦใ๛4ƒOฏปิ44ฦ;ว;(Jpœ๘~ทY–(Lp0-สŠอจว “ )*ž`ˆ๎1๛;ูิะE‡๓nเฒRL‹G'K๋w๒ภ{๛ „อๅ$7.’–”’lทเ †x้h+ฯj:อb๊ pถEิ$k_ฤ๊Kb1™แฯ/พNsGงๆ๚Oง“Yฟ๎6fLOCUTŽิี๓๛g_>'m‰vฐ ผ„ข์$ฦวb1™‘e™p8Œื๏ฃo`ˆชใ๕์9x็ุษ;อii=ฌ/สหณ|แผOUโฝJ{wฯิ~ษ’$ส r™SRฤ๔”$"mถef“Qวเำ๗J!คบF=jเ({๛้๕b3๊งt{Uๅ; ๒OTฎผZำฦS5ƒฟ^wiC]'…ŸR“ xu2๙๑QคFE`5่ั๋dยŠŠ;ขร้f_็›บ้uyฯI,?Y^ฦผi๑่$จ้s๒ภ{๛)ฺ๛พอโ‡KJHฒY๐Cผxค…็7Ÿ“ถจจฏeuึuk[ฤ๋ึUT8ŒA}…$๑ƒsqU‹p้โ‹ฑ˜อ ŽŒ๐ุหoะฺุฎฝb ึ฿u;ูำงกช*ี๕๎™ฯj"mV.ž]Na๖ ’โNˆ(EQ๐๚๔qดพ‘ํ๛~ฎˆšฤl2๑;o!}b=ฌC5ว๘๓[๋หrูาEฌผx>“‰กโkดtvMู๖ฮ,ฬg^Yำ’“ˆดู0๔ห$Iฺ*๎Z@๐ฉ๏_!คTUUa…!ฏŸฦม1๖ด๗ำ>โฦ>E+ฆTu|อฅd๛ธ(xนบ•gชš4ƒNLs›\w้๚ฎฯR“ xDtไฤE’A„AN–ฦลT0Dˆ›ผWืyึwธ๛ูŠrๆคล!G๛F๘แป๛ะโทmQขƒณธ˜$›W ฤsUอผTrNคจ<า)บ˜ZWQแะ๛๔๋u2๋‘‰:WํXพp._[v f CN'Oผ๒&uอญš์ำ๕wFN๚tTUฅฆก‰฿<ยY9o„ลฬลณห)อห!)>‹ูŒNึ๏0๊๗ำ;8ฤฑฦfvจขh๘oฯ`ะ๓ƒ;?™~XU[วŸ^xUำใ}อ% YsษEXฬf†‡y์ๅ7hj๋˜Rm”e™น%…”ๆ็’‘šŒfร q+ 1ๆv/‹‹vlO\@๐ BH เ …UฃN V๖จprฐkˆ–Vริซ˜๚๎‚|’์ใขเ…รอผpD๏็?ZZสย้ŸLs๛ ฑ›‚xว฿ˆ/0>•/Ajิธ˜’%)*ฎ@ง‡=mผW฿‰๋,ํtWฑฒœูฉใB๊H๏0ผท_“9)MŠๆ๏/)&มffฬโ้CผVำ6%ฺvกŠฉฉ"ข&Y2o6WฌX‚ีbaุ้ไษW฿โX“6S๒;o!/3•ฺฦf~ิ๓g๔|f3๓สŠ™Y”OR|,6K:UฏˆแXc {ฆซ๏‹๏\(ห2wืิ˜~xบXyั|.]z๑ธ๘œจฤk˜"•xzฝŽน%E”ไ2-9 ปีŠAฏ  3ๆrัึรกc๕ห๎พ๎สญโ‰K >A)@ ถท๔ชูฑ‘D[Œไq1TFผG9ุ=D๓ ‹A7%ฺซช*๗_THโ„(xถช‰WŽถj:Ÿžๆvดw„mอฝฤฑ๗mง?@Uฯ0%‰ัฬLŽ!ษnSฒD(ฌ0๊า๎tณฃฅ—ปฯ๘Žwญšษฬ”X$ ชg˜mะฆ*OŽแ_RDผีฬ˜?ศ“y๓ุิ™Žฅ*8ร2•Šlฌบn้ศ๙~Ÿบ๗ม‡ึIชT9Dิ$‹ๆฬไ๊UKฑZ"ๅฉืฆฆA›ีš๗฿q y3า8ิสฏž|๖Œœวd42ฟฌ˜๒ย]Utธg˜:๚ษމJวjwบ้๕Rœไ 4)š›ณ^‡,APQ๕hco{?›บN๛xฟX3›ฒคhTเ`ื ?y &s27-Ž๕aย้ ๒ง๋xฟa๊.$|พ‰ฉฉ,ขNŒ‘า"nธtvซงkŒgx—รว๊5ู฿฿นF ณg !Q฿ฺFๅcOŸ–ใ๊t2sK‹™U”OZR"vkzP ƒ ŒR฿ฺฦช๊ำถ wLŒ เฬV{-.šUฦ5ซ—c‹ฏฤ๛ห๋๏pด์ŠO“ัศผฒbJ๓sHKLภnตขื ล@0ฤจหE[WGŽืำ78D„ล™ฯ !%'#„”@ ๐Y!ใHฅ'ฐtF9S๙ŒบqกPTœอรc์ขqhฃ๎์Š),๑ฝฤYวEมŸ๗ีณกพSำ9๘tUัก๎!u ‘m;ฅcถ;๔บฝ'FS’M‚อŒIงC’ 8!›†\์h้eKS๗i ๏‘ตณ)IR๛;๙ู&m ฉำใ๙ยb"LŒxฃใli๊™๒ํVTZeIชุx๗ชวตุ๏๗๘แฅ:ิJสฆz[gpำๅซ‰ดฺuปx๎อ ฌ9ฆษ๑~฿ญ7Pœ“…$I4ดตS๙ุ_8•วdIšmญ„้ษ‰ุmใSบTuผ’fxt”ฆถvS“Sภš†ฦ8า3ฬ๑ัำฮ4zYๆ…๙ใขภเี๑Acทฆs๐ฯkfS:QUt sฃ}#Lฒž–cท;๔{|”'ลPเ jยคื!๑Iๅ[รเ4vณฃฅ๗”vฤ“€นtE `_วธ๙&sฒ(#‘๏.ศ'ฺbdุเ7{ŽฑฝฅW3ํืš˜บ็ว/•ตB’ฅ%Z้ใ๒‚.>G.^|็3&>ํึๆL์š—๗™Šจ@0ภ๐่-]Tีgdิ๕7Eิ$BH ม็ผ!%_LHM2+%–YษไลGg5aึ๋คOอnqOๅC’ฮฤหตŠอhเพ๙yDOˆ‚_๏ฎeงEม$zYโ‘ตs(Lˆ:!qZ†]$ู,gไ|ํN7๎`ˆข๙๑QD[Lu2’ะธ˜ช้แรๆ^๖ถ91eิษ<ฒv6๙๑Q„•=ํ|หaMๆeyV2๗ฮหลa62ไ๑๓่ฎฺ/SŒmŠขTlบgํ”x)ผ๏Š Uฏฏ๎ิj‡ๅdq๛U—แˆŒฤๅ๑๐๊ฦุu@›๖7\Myaฒ,ำฺัลฃO<ƒ?BŸ-ฬžมา"ฒฆงeทc4่้ฤnkอ]ฌ9ฦฺ๊ณห=7^CYA.ฒ,ำาัลฃ?C ิ์…[šŸรญW^zFลgคอสา"J๒r>Sฅช @‘ฑ1š;ฉช=Žsฬ…Eิ$BH มษ!%|9!5Iyr +ณSศฏ˜2๋uศปนWLนจ๊ฆap๔ดถ5ค(D[LŸฟ]หž6ํŠ“^ว?ฏ™M~|$aEew[?]ฃโญๆ3zvง8L~ค˜฿]QU!3่๑Sำ็dKS7๛;ฟะ1อฑไMฤฒณตGถัd^ึไคr๗œขฬ~Uรว็ร%Nลิ๙ ข&)ศสไ๋ื\Ntd$nฏ‡ื7mc๛วฺU๒ฎ๋ฏbfa>:L[g7ฟ|๒Yผ>๘™์๔i\4ซŒำำˆŽดc4ว+gC!ฦ\nฺบ{ูwไ่YŸฦx๗๕WQฉX}โY|~ฟfวYqnท_๕5ข์v\ฏlุฬ๎ƒงG๔ญV.šUJA๖ Rโ‰ฐ˜ัษŸ์~82:>5๏h]ร—ชˆ๚k„‚“BJ ๘jBj’Y)ฑ,ฮLค(มAฌี|b7ทขโ๒iqqดw„c}ฮ๑นiงH ฌ`3sฯœ\ขฬ=~*wj[D๔bอ,rใ" MHœ~ทX‹้ฌœฟ้&V(IŠ&;ึŽรd๋}šŒๅฮkฏ`vq!zŽถ๎n~๕ไsธ=ฯำRY0ณ”์๔iฤDEa2Ž‹จP(ฬ˜MGoQ_uชณ‹(ศžมืฏฺ'โ๓ญl฿wj—QคอสยYe็d‘{’ˆvŽาฺุฮ‘บFฦ\๎ฏ,ข&BJ NF)@ เิ„ิ$eษ1,ษLค(1š๘ฯSฃ๗ Swjปั{ƒ!ฆ;lŸ;k8ะฉ]Qi2๐๐๊YdวFRถ5๗0๊ โ0ฯj;:F„•าคh2ฃํDYŒe ๐ร๔น}T๗ณฉก‹ฺ>็Šๅรๆ^mป6…ิ•ำ๘๚ฬ,์&}.พใ(UC็อu.„ิ๘zl9_๚0'c:๋ฎฝ‚‡ฯห;[wฐyืGšŒๅŽk.gNI!zž๖ž๓ษ็sป?๓7ำ’“X8ซ”ผฬtbQ˜F@"ใ๒z่์้็Pอ1๖T! ณXnฟ๚kฬ+-Bฏำำัำห>๕<ฃ.—fวY„๘Œ> โ3ึลผฒbŠsณIˆ&ยlAงำกช พ@€ก‘QZ8|ฌืwส"j!คเd„Nšค0มมŠฌdŠ$ุ,X&ฆ๒…ย c ]ฃ^ชz†จ%๘™uBdลฺนcB๔ป}๛๖ฃาฐ(ˆฑ˜๘งU3™c'Vุาิ?ค`3๊ฯI{บฦ๋0yh๕Lฒb" †วๅฺqT“yนถ([สf`7้้u๙๘ฟVS;|\๗BH:3ฆงqืuWํภ๋๓๓๖]lพ[“ฑvีeฬ/+—8ฝฝๆฉฟพSใY8ณŒ‚ฌ bใQฒ$Vยธฝ^:บ๛จฎk`ืC_xฉ3ษญW^สฒ z=]}}๚//0์ี์8;I|nูมๆ_N|ฦลD3ฏดˆขœ,bcˆ0›ั๋u„ร @€!็ธˆชชญร๋๓Ÿ65‰R@p2BH งWHMRลสฌ”q1e5c6่ัOฌ1ๅ „่๕pคg˜๚'/!ฆFA ขธต,๋„(๘ทํี้ัฎ(ˆทš๙ว•3ษˆถ ‡ูิุขจD๔็ด]c^๔:‰œุHfฤุฑ› ่e EU๑ร๔บ|์ไ†.Z†วซb#Lำส™dNศตปytW&๓rCI7•fb5่้uy๙ืซฉ9ล ฟฉ„RงNFZ ฿ธแ๊q!ๅ๗๓Ž=ผปmง&cนๅŠต,(/Œฤ1 ใ"*;“8‡ณษ„,ห„รa<>/]ฝTซcoU๕”šwำืึpัฌRŒ]}๖้ึ๎ต›5=ปฎฟŠX‡ฏฯว{๎dใŽ=_่ณq1ั,,/!?+“ฤธ,&3:LX™Q#ฃิทดqธถ_ pฺEิ$BH มษ!%œ!5I^\+ฒ“)IŠ&มjฦbะก“$BŠŠ;ขร้กฆw˜c_LL๘”'วpSi&6ฃž——ฆmQdทPฑขœt‡ 8ฬฦ๚. ฒŒQ'O‰๖๕น}ศไว;Hถb7ะ๋dยŠŠ'ขwฬหฎAฏ๏ย Vฎ5t๓ซตšฬห-e™\_œA„a|œ=ฒํว๛็อu/„ิฉ3=%‰{nผ†๘˜h|›wํๅญ-5หง%N฿เ05 MdNK!!&ณษ„N' …๑๘|๔ r๘X{UOษฉpืฏ]ษข931 t๗๐๛g_ขw@ปUด_E|ฦวDณpf)Y™ฤวFc1™‘eEQร#ิทดqคถ0xฦDิ$BH มษ!%œY!5IN\$+ฒ’)IŒ&ษnมbะ!Kแ 1ี5๊แh๏ี=#”0ฒ๔๙+ zฬ›ว ็‘(H‹Šเ'หห™eล ๓n]Vฃฝ$Mฉvx่ๅ๑ฉ|ฉ‘VlF=LhBLuz8ึ๏dvj,)๖ˆrํ7{Ži2/ท—gqmัt,=c~ฑ๕ศi฿5๒\"„ิiธv“ธ็ฆkIŒมฐuฯ>^฿คอw๎.[ล%sfb4C„ย!LF#:Y&V๐๚}๔ sไx=;๗WM้5™ฎYฝœ%๓fc2่โฯฝLWŸvwb2โ3)>–ๅใ"*6ฺลlB'ห(ŠŠ/เงh˜บฆVฏ' Ÿq5‰R@p2BH gGHM2#ฦฮŠฌdJ“ฃIฑG`1่ัIVU=c^v๑Q๛!Eมn2|ๆ๓n‹าฯ+QmใวหJI‹Rok'ฦbBšขํ๐๘1้e๒ใฃH‰Œภ:13<1•ฯ “1้d|!…w๋:๘ใšฬหบYู\Y8‹^G็จ‡_l=Lำะุys !u๊$'ฤq๏อื‘‹?dG๛yuใš‹รaแท@ึ๔4dYFUAUUUม็๗ำ;0Dmc3;๖ิฤZLWฎXยฒ…s1๔ ๓ว็_กฃงWณใ์‹ˆฯิฤๆ—“—•A\ดใ“ฉyแ๑ PืJu]@๐ฌ‰จI„‚“BJ 8ปBj’I1U–C’Bฤ„ิ)bสๅแPื{๛ )๊ 1ี๋๒ฒ|F๒y% fฤุypY))๖ผก0oิถaš๒ํ๒๚‘e‰ขฉ‘Vฌฦ๑ช7I’€ฐขRำ7ย?n>ฤจ?จนผ='‡+๒งaึ๋่pบyxหaZG\็อu/„ิฉ“ห}ท\ORธฺพ๏ /ฟทI3ํทZ,,(/กผ0๔ิdLฦ๑=Uผ๑ฉyต อ์9t„ํ์dzูาEฌบxf“‘ก๋ลWiํ์ึ์8๛Ÿฤgjb๓หKศ›‘N|t๔ฤ:_aๅีะาNck;พ@Y>7?u!%'#„”@ pn„ิ$ำVVfฅ035†$[Vใ๘Sa<มcชบ‡ูำึ‡;"จ(ฌฮI=ฏDAn\$,)%ูnม ๑jmIV‹&ฺ๎๔8ุ5DQขƒY)ฑคEYฑ๕'ชปผม0uNvต๕ฑฉก›1 ‰ฉoฮอๅฒผ4ฬzํN7๔AN๗ys !u๊ฤวDs฿ญ7’G d็*^x{ใ”oทลlbni1ณŠ๒INˆรf‰@ฏื๘`(ฤพ#5lฑ›ฎ^ํMu[ณ๘"ึ\ฒ‹ษฤภ๐ฝ๔:Mํšg‰q1|๋–๋IŽรฒc฿A๖VU3ฟฌ˜้ฤ9˜Lใ;*Š‚ื?>5ฏฑญฆถNมภ9A)@ 8!ค€s+ค&IŒ`UN 3“cI‰ฏ˜าM์่ๆ †้๓Rี3ฤ,ฯJๆาqQะๆt๓ะ‡่pz4 Qpq ‰6 ž`ˆWถ‘lทh*†vง›๊.อMeษŒคฯ,ศ +Œ๚ƒดŽธ?{๏วu๊ฟปณฝ` ,z๏ฝ›%ช’bษ‘-ษE๑M์ุq๛In๚Mrปพ7ืNs|ุฒ-ห*–ฌb‰”HŠTฅ(ฑ ขwA5› IDAT`ภb{›฿ @$%+†9๏_|agฮ์๙fฯฬซ๏|ฏyyed–H2ตๆฏ้‹›šธตกฃ$1๎๓ตG™ Eฏ˜๛^ฉ๓'ฯ้เ‹GI9•b‘}nวšฏ^ฏcKWM ”bต˜ัI้t†t:$Ih4เ๓๘ึฃO213งสyนi๋&n฿v5“Ÿ฿ฯw๒,Cใ“ชณ|W._|เใหqฆเ]\4ธœ9๏ี๙Jง‰ลxšœblrfMˆจ„‚๗#„”@ ฐ6„ิ %9nช+ฆป82‡ซAทœ1•ญOไฤษdฒ…ภ๕Z-ใ0s๗fC1ี~m…N๛uํฺL„)ž๊ฃฬaUuผ9แลŽ๓;)ฐšศr:ƒคั iฒ&d†ƒ์ŸœgืเฬYuVผ\|yK37ืฃ_Rน๋žp์Šน๏…:œ9vพ๔ฉ๛(+,DNฅ8p๔<๛โšงN'ฑฉณฎๆFสŠ ฐY-่$™L†ค,ณ่”eJ ๒1่๕ฬ๛็?elzF•๓ฒmำ>zใuXอf|฿๚yNŽŒฉ6ฮ\N_y่A ๒rP”4&๙PQˆลใx| ŽO0>=ทฆDิ BH ม๛BJ X[Bj…’ 7ิฑก4ŸR‡›A‡N›ํ่–ษdะ-ืม˜ฦ๘ฃ๏โ วU๛wป๘ฝkZ)ฐ™%dž86Fฅำฆบุ๋?97็ฏoงุn&ก(Œ/E0๊ดฺฬuZM6c*—๑…xsยห๎แYไ5(ฆ~gk3kณBjิโ/vf!šธb๎{!คฮŸ›•฿๚๔'(/*"ฅคxง็8?๘้ฯึฬ๘ดZ-ฺ[ุะBYQ!๖e”“๘A†ฦ&ุด—บŠ2nฝv+f“๚ทน]ปก›ปo†ีla)ไ?'†FTy-5ๅฅ\{ี:6ดท`ะgk)f2 ( ัxœ๙E'GวŸ™%•Zป™งBH ม๛BJ X›Bj…B›™k‹ุXๆฆ$gYLIฺีE‰”ยั9^89ล o@•฿บา<พzu nซ‰เฒชRก:0ต€7[~‘S<3สqฏŸํตลดๆโถ™0-ืช‘•4x’_˜}ฃs์CIฏP๊5ญlซ.B/i๖๙๓—ณK^1๗ฝR็ีbๆท?๓I*Š‹I) {๛x๘ฉ็ึฤุ:›ุAEq!9vzŽL’ฒŒ?bdrŠร}'Y\๒“RRด7ึsว๕ื`6™X๔๛๙žŠทนm้๎เž[ทcณX„B๐™่Rี54TWฒพญ™บส ๒œLF#šๅ…/)หŒOฯ286ม๐„:ๆH)@ x?BH k[Hญk6pkC)W•ๆำŸƒqYjd€h2…'ใธวฯพั9zๆ–T๕o,ห็wฏn!ฯb$—y์ุ(ตนvีลัม™Eผแฟ]E63แdŠGŽ๐“๑์ rฑ‹›๋Jh.po1bิIh€ค’ฦK2ดdืะ oMฌ"สฟmืUขำjZ ๒g/ReทภŸ‡R็ูdไw>s?ฅล(Jšร}|๗ษg.๋˜:›ุะึBuy)9ถฌˆZ‘มp˜ัฉŽž8‰ฯฤj1ฐ่ะ\[อGnธ6[w)เแงžc`t\•๓rUG+ฟz๛อุญVแ>ทƒฃ'T1๖ฦš*ฎjoกถฒgŽฃํ\ชFC:arnŽgwํ[^ีR@๐~„ิ!คVp™ีอิๆูัj4d22@:๑”ยb$ฮqฏŸWFๆ8:๋Sล5mญ(เห[špYŒ๘cIํฅ!/Guqtxึ—Rืถ-o?L๑ศ‘a~ฺ7q๚หbY>k‹it;p™ ซr1+ฆ๔{ผ:๊แอ ๏eฝž?ธฎkช ัi5 ,๙ำ—ฉข๛ู"„ิ๙c4่๙ฯ>@UY ้tšฃ'๘ฮใO_–ฑด7ึฑพญ…š๒2v}VDษฉP˜ฑฉŽใY๐ญŠจมตๅนzฌf3Kหu—๚UZwฉปฅ‰Oy+9VมH˜ว๖{Oฌ้17TWฒนซš๒ฒฌˆ2่ัhดค”ฑxฝNั`@Q๕๕sเ่1Uอ‰R@๐~„ิ%ค่๚vฎฎ*Dฏี“Š‚อ G/iIง3ฤS ั8ป†gืผ˜บฎบ/nj"ืl`iYH5ชPH๓แ ว๙ฝk[q[ณ๕ฐพh˜็๚?xKษี•\[UH“Aฎูธฺ™/ฉ(,F“œ๐๚yuฬร“—'c๊ทuฐตยคีะ?ไOv$žRฎ˜๛^ฉ๓Gง“๘สCR]^J:ฆง๗ุS—t M5Ul๊jงถbED้ )%E0YQณ…๗‰จ‚แ%ลสRwฉฃฉž๎บ‡อN(แษป8pดwMŽตฉถšMmT—•’๋ฐcXฮˆJ))"ัs ‹๔ ฑฎต™Š’ขU๑นHบึ!คเ}!%จOHษถถ,‹‚ม…ฏyุXžO™รJŽ1+ฆ2™ฌ˜šฤ้Ÿฐgx–ร3kSLX[ฬ็ฏjภi6เ‹&๘Qฯ(อ๙ีลั1ฯHดzX฿}w~qaไ๋ช นถชฦ|นfร๒ABQ๐E๔y์šแศ%‹zC›หณqvยเv\“ลืฯ!ค.ภƒคFรWํAj+สษd2โ฿}๒’œปถฒœญิV”Ÿ.2R)‚แ0Ss^๗ฤณฐ€ลl๚…ว Gc”ธ๙ุ-7žRw้g๔ ซr^ฺj๙ิมaทŽFyj็n:ผถN{c๋Z›—3ข‰Šข‰ล˜๕ฮำsrท๗”eพ๒ะƒิT”‘Ig860ศจjN„‚xŽBJ ิ'ควl,w#i ฯเ›o๖a3่i/สฅปุEEฎ ปQ^ซY‹ั}^?ฏy/[ฦอฯใๆ๚~}C“žลh‚GŽา์VŸ๊›๗ใ ว๙ส)๕ฐใ^ๅญใ5ภ5U…lฏ+ฆึeวนœ1uชX์๓xipšใ%3-ะ็๕๓;r%=7!ua๘๊ฏ=H]e™L†พก๕‘ว/๊๙ชJKุNCu%.‡cyk—†TJ!2ํ๑r์ไS&ƒแฌŽ‹'ศฯu๒๑oยfษึ]๚ั3/าsrP•sา\Wรงๅ#ไๆไ‰Eyๆๅฝผ๖๎แ51ถฮๆึต6S]V‚รnCฏหฮŸข(„cQฆ็ผ๔ ๓ึแโ‰๗บz~๕sŸขnY|๖๓๚ป‡T5'BH ม< !%๊Rฑฝ‹ e๙hใ^?฿|๓ลv3ํึ่vะ^”KMฎ ปษ€^ซ!ษn๓E๔ฯx}ฬ{ูkญpGcญฏ#วจg>็ฑccชฬ๊_เ ว๙ญอูzX๑$฿>0ภžแูณ>†^าฒญฆˆ๋ซ‹จฮต‘c2`XžฟxJม‰ำ3๋cืะ,'.nWลฟผฉ‹๕ฅู8๋๕๘๙ร๏^Q๗ฝR†฿}่ช*ษแฤ๐(๒ƒว.สyส‹ ูาICuyนฮีbื๏‰ŒyNŒŒ215‹ม ๛PวN$ev๗q vซ•`8ฬฃฯํเศ‰“ชœ“ฦš*>๓ฑโr8ˆฤb<ฟ็U๖พ}y๏฿ฮฆฎ๊hฅฒด˜[ถฦืฉ"qjฮKOzŽŸ&ขV๘สCPฟg}C#ผzเ jๆ#“žA!คเL„ิ'ค็Mฌ/อC›[โ_฿๎งะf>ํod%Mmžฎb5.๛ib#ฉ(๘bI‚ผ22{ูปบี\ฮงปkฑ๕xรq?6ฆส ฉมล Hœ/oiฦe6เ%๙ททOฒot๎Cหจ“ธนฎ„-nชฯ‹๑”ย\(Fฯœ—g๖….ส๕ีอt—dใฌgn‰?y๐Šบ๏…บ0๖g๎งฑฆ€“#ใำ๗ฝ ว/v็ณu]'อuีธœL#ZญEI‰ล˜š๓prdœฑ้๔ห >,rJมb2Vฑ็_โะ๑ชœ“บสr~ํปp9Dใ1^๛ป|๛ฒŒฅณฉmT”’•-6ŠD™๕ฮsฐ๗{๛H$~ฯ฿๙์4VW‘!C๐{฿~G5๓!„”@ |0BH ๊RงŠ‚#ณ>พฮึฎ‘’H)TๅฺุX๎ฆาiลi2dท‚‰Tถซะb=ร—OLำZษ5ุ:<แ8OฅูํT] ๛BฬGโ|i๓{ฺe?ฏyฮ๙˜ฝŽ[๊KุX๎ฮfผ๕่ด” ฤไžpœร3‹์œfยน ื๓7ทฌฃณุภัY๚าก+๊พB๊ย๐[ŸอตีhะprtŒo>|a„Ta~[บ;hฉซ!?ื‰ษhD’ดคR ัxŒน๙E๚G˜˜Cฃ9ฟs)้ Ž๎บ›P$ย/พฬ;=วU9'5ๅฅฺว๏&?ืI,‘`็koฑ๓ี7/ †Fร๚ถfบšฉ*+!วfEฏำYตฒต๒๐๑~9vYๅ;ฟ้OาT[ตg{: š๙Pาz‡F„‚3ื !ค@}B๊onYGWฑ‹ pxf‘|w|๋/.ฺ›ช]v6”ๅQ•kรฑ,ฆ +ฆ–โ ๆƒผ1๎ๅี1ฯ%ญ๔‰Žj๎kฏยขืแ วxฒwL•Bjฬf>็776f ดว’ำ›}D๔9Lnฌ-bkEๅN+๖ๅฎŠฉt†จœb.ใะ๔"/ อ0ธ0b๊oo]OgQ๎jœ—_Q๗ฝR†/=x-u5h4ว'๘ฟ฿}ไผŽ—๏สesW;-u5ๆป0MhตZE!ใ]๔qbx”ฑฉเยNi4ผ๋ŽๅBเžฑ›ทSๅœT–๓฿~๕cธ]Nโ‰$/ฟฑŸ๖พ~ัฯ+IZ6ดทาูิ@EIัฒˆสnŸ”S กH„ษY=ผำs9•:๛8๛ิrœกaplฒe| Š’ฆwxT)@ 8sํBJ ิ'คึ๕ด/‹‚ƒำ‹<|hˆ<‹๑ฌ>—j\vบJ\หลณ ่ตY1•Tฒbjh!ฤ[^๖ฮ‘J_ฏๆฎ>Z‰Yฏc6ใฉใ๊R ‘8ฟฑฑงษภb4ม7฿ไ”B฿ศ˜R@pๆา+„”@ จKH้ดถ ด8P2p`rž๗Œโ4ฮ้xแ„Lนำสฆr7ตyv\f#F้ฝŒ)<ษˆ/ฬ‘Y^๓^1๕น ๕ูTŽI'1Œ๐ฬ‰IUvู› EYŠ%O๋๘o๔qhz๑ขณศnๆึ๚RบK\”9ฌX๔:ดšlอ’p2+ฆ๖Oฮณs`š`Bqฆๅ๏o[Ÿณt†ท'๘๋W„:_ฎD!๕๋๗}ŒฮๆดZ-cS3|ใ{?")๒Xณšอl์lฃปฅ‘"w>VณI’Hงำฤ“ |~FวŸž!–H\‚+ั๐ภท‘ป†:ำ+…๙.พpว)v็“Hสผ๎ažฑ๋‚_ฏืฑฑฃฮๆ†S2ขtd2 งd‚แ“ณsผำำว‘ค/@ฆํฉq6:9อŽW฿ไBnูผ˜$ๅ'Fว…‚3W^!ค@]Bส(i๙ปึำไฮŠ‚ท&ๆyฒw ว9 ฉ"ษeหช˜ส51๊ดh8]Lฝ9แe๏๐, ๅย‰ฉฯolเ๖†2L:‰‰@„็NLจrห\8†?žไ3ห็#qพฺqŽฮ๚.๚น+œVnช+a]Iลv3ๆๅŒฉ”’&”™ Dxcห๎แYBg!ฆŒ:‰ฟปu=M๎”t†7'ๆ๙ปฝ=Wิ}/„ิ…แsฟ›ฎ–&$Iห๘๔,฿|๘Qโฟ@ ™Œ6uถำีาHIก›ูršˆ๒๙ƒœใp_?Zญƒ^wฉ‹นฃทฎvฆ{แ•ืูญขยูง’๏สๅ‹|œ’7IYๆอC=<๖ณ็ฟ๔l๊lงญฑŽ๒ขBlV :IdV3ขฦงg9ืฯกใtK็~๕W่niDซอฦู {_Sอ|$’2cBH ม™+ฏR@ .!eึ๋๘ป[ืัŸoŒ{yบo‚ฃ‚?œLQแดฐฎ$Ÿ†\fIสถ้Vาโ2รพฆๆู=4K<ฅœ๗9ฟดน‰[๊K0Jใ0/œœขQ…R๓ั8x’:kW;~ต^Žอ-]ฒ1T็ฺธฅพ”ฎVำib*˜[ ๓ๆธ—]รณฤ~Ag+‹^ว฿.วYj9ฮa฿ฑ+๊พB๊ย๐ู{๎d}[ :Ibbv–z๘วDb๏ฏ_ฆืุ้AGc=ๅ%…ซ"*“ษ“ซ"ช็ฤ Yฦj1]โงb Ÿศญไ9ฤโq^|๕ ^~}ฟ*็$ื‘รo}๊>J S)๖้แั็vœ๓๑Œ›บฺ้jn คภีb>MDBaFงฆ9าืฯแพ“ๅšบ๗.ึต6ฃ“$ฦgfy~ฯซฉ–ุ…G)@ ๘9KฏR@ .!e3๊๙››ืQŸŸC*ๆตQฯŸœฤfะ_ะ๓„2e ๋J๒hr;ษณœ.ฆVไฦ[๓์ž!&Ÿป˜๚ํ-อTWŒ^’[ ๓สศ,UN›๊โh1– —๙dg56ƒŽนpŒ๕j/ว=K>–๚ถืำU์ขภfฦค“ะj@Ngล“ -†ุ?9ฯฎก’ํf7๊๙›[ึQ——ณWG=|ต+๊พB๊ย๐™}” ํ-่$“ss๓๗#yฏำฃN’–ทๆ5QVTpZVM")ณ206ฮัฤโ‰K/ขVŠัp฿Gn!?ืI,‘`็koฑ๓ี7U9'96_๔'(+*$ฅค8ะsœ๔g๚8ณ‰ ํญtท4R\อlAง“ศd )หB!ฦฆf8ะำK๏ภ๐%ณ็vฟŠZถ์ลIฦ'…‚3ื^!ค@]Bสi2๐WทtS๋สAVา์cวภ4VรลูึJศุฬlฉpำ˜ŸCžลˆQ'eท๒-หqv;ุั9ยขNั _นบ…jŠัKZF|!L.Ph3ฉ.Ž๑$ม„ฬ}ํUX๔:<แ๐๊1Nx—mLอnช-กฝ(ที„Iwzถˆ/ฤ๋ใ๖ ฯV์ƒโ์ผ~Šบ๏…บ0ะจDHล LL !%g „”@  .!•g1๒ต›บฉvู‘•4{†gู=<ƒ๙"ืY %d๒ญ&ถVธit;ฒbJ’ะj5ซSใKa๖Oฮณwd–@์ลิ๏]ส๕ีE่ดZ†ƒ๔yุ/pฦืฅ ”” &d๎mญฤขื1Š๑๗๛zX^๖ฑตๆrs} - ฌฆ๗คโr}ฐแล{– ืg2™ŒณoผูwE๗BH]๎ฟ๓66wu ื้˜๑z๙ืGž ขธˆmT–‘cท&ขกฃ“ำ๊๋'Šฌ ตXฬ}wŒ•K<‘dฯ[xnฯซชœณษศ๏|ๆ~*J‹Q”4‡๛๚๙๎“ฯาฯY-fถtwะRWCiQAถะผ6ปญ2)ห๘C!F&ฆ8ุ{‚ใƒร—5ฮž~y/•dHEcq'ง…‚3W^!ค@]Bชภfโ/ทwS•kCV^še฿่&tIฮJศธญ&6–ๅำ\เ\อ˜าj •ฮJศŒ.…98ฝภหƒ3gีูํฎkใšชBtZ  Aฦ—ย่ดZีลQDNJศ|ฌฅณ^วl(ส฿๎ํah1ดfฦุY์โ–๚šWคโ)bสK2ธdฯศ,C‹ม๗ลู?ฟuโŠบ๏…บ0๗‘[ธz]'ฝžฅ@ษ9%nvzžS ^Nอะ{ryŸ ‰จๅ‡b4{๛Mๆนˆ'“์.ฯ์Rง?0่๕๎CPUVB:ๆ่‰พ๓๘ำ?๗๏ญ3Wฏ๋คญกŽBwV“yตพWR–Y Ÿไเ๑K–๕‹โlฦ;ฯS/ฝข!‰ฦšBJ ท๖ !%๊Rลv3ฑฝ‹Jง„ขฐs`†7'ผคK+pV2ฆ6”ๆัZเ$9๋fฅNQ8!3ˆp`jำฟPLษถถTธ‘ด๚็ƒxยฑ า&RK)„“2w5W`ึILณBjฤZscT๎ๆ†š"Žำ ื'Ri–b ฦ–ยิๆูษท˜Vใ์฿๎ฟข๎{!ค. ฟํ&ฎธƒ^ข((iฝNhSห"jršพกๆๆืœˆ:•{oป‰ข<I™}๒๔K{T9'’คๅ+=HME™t†ž“ƒ|๛ว?y฿฿9s์l์lฃฃฑž‚<ณ Jก๙คŒ/`xbŠG{›ธ์qvอ†nŒ=ณ๓ <ตsทjๆ#1<5#„”@ œR@€บ„T™รยธฑ‹ ‡•xJแล)L. —.OFQ(!“c4ฐฅยMKก“‚ๅ:EงŠฉ้`”“๓ผ2<หB๔ํเ์†N6Uธ‘4ะ็ 0แc”$ีลQBQ'SูTŽI'1ˆ๐ืฏ๔0๎ฏู1_[Uศี•48p™$mvk•’Fงี “ด$R ฯ๗O๒ํWิ}/„ิ๙S_UมwFIFC๖ฑ2ณ,ข"Lฬฬัำ?ศยšQ™LVzนณB๊ตw๓“ปT;7_งจซ('“ษะ;8ฬท~๔ฤ๊หs:ธชฃ5›•๏ยb2ฃำIคำiโษไr}ฏ)๖9ฦ๐๘ไšธžr#ืo\ั gnaQUsŠD™žBJ ฮ@)@ @]Bชาiใฯn่ lYH=ื?ษแ™ลหพล-”ฑ๕lญ( ฅะ‰jยฌ“–kL…’2ำ(๏L-๐สศž๐{mแb{ส๒ัวฝ~๚ผ~๕5—ำi"ษw4–aาIL"|mฯQฆ‘5?๖kซ ูVSD}^นfC6ใNฃAd2ฦ๙ญ›[บb๎{!คฮšŠ2ถtwPWQNAž ๒–แL&C0a|z–ฃฬz็1›ŒชธฆL๎นu;%๙$e™7ๅ๑^Rํ๎CะPUI† '†F๙—>Fžำมๆ๎šjช(r็c1™$-้t†D2‰/dh|‚ท๖221ตฆฎ็๎›ถฑm๓LžE?ูฑ ตผว„ยQFf„‚3BJ P—ชqู๙“m”ๆXˆฅžํ› วณ„คัฌ๏„Œอ gs…›ถ•Œ)ฝiนฦT8™b2แ๐ฬ"ป†f๑„c|ํฆnึ•ๆกzๆ–84ณH•ำฆ>ม‘ษ‘SึPŠQ’๗‡๙ฺ๎ฃฬ„ขชฟVฃแบ๊Bnฌ)ฆ!?+ฆ4หq%+iฆƒQŽ{ผ<4CŸืฏ๚๛^ฉOei1[บ;จฏช$ฯ้ภhะ/วˆ&ปๅ3)๓๒๛้ลจฒฦ้ sห”ธ‘S):|”?ฟSต๑ŸนŸฦšJF&ฆ› ฅถwž ‹ษ„Vซ%I“Lส,๚ ŒM๐ฮัใŒNMฏษ๋๙่ ืฒ}๋&LF๓พ%žฑ‹๔)Aื2มp„ั™9!คเ „ิ%ค๊๒r๘“mํ-ฤไOŸ ฟ*ึ ก„ŒEฏcSน›๖ข\Šl&Lz ญFƒฒ,ฆf‚Qอ,าQ”Ks€ฃณ>^๓ะ^˜ซสXŠศ)nญ/Aฟ,คrื‘ำฒมิภ5ู IDAT€Aา๒ษฮj>V…I'eหg2ค3O)xย1zๆ–ุ54ณ&:žณ€B๊ฌ)+*`sWMตีห"*++•ด‚ข(่uzดZ ๓พ%ใ๑ŸbณšU>vห” งRผ}๔?z๖Eีฦ๗o}๚ดิึ ีjI$“ค”fใ{Q๑d‚…%?ƒฃผ}๔3sk๚znฟjnพf3fฃ‘…%?Oผธ‹tZQล\Baฦf=BH ม!%จKH5บั๕ํูฬDๅO๔Ž1ผZณใ %dŒ:‰อๅn:Šr)ฒ›1Ÿ’1‘S่ตZฬz %แะฬ"/ ฮฐนญสXŠ+ 7ีฃ—$ฦ–B๙ฎ#,DโชปŽฦ|ผญbป%IEAงีขำjฒbJNแ‰ฤW3ึbแ๖_. „๚eป๓ูฒฎ“ๆฺj๒r˜ FดZ-Šข‰ล˜๑ฮ“LสิU•ฏŠ‚๏ไY,*ูฆwzฬF#ทoป‹ษŒฯ๏็{O=งบํzห๑ภ7@yq)%ลปว๚๘ำฯซ๊J l์lฃฑฆŠŠโขำj{Eb1Nฑศ1ฆ=^U]๖-นใ†kVใ์ษป‘Sฒ*ฦพ1แ๑ !%g „”@  .!ี^”ห๏_FกอD(‘โัža&Qี|ืก„ŒNซaKE]ล. lfrŒบำj -†8:๋ใ๘ผŸjงMUb*ษp]uzIห๐b?{๙0xRu๗D[a.บSโ์่{†gนฑถ˜ญ•”9,ุ z๔’–T:CTN1Œrpz‘]ร3Lึ~L !๕~\N[ืuาR[Mก;/+ขดZ%M,วณเcljšคœ"ื‘Cศfฃ‘p-Vณ_ ภรO=‡AฏS]ฬ+JšปnฺFeI1)Eแะ๑|๏'ฯชb์ฅ…l๊jงฑบท+ฃั€ค•XูษO$y๕Cผy๐ณ๓ ช\งทmฺภGoผซูฬR ภ;v!ห๊Rพ@IฯผR@pBH ๊Rล.~ฺVVก„ฬŒ0Œช๎;%d4ธฆฒ6•a^~อJ:MTV˜ F9:ทฤqฯ•*SืVขำj\๒g/"”U7?ล.~๏šV l๏ลู3}8Mnk(ecน›2‡๋rฆ›’ษI*L"šYd็เ4๐ฺฎ(„ิ{X-fฎปjญ๕ตๆ็V๔:Oเ]XdbึC<™ภiทฏ~nฅ]7]ีla)ไO?$iU๓)%อ]ฏงฒดEIsคฏŸ|๒™5=ๆฒขB6wตำXS•ญํe4db: $-™ L{<|;฿'žPŸ_]ะอ7o[ณ'w์"™Tว๕,๚Ly„‚3BJ P—ZWšวWฏnมm5Lศเ๐0sก˜jฟ๛ค’ๆ ฉqูะญ+ฅั€คษ Žจฌเ ว82๋ฃืใงยa]ำbJซีpue:ญ†… ฒ๓Q9ฅบyYW’วWฏy/ฮ>4ฤฯ๚OoŸo1r{c๋J๒ฒbส ;ญht0ย;S ฌŠเ”rBHอbasW;]-ๆปฐ˜ฬ๏ฝN$๐,๚˜œ™#)หุฌ–๗}~jึC4็cทˆอbม ๒รg^`๕X8+Rฉ4wnฟŽชฒา้4GO ๐วŸ^“cญ,-fSW; Uธœฺู^’ค%ฅ(ฤโ ๆ}่๕zŠ๒๓ะjตLฬฮ๒O˜HLฝkล–๎๎นu;6‹…@(ฤ“;vOจc;๔โR€ฉy!คเL„ิ%คฎ*ห็+Wทg1ˆห|๏ะ*‹fฏ`ั๋ธทญ’บผ”L†ฉ@„DJก4'+8tฺ์–ฐlญข8วๆ–่๕,QšcY“bJ'iูZแFาj่Ÿ๒ว;’H)ช›— ฅy|ๅ๊V๒ญู8๛ฯwู9๘มํเKs,TWยบ’zรuT——’Nง้้ไ=๖ิšcUY ;hจช ฯ้ฤd4 ัd‹ฬGใqๆ}Kฬxฝ„#1์V Wuดข“tLฮอ๑/?xŒ`8‚Zนชฃ•๛๎ธ9gแOํุM4ฎŽตoaษฯ๔ขR@pBH ๊R[*๖–f\#x’|wฅ˜zทaุ :๎iซคึ•ƒฌคู;:วพ‘9บK\ดๅRlท`3่ะ-gmฤไsหb๊ธืO‰ผฆฤ”AงesyVH๐๘ฃ‘•ด๊ๆeSน›฿ฺูLrœฟ์ž…Ÿฉฬตqs] ลฎีnŠ:ญYIJศŒ๛#ผ1๎aฯศแ51๕_QH๔zถtwะัTOYQ!V‹$‘NgH$“ูขื#c ŒŽS_U๑YQงฒ2ํ๑r฿ท`ทZ †ร<๚ีt?;•”ข๐‘mืRSQF&แุภ่“kblตe\ีัJ]eEvkžมํš—ฮfDy}ฬx็ั๋t8s์ŒMอ`1›ุิูŽ^งcฺใๅ_๘8Kม jืŠuญอ|โฃทcต ‡y๚ฅ=„ฃุ๊ฎ>๏[bfม'„”@ œR@€บ„ิตU…|qs.ณฅX’๏ผ3ฐ&ทC-zIหงบjจสต#+ ป†gyฆo‚B›IฃกนะI{a๎)Sาˆห)<‘8ฝsK๔yฺึ†˜2้%6–ป‘4pใ็w$ญยตvke_าŒหlภK๒ooŸd฿ู่ต‡ฏฮตqK})]%. ญ&LหSr:C0!3โ ๑๖ฤษ'๏ผ5+ "a{%’ฒ๚$yJIs๛u[ฉซฌ “ษp|h˜{ไ‰ห:ฆ๚ชŠeUNnNฮชˆJงำDใ1fฝ xั๋๕8s์ง EQุา‰Aฏcฺ3ฯฟ่ —ช]+:›xเฮษฑูE"<๒B*ษ๘๒..1ป(„”@ œ‰R@€บ„ิถš"~sc#Nณ_,ษw JชWHฅา>ฟฑ*งคขฐsp†NNแถšVFาhh*pะZเคiลnะ#"ฆๆฃ z=Y1ๅถ˜.ซ˜ฒtฌ/หG ๔zแŽwU9/ืV๒ฅอMไ.‹ฯู฿ฯ๋cžuŒฦ|๋Š้(สฅภfฦค“ะฎˆฉx’_ˆท&ๆู54C๒2d‘WR’คeC{+ฺZ(+*ภfต “ฒ ’rŸ?ศเ๘ƒฃใไๅ:ฯZDญ”ez†xเฎqุ์„"žx๑ebq๕m#Vาnฝf3๕U•dศะ74ยฟ๐๑ห2–๚ช 6uตS[Q†3วŽษp๊ึผฌˆš๑.`2NQ+Dc1–!ฎูะ…Aฏgv~o่Iผ‹>ีฎํu็ˆซฐ‹ฉ|ac#ฅ9b)…g๛&xkbž“—~NษdhฬwะU์ขยiลfิฃืjศd ก(ฬGโ๔y๔{8L†K"ฆœfล. +คLฅB๊ถ†ฌ๘ฬ1fล็?พqœƒำ‹ไุ๋K๓ุ^[L“Ižล€A’ะh ฉค๑ลœœ๒๊่o\d1uฅ ฉŽฆzึท6SSQ†รพ"ข@N-‹จษiF&ง0DDญpไฤI๎ปfr"ฑฯ๎ทFEม/็๚hฎซFƒ†ฑqพ๑ฝ]ิ๓ต7ึฑฎต™š๒2œ96๔:}ถXนข‰ล˜๑ฮ3ฟธ„คำ’็tž๕qEม็ฐกฝ“ัภผo‰xงŒฯฬชvhจฎไณ๗‰k9ฮ^ุ๛:ลล53>ƒ@EIu•ๅไนฐ˜M่$jVb พก0฿ต@ ฌ"„”@ 19•Yษึ•4แ„ฬL(สปS‹์c&ธvถŸRมงบjฐ๕xรqภIUl5yh5พฐฑbป…˜œโฉใœYภfะŸ๕1R้4Mn'ํEนTๅฺฒSห5ฆ’Šยb4ม o€๙6ƒ๎ขŠ)—ลHGQ.เะ๔"พ๋ฐ*็ๅฃMๅ|v]-๖e๑๙^?ฮแ™ [fkEืVาไvเ21HZเ=1ี็๕๓ฺ˜็œท 2ฎ!ี\[อฆฎvjสKqุํ๔:@CJI …›žexbƒ^AEิ วNrฯญq9Dใ1ž฿๓๓>uึ*บ๎ช๕ดิี ัhŸเฟ๛ศE9O{cWuดQUZ‚รnล ฯŠจTJ!‹ฒเ๓‰ฦt:I:—ุฦ ัีาˆูhbai‰|โFงฆUปVิWU๐ะ=wโr:‰ฦcผธ๗ ๆ.๏ฤt:ƒษhคฆข”š๒2 ๒r1MH’D:FNษ$’2IYๆ‘็v๐•ฯฟ@ ฌ"„”@ /ฮd\#&„คีRาDไ3ม(Gf}ผ22ว๘R๘ฒ๕žึJ๎๏ฌมnิแ ว๙ึ'Iฅี+ค$†/ljคุn&’L๑ฤฑ1Žy–ฐ่u๚XJ&CหNgฑ‹๊\v“รฒ˜J( พh’…ฝES๙Vm…Nาภม้rืUฮหฏดT๐`W-vฃo8ฮื_๋ฅgn้ขœฺ๋ชBฎซ.ค!฿หl@/iษœ!wฯrhๆยfCจ]HีWUฐฅปƒšŠ2rs์๔ูlrJ&Ž09็กx ฃแโˆจŽs๗Mศs:‰ลใผฐ๏uๆๆีYซ่š ดีืกัhž˜ไฟ๛๒YนญกŽอ]ํT”ใฐNQ)B‘พ@€h<Nส’s%“ฮเ…่ljภl2ฑ่๗๓Ÿ<ห๐๘คjืŠšŠ2~ํปศฯอ%ณ๓ต7™š๓r_ำ9‰ฦฑXLดึีR[Y†;7ฃั€Vฃ%I#ห2I9E,‘  ‘ฮd๘฿฿}Dtู‚3BJ ศvู๋,vq]U!ญ…NVfฝ„Vฃ!ษ•fCQz็์ใไ|เฒ๕พ๖*๎๋จฦfะ1Š๑๏T-ค๔’–ฯol ศf&œL๑่ั‚uา93•NSๅดฑก,ŸJง ‡ู€Aาf[งฃIƒ๔ฬ๚ฐ๕NLi ภjขฅ +ค™œ็k{Žชr^๎mหŠO›Aว\8ฦื_ํฅืsqทa]SUศMตลิๆูqš t™ฬJ]ฐlฦิKƒ3๔z.ŒSซช.+esw;๕Uไ9ซRCNฅGขLyผ M Iyฎ‹~Mรฃ|ไ†kษฯuK$ุ๙ฺ[LฯyT๗[ืuััXFซadbŠo|๏Gค”๓฿ฝฎต‰๎–&ชสJ–ทSf3@SJvฮb‰’V‹Vซ=๏๓e2มํ๕Xอf|?๕ฃใช]+ชJK๘ฏ nW6ฮvฝ๑6ฃS3HฺKgค"ั89v+ญ๕ตซ๕พŒ†ำET<™$Oโ…Hฅtหk™R@๐ฮBH AVHญป1฿มถš"ฺŠœ-XNS19[—่ธืฯซฃŽฮ^๚m)tึpo[%ฝŽูPŒ?p’ดŠหM:‰฿ธช›‰PBๆ‡GF[ ฏn฿:)…Z—๕e๙T9m8Lูโ็ูถRฏ(ภษ๙FtbJฃัP`3ัRเ@ษภ๓๕+๊RŸ่จๆพ๖*,zžpŒx๕'ผ_ฤ๊ดZถีq}u!ีนv&=Iปšๅๆ ว96ทฤฎก๚ฯS ซMHU–ณฉป†ชJ๒r—3ขE!2ํ๑229คqป\—์šFวนํบซO*ญUดนซƒฮๆดZ-ฃ“ำ|ใแ!ŸcำญVห๚ถf:›จ,->ฃฎWvฮๆ}K$e๙‚‰จSY๔่lชวjถฐ ๒ƒงŸ็ฤ๐จjืŠ๒โ"~พQ—K<‘dฯ[›X>“H4NฎรNGSUe%8ํ๖ีˆJZ!™LK$I$“„#QEแฬิ-!คเž…‚ำ…ิ Uน6ถีัU์ข$ว‚อ Cาพืษm1š•{Ff/Xฑ็ณแำต|ฌฅณ^วL(สฟฟ}5’›u:~cc=nซ‰`Bๆ{‡˜ ว.เ>Œ1ีU’Gห†รd@ฏ}ฏ^ัR,มภB“  ’–ฒœsSZญ†B›‰&ท%แญ‰yvo*็ๅฎ๎m}O|พ.]‹uฃคe{] [+ จฮต‘c2ฌึ‹ง<แ=ณK์ža๐วฅ!URเf๋บNšjซp9˜ FดZ ฉๅยืำs^ฦฆfHง3ไปœ—ซีj้nidcgeE…ุญึUY’”ณ5ข|Kฤ ๔:’tq3{ฒBช›ลJ โัgwpด@ตkEฑ;Ÿฯ฿/E๙y$’2ฏพsˆc'1~ˆgK")SVT@}U%•%EหR1{%ญR’I™`(L8;ซc !%๏G)@ เ ฉrอnจ)fSน›r‡%๛b|ส๖/<ษเBwฆุ;2Kโ"uพ๛๕ซ๘Hc&ฤT ยทœDซb!•cิ๓฿6ิใฒ๑ว“|๛ภก„|Qฯ‘ST:ญl*หJงู€qe.•4X’_ˆฃs>L:้ฌ3ฆt’–"›™๚R้4ฏy๘_ฏ๖ชr^~m}=w6—ฏŠฯฟyๅ(ฃ—ฑจŽQฯตล\]Y@นรบzฅาbrŠนPŒƒ3‹์šaย9ซcฎU!ๅvๅฒนซ–บ ๓๓0ณQŠ’&ใYXddrEQศuไ\๖X™˜žๅฺ๋NฃcชŒ๛๕m-ฌokA'ILฮฮ๑O฿1แh๔—๗:Z้jn<-#*“aตภาฑxƒโ‹จ—ด5ึ‘cตŒ„y์๙—8t„jืŠ‚<ฟ๙ภว)v็“”e^๗‡๚๚1 ์)Eกขธ˜†๊JJ ุ,๔ห[SJš”’"‘H GˆDch>D*!คเ!%œZมขืฑฝฎ˜ซJ๓ฉ>c๛—œNˆ'[ ๓ฮิป‡g‰$Stฌ_ุุศํฅ%‰‰@„oฝ๎"m๙ธ8M>ทกž\ณฅX’oฝ}’จœบ$็Žศ)*V6–ๅS›—อ˜2๊ดh85c*D|Iฃก4ว๒ ง—ดอิๆe…ิพั9๗kวU9/gŠฯฟzๅ่Y‹ž‹/7ี•ฐตาMiŽ›A‡n9c**ง˜ D88ฝศ๎กYfBฟX$ฌ5!ๅดนzCืชˆ2›Œูbษ้4ฑDฯ‚๑™Yโ๑$.gฮš‰•‰™9ฎูะuš(81<ขสธ๏jibcG+:Iวิœ‡มcร,b๕z›:ikจฃขคปี‚NาไTŠ`8‚gม‡œ’/IFิ™๘Z๋kษฑูE"<๑โหผำฃฮ฿#€\'_|๐W))p“”e<ิร;=ฝXฬฆ๓>ถVฃฅดจ€ฆฺjŠ๒๓ฐšอซ๓•RRศrŠx2I0&Ÿำ6K!คเ!%|8!uช|ุVSฤๆr7uงศ 9!”™ Dุ?1ฯพั9ฃ‰ 2ึ/mnโ–๚Œ’ฤธ?ฬทœ\bช|ษฐ๙์บ:œfพX’‚x๊าv \ษ˜ZW’G}~น&#I‹F๒)bjh!(ฑฐ˜2่$Šํfj\vd%อ+#ณ฿7๚T9/gŠฯฏํ>ยt0บfฦ็0้๙Hc9ส๒)sXฐ๊uHฺl๓H2ลd สม้v L{oญ)ปียึuด7ึS˜Ÿ‡ลdB’ด(้4๑Dฯ‚ษ™9ฒLŽอบๆberึรึuง‰‚ใƒCชŒ๛ฮฆ6vถฃื้˜๖x๙—>†?x๚VUฃAฯฦŽ6:›)-tc;CDBaๆๆI)) z%Q+๘AZjซqุํ„ฃžฺน‡ทซณฆ@ฎ#‡/=xฅ…ศฉ๛๔๐ึแl๓9ำ 7PQRD]e9…๙.ฬ&:IG&“ห•ฎyK ๑D๒ผ จ !%๏G)@ เ„ิฉlฉpsCM1uyv๒,FŒ:)†]INสฬฃผ;ณ๗qr]๗a๏๗ฮggfgg{_๔B€€HŠฝIฆ ญjR=rไ’(ฒ_ž_โฤqlวq^œYv$[qlห‘"Y–dI‘eหN‰^ป‹ล๖:;ฝž?fwY@ษ,v/|พ๑ร™;็œ๖|๑+žผ0๗ถ7๕Ÿฟa3w๕ฆqษ2ณE์…แซฺ๖๚rำ๔๒‰=Dฝn2…/<‡j˜๋2–’ชำ๕ณ;gS2Bฬ๗j1•ฏiŒe‹ -ึ‹hงCฏyœ2อaฑša๐ุ่,_zฮž)2ฏŸฟ๕ุ1ๆŠี 7ฮTะว=}อ์i‰ำpหศ’„nZ”T้B] ?<ฑk.ง“™…Eพ๒o“ษีป9z=๎•”พอ47ึำนœNy-5/W(1ฟ”AUU<ฯU้๖ำศŠ๔wu ‡)W+|ั'xๆศQพ+"ก ์ใฅต)…n่>~Šง%xs๗†iZxฝzฺZ้jkก1€ฯ๋YkZข้[DE IDATšฆSUr…"Šช]–s)„”@ \ŠR@ภRซ์m‰s[wšd˜ธ฿‹ื)#K`ฌDmL*Ÿอ2xa–‹oฑฯฏด…ปำธd–‹|ํฅ[ฏ}:ไใใป{{\,–k|ๅะฺ: ฉUŠŠF:์ck’DdE2ฎค๒™…•ดฬำ๓9‰tฐ.ฆผ.™ๆฐŸŽhอ0xtd–?~žB๊ีโณฤฟ?p”ลrmรŽท-เฎfฎki ๒ใ_‰˜า “ขข1™/๓ฤฮ’ฏi+ำ๕RAฏgpฮํ์ฺOsช๑ี"JUXฮๅ™˜ย4 >฿†ฟVfๆุปc+ญฉ—Eมั3Cุฑด–พnบn'n—ซ.คืwจTซ์พuๅ|% ๚8๕(Uำศ๋"ชฆ(๕›uQซไ‹%๚:ฺˆE"”ซU~๔๘S!ตสฮt7wฆุ–Š’ x๑นdR=จขฬซœšฯ๒ฤ…9ฮ-ๆ฿ิฑๅ;ทrkwN‡ƒัLฏลด๑ฃผ5เม]„V„ิ—žC฿ **A7ถฏŠ)๗Z๔›j˜k*cู็๓Mฑ5!ฅŸŸแ+‡ฮู๒ผผR|Ž-๙wŽฒ|™าNฏ$ฑ ๗๔6ณป9N*่ล๗1u1[โน‰ฅXSฏบ๚๚ถ}ƒm้ฟู!ื;vช*ห๙<็.\d๘โฝํm~[\+ณ ‹\ทu๓Š(ะ9r๒ GNžมaรศอM]|n.๓Kห;;D[บ‰ึฆ—ลa=k5"j Uำ๑n€ˆจืR(•้ikก!ฅRซ๒ใ'žแฑ็๖]แ๗z๙ง ฃ9n=}–GŸ=D4ฉ฿+Wj„Cถ๕๗าำJ,ฦํvฝJDีT•ZM%W, ๋ๆ9—BH มฅ!%\~!ตJ"ฬmMloŠญDmผ,ฆjšมbEแฬ|Ž'ฦๆ8>ป†Ž๙kทlใๆฮN‡ƒแฅ}rl#Šฑ ูEศใdพTRๆ{7ฆ๋[lJึ#ฆฒŒCzน^ุุr‘‘L‘;’ด†(†ม‡ฆ๘๊a{ถY—๏ส-]Mธไบ๘G^’๒ถ‘้iqw_3ปา 4ผx]N่ฆEAัอ88ฑp๛nฺzU7ˆห๙ยmAฟopตๆขj,็๓œg๘โษ†Mษ„ญฎ•นฅ%vnX/>หกใงl™J฿ีษญ๛ฎรใvฃj:Šชโ๓xV๊@ี#ขฒ๙"ณ‹K†Žืใp"jํนUฎะู’&‹Qญีx่้็xไ้็m๛ฎ๐ธ|แSะัฺŒiš;3ฤCO=G,zฯ—+5ขav ๔ััฺL4ยใv!I ร@ีTชŠŠขช”ส รเJ†๕ !%—"„”@ pๅ„ิ+ฅหm]M์jn 9\/ภ์”๋๕*jบAฆข0ด”gpt–#ำ™Ÿzฌ}๋vn๊hDvH -๘๎้‹(บ}…TOCˆm๏$่v2Wฌ๒•CCNHญm๐V"ฆ๖ถฤู!๐โ‘ๅz)ำขข๊๘\2ูAU7๘ัน)์{ ฉ_ปy7wฝ,>ํฃ/QR4อc แฎž4;Vล”S^ซ VPด“๏U Z–u›e1จj*ห๙ฃSœง!ถˆZe!ณฬถ^:Zา˜ฆษั3C<๗าqœฒš-Huำ~บ:p8ิCึZjrพภ์ยšฎ๐๙7ฌˆzYศTiK7‘lˆRU<{ˆ?๑Œm฿.ง“/|๚AบฺZ0M“็†๙ัเSฤฃ‘W}ฎRUH%โlํ๋ฆฝ9M$ฤํr!I`˜&šฆฃjลr…rตŠi^๗งR@๐:o^!คเส ฉUšC~nํNฑท%AKุOะใยๅฐE7ศTTF2žพ8ฯ3ใ ผ3๚฿พƒwด%‘็๓|๋๘v~’$"์ึv.'ณล*rhkƒฯจจh4๘<์oOฒ%!ฌ‹)‡H`˜ว็–๙ž<ลrUฑyyฅ๘<ฟTเื~‰ชฆ๖:šŠrwo3[ฃ$ƒ+b n—$้ชnkŠz[พX˜bhlœh(h[ตสโr–อ=]kขเ๘ู๓FoG;–eqzd”๏=ฟŸพŽv๚ปฺinL๐๛qญœ+0ั šขR,•)W*HŽ๕‰BJ .E)@ เ๊ ฉU|.'w๕คูš ป!Hฤ๋ฦต๒วฒfšjs%^˜Zโภศ eU็ท๎ลžึเิ|Ž??2Lุ๋ฒํฺoOลxฯๆ6|N™ฉB™?94„รfํนŠŠ†ฯๅไ=›ZyG[rM.Z+ขขช1™+๓โL†#ณถSฏŸg๒๋‡Žl˜b๓—ƒ}ญ nํnบ๎ๆซบA๙฿๘๖nนyเšYหlพ@w{+ฝํmX–ลฉแQ๐†R’$ัำFoGฉD>ฏwญธผiškR*W,๒ฤ๓G่lkม๋q๎ผิ•TผฆdEีx๚ศQพ๛ะ[_k_๘๔ƒ๔wv`a1rq‚Sรฃl่ฃ)'เ๓แtสX่†Žฆ้ิT•|ฑDฅZC^็R!คเuษBH ม๚ ฉUœทu7qC{’žxˆ˜ืƒว้@ิ•ขูS๙2‡&นพ%มถฆpb.หW‘xmป๖ปา ฬ@+^งฬdพ.ค์X yฆP!ไq๑›w์$๎๗`Qฏ+%KาZ1ํ’ช1Sจpd:รC็ง7tืบ็.๖ฎˆฯำ 9ี฿แZ๛‹ม4อซe๏็ใwoป~๋ๆม›๚ฎ™u,Kด77ัท" ฮŒ\เัgโqo,Q๎r:้lmฆฏณƒdC ฟื‹ผRหOQU2นŠชั’jฤใv“ษๅ8|Tฟm(คTU#‹ะ˜Dี4ž}้8฿ปGl}ญ}“ฐฉป‡Cขช(†ฑูถฺpUDe๓jŠบa"„‚KBJ X!๕J๖ท%นฃ'M_—งCB7MJชฮLกยK3c*_0๓๙ปwณปน.คŽฯe๙7BH]~7~๗ถwn}อฌcMQH6ฤ่๎เุ87๘4>ฏg]วๅ๕x่lmฆปญ•Tขฏวƒ์p`˜&Šข’ษๅ9?6ฮมc'˜œเ๖w\ฯ}ท฿Lภ็c9Ÿ็ิะัHศ–BJ7 ยมญฉบกs๘๘)พƒnํญ์ถ…ฮ6ZR8ๅบhฒ,‹ชขิETญFฎXBำ๔ [๋K)@ ธ!ค€)คVูัใๆฮšb4ผฮตข฿บi2ถ\โไ|–#SJชf+1e˜ทtฅธฝ;Kv0ถ\ไO_8ว†BjพT%ๆ๓๐/฿น•ฦ —ขข๑๕cXฎ(NŠŠฦถTŒ้A~—Œ,I–EY5˜.”96ณฬcฃณLn1๕๎นŽ้Žอ.๓ผtอ๗๋%คnฺฝcpำŠผนะ4h8ฬๆ.$$†/Ž๓รวžฤ๏[ŸTbฏวCw[+]m-4ฦcx=’ำ2ฉ) ™lžs.rุ่Iฆ็^๕[ฎฟŽ๗u+ŸŸlกภะ่E~nทหŽื7Ÿถtบapไไi๊od›๑o๊๎d๏๖-tทท ‡qป]ศyญขชฉฬ/-S*—1LkรBJ .E)@ `c ฉUzใa๎์I๓๎|ฏ่ไf˜&5อ`ฑขpv!วแษ%–ซŠ-ฤ”f˜ู“ๆึ๎&œฃ™_}aฏำiปkhก\#๎๓๐ซ๏B2เฅ h|ํฅjšมuอq&๓eฦs%ถฆb์J7 ๚ธe’„น"ฆf Žฯ-๓่๐ฬบŠฉ฿ฟw›bXภKำ~๓ภัk๎พ_/!u๓u;๛ป:ฎฅu$เ๗ฑตทI’Ÿเ๛ ๘|Wq ^‡พŽ6z:ฺˆว"ฏŠˆช) ‹หY†.Œs๐่ f—^๗87ํูลw฿Nะ๏'W(0:1…ๅฒฅฒL ฏวM{Kร09zๆ๑l๘qoํ๋a฿ฮmtดค‰†Bx.$ษaXX8ๅzแ๒L.วา๒บ+ฃ!%—"„”@ `!๕โ็_|ฯ>zใ!$๊Eณ%ภนR˜ทฆd* ็—๒œ\bกTะbJ5L๎ํkๆ)œ‰๓Kว ็๑บ์'คห5~ฟrำV๙šฦŸฟ8ŒDฝ“เ+™ฬ—น-ฒmELฅC~๎WGLอ—ชษp`t–๑l้๊qป๖ฒ-ล^œZโท;vอ๗๋%คnป{ฐงฃํZZG|^/๛{‘&ฆ๘#ธ RๅJ ฏวอ๖>๚:‰GWD”์@7 jŠยB&หนั1Ÿ8ล์ยาO=;v๏เƒ๗Aะ _,2>=‹ไpป์(คL<-iLำไ๘ู๓ูทvCŽีแธn๋fvoูD{sแ`ทห…$ีัEี4t@ำuย’C"“อ‘+1mฒ—BJ ^็oN!คภ>Bสใ”๙๗๎aS2ŒaมtพLYีi๛ y\ธ ่หU•‘Lง3LไสO๒(บม}›ฺธฑ=‰์8ทXเฯœวgC!•ฉ($พpำโ~นšสxaฟI<บ฿™ฬ—ห–ุaw:Nsธ.ฆœบiQัtJUŽฯfylt†‘L๑๊l%‰๔ฎ=lME1,8<นศ๏<~šป๏ืKHพoฯ`w[ห5ณŽšฆแ๗๙ุฑฉ‡รมุไ4๛ศใWด†TนRรใrฑ}S}]ํ$bQผn‡ร4จึj,.็8;rCวO1ฟ”yCวฝ~วV>๎ป ไKEfๆั ร–Bส4M.]m-Xฆล‰กaพ๚ญ๏nจ1บ].๖ํฦึพnฺ›ำ„\N0L0PU•|ฑLพXย๏๓าฺ”B–d๓ฒ๙†iฺโ|!%—"„”@ `!ๅw9๙ฝ{ฏฃ?F7-žน8ฯŸโฮžf๖ต&h๊bJv`Y ™&นชสpฆภฉน,ฃหE<จฮFUำนK๛“ศœYศ๓G๊วnd* ฉ —ฯ฿ฐ™ฟ‡\UๅO‘x้Œ๊w'๓eฆ zใแzฤTุGะํ\I7ฒจj:sล*'ๆฒ<~a–แฅย•$สใฝ{ุม0-N.๒O\s๗z ฉ;๗๏์lmพfึฑฆ(|>vmภแp0>5ร๗y๏่ฒทตฉฮvโั(ทI’0M“ชRc!“ๅฬศž้8™\Mบญ›๙่{๎!R(•˜_ZFQU\6”ไฆiโrน่nmมฒ,N ๐'๋;ใ]ๆ๕ฒืvถ๔’N&๘+" tรDืujชJกTฆRฉ 9(ชJ( ต)…S–ษ dry C)@ ฐ+BH ๖Ra‹฿ฝ็:zใatำไฉฑyเ้S๘\N๎์Iณท5NOCˆˆืหQฏญก™&…šฦxฎฤ‰น,รKydว๚ืจh:ฺม๕mIภ้…๙โศ†Œๆ๚‡ศVšB~~๙›h๐นษVUพ|๐mั-!:†fš,–kฤ|nzใแzฤ”ห‰์0-จi:sฅ'WฤิะbŠฬล็’๙ฝ{๖0 c˜ฯŽ/๐๛Ožผๆ๎๛๕R๗ฐoฐญน้šYวrฅJะ๏c๗ึอศฒƒ‰้Yพ๗๐ใx.cบpนRร๏๓ฒ} —พฮv"nWฝXนiRฉี˜_สpntŒ็Žž`9๗ึ๎]›xเฝ๏" R(—˜]XBืuœN› )งLw[gF.๐ๅฏ{]ว‹„ูป} ๛{I%โ|>œฮzM(ะัดบˆสKTชตWี‡RUp0@s*‰Sv’/•ศds่†a‹๓!„”@ \ŠR@€}„Tฬ็ๆท๏MOCอ0yblŽ๚ฬ้W}ฦ้ppkWŠฺ้M„ˆy=xœ$@5-ŠŠฦtพฬฑู,็—๒˜–…cฅk฿U฿ศช:ัลž–8pr.หืฺ2e/WSi๙๙ลฤ|n–ซ*Y๚a’7_Kgนชเuสt7„hV"ฆ๊ลฯkšมBนฦ‰น,O\˜ใฬB๎ฒฮ%่v๒ป๗ผ‰๗๔ลy๓Sงฎน๛~ฝ„ิฝ7ํlmJ]3๋X(–์ูถง,39;วwz ฯeหซ"jืๆ~z:ฺˆ…รx๎zjžaPฉี˜[\โ๔๐(/œ8ฆ#ข^หŽM}<๘พw †(UสLฬฬcYฆ-…”eZศฒƒ๎๖V†.\ไ๊[๋2–T"ฮ๕ทฐนท›ฦx ฟื‡,หX–‰ฆฏˆ(E%[(PSิืํ˜งj‘`€ฆd—ำIฑ\fq9+„”@ ุ!ค๛ฉ„฿รo฿ฝ›ฮXอ0xlt–/=w๖'~~[’ป›่O„i๐{๐ศ2‡„n˜U™Bฝp๖K3หฆyี  ๏๎aWบ€ใณห๕‰ฑ •V๘FษืTZ#>ทฏŸจือrEแKฯŸeGSŒศH]สVUd‡D_"LŠ˜rส๕TพšฆณXQ8=Ÿๅภศ์eSฏ‹฿ฝ๛:zV"๑ž›ใฟ<}๚šป๏ืKHฝ๛ๆ›“ืฬ:f๓Bม๛vlล);™š›็ป=†ห๕ึ๏ใrฅF0เg็ฆ>บ[iˆD^ีmญ\ญฒYๆไะ0‡Oœ&›ฟoฝ๓กiZhบ†ช้ิ…\กˆขjฏ+ขVั4H(@*‘ภํrRชT™[\5คภฦ!%ุGHฅ‚>~๋ฎ]tFƒจ†ม#ร3ทƒ็ม๏ํi‰s[W›’โ/^งŒ,ฑาัMgฆPๅ๘2‡'ัM๋ช‰ฉ‚ข๑ฉ๋zููรŽฮd๘ฮฉq6iใฺนดG๕Dผ.2…/>{†mI|—Aฐ-WdIb กeฅˆ+ป+.”kœ]ศq`d–S๓ูท๕[ พบ๘์nก&ƒf๙โณgฎน๛~ฝ„ิ{nฝi0•ˆ_3๋ธดœ% ฒ็v\N'ำ๓ |๗กวp:฿}\ฎิˆ„ƒl๋๋yED” ึ"ขๆ—2œๆเั“หๅห:—-ฝ|โ๛ˆ†ร”ซFฦ'qสฮŸ*J64–EoG;’$1<>มๅ7ธ๚๗ดทฒg๛๚:๊=w=ฝา2ัดบˆชึjไ %4]C๋ซ๋:‘Pˆฦx ทหEนZcf~Atู#„”@ `!ี๒๓›w๎ค#D1 :?อŸzร฿฿ใๆฮ›b4ผ๘\2ฉžVั JUNฮๅxaj‰’ช]q1Uจฉ|foS1Lเล้%~xfู!ู๎**ฑ doa‹ฅฒย}๖4ทv5!_ฦ”ศลr งรม@ฒ1XํฎhAM7X*ื8ฝใษฑ9Žฮ,ฟฅ฿Hผ๖]ปึ"๑Œฮ๒G?%ฯฎฌ—z๏m๏lŒ7\3๋8ฟ”! rร๎ธœNf๙๎รฝฉ๛ธ\ฉั ณฅท›ž๖6ขแn— Iช‹จrญส์"งG.๐์‹วจTkWd.›zบ๘ไ๗‹D(Wซœวใv!ห๖R†i2ะูไŸไ‹๙ L๓สฝ๎6๕tฑw๛zฺZ‰†C+้•†iขjŠขR]‰ˆ2 ๓M‰>]ื‰†ร$bQBช#ไ7n฿Ak8@U7๘ัูIลแท~ผh[ปS์Nวi๛ื๊ญฆ-Wฮ/x~b‘…R๕ฒ‹ฉ‚ข๑น๋๛ูม0-N.๒๐๙i ฉพD˜๏๊&ไqฑPช๑GฯŸๅถฎ+Mmก\รแ€พx„ฮX๐+ฤิ๊9<ท˜gpt๖ ‹ฉๆฐŸ฿ผใญGโู…๕Rธ๋ถมX$|อฌใไฬัpˆ›ฏฟลb†๏<|€ŸvฏŠจ›๚้lm&z9"JืuJี ณ K=3ฤ‹'ฯPฎVฏส\๚:๙๔KC4JฅVๅฬ๐>฿ซบฝูหดะMƒM8ฦงf๘รฏ}EU/ห๑‰=ถฐk๓ํอMDBA\N’†aฎีˆ*–ห”+ีทZg&ัp†hฏขjŒOฯb!"คภฎ!%ุGHu7„๘ทท๏ 9ไงช|ฬ8๕า่>n*่ใถ๎&ฎoMะ๖๔ธp;$,@ัM–ซ #™/อdฯ– ธ/O฿’ช๑น๋H†1L‹gวxdxฦ–){UMง?แ„x๗ํƒัp่šYว‹S3Dร!nฟฏอา2฿y่ผŽ((Wj$ใ1ถ๖๕ะตๅvีEทn่”+Uf9vfˆร'NQSิซ:—๎๖V>๓ม๗‘ˆลจึjœ—ฬ=i๖ถ$่‡ˆxธ^!ฆŠŠฦลl‰sY†—๒ศoqใQำ >ทฟŸž†0ša๒ไุFfถ่Z`K*ส‡ทuโw9™+U๙ณ#รุ–\—๑ฬ–ชH@"LW,Dฤ๛ฒ\T ƒๅชสะbžgวxf|แU…_‰๗ƒ3|ํฅ‘k๎พ_/!๕ัw5 ๘ฏ™uBjk*สฏฒฆ ’ช๓ญใc|็ิล+๖{N‡ฤ-]MุHoทo€ฎ†z7ทวFgy|tŸหiปkH5Lถฅข|pk>—“ูb…ฏฝ4สพึฤบŽkฆXซ]ื๕บqษ,ซ.ฆฒ5•‘ฅ"O]œใ™‹๕๊๕HผํคC+‰ทQX/!๕ภ}๗ ๚ฝkfฯŽ\  sฯอ7เ๓xXสๆ๘‹๏H0ภพiKงkฉyšฎS,•™š_เลSgy๑ิ™+Rh๛ญะ–N๑๓๙๑5Uๅฤนaย’ ำˆ ร@ี4ถ๔๖เv9™ž_ไ+฿๘6™\ }ฟ)g๏๖ญl๎้ข1€฿๋E–e,หDำuTMGQTฒ…<5E{S๓*>‡ฦD^w]HMอฮc˜BH ]BJ ฐฺัใพyA/EE็วF๙™‰ซ๒๛’D_"L๏ม#ห8บaRRuฆ Žอfxiz4฿Ptอ0๙…tFƒจ†ม#ร3 ^˜ปl5ชฎ&šaฒ#ใ-๘œ23ล ฿8z=-๑ 1พ้b0ูšŠั$บRc  ฆ›dk ฃ™"ฮ’ญชkโณข]นHผ๕fฝ„ิวs๏ ืในfึ๑ิ๙"auหM๘ผ^J• s‹K4D#kQ–ต"ขสeฆ็8r๊ /:‹nj.อฉ$Ÿ๛่I%จฉ*'ฯ  lูhAื๋ัK[๛zึบ้7ฟรBf๙ง~ฏตฉ‘};ทำืูNc<†ืใAvิ›]hš†บ’š—-Qีซ#ขV๑ธ4%โx=jŠยิ<†!„”@ ุ!ค๛ฉอ _๏J2เฅจh|ํฅQ~tn๒ชŽแบ–8ทu5ฑ9!๎๗เu9‘%0,‹ฒj0Sจpr>หกษE4ใง‹)ด๘…}ฏ๊ๆ6xaŽ๐e๎ๆwU6ฆษฎtœ๗nnร็”™*”๙ู๋nุPใœ.TะL“ญฉ(ฑQ_]LI€bิ๋„อ—ชดGƒDผ.Jชฮ_ŸใoN^ผๆ๎๛๕RŸx฿ปWฃ…ฎŽŸ"ัใฝw‚ๅฦฒL ำDvศ€ต51;ฯฑณ๕ฎyMDญ’N&๘ค)GQตต”=; )MืQTm}=๕๎‡Kพ๚อ๏2ปธ๔บŸ๏้hcฯถอ๔uดF๐x8$ฆeึE”ฆQฉ)ไ %4]ฟช"jทหEบ1QBJ lR@€}„ิž–8ฟzำVEใฯ ๓ะ๙้uหถTŒ[บRlKลHฝ๘\2Iยด,*šมBฉสษนGฆ—(*ฺ๋Šฉšn๐/nุL๋J7ทฟšโษฑ9ข^ทํฎ!รฒธฎ9ฮ}ญx2S๙2฿9=ฮŽTlCŽwบPA7M’zใแตˆ)Iช‹BY’p8$jšม<:zESCื‹๕RŸบพAง,_kธธผLตฆ๒ฮฝปH'“8–Uฏ๗ฃ้…R™‹S3?wžOม47๖ฃถ1ภ/>๘!าษชฆqb%BสŽิk;ฉl๋๏ญw?ฬ,๓฿ฟ๕=ฆ็_ฬ`sO{ทoกปญ•h8„วํฦแ0Lณ~ Eฅช(ไ๒E ำ\ตŠ้$Jฎ ฉ้๙… +7_‹R@p)BH ๖R๛’‹7๗{ศีT์…aŒฬฌ๋˜บBั“fGSŒtศOภ-#K†5Mgฉขpv1ฯ “K,Ujkbสด,rU•_ฟm-แz๑์žเ้ฑy๖Kgฒ,‹=- =ะ‚G–™ศ—๙ม™ ถ6F7๔ธง e4รค/a &ๆ]ฉ&IH+็iC=5T5-สІื%ใ‘e$ 4ำขXS].๒ยิFfm)ฆ„๚‡ษdsศฒƒŽ–fา‰KDTฎPโยไลR™ปท)”J|๋Gs๔ฬ9]แ`€‰Ÿฃต)…nิ#ค~{FH-็๒ไ E๎บ้ธ]ฎW›wบa ฏtฬ+”ห”+U6z*ฉ์pะาิธ!5ทธ„ข !%vE)@ ภ>B๊๎f>{}Qฏ›LEแŸ=รแฉฅ =fฏSๆŽž4{[โ๔%ยDVŠg[Vฝ๘j๑lE7๘_ว.๐๔ลyšร๖พณ3ลm]Mธd–‹<>:KOCศv๓x~r‘_~ว&V"๑Œฬ๕นูา%๎๗\"ฆฦseM.๒่ศ eี>bJฉŸ&3 Htทตะ”L๐๙p:e, TM%[(rabŠ#'ฯpfไป6๐ภ{฿E8คX.๓ํ?ย‘“glwํ~>ษŸฃ-„n+Bส~‚<_,‘อ็นn๋ใฑตˆ'ำดะ }ญXyฎXขZซญHฦ์pะ’ชG|ี…นฅ Šชฺb์BH มฅ!%ุGHฝซฟ…ฯ์้#โuฑTV๘โณง92ฑษFBโๆฮ7u4า๓ี‹g;V6J–e1]จr`dš‘ๅ†iฎ?ปpKWŠ[ปšp:Œf |ป‰C”*eพ๓๗8t”ํฎ}ฟืห็?๕อitรเิะ0~ป)หขPช ้D‚dผ€ฯ‡,;ึžฑีšBฅZcนGQดuํ˜๗Vp8ดฎ )Ue~)CMQl1v!คเR„์#คณฉO]ืCศใbฑ\ใๆ4Gg–mท๛Zั“f{SŒ„฿ณ๖ฏ๗๚Šุ˜)T8:›แ่ฬ2šaฎuๆ่ึฤอ)œรKž›˜ง=b?!๕โL†ฯํ๋'๊uณ\Q๘ร็ฮฒPช’x)*1Ÿ‡;’lJFH๘ฝx2‡„f˜u1•/spb‘'ฦๆXฎlอขR/“+qส2m้้ฦ$~Ÿง,ื›h*น•ˆจCวOq~l’๏oํ๋แ๗฿G$ขTฉ๐ท<ฮs/ทต๏๕ธ๙Ÿ|€ŽึfLำไไะ~฿ฦฎiง๋๙b ‡$‘N%H6ฤ๐z<ศ–eญ<_%4]g~)S?ื6Qซ8$ญ้zสžข*ฬ/-Sต’852z๛‡๎ฝ๓ @๐๒๓Q)@ €ฟ๕คตTู๘ิฟฅํ๊!ไqฒPช๑OŸโฤ\ึถ๋๑]=|dGฝx6–…iCชง๒•Uƒูb…“sYN.ฺBLู“ๆฆŽFd‡ฤะbรSKดฺ0๐๘2ŸOฤ๋"SQ๘โณgศืT"^๗ฺgVลิ;ฺ“lIFˆ๊bJ^Iๅ+)ใน2/N/๑ุ่,™ x !ู|E[บ‰ฆdŸื‹์ฑฌบˆสๆ _œเลSg_WDญฒนท›Oฑp˜rตย}‚งตต๏r9๙ยงคซญeร )]7ึ ทค’4D"xฏง\Q5Ue9—g๘โ/œ8อ่ฤิ?xผ๎N>๕ณ๏!‰PฎV๙ัใO๑ฤก#ถป๖eูมฏ|๚ctททb™'ฯเ๓z6ิu RญโrนhJฦ‰†Bxn ร4ฉ) ™,รcใไ‹%๎น๙ยม Šช257nถ}gผJHi*‹™,ๅjuCŽ3๐ใqป๑z~O{+Ÿ๙ะ๛‰GฃTk5z๊Yyๆ -ŸKฟ๒™ัืัŽeYœน€หๅ\ซsท†ฎ8Nใ1"ก .ง IชKชrญสยา2C.21;iึ#  ฅ2ก@€ฟgˆ†ริ…ูล%TMรฎผVHer9Šๅสบหแp๔๛๐yฆeแ$’v6ลhz\ธ ่&ูชยpฆภั™eฦs%.็บฮ็พMญ์iIเNฮgอ‰พข๎’]ธ˜+๑ั]kฉก๐ิi|ฎ7Wนจhx]27ถ7ึ#ฆ^!ฆ ำคฌ๊Lๅ+ผ4“แ๑ัYฆ W๚ว คJๅ ท‹–ฆT=5ฯใมแxนฦP&—g่ยEŸ8ลฤฬ[ฮึf>๛แ๛IฤขT…GŸ9ศ฿?๙ฌ-ŸK_๘๔ƒ๔wv`aqvt ‡$!หWฟธฆ้่†฿๋!ู#เZ)Fฎ้ๅJ…ูล%ฮ3>=ปึMo•ชขโr:๙ไ๗‹DPT•ูล%Uลฮดง›๊BJำXฮๅษK๋& eูAะ๊ˆ(ง,c&ชฆขจีšยb6ห—็_‹.{@๐„เ—pะบซ7อŽฆฉ Ÿห‰ำ!กฏlœg ีต๓dพผnใ'{๚xฯๆ6|N™ฉB™0x‚‹ู’mื๕บ*บyIญ"ร4้KDุัฃ#$์uแrิ7_ši’ฏฉŒfŠœœฯ2’)เ–ืงƒิ๛6ทฑป9Žœ˜ห2‘+t;mw^& e>ผญฟซ.คห3ง๑ฝลฎ\EEรใ”ู฿–`Wบฦ oญF˜aZTดzDโฑูežaๆ*ŠฉkUH้บNพT&๐ำœ&‹^"ขณYฮ_็เฑ“Lฯ/ผํ฿์hN๓ูOฒ!FMQy์นCh๐i[>—>ษ่๎`่ยELำฤๅบzQ˜หน<บnะ˜h )'เ๗แ”๋ฯMื)–หLฬฬ12>มไ์<๎Ÿ โ ำBื๕ตTJES™_ฬุข+Oธm+BJีt–syrลโURk"สํฦ๋๕เvน ำDำ4UฃRSศๅ hบŽํโฟล7„‚ื>ื…ธ็ฯฑบbA๎๊mfWบTจพqฎื0ชw}›.”96ปฬc#๋#ฆ้๕ฬ@+^งฬdพฬ๏<~|]ูๅ•]WSร’๔k)บAo<ฬึT”พx˜จฯฝ๖Yอ0)(น2ว็–^*ผกณหษ๛ทดณ+ภ๑ูeๆJUผฒฺซฯซ|`k๛Jjh…/>wฯœGQัp:$nhodG:F:ไฟDLอซผ8“แภศ,SWแบพึ„ิโ™\žŽๆ4]ฤcผžWคๆ) ‹หYฮ]ธศแใง.‹ˆZฅต)ล?่hŒวจฉ*ƒฯฟภ{า–ฯฅ‰ŸcSO'Ccั4็สG:ๆ‹%jŠJsc‚ฦDŸoฅะƒ๙RuEฮ0v#ฏ!5ทธฤR†žถVบ;IฤขxW"ข ำ ZSศdsœใะ๑“ฬ.,]๖y57&๙ฯ}Tขšช๒ไก๙ฃƒถ|.๒ว?ย–n$$†/ŽSU|^๏•๙1หขPฎ ้:้D|ญะผ,หX–‰ขึEิ๘๔ '†ฮ“ษ๘฿๘XTM็๗฿G"Eัด •๎ o\^QCJีtr…™\‡รqๅ~ิฒe™P0€ฯใมํvฝ*"Jี4jŠŠขจ”*etร|ร!%ฏ๓\BJ .RซดGีำฬฮ๔kปพYฏฺ8?:2ร๘UH๛g๏ุฤ=}อธe™‹นฟuเ๓%๛n.ุูล‡^™๖๔›ซUTT4ƒ>nhOาŸจ‹)ฏSฦแะ_Q@๛ุ\†ฃำหจ†yE  pkW„ิ‹ำr5{ ฉLEyUj่<‡๛2o๘ŠŠ†[vp}k=•ฏ)ไ#เvฎl๒^Ž˜:>ปฬใฃณWคVš…ิโ๓™ez่๋jฏ‹(ทI’0M“๊JDิ™‘ /ต„ใภป๏ผ•ถhบi’Hฅษๆ๋ทอ[Bขซฝฟื‡i™ค29โฉิІฮ›ฆ…K–hŒDฮQฒ,cYZ5ฌผฌiไ E,~M๏+„”@ ผฬบ.„”@ ผบZข+์็ๆvvF้๛(nฎJEว…|p^โSืnaฯ๊v—ฬh2หง"ฉR๎˜๔ั๋ธ๛ฬJœgฟก@๒œfTvwGhkXฮ[jท, …2Gb)ฬ$ศ”๕Sฒ$q฿ภ*ถด5`9ฐwj‘xAซหP๓œnœี๚ู็Oโv]๘J/ล%ณน5ยๆึFบ#~หb๊๔ุšK๒๘XŒกลฬผzRฑล8๑dšuซ{้_ีKSค*3ชส%ญLl1มเศ{#~+ขฮฅน!ยo}เ=tถถ`˜&ฯพxˆ๙ัฯ๊r]๚๘๏bฦuศฒฬ๘ิ ษL†p0๘†%Gฑ\Fqปioiฆ!ยฃจศีVีRYc1‘dxbŠมแQ4xC"j วqธ๏Ž[ioiฦ0L้ ™|n„!IญQ>?ฆe‘ฬdHคา+"ค*"Jฆฑ!Œฯใมซช(Šฒ\qจ:eM_Q๖๋|†BJ ^f]BJ ^ปZข5่ๅ๖u]์์jฎด๒QัQ6Lb๙Gbic(žYฑใฃ๋ธ~unYf$‘ๅ?=Yอจ๋~nH๛?>w๗ ด^ไ4ฅฺถญฝ‘ฎฐฟา&IX”M‹Dกฬ‰ล ฯOว‰สoXLนe‰๛๚ุิมฒž›Zd!_ฆมซึธ “ึuข, ฉ!—|ั>฿ใ’Yืak[‘ภYโทl˜,สO๓ศศ,'€˜ช![Œ“Lg่๏๋emoM aผชŠ$UDTฑ\b!‘โ่ษ๖>vQ*ขฮฅ!โw>x?]mญฆษCG๘๊~Z—๋าฏฟ็ถoZ,หœšž%žL ‡ฮ[rhš†ชชดE›ˆ„‚จŠŠ$Igศฤ$ใำณ่บผ‚ีˆ–es฿ทาูล0M’้,้\ŽzE’$:ขอ~,ห&™ฃRE IDATฮOฅpปฯ_›ฆ…%ำ ใ๓z๑( nลฝ\qXฎถๅiบ~^"j !คเeึu!คเ๕ ฉ%Zƒ^nํ๏ไ๒ฮfบ#‚๊้ŠŽrตขใp,ลใcs_xใb๊฿]?ภต}mธe‰“๑,๚๐ tณnฏ๛น!ํ+]‰“ำ \ฒฬๅMหํ–!Uมํ’q‡ฒi‘,jœŒgynj‘XฎtbJuษ;ะวฦ–0–ํ๐ฬdๅข~O‹fYgต†~๖๙!T๗ลo=T\2๋ฃaถต75vถํP2-๒%Ž-คytdŽม…ื/aj]Hลใคฒ9ึ๖vำฟช‡ฆHชR5ฌผาšwdh˜็ฝคข! ๐ปฟ๖^บ0-“}‡๑ๅ๏ธ.ืฅ๗vlˆห%393G,ž 1~]๏aY6ฆeโvนi‹6PJ๕฿, ฅ๓๑CใLอฮc;ึ9หฒน๗Ž[่lmมด,R™ ฉl )Y’hmn" `6‰ิ๙ )ำดpป]4E*Qช[AQ”ำญyบNIำชQฅ๓QK!%/E)@ เ…ิmA7ฎigwwtYLYัฑXิ8:Ÿ:๏็%รžm\ำ‚K–8ฑ˜ๅ?์eำช๋พา~ก+qlวA’$6D#์่hค+ ไQ–s‘4ำ&UึŽg94—b<•วฏผ>ใqปธo`๋ฃaLแ้‰fณEฺƒพบำq–[Cว’9>ทw๛าeaษ’ฤ๚h˜MtEUท,แTซ*S)ž‹q$๖ฺิjUHลใไ๒๚zบ่๏ํก!Ri๏’คŠˆ*–สฬ-ฦๅ้‡j"ค:เ๗๑{z/ฝ˜–ลฃƒ|๑;?ฌหu้ร๏บ››qป\Lฮอ1;ฟHSCไ5ฝึ0MLหย๏๕าาุ@0@ฉฮรด(‹ฬ.ฤ99>มไ์ ใ>๋x ‹{๏ผ™๎ถ6,"•ษ’ฬdฉW$ ตน‰p(ˆmฤSiโฉ4ส๋RgŠ(ฏg)#JA’dlB7Ltร —//•Xฉg%!คเeึu!คเ ฉ%Z^n\ำฮ•=-t…„0๚7๗ฌขฟ9Œi<9>ฯLถ@w8PWcb;Hpร๊๖ๅึะฯํยง\๚,,วqุิฺภึ๖Fz#‚žŠ˜ฒซb*^(sl>อSงๆya6๑๊็ZcBjID๕๗๕ฒบง‹†PZษฐ1ซU5s๓‹e๏กฃ5Nํ๓z๘ฝฎ,หๆลม|แ฿ฏหu้C๗ผ][7ใvน™Šล˜ž›งนฑแWพ&•ษb&mั&ฺZš ๘|ธ]•9c˜&นBษู9†OM2=ท€z‘ฒๅtเ;nกงฃหถHgs$า๊วqhmnข!ยqS)โษŠ๒๊•ญฆiก*nย!|^/ชข *n ’eZบก“ษ.ˆไBJ ^ŠR@ภส ฉ%ผ*ทฎ๋ไŠ๎(=‘j5ฮREวbตขใใ๓šKพๆ๗๔Mูำ‚ .ค๙w žื๑?ผv 7,…ด'ฒ|v๏I|สลฉฤัL‹5M!ถถ7ฒฎ9LƒO]–O†e“ำ &ำฦ’ŒฤsHฏาITผk`k›ย–อ/ฦcLฆ ฌn ึี˜่–ว-Ÿี๚O๛NโWj+œ}]4ฬถถFV5—ซ–ฤTขจq|1อใฃฑ_)ฆjEH-‰จuซWัืIc8ดœ3dZ&…b‰นล8‡ŽŸd‘Ar…ฺ%อฃ*|๒ร๏gUw'ถmsppˆ๓ญ๏ีๅบ๔มwลpปLฯฯ393Gดฉ๑e6›/ iญ-ด47.‹(วqะ ƒl>ฯไlŒcรฃฤ๘}ท…ทฌ๋{๛-ฌ๊์ภฒ-2น<๑K1ถRุถMKS#M‘HEH%S,ฆRจฟBH™ฆ…ช*ๅ๑ * Š] +wlLำDำuฒ๙"๙BW]์ฯ!คเฅ!%ฌผZ"์Qธ}};;›—œ—*:ดฅŠŽ…4Ožš็…™Wฏ่xเ–์์Š"GๆS๛‡ิ๕uใ๋ธฎฏา>ฯ๒น}C]|ไu“?ปบข๔7‡i๒ฉx.$IZSณู"‡็S -d0l๙eXB…{ทฌbuSรฒy|lŽSฉ<๋šรu5&eำ" บyหชV\ฒฤะbEHjqท@ึ4‡ธฌณ‰† a‚rF>Xผ 1ด˜แััน—S—ZHลใหeึ๔tณบป‹†pจฺ:Tyˆฮ—Šฤโ<~’ฝ‡ŽR,—k๖พqป]มG>ภ๊ž.lๆ๐‰a้฿ฉหu้oฟ“+ทoEqป™™_เิ๔,-อg)ว![(b˜&ัfZฃM๘<^\.Žcฃ้้\މ™9Ÿ8I2]‘๓ฮ‡bYใพofUw'Žm“ษXLฅจW,ห"ฺุHดฑ‡…dŠx"…ชพTH-UD5V3ขwฅ"ชา๚zZDer ฅา?v!คเฅ!%\8!ตDƒWๅฆต\ั}ลŠŽ‹ๅลูWฎ˜๚‹[/ใฒฮf$เP,ลY} ฉรึsฤวUน$ว’ำ Zƒ>ฎ๊‰ฒ>กู๏ม๋v!หฆeS0Lf2Eฦผ8“Dท์ณะ#^•wm้ฅฏ1„aY<2:วx2ฯฆ–H]I^7i๒ซgต†~~0มZRKฉถอฺๆ0;;›้m๒ชจฒ„CU5N,dx๒ิ<ฯO-.ฟ๎R ฉ‰™Y๚บ:YำE$ชถQiอ+–˜™_ไศะ0ฯฎ่iม%มฑ๙4g๐%—ื‚nูlˆ†ูัD_cฅbส]อำญฅVพ ฯL,๐๔ฤย%R๐ตo๏ู=ฐ้๑U]„ƒมๅ หฒศ‹Lฮล8zr”็Fำบบw>๕ัะฟชวq862ส?~๕[uน.ฝ๛ฎ[นv็TEav!ฮ่ฤ$ถใเ๗y้jkฅ1ฦฃชห"Jำtโฉ4#SฅT*_rตDพXโ]ทฤฺว!W(‹'๊VHฆIs$Bkด ‰…d’…xGล4-ผ•H(ˆ฿๋EQ”j |ED™ี๓2ู<…b Iพธื@)@ x)BH OH-แW฿ม5ฝญฌjจ๎๘ๆ’qœำฮƒ ž:งขใฟพ“mํ8ภ 3 >ศ‹u}ฯอฤ๚ง}รDผต!>ršAPUุีฬ@[#ํ!3ฤTษจd‰ฅ80“ มง๒ฮอฝฌjขYœa$‘ใฒช@ฌ%H€•ึะฃ๓iพp >„ิšiฑ>ak{#อกŠ๘ญfธ้–Eฒค3ฯ๒ฤุrูE}@\H$๗DBมว+KืEfๆx๑ุ ฌ;ตฤ'?๒~ึ๗ญยมแ๘ศ8•oิๅy{ว-\ท๋2<ชB*“er.F{4J$ฤฃจศฒŒe[”ส‹‰$'OM28<†n5#ข–ศKผ๓ึYทช‡\กาzฑeฬJก๋ a:ขQ$Ib!‘d!‘$เ๗ำโ๓xฮQT[๓ช"*—งP,"ษ๒%9v!คเฅ!%\|!ต„OqsKW๖ดฐ๚WTt<=ฑภณ ๕ปhkภLวyเัƒu}ธeปบขHT2ฑพฐ„ฐทถฤGN3pหWtทฐญฃ‘ฎฐŸ€๊ฦ%หXถSmน,3•.ฐ.&๊๗R6-~24อษx–+บฃu5&‹ล2kC\Ui =K๑ๅGk3C๊UะL‹พฦ ปปฃ๔5i๐V‚๋+ญ|6ษ’vcWุQวู<ฎ&๙B™๙EdัALำช๋๙{zึฌ`h์๓KR—็qฯm7qำีปQ –eaูึ้–/หขX.ฑH121ษ๘๔ บn"ืจเ)–สผํฆู๋ฐบ‡|กฤ์ย๒%’2oxN๋: แ]ญญHฒD6Wภq์สŽyชŠRอณ,ร4)k™|ฅ"๊RŸณR@๐R„.Zย๋vqsWvทฐถZักV3ฆ–*:FYV5้Ž๘ฑุ;ตศ_ฟo˜'วcิ๋Jืw์dk[E|˜I๐อ#ใuU‰cฺ๖้ฌขฆ>u9,ุดmE๑dž‘Dh๚jœ&3.๋lZ—3qพst๕ฆ™๏Eรค'dggำ7ฌiฟจˆณฟ#;[–฿็ฅ!ฤใ๑ ธ]ธ].$I^ฎŽ*keๆใI๒ลbอK6!คเฅ!%ิฎZย-Kนก›\Oะฃ,WIูถƒnูdส:ฃษฟ<5ฯ/ฦ็1ํฺorห2ํŽlj`ูฯM-๒รใSu[‰“ื ไ๚ญด‡|ุŽCAทP]žjศฎaูไ5ƒ™l‘แD–‚fา๐ ิุ_๓ว’9ฎ^ีสๆึ–ฯO.๒“กi\uบ+ื+1—+2™ส฿๘ˆR+wn๏น‡ํ›ึ#ห2งฆg๙ฬƒ_ล0ฬš9พฮึvmฬฦตซimnฤ็๑โrนpM7H็rLLฯbZ—mˆฯ๋%‘N๓…>Z—cbšoูนƒ›7, ฉุ้|MgHY–…ฯ๋ฅ1ฦซชธ๎ๅ]๓tร\1‰D:อ|~๋e๔7‡pะ, Y’–ย™ัชbj,™็๑ฑ9~yjพฆ+ฆผn๕๖lh cูOO,๐ำ“3u[‰S4L๘๚ฺƒ>๒บษำ ธe‰ ัอ~^ท Y–0-›‚a2›-rh.ลX*GGะGO$P็1’ศr๊v6VวๅูษE~6<ณ\™๗f!–+1‘ส !ต‚|์พwฐc๓F\.™‰™9๎‹_ฃฌ้—ธVuupลถ๚๛zˆ66,W?ูŽฎ$3Yฦ&ง8p๔8'ฦNqำีปy๋ืแ๗๚Hฆำ<๘ึeปTชŒฎฺฑ•หถlยํr‘/kVHูถ฿๋%แ๕จ(n7n— วก’ฅ่†AฉT&ฺิˆโv“ฮๆˆงRหู~ตŠใ8!คเ%!%ิ‡Šผ๙-;่k aXว2” “ตอ!ฝr๏นผ*>&f็๘Ÿ_๚:ลR๙’ฯ๚ีซุ9ฐ‰U=4FยxTuYDišN<•fxb’‡ŸžY~ž+w๑ถ›ฎ'เ๓‘สd๘าw„ปN+7maืึอ์ฺบทห]ูe/6TCBสถm>แPฏวƒโvใ’]€ƒaZฆฆ้คณ9rล"^J_W'ชขษ็Iค2ฆYำใเุใ‹BH มน!%ิ‡j ๚๘ฯท์`UCอฒxxx–ื๖ตr]_๋šร4๚TT— Iชdฅห:ฃ‰{งใ<::‡VCหGผ*y๋eฌmcX6OŽวxbอ‰ล อ~ฯ%S'YnZำqZŽฯ๓ฤx์M7็ eฦBHญ$บ็mหโc*ใ}้ไ …‹~›๛ืฐ{Vww ๑(*ฒ,cูฅฒฦB"ษะุ)๖df~แ%ฏฟnืeผใึ=|~Rู,_๋V”;์ุผ+ทT„TฉศฬR œm>B!TUฉŠจŠ([QฅฒF&—งฌiธ\.4]ว๏๕าูŽช(ไ EๆI,หช้qฐm›ฟ๙โื…‚sBJ จ!ี๖๓้›wะ  Y?šแณ{‡*‹9pm_W๗ถฐฉตฆช˜‚สno™ฒมฉtžg'xtdŽr ˆฉfฟ‡?ฟๅ2V7…0,›วF็xfrกn๏!หq๘ิ[6ำ๐’ี F9 ห:+หด$ฉr๎AUมงธ๐).\ฒŒe;”M‹Dฑฬะb–ฃ๓)ผ๊ESC๑ ทฎ๋dmSEb<ฦSงๆ฿ts>^(3*„ิŠ๒มwลU๑1=?ฯ?|๙›คsน‹๖๙;6m`ืึอ๔vถ …PI’0-“bฉฬB"ษเศ๛ ฒHพโ๛\s๙v๎นํ&‚~?้l–ฏ|'ิsว๊ึ ๋ธjว6Tล]mู[X ๔Rเุ6~Ÿ†pZiอsนd ำฤ0 สšF*›ฃฌ้gUง้บ฿็ฅปฝ ช/•˜-โิ๘^ณ–e๓™/ !%็"„”@ PBชท!ภŸธ๎H€ฒi๑ใกi>ฟ๏ไK~๎สž๖ฌiฏfฉ•Pm@ทฒe‰t_žš็๑ฑฅK8๔๒ภอ—ฑบ1ˆnY|dŽ็ง๋๚>๚ไ5›‰tr†3‰บฝ‡dIโ“ืlขษ๏!]ึ™Hะ-ล๕ซ โQ จ.ผŠฟ…ฒToVฺ.‡ใYF’Y\’DgศAฯใ๘b†ป6tำืฤฐ,™ใน:…/Gฒจ1,„ิŠr[o็šหทก* ณ ‹ใWฟE<•พ Ÿๅ๕xธjวVถฌ_KW[+Aฟy'6รดศ‹ฬฮ/r๔ไฯ:Bฉฌฝๆ๗ตu3๏น๋6Bู|žฏ๐!Lหฌqูฐบ๋v_^ฉ(*–^ถM๑‚โ8~ยม^Oeื<—\‘๏บab˜fต"*‹ฆฟ2ฏห4MBมญMMx=*%Mgj6V๓Rฆi๒ท_†R@pBH ๕!ค๚›C‡=่ ๙)&฿œไห/Žพ๊๋vwGูณบญš|ี$0l‡ฌf0žฬฑ&ฮ##sUL๕DูM้ฉV|dhšƒsษบฝ‡—ฬ๏^ฝ‰&ŸJบค3•) Y6๎ื˜ีโ8๖T๒ฅŠฟ๊FYสSฑ+;(žJๅNdqh๚.ศy .ฆน{cฯYYe๛ฆใoบ9Ÿ*้œ\L !ต‚ผ๛ฮ[นvืTEan1ฮฺทek๙ูฝm36ฌฃญฅ™€ฯ‡ๅฎฬำ$—ฯ39ใศะ๛;ฏฐ๋ห6oไฝw฿N8$[ศ๓=Œn่u;.ซzนแสxU•Bฑฤ๔ERŽใ๒๛ ‡‚xTทห\QKญy%M#อฝbEิน˜ฆI$ขน1‚ฯใAำ &f็จ๕็0๘Ÿ_๙ฆR@pBH ๕!คึGร๛ถั๒Q4L๕่_;4๖š_ฟป;สต}mln๕{๑œ!ฆršมd:ฯฉ8ฬ’ฟbชฏ1ศŸธ๎p€’i๑รใSOีํ=ไuป๘ํซ6า่SI•tฆ3• ฉื†lู!BƒWลงธ๑*๒i1eูไ4ƒฉL‘กxวจ฿ณข็qd>ล=[V•U๖ยlโM7็ำeก!คV’wvื_ฑช‹'๘ื•นล•‘™ญอM์ฺบ™อkhmnย๏๓โvนซU6™\ŽษูŽ๒โเะ๚ฌํื๓ท฿I8$W(๐ญŸœRน\ทใฒบป‹›ฎพฏง"คfๆ/|ๆRภ็#ฒ$ขฮฉˆา ƒRนL:›C7ฬืตƒกeY„‚Z๐zฝ่†ฮฤฬv?ฯhบฮ๚๊ท„‚sBJ จ!ตฉ5ยŸ\ฟ•๖ผn๒อรใ|๓ศฉื>;:šุณฆ-ญ Dณ*ฆršมtฆภณ“‹<6:Gบ|แช^ฎโ๋ฤbฆn๏ก€๊ๆWl มง’.WŸi;œ๏fVฆๅเW]D^|Š …๊’‘$ รฒษkณน"ร๑y ู๏YWo„}3q>ฐ}อrVูNLq8–zำอ๙lูเ๘BJฉไท์aฯUป๐ช*๓‰$๔/฿yรํam-์บ… k๚hmnฤ็๑โrนฐmอะษไ๒œšže฿แc]‘๓ุบกŸผใ."ม๙bo?๔(…bฑnวฅงฃ[ฏฝ ŸวKกTษบฟ๐$$~แ`UQP7ฒ์ยq์ำ"JำHฅณๆ๋QKX–Mะ๏ฃฅz/ิ‹*—5๋฿BJ ฮ!„”@ ิ‡hkเฏ฿J[ะKN3๙๚ก1พslโบฺนym›[hYSฒ„น\…Sเ๙ฉ8OŒอ/j+~>ขไ†ญt†}t“o=ลH"Wท๗Pุฃ๐๑+ึำเUษ”*]ถใผแ์`ำr๐ธeZ^ชีUSฎ๊X “ูl‘รฑcษํA฿ @ๅฤซŸBj%น๛ฆ๋—+q)>อ๏25;ฏ๗๊๋๎d๗ึ-๔๗๕ะาุˆวฃ"K2ถcฃ้:ฉLŽัษ)=ฮะุฉ=-๋ึ๒มwEC8LพXไป?F6Ÿฏq้lkๅŽ๋ฎม็๕R(Uvู[ั‡ $‚Uล๏๕เQ=ธ\2ถ] +ื “RนL2“ล4ญ๓QKุี๚–ไรฮ@‘ IDATคฆ๋Lฮลฐํฺ/–Jใฟ|G)@ 8๗wˆR@PBj{G๖ฺ-ดฝไ4ƒฏใ๛ƒ“o๘}ฺธym'[ฺNWLน$0m‡œn0•.ฐ&มฃฃsฤ +ืถฒนตwภrลื7s*Uฟ}>•ณk=ฏBบคs*ฝฒ็bZช[ฆู๏! ธ๑ธ]x2ฒ$a;Eรb.Wไh,อ๑ล ก๓SŽฮ๒;WozำTฎฝyเXLฉ•ไฎ=ืrห[ฎฤ็๑ฐ˜L๓ฯ฿๚งff_ื{lXำวฮMฌํํก1ยฃž!ข4ลTŠ‘SS์;rŒSำณไ<6๕ฏแื๙Vรa ฅ"฿๙คฒูบ—ถๆf๎บ๑Z^ลR‰ฉุส์J'ก`ชโ๕จxT—\5ฯ0ะชญyฉl๎ ‹จ%วม๋Qi‹6/ ฉฉนV ฉ|ฑศgฟ๑]!คเ฿%BH A}ฉห;›๙ิต›i xษj_|a„Ÿ˜^ฑ๗hkเ†ีํlmoค5เลซธ‘ซb*ฏLgŠผ0“เกแ’+P1ตตฝ‘?บn`นโ๋k‡F™ฮิo[LKภหGv๖๖(คซไำrpษั€ฟโฎไL!ฆJ†ลbกฬฑ…4'24๛=ฏKLth†?ผnหYYeร‰์›nฮt“ฃฑคR+ศํื]อํีJœx*ลพ}ฦฆf^ำkทฌ[ห๎m[่๋๎$ โUU$Iฦฒ-Jๅ2 ‰Ccงุwไณ๓vืวk๚๘ะ=oฃ1กP*๑ฃวž$žช฿ถีๆ†๎พ๙>ลr‰ฉน7&คdI"๐ใ๕x๐จj5ฐUฉˆชŠจbนL*“ลฒ์Qห8ชขะฺR 5ื˜ŠอcYต-คr๙Ÿ๛ึ๗„‚sBJ จ!ตป;ส'ฏูL4เ!S6๘็รlxfล?gcK„›ึvฐตฝ‘ถช˜rU3ฆ zฅ=l฿tœGFgYศŸลิŽŽ&ํu[h T*พพ๔ย(ฑ|ฉn๏ก๖]ถ–G![6Kๆ*%ำreh๖yจ >ล…Oqแ’+ญ2%ำ"YิZฬpt>Eฤซพช˜า-›‡Gf^’U6^ว•kฏDั092'„ิJrห[ฎไฮ‚฿๋#‘N๓เฟ€‘‰ฉ_๙šห6od็ภ&z;;ˆ„‚จŠ‚$I˜–IฑTf!‘dpdŒGW|วพWb๊U|๘]wำ‰P,—๘๑ใO]ดฯพDB!y๋~Jๅ2“็ูF้’e~^งRฅจธ.,หฎŠ(bน’eู+,ขฮภํrัีึ‚ืใฅฌkฬฤ0-ซฆว “ห๑๙o@)@ 8!ค€๚RW๕ถ๐๛Woขษ๏!]ึ๙“<6:wม>oC4ยอ ด5าฌˆ)ท\ ิ^ส-:Pmๅ›อพสฆ]]อม[ถ, ถ/&qฒช.]a?ุฑ–ว]Rฉ‹—‡%KŠ_uแuป๐)nWeW+อดHuFYาxฎWSษ’ฦ๓S‹ั๕ด}ไ4“9<ฦdบ๐ฆ›๓%รโ๐\Bฉไฦซv๓ึฏ#เ๓‘ฬd๘โw~ศษ๑—ๆ)Š›[ท0ฐพŸUํ„‚ท€i™J%ๆa๏แฃd๓๗[ปช‡๛vš(•หไฟ$ถBป^ ~?๗q3A€RนฬTlวถyญw.Y&่๗แ๑xชUQ .น,ฏ:บaP,iค2,น`"j๙x\2mmx=สบฦ์"†iึ๔ค2Y๙;?BJ ฮA)@  >„ิต}m๖Ui๒ฉคK:๐ žŸฟเŸ฿ๆ–ถต7ั๔โWธd หถ)่&3U1๕๘ุ๋jนปชท…฿ปzอUม๖O{O’ีŒบฝ‡zผo๛‚ช›LYg,•ฟRฏHุฃโฏVK๙ซb @3mาeแx–ัdY‚ฮฌืNe _ฬœ•U๖ีƒcฬd‹oบ9ฏ™g…ZIฎ฿}9oฟๅ>?ฉL†/}๏วœ_wŸืร•ท2ฐพŸฮถ‚~?JU^ฆEพXdf~c'Gy๎เสฺฅิkzบ๘่}๏ ฺุ@Iำ๘ูSฯ2›ฏq๑z<ผ็ฎ”ด23ฑ ำD~•]9]ฒL0เฏfDUDิRž—ahzฅ5/ษ]ะŠจ—;ฎฎ๖V|ี ฉน๙E๔R‰t†ฟ๛#!คเ„๊CHฐบO\นFŸJฒค๓๗ฯ็้‰…‹๖๙kšBผถƒMด…|๘ืrnั’˜:8—ไแแูืT1u]_ฟu†`๛ว็‡(fCkšBผgk~ลMVซTHI—่XBžJ_@qใW(ี‡OรถษT3ฎFYlฺƒ>Ž/f˜ษ๘รkทœ•U๖FZ3kฒyq&.„ิ ๒–;–[ราู,_๙O86-ขสšNYืษๅ X–ลJ๔ur–า4ๆใ่Fm1c1™ไK฿ฉR@pBH ๕!คn^มวwฏงมง’,jณวynr๑ขG_c[๚;ูHGุฟ,ฆ,กh˜ฤr%^˜M๒๓‘Yฆ3ฏjณgM;ฟyลiม๖ฟž=ŽQใมดฟŠuอa๎X…_q“ั N]เ ฉื‚e;= ^Ÿโฦใ–QซS†e“ำMฆ3N,fศ”uEผfœีJ๙ฯ†W$ฤพึ0,›„ZQฎฺฑ•{ซญa™\Ž?ก`€อkhmnZQŽใ ™\Žษู๛Žใะ๑“5s=ํ|{hijคฌ้<๚์^NMฯิํธศ’ฬ๛฿q'‘`ˆ’Vfn!Nฉ\ฦํv/ŒiZธd™†Hฟื‹ช*จสูญyeญ๒_พXผค™Mฒ$ัำัถœ![Œฃ้ต-คๆใ พ๒ร‡„‚sBJ จ!uบNอฎ๕Dผ ‰ขฦgžd฿๔ฅห5้k rำšvt6ั๖Ÿำสg1Ÿ/qp.ษฃ#ณ/Š}K'ฟพ{ Š`๛gŽcื๑๏ค-๎ู‹Oq“ีtฦ’๙‹]8๐Š˜–ƒ_u๕{๑).ฒณŸฐGaฑPๆožไ…™ฤ%?ฎU AnZมๅMด‡|T7.Y>ซb๊ะ\’วF็Mž๚พ}}นŽˆW!^ะ๘g๋๚ฺาฺภ7๗โsปศhใษ\อฉ%Lหมใ–i x๑ซnTWEL™ –ี ข~^ท‹ิ› •๒•ฐ‡S‹BHญ w\ wํนUQ‡สwL ฑัtT&หศฤŽูฐ๓Zกณต…฿x๏ฝดE›ะtƒ_์=ภ๐ฉ‰บำฒ๙ะ;฿Jc$BกT$ถ P*ก(nB!|^>ฏUQ‘$ฉ* 4]งฌiไ ElปvชWeIขปฝ ŸืKYืYˆ'(iต]ล9=7ฯ7zD)@ 8!คธ้Ÿ~ภe๓’LคV๑๎M=|่ฒต„ชB๊ฟ?uŒƒsตณygศฯ-๋:ุูฅ3์[ฎ˜ฒ(&sน‡cIฉˆฉทn์ๆร—Ÿlใ—วpฟJศn-ณตฝ‘ป7๖เuปศh:ใ—(ิต= :จn™fฟ‡€โฦใvแqหศ’„ใ€,’Dูฐ๘๚ก1ฮ% y”7ีœท‡}BHญื๔ฑs`[ึญฅฉ!‚T5ฑถmS,—‰งา Oฒ๏ศ1&fj?‹ฉ-ฺฬ'wํ-อhบมS๛_dhlผnวG7L>ฎปiŠD(–KLฬฤdhŽD–CหeI^ฎ`+k:Z5#ชซVฯRšฎฑORฌa!U*k<ฝะ??ง>๐@Z|ใ‚ำ!%U๖<๘Dƒา?#K|ธ๏ž-ฝผ๛ZB7๓๙2฿SG9Kีร\ะวํ๋;ูััLwฤO`YL9” ‹๙|™รฑ$–ํpบ.B7 ี๓๑\ค]š.;:šธkC๗i!UC-{ฏ„i9ธd‰hภƒฟ*ฆ–ฦ ภq e๖Mวู? YาTb๊๙ษ!ค[ึญๅŠm[XีIC(Tg;‹ษ๛c‘Af๋ๆผZ››๘อ๗Ggk0๘ๅƒซq*i:ฝ๗ํD0-ซRๅvใU=gดๆ้”สšฎ“/–jช"๊\ฮnูำYH&)–joใ…RYc๘ิฤใ?๕์ว๗_?pJ|ห‚—"„”@ œรž๊s[๒ต&ฆฝต๛ทญ&จบ‰ๅK?ฟ8สเBํฑต#ไใฆต์์jฆ; xF+_ษ0ั-›GA‘%ๆ๒%฿'โWu{฿์์jๆŽ๕]x\.2ๅj…”TวnZH๖(Dผ*ํA๏้*—ชHL5†โž›\dฑP~Sˆ)!คฮใ‹ฃ—mศฮอ๔tด QI’ฐl Yชd“้†ฮOž๘%=๙Lcscฟ๕wำีึ‚n˜<๛โ!Žžฉห๑*หtถต๐๎;oล๋๑Tฏƒ$I˜ฆEYื—eTกXฌ‹?‰Jจนฯ๋E3t) ฅRอŸQ@๐:ึt!คเๅฉ51๕ํซy๗@ีอ\ฎฤ_โC‹™šฟŽํ!7ฎigww ]a?Aีโ’+๙2U้QิM>ปwˆSฉ|ŠŽQn_ื‰๊ชŸ ฉs1ญŠ,ผขงฏโยqlวม%WฦK3-Eัd–}ำqฆา‚u,ฆ„zํ(Š›+ถ 0ฐ~-ฝํ„‚A”๊.mฆeR(•ศๅ‹D๐z<JE~๐ศ/xr฿ uw_4Fยฮ฿Cgk+†i๒มรฎซs(ห๘>ถm่gsZšฯjฅ,”JhบAYำศJ8ิฯ๓ภi!ๅAำ โฉ4๙b๑’Wฉฌ18:ถ๗—ฯ์ฝ_ˆ(@ xkบR@๐ซนํมŸ๏ภv>p)ใƒ;ึ๒ฎ-•]ๆrEหGId๋ๆ:Fn๎๏ไสž(a?zVkุlฎฤ™ฯO-’ำŒบSW๕ดpKŠหEถFCอ_ ฆํฐ>&์Q(๊&ฑ|‰ฏZฉfsU2พtห&UาId9K1žศแQ๊ฏRฉWว็๕pีŽm ฌ_Kgk ฟฅฺZk˜๙B้๙Ž%™ษ๐ทAS$BกTโว?ลใฯํซป๛" ๒;ฟv?mm˜–ษCG9x|จ.ŽฝP, Xฟ–U=4FยxชปๆIReญMฆ3,&ST3็๋๏แ่้hฏ)ร ‘สห็นT nฉฌqth๘่“/๚=Z.ฏsMBJ ^ท|ก=ฒ,?ภ%Sนผyท™l‘๒ฤaฦฮุตฎn๖ผ wฎ๏ๆญปi TZรภฒสฆลbกฬั๙๛งค๊(ณ่-ซZนqMŠK&[g-{gโ8ะ฿"ไQXศ—y|,FkะKณ฿CGศGฤซขTร็ &[68•ฮs$–โd๑4<๓|อP ภ๏~่~zฺ1-“Gyแุ๑š>ๆBฑL8`†uฌํํ!VE”„eูศฒ„$ษ˜–ษ‹ƒ'P ^Zทฟ{ซBJ7L้4™\~น์b!D”@ ผq„‚ืษฅSตnyท้Lฟ|0้|^ว฿ฝzwฌ๏Bqษุvฅ5ฬ-KX”M‹DฑฬเB%ณจฤิu}mฐบฝ"ค๊ดe*bp}4BศSษ*{dx–ๆ0ฒ,aชหEgฤGฃืƒวฝ$ฆršมtฆภมน$'38Tย‡k!ค^J[ด‰][ทฐน ญอ๘ฝๅ8บก“ษๅ™œฑ๗๐QŸ8ปmMOฝ๏D)•ห์ฉg๘ูSฯึ๘|‡฿KoGฆeqเ่ Žึไฑ.‰จ›6ฐบป‹†pช,หงBฉD2•กฉ1B8@7 NŽO`;ตŽ…Tg;>OEH%3า™’|qึ›RYใภเ‰…็ฝ_ˆ(@ xc!%็ษm๓ุŽ๓€,ฑ๊b|o\ฑž;ืWvq›ฬ๘‹ว2)ึํ๕๛ฤ•–Cภำe๙|‰ถ oน5ฬฎVLล‹eN,fxfขถลิ ซน~unนพ…”ใภบhธžŸ+๑ศ่,ข‘ๅv=ำr(š&>ท›ฎฐ&ฟฏU Iถlrบมlถฤ ณq^œญ์ฆXซc&„ิiบฺZูฝm Vฏขฅน ŸืƒKฎฬCอะษd๓ŒOฯฐ๏๐1_aวนพฎN>๖๎wาาิ@Iำxไ้็๙ษฟฌป9เ๓z๘ฝฎ,หๆลม์;|ดฆŽฑP,ำ 1ฐพŸ5ฝ4†CจŠŠ$iVvา‹-ฦ9x$#S|๘]w/‡ดNNašชZฟ๙oง…”A*›#™ฮ _เ๊Lร0yศฑ…}ษ็๊ฯ฿‚เ#„”@ ผA.–˜๚ญ+7r๛๚N<.้<๙ัƒฬๅJu{~๛ชห!เงาy๛SGูาึศ5ฝญ๔4yTนาฮW6-โใ i๖Nวkr—ทืดsm_nYชfHีง’‘XยฏTย๓›cSK๙œ๊ำrศ๋ช[ฆ' ู๏ล๋vแ’ภr บษLถศแน{ง1kPL !kzปู5ฐ™พข x<*ฒ$c;6šฎ“Lg™˜ไ…c'89>๑+฿ซงฃ฿-M”5GŸห{ฒ๎ๆ€GU๘ฟŸพ๎Nlๆะ๑“wัึDYืyโน|‘'๊oธ]มG>ภ๊ž.lๆ๐‰až}๑ะ%=ฆ%5ฐพŸพ๎."กภฒˆฒ,‹|ฉศิ<รใ“Lฬฮ.๏€P,kึ–3ฑ&g็(k:๎3~ฆฎ^่๎hล็๑bZ&้lŽลd—kๅ…”ฆ้ฦไ\์มท฿|รoˆo9@pึt!ค`ๅุ๓เ ฒญหๆ$™ศJพ๗'ฏูฬMk+กูใษŸ~ไEEญnฏีงฎยžjๆาh2หONฬ0ะvZ|ฬๅKธ$่oณถ)Dศซขศาฒ˜Ju†โ๖N/2—ฝ๔b๊ถu\ีSR9อ`ฌN+คT—L_ch9<™ษึGรฏ๚:ำrˆหธd‰พ† ํ!?ี…K’ฐ‡ฒaฑXฌTนํ›Ž“(^๚๖ห…ิ–ukนb๛ซบ:hฮฒ,‹’Vf!‘โฤ่8๛ 2ปฐ๘บปฃ%สoผ๏^ฺฃอhบม“{๐‡ซป9 ห2Ÿ๚่Xำc;99ฬำ^’c)ห47Fุฑiซบ:+Q’$aš&๙b‘™๙Gฦ˜Ž-,๏€x&ลR™w฿uซ:+™X3ฑy ฅR] ฉฎ๖^฿B*ตบฟhšnœšž๛ฮ๗žyโ>๐@Z|ป‚ ดฆ !%+ฯ…Sxํ–ๅะ์ัd–?{๘Eาeฝnฏั_7ภuีฬฅแx–Ÿ ฯฐๅe*qb๙Ž[#ฌn ๖ชจ.วqะL‹dI็d<ห3 ฬ็/˜บc}W๔ดเ’ [6๊v—=หE_cp9<L‚ีMมื๚%1e;kšBt†7๎3rมE“๑ ฯMลYธ„c๖‹’$‰หทlไ๒-›่ํl?Cj€iU๒†)Ž ฒ๏ศ ๑d๊ผ>ง-ฺฤoพ๏>:Zข่†มS๛^ไ=R—๋ำง>๖A๚{{p‡ฃรฃr ๕๓ ล2ญอMlะฯชฎ"ก Šป2f†i’/™]Xไ่ษfb‹จ๊+หฅ|๑g๏พใใ:๋DN™>าŒzณ%Y๎*.)NO์!$ค„ะbฒ–ฅ„ฝห.q~„ป๗ {—ป\n–5,BIHœุ๊Ž‹mYฝKฃ้ํฬœrI‰Yž‘ž๗?๐bญ™93ฃัœฯ>ฯ๗คธ๏ึ[h˜ฺ‚8<>A<‘œี€3ง๏g$j*ห๑บ่†A(%š %B” ยฆ‹ %‚pแฬf˜zsภyไ้}ฤดlม>7sCื4Tขศ'&ข<}j๘๗ฉiรฑ†eาZUBฃ฿‹ฯiห…)@ำM‚)“(ป'Š$็‚ผ Q]Q†ืใฦ4M&Cกะyญ๘Jฅ5:{๛ž๛๕žw‹%‚0‡Ÿ้"H ‚ \xท?ูจrป,๑เนฦงnhใฺฉ€s2ๅoณTV/ุ็ไ๏6ญ™ูโv|"ยoปFhฎ๐ัŸŽฅฐ,‹U>–”z๑9ํุฆNฌ2†I(ฅq|"ย‘ }แฤœEŽทฏZฬฅueH@8ก?œ(ศR^ปE>๗ฬ๐cใjŠ]็u›๑4‰ฌAฃ฿Cๆ‚MฟfแT†ฮษ(‡GCtc8ินYน1_ƒ”้ไŠumดฎXFme9ท{f+WV7ˆ' ŽŽsธณ‹‡Hkณณาฒิ๏ใ#๏น—ฺส๐์Wไป?ฒ ?Ÿ>เฌ\าˆ…ล๑ฎ^v์ฺsA๏/ฅiิTTะผฌijt<”ฆVD%่ฅณท‘1gq…ผh<มฝท2ณq<$n’$*หJ)๖zฮ;HM‡จ็^๙ะWm๏฿VAๆ๘3])A„นs>a๊ำืpU๋็oŸ‹f˜๛\|ๆฦต3[ŽŒ…yพgŒีgพˆl8–ฤด`YY+ส‹๑อlๅ› SiS๛†๔…ธm6Lฑz1๋ksA*˜ส0Mไ๋R์ฐQW์žž2กบศ5+ท=Kก้&ีล.šJN‰Yำ$šฮาŽsp4Dg ‚rO˜็[*).ๆฒ5อดญXFUy— UUฐ,ศ๊Yข๑รฃt?ษCGั cV๏฿_TฤGw‹ชชะ ]‡๙๖OUฟ{ปXูิภ‰๎>ž{u๗นŸTZฃบขœถ•หY\]I‘ืsฺึผh<มภศ(GO๕04:†ห้8๋๛ˆฤณ๕&–7ิca† EฃH…XฬษฉŠ?พโ",ำ" 3 6ศLžwขA๒เ3])A„นทq๛“6SzLBzว™ฬ#›ึrE}.เ๐7Oพ†n๎gx๛M๋ธlQ9ph,ฤห}ใฌ*?๛]ƒั†™[1ตฌฌ˜—ดญ|แt†๎`Œ]๔Gx.P˜บณนžต5ฅL&5Fbษ‚|]N;5E๎™แ๙]ม•^็ฌ‡ฆ›คu"ปZŸ‹ง‡*#ำ"ฆeŠ$80โD ‚eYศเไyพฉช๒2.okf๕ฒ&*หJp;](Š‚eYdฒ"ฑ8}C#์9x„ƒ':/ุฑy<|๔๗ฯ\อํตCG๙ๆO~Qฟพ๛Yฝt '{๛๘ํ+ปf๕๖Sišส ฺV.cQuE๖ฉฯฆLV'30:NวฑŒOฯ)DM‹ล“ผใ–3+พ‚แมHด †–—๘)).ฦฒ,&Ba&‚มฉซ๑็๐‰ฮร/ํ=๔พฏ}แำA.*คA.ข›rฃ,หํภ ์฿~v*เศภ‘๑0๋ื ๚ุแ–๕3+Š:FC์œ`Yi๑9฿`4A:kฐฆฆ”ฆา"J\ถI)ั IDATvSยดฉmaมป'่ ฯ~˜บปฅ5ี%X@ ‘f4ž*ศืฅิๅ ส๋šž฿JPแq\๛า ‹DVวฅ*,๒น)u;pชJ๎ b†I,“e8šb๐$๛†ƒฆ9ซณม =H-ชฎไ๒5ญฌXา@Ei .งEฮ ืฒยั=Cผv่(GOu_๐c๓ธ\|๔๏šนšพ#วุŸ?+ศ฿ƒผ็>š—5!I}<๓๒ซณrป้L†ลีUฌljคพฆš"ฏUQ‹ฌฎ‹'่ๅHg7#ใ็ขฆล)nฟ้zV65"!ŠF „ {LRฉฏ˜2ฟ ‹‰`ˆ‰ษ๖ทุฦ8ข^ุื๑ฑว?ศA„ผ ‚” B8“0๕น›ืฯฬ(:4โo~ณท ๙ ›/a]M)p`$ศแ KJผ็}ปƒัรคนาฯฒฒbS[๙$ ฒFnลTg JวHn^‘ห6;๓Š๎im ญชงง r†T™;คฆ‡็F”น๔>uร"žษbWd๛<”yœ8UEรฒHdt†ฃ):Fƒ์ 5f'LjZZฟˆKšYึฐ˜rฟ‡รŽ,ษ˜–‰–ษ G9ีืฯรว่์ํŸณcs:|มhจซม0L=ฮื๔ำ‚|๚๐ป฿I๋๒eH’DWOฟ*็๓9“ัY\› Q‹ชซ๐ž6ืKŸYลvฌซ‡ั‰ภฌ„จi‰dš[7^“ lHDb1ฦฯ๑JŠ๙ขคธˆ๒’’?คDˆAศo"H ‚ ไ‘›r#ฒ˜ k฿{ใŠข#A๎ฉ}}ฌnน”ถฉE{‡&9:ฆฏๆ๖๛ Fhบษส +ห‹)q9ฐ+ฏฯ+Šค3tใž<๏Aฺ’$qokอ•~L`4šd"ฉไU๖*ฤas*•ึุ{๔๘๘ž‡>๕ต/|fป๘v!‚ŸDAศC›ฟ๔6ำฒฺe‰†้ํ+Š๖O๒™ง๗์๑ษ’ฤฃ[/ฅe*เผ6 3ฅฎุ=๋๗5Iึ VU๘XYแฃิ•›WนS‘t–pœืtŸc˜R$‰wถ5า\้รฐ`8’d2™F.ภ"UๅuQ๎vL ฯ2‘Hใsฺๆ๔1่†E ™F–$–”xฉ.rใฑ+(’„aYคณคฦั๑\˜šLj็ฆ %HตฎXส†5ญ4ิีโ+๒โฐ$ร0HฆำLL9ึหk‡Ž02ธh๏E‘yx{fฎๆv๐D'_๛ไgิ๏ป‹ตซW ห2=C<๒ซg1>ซิUUะฒ๙๗ฒด~1–eqธ๓_ฮ ๒3๊ก{๏d}๓JdYฆop˜ง_EVฯžม{ึ พฆ†•M ิUWโuนงฎt8ข๔ s่ฤIม ขฆi™,7lธt&ฐ%’)†ฦฦ :HนNj++ค\'Oค=้ฟเ๛I|{A( "H ‚ €-ฬถ/mนไฑ–*ฟozEั็~[ธrจ _r)ซ*Š1L‹W๚'‰&ฉ๐\๘“ณH‚xFงตสฯŠrฅn;EA–ฆฎ๐–ฮะŠำ1ไิdU–ฯเxd๎iidๅิ๑๔…„ำิ RPWไฆฤํ@ŽŽ‡‰jYงยT4ฅ/œ Šส๏ Š๙คJ>.kkฆmล2*หK๑8]S[ผ^z=02Fวฑผv๘(†aๆ๕{่c๏+›8ัว๚ๆw ๒s๖}wฦๅkZP•ัQ~๛๒.’้ำƒิโ๊jV-[BuE— U™ž•!‰า30Dว๑“Dc‰‹ขฆ้†ษeญซูฐถUQIฆำ ŽŽ…คRi Y’[๓๒%_ดl"D ‚ 0คA ิฦํ;ฒ™yX…ฯฺc/u;๘ฏ7ฏงฉดˆฌa๒\๗šnโฝˆ[ร" &“kชKXUแฃย๋ฤ95c*k˜ฤด,‘๛†'้šŒ๖ณ^ปสญ ,--&k˜tNFIft์ja)รดh,ษลศ]อศ๋ญ‡šnึ Š์vj‹]”L ญ—ศmมŒkY# :ฆVบฆ…</fชฎ(ใฒถV/]BeY)nงsj‹—I&›โี?<ย๎ƒ‡9tโTมผ‡ฝ๗ณzู$$N๖๖๑ๅํ฿)ศฯื๗q+ึถaSUวฦ๘ํ+ปI$“duƒe ‹YฑคแดQ–e’ฮdˆLmอ๔๑D๒ข†จ7^ฏ[ฝ‚+ืญมnSIฆา `Jฅ5:{๛ž{๎ล}๕ั๖^๑-@ก๐‰ %‚Pเ6nฒQ5ไvYโมByฬ'Ÿปy=KJผd ƒgN`ZnลŸU4I0‘Lณถบtj+Ÿ—ช ห๚T˜๊$่ qd<„M–๑9lีาภ’า"ฒ†ม๑‰i8ง+๖]์ืฦ/~ง}ๆjŽฒ$ฤpvฐHdu\ชBฯM™;ทาm๚u‹gt†ขIŒLฒo(ˆหฆ\” ี/_xีšึ็–/i ฒฌงร"็†ดk™ แXŒž!๖:สฑS๗y๔g๏น—–eK‘$‰S}<ถโWอn฿ส•๋ึ`SU†วว๙Ÿ’ีห–ฐz้jซ*p9(rnF”–อŽฤ8ีืฯก“]ฤโ‰ผQำ, ZV,ๅšKึbทูHคา ŽŽฬk!B” ย%‚” ยHM‡จ๖u|์๑ฯ?ฒAaAJažบ๙๑'7สฒN†ฉ†/lZรขb)เว(u;๒zขษ@$มh>u}ีE.โŸ๎ฃถุ]ฯ็@$มH,ลวฏ^ืa˜Yy`˜i •5Hfuยฉ &๙๛wื˜š!ๅŸ Rป&fŽiพyฅoœ฿ž๔“๗ดc.๏๗ƒ|~ฃ๓6Hฝ๏ฮธ|M ชข28:ฦฟว๗ˆฦy๗8ซ+สธr].D••๘q9_ฟฺaZำศ๊:ท EV˜๙ทืํ*ุืฅบผœญ7\ƒวๅ"•ฮอบAJำ2ู๗๘ฟ{๐?…AคA€›ํฉ^Y’ฺŸz่–ํ ํุo๙๚Scา.ษ๘ๆ๊>ืึ”๒WืตPแqำฒ๐P/ ~oม>‡?;>ภ฿ะFตืE2ซ๓b๏อ•~$ NปŠ*K่ๆt˜า๓2L™ฆEcฉฟำAึ0ุ5 ุ9?WHํ๐รC="Hอฒwฝ} Wญ_‹ฆ246มฟ~๛y1+ซถช‚+ืญaๅ’*JKp:ศฒฤ๔ีก0งz8ูว่D€ตซWฒ๕บซq9B!?†้(ุืฅขด”6]‹วๅ&•N346Žašsvš–ษ๖Ž๘‰Wv|x{{{X|๋A@)A6) ภด่[ˆajใ๖~ูฬ<ฌ˜<<a๊ฒบ2>qM3'Q-ห๖ฐคคจ`Ÿฟ_žไฏฏoู‚ธg0€SUฐ+2ฅn^ปŠfรe“งฏ๎6ต•O'‘ั™ศ“0eYะXโล๏ด“1 ^ํŸภ็ฒฯห๗กIพ฿ั-‚ิ,ป๏m›นๆาตุm6†ว|๕;?d"บhgqMึถฑrIๅ%~;ฒ$cš&)Mc"ขซ€๎!2ู “แ-ห–r๋ฦkp;]รaถ๘็8์…gK}>nฟ้zผn)-อะ่)ขA„ท"‚” ฏฉi&t`šฯ๕ษ๊ล6Wa๊Šล|๊ี”น„ำพฐ—eฅ…ค~}r่ด-ˆGฦr'เN›€,I”ธ์ธT—Mมฉ*(ฒ„iYคu“tึ žษHค1/b˜zcา ƒ”ฬำ u`$ศw๖w‰ 5ห๎ูz3ื]ถ‡ฦ่ฤ$_๎ Lฮ๙ใX\Sอ•๋s!ชิ๏รaทฃศ2†i’JkŒ&950H๏ภ๐LˆšŠฤXฑค6]‡วๅ"‰๐ปญpทฏ๚ŠผผใๆMy<ค5กฑqtรธ`๗—Jkt๖๖=๗๋=;๏!JA๘CDAเwƒิyM ๅQ-K_(NZ7ฐ‘ฒ$แsุqU\๊t˜’0-ะtƒิ‡ฉ7ฉ}ใ”yๆg:6แ฿_;)‚ิ,{๛ฆ๋ธ้๊+p:์LC<ƒŸะ?นœN๎|#โbาšฦศD€L6{ท›Jk>ัy๘…}{๓์@Aฮ‚R‚ œyšfZ|CWฬ๖…ฆ6ozฆ๕pร๙ฮ–uOƒิษ@”วwŸAj–zร5rํ•ธก0๖ร'่ž๕๛YXฯ†5-,kจว๏+š Qฆi’Lงะ?HฯภฆuvรปำZ†_1๗ฝํผn‘xŒ๏์I ำ(ุืล้pp๗–›().&ญฅ L’ึ2Hาน}–ˆ%‚ ฬคA8๛ ๕†๘ฆn~ษฒ,ทsŽa๊ถU‹x๐’e;lL$า|`oAฉ—๚ฦ๘ุUซ)า>Iๆ‚ิYn๑™S.›‚ฆโถฉSรฯsaJำ ™, ฌaฮz˜2-h*๕โsุIfu^้งยใœ—๏แฎ`Œ๓๊qคf๛ณ๔ฺ+ูz5ธœN&รaG?Z4;–7ึsๅบ6š๊แ/.ยiท#I2†aHฅpขงแัณQำ2Yฏอo฿LฑวK4็{ฟ๘ Y=[ฐฏ‹f็žญ7Q๊๓‘าrณด’ฉฒ|vsฑDˆAf“R‚ œ{ฐL"†ฬcฆllวถ ๊jB็ฆั\ฯ{ื5Qไฐ1O๓ƒC…คvLœ6ค} ’ c˜(็8sFFขศaรm=LฉŠŒiZh†IF7Hduฦใ้Y SฆKKs+คโW๚ฦฉ๔ฮฯ ีŠ๓ฟwAj–tีถ้ZNมp˜ํ?9ฝ็}ป+–4pลฺVš๊QR\ŒรnC’dtC'‘J161ษฑฎFฦ8฿ญu†aโฐyเ๖ญ{ฝDq~๘ซงIkZมพ.6ีฦ=[oขฬ๏'ญiŒƒฤ)”3ิžJk์=z||ฯCŸ๚ฺ>ณ]|kAfƒR‚ œ_šถ ริฟ=u'๐˜,ัp&žึX„ืฎ2O๑ฃCฝฌฎ๐์๑๏ ฬ iงณ๔‡ใ่ฆล๙ฮ@–ฆย”วฎL]™Oล6ฆfVLes3ฆ2๚๙‡)ำ„ฅeE๘œ6bšฮ+cTy]๓๒=N๐/ฏAj–mผโ2~ใ๕3Wง๛ฦO~ม‰๎sพฝ–ๅKนคeK๋Sโ+ยnณาi+ขŽw๗า;8|ฦqๅ~–[ (2๏พใV|"b‰?z๒’ฉTมพ.ชขrฯึ›(/)!ฅฅ รฤ EyหŸ3 ƒ‡EˆA.คA˜ 5s2ณ€ริๆฏ?ฝอดฌ๖?ฆ๎_ณ„๛ฺ๑ุUFbน ี\YธAj๏๐$z๙ .;‘t†พpรดfiGe1uU>—}*Lษ–Yำœน*_ ‘F;0•[!5คฒผา?Aี<]!5I๒ฯ/Aj–]wูzqหF<.7กฉซำ;ี}ึทำบb—ถฎฆi๑"|E^์6ิU๓r!jhlœฮž~z††PHT9วฏศผ๗oรWTD<™เ?๓,๑Dข`_EVธg๋MT”–ึ2ŒMNO$QU๕Œฆeฒ๛Ž๛C๗๙๑-AAธDA`vƒิN๎๛dIj๊ก[ถ/ผ็๓ญริ{ึ5qOK.›สH,ษ๔๔ ฉ#Aไ๒ๅ๘v"้,ฝกุน—eAฑำ†KU๐8l3aสด k˜dŒฉ0•ิHgณ S–Hฐฤ๏ล๏ฒีฒ์ PๆžŸCอGข)้ฅร"Hอฒซ/Yห]›oฤ๋vŽF๙๖ฯ~ลแ“]g๓-ห—rลฺVีžขt žJ2<6ม๑ฎ^FGQd๙‚‡$ษผ๛๖ญ๘‹‹‰'“<๑๔sDbฑยย/Iณ๕fชสJIg2 ŽŒ‘ึ2ุlฟค4-“ํ๙๑ฏ์๘๐๖๖๖๕TAๆ๘๏“R‚ &HM[จajใ๖~ูฬ<ฌ˜<,ษœ6 ๊มK–๑Žๆz\ชยP4ษOŽ๖ั\ภA๊เXhๆช‘t–žะ…=qต€"ป ทMมcWqmุงร”™ส—ฬL$าgฆLำB–%^J\นฐvp4H‘ร6/฿ŸcฑใEคf†ตญ{๋อง]ฎใ๘ษ?๚sญ+–ฒamu5๘ŠŠขtโษ$รใ้์bhlญˆzำd$ธใVJŠ‹Iค’์™็ F"๛บศฒฬ]›oคบผ -“ฅgpˆlVวn๗[„(Aaฎ‰ %‚ภ… R3'"Lอ„ฉ‡.[ฮํซใT# ~zl  ‡šฯ\50šฮา𛕖^ป:ตZJมcทaWd,๋๕Sgฆtำยฎศ,๖yฆfae8>มฉ*๓๒}9‘H๓ฯAj–]ฺบš๛oB‘ว3suบGมฟพyPkย IDAT๋šWาดธ_‘›š $บกO$p๔T#cุm๊\~EๆทR๊๓‘Hฅ๘ลณ/… ๖uq:œผmใ5ิT”“ษf้์ภ0 v;ฉดFะ่๗EˆAๆšR‚ ฬMzƒ็MำlŸ๋แ‹ํa๊CWฎ๐ฝmๅ"ŠB$มฯŽ๖๔ ฉ๏[ฟ”ข9RำLำขศiรcSqูTผ๖๐s€Œa’1L™,“ษ ษŒ{รTึฐpช2u>ฯฬีปC1ไู„•g‚IGwAj–ญ[ฝ๒wฎN๗ฺกฃงY–ธคe5๋Vฏคกฎ†bฏw&6้†A,‘dptŒฝ aทซแH$x๛J~’้ฟฺ๑cษ‚}]Šฝ^nผjuUdฒY๖Qฐฏ‹ชจถ้:๊ช*ษ๊:ป:}ใป?yปQ‚ ยล$‚” -HM{Yz๘ฉmทXHฯy2ซ7ฺฅฝ7{๐ู๎Q–๘ฝ{,ฝแ8๗ฏY‚ืฎัฒtO^œ 5อ0-Š6ผSWไ๓ุีV>^_1•สๆ†Ÿ'ฆย”ฆจฒฬาฒ"N;มคฦ‘๑๐ผ!ีฒ|ทDšeญ+–๒w6uuบ$?y๊Yv๎๏`๊U\ฑฎ•EีUงญˆส๊:ัx‚กัqž่ddb—#้[๗ฟ}3ๅ%~RšฦS/๎dptฌ`_—ด–แทยโ๊๊็uC฿ๆฐ{ล_~AแbAJ‹ค0-พก+f๛Žm[ิ‰ย๑@ค๑ห/๛โๅue๏Z\ ซค" ๎mkฤmS‰jo…ิ›ฆ…วฎRๆvเRUœ6ิŠฉŒiข้&้ฌฮxB#šฮ ศ+ส}๘œ6&“{‡&ฉ๔:็ๅ๛.‘ั๙3๛Ešeซ—5๑พ;o›~๐x'.ง“๚ฺjŠ<l๊tˆสฮlอ;|ฒ‹‘๑ œ{ˆ๗ถ…ŠR?i-ร3ฏ์ขohธ0฿๋ษ4แh๔ล?}๗;พiQA!Oˆ %‚@~ฉi 5L๘“WV๘ฟti]ู†B Sรฑ$๗ด4เสณ 5m:L•ปธl 6Eฦฎผพ•/5H๋ฃฑ %^Š6&i^๊gI‰w^พ฿RYƒฯ>ฝOฉYถชฉ‘๗฿};ฅพb รDหdPUิp้QรนญyCใใ๙ฑ"๊๗ธ๗ึอT–•ึ2<๛๊z ๊ตH$ำ‚ก—รษไ#‘‚ BžAJ Rำraส๐ŽmิU 1L%Rนบ>oƒิดำVLูr๘loš1ๅRTYb<‘ๆฉฮaZช๓๒}ฆ้&ŸyjฏRณศfSy๛ฆ๋น๑ชหฑlไพbZXVnET4ž wp˜c]=ŒŒpปy|4๏z3Uๅฅค3ž฿ต—S}๑:$’iฦBกX"๙~ขA„|&‚” ๙ค,“ˆ!๓ุฒŠฒวพv๛ฅ"Lๅฉ@Rใ๖ี‹qฉ Q-Cw0N>_œN7-ผv•R—๗ิ|)ป"#H ้ฟ:9DฉำN‘sอ‘ส&๗คfƒfใ’ึี\ึฺLC]5E๏ฬ๛_ื ‚‘รฃt?ษฤdปถZpฯญ7S]^†–ษ๒ย๎ฝœ์ํห๋‡<ข๑ฤ฿ํ Ÿู.ฒ ‚ ๙N)A๒7HM[Yๆ‹|jSc5^ืcŠ,…ึk๓๔ถฟ็๓WีWิๅk˜ ฆ4พj1NUษ๋RoฆnปB…ว‰หฆโRs๙ฆฮว ฅ4z‚qbZงช`Wๅy๓พ2L‹ฟ}๒5คฮƒรngรšึฎ^I]UEชช’”ป๊ใ๐๘8ฟ|๎EB‘Xa„จ)pฯ–›จฉ('“อ๒โž๏๎ษหวšษ๊ ŽOˆ%‚ คAศ ี\้็ฏohฅสใŠ$ฒ๚cล[๛ย{rajหŠฺ:_> ?"Z†ทญ\„C)ฌ 5M7,ชLu‘‹ Y’ศํถฒศ˜‘T†pŒมpงMกบศU๐๏'ห‚OzR็ภaทsี๚5ดฌXสโ๊*ผn7ŠขY@QdY!“อ๐›wฒ็เ*หJ ๊M ๎ูr#ต•dฒY^ืมัฮฎผ{œฉค |๙หŸซGฤ_rAกะˆ %‚@ฉถ๊๊บVชผNbšฮทt๕=qt ฉ‡nูพะ^ซ?๑๊Gฏ\\uล๙ณ"žัูฒขถ`ƒไ†|๋ฆษ†E8m †iข›6YF’ kZDำบƒ1๚ร ๔ฎwR]‘โž}์^=pˆมแณบmฏอวฎ™ูK{ๅลื๖ใv: ๋ย‚-ื_MS",ำโเ‰N^ูw`ึ๏F„(Aa!AJR[–ื๑ะeห๑9m_~ๅ({็qฒe}9ฃ8ฺwlธ ฎึtกย”CUธขพE‚˜ฆำŠsc˜~—ล>ช,“ศd9ˆขศg^ึtรB‘%*งfL9Uปช LmๅKduย ๚#‰ฉซ๗]0ตะ‚Tฉ฿ว5—ฌeีา%นญy'ฒ,cZf.D…#œ์้g๗มรgขฆนN>๖wัPW;3{้…ื๖ใrุ ๊๗มฒ`หuWฑด~1–eqธ๓/ฝถึn?‘L3 $โ‰ฟฺ>ณ]A"คAศ ๕ถ•‹ุv้2Š6&i้ฅฃ์ž<ฟ.“ˆ!๓˜)[ˆajIi๑ทฎชฏจ›0ๅฒ)\พธˆerƒฝ iZ”ธิปsณฐ2Y:ฯ2HM›SๅฉSUF–$Œฉ05I2ŽcSeJ]g๕ฬB Rฅ%\นฎๆๅKฉ(๕Ÿถ"*ั„ยt๖๔๓๊ƒ ŒŒื}9์v>๑เ4Lmu;p๔/์ู‡รn+จ฿ห‚อื^ษฒ†z,หโศฉ.^ณ๏ผo7“ีŸ!JAAJศ ๕Žๆzปฎ‰"‡๑xš๑า:F‚ณsโต€ริๆฏ?ฝญม๏๙๙†)]ๅาEๅน ฅe้ `ฒ ํ ฆศ5ณ๕๐ไdU>๗ฝ‡บa!KPแuโฑูpูoZ15MัŠa“J=sปŠfพฉšŠr6ฌme๕ฒ%”—”เr:Pร0gBิษ๎>vuf`dtV๎ำฆช|bปYฒธำ49xผ“็w๏ลnS ๊๗ม0L6_w+ฐฐ8vช‡็wฟv^ท™Jj๑๑P๐/๛฿ลuAA)A„ฉ0‘฿A๊๎–XD‘Ce,žๆ_8ฤแฑูmG–I™๖งฺุย{sajหŠฺ:฿9l-*rุธคฎ ˆjY๚ 0HYTxœTyฏฉ@U9aXบa!หLอ˜z=Lฉฒ„nš$2:#ฑ]ม( Qแvฮส1๓5HีT–ณaM+ซ–.กฒฌงร"+ฆAZำ˜ G8ัีหซ146>ซ๗-ห2Ÿภ{ff/:ูษ๓ป๗ก*rA>duƒ-ื]ลŠ% œ่๎ๅนW๗œำmฅ’Z<e$xt{{๛‚Š‚ ‚๐VDA ƒิ}mทf ›สX<ล{แ0Gว/ฬyiั'KR๛Sฒ}กฝไว/u Uิปg๓s>—u5ฅภT ล‘คย;J“ ฏ EสGg †mรnXHR.Lyํ6ช‚ห&ฃศ2†i’ศ G“Sa *=ฎ ฆๆ[ชญฌเสumฌhj ข4ขdIžบjžF ๆXWปbd"pมŽ๑“xฯi[v์ฺ{N[?/&-“eหuWณji#'zzyv็๎ณบ ขAแญ‰ %‚@ฉwฏmโžึ6•‘XŠ/=ˆศฝฯ…ฆ6n฿แ฿PS๔อๅพži˜๒O) ˆฆ3๔†โgtuบ|SใuQ๊qฮl=์œŒbป+[tร *NผงชโTsWๅ3-‹Dฦ`$–ค'รฐฌ ฆๆKชซชไŠumฌZฺHน฿รaฯ…(ห$ญiŒ‚œ่้c็ฦม ~Œo{7ห฿ฐีmวฎื .ะฆด [ฎปŠๆeMHHt๖๖๑ฬ+ปฮ่gษ4‘hโK"D ‚ ย[AJR๏[ฟ”ปš๋qูT†cIพธใ ง&็ๆJn&t`šฯ๕‰๛ลv6aชฬํ ญบ ˆL)ฅƒT]‘›ทcf๋แฉษู]!๕fบaa‘ NปŠหฆโฒๅ†Ÿ›–E2k0Kq*ร4M*f9LzZ\Sอ•๋Xัุ@™฿—[%หฆA*ญ11ไhW{ftbrฮŽ๑ใ>pฺVทgw๎)ธ •Lฅูrีด,[Š$Iœ๊๋็™W^ๅญพ6'’iยั่wรมะงฟ๚h{ฏ๘ห*‚ oM)A๒?Hmปtwฌฎวฅ* E“|แนzๆJnฯ›ฆูพริe•ž/7W๚฿‡Ÿ—{œดV๙1H*Co8~^รภ/ห‚ล~~gn†VTหา=›“YNบaM=นซ๒นl .›2ต•ฯ"™ีง่ ลษ&ๅณ4cชPƒTC] Wฌmcyc=ๅ%~v{.D)-อฤdˆฃงบูีq˜๑ษเœฟ—>๚พwถีํ้—w\ %’lฝฺV,C’%บ๛y๊ฅพ๏อ"D ‚ ยนAJRผ|ทญ\„SUŒ$๘‡็:่'.ึรY a๊ษฦตๅฅsmuษ]ง…))7{ฉฅสaA8ฅัŠ_ญn’iAฃ฿ƒฯiŸูzุšฐ6ฝ•ฏz˜rช ช’ S้ฌฮ่T˜าM“2ื๙…ฉB R‹jงBิbสน%Iาi+ขŽœ๊f็ƒร‘‹๖^๚ศ{๏;mซ“/๎,ธกๆัx‚-ื]อšUห‘e™ž!ž~้U ำ˜๙7‰dš@0๔r<}ฏQ‚ ‚p๖DA ƒิ‡6ฌไึ•u8…H‚๚ E“๕1YX?อสึร;ถm]P'boS’$Qๅuฒบา‡aZ„ำz‚q์j)ำbIiลฬึรžะล[้%~—ทMลฅ*8mนซ๒™Sa*M_8Nฦ0)s9ฮ)LJZZฟˆหืดฐผฑžRŸป63ฌ<™N3:เXWฏ์๋ ]๔๗า‡฿}/ญหงถบ๕๐ซ/aS•‚๚}Gclฝึฎ^,ห๔๓๔Kฏข๚Lˆ '“<๙Gv ‚ ยน}฿AJ!ƒิGฎ\ลๆๅต8…พpœ๖฿`4–สa๑ ]1b˜j)๑๎ึ† U^ซ*Š1L‹P*CO(†ฃภNภ ำขi*HMo=์ _YX2>——ชไโ”M™~žฮŒ'านS†E‰ห~Va*฿ƒิา๚E\นฎฅ ‹)).>mktˆ:าูล๎ƒG.๊Šจ7๛ะ๗ะถb๙ฬVท_<๗bมฉ`8สึ๋ฏf]๓*Eฆoh„g^~•ฮž~ขAa–ˆ %‚@ฉ]ฝš›—ึ`SzC1>๛ฬ&้ผzŒ 5L๑g6>z๋eํ+ส‹oะงƒT0†ำV˜Aส็ดอl=์'๒f๖,I;r๘<ถพq+฿DRฃ/Gำอ3S๙ค–7ึsลฺVš๊Q๊๓แฐค\ˆJคRŒ&9t๒{:ŽŠF๓๎ฝ๔ม๛๎šYYิ30ฤฯŸ}กเ‚T f๋๕ืpIหjTEกoxค๏ฑo~w?_ํ@A„Y!‚” ๙คพฆ™MM5ุ™๎`Œฟf?มค–—U‡ฯ™ฒฑ6.จห๋ฆตQ7อ๖pโ†๑DW!ฉฉฐ6M’oณจeIขศaร3ตZสmSQำดH๋นSแ8iฤ๏|๋0•oAjUS#—ถ5ณฌ~1%พbv อฌˆ Lr๐D'ฏ๎?D,‘ศ๗าC๗ษ๚ๆ•ศrneัฯ~ปU)ฌ฿‡๑ษ[ฏฟšหฺš๛TEmWyป๘K)‚ ณK)A๒?Hๅต-ะT*หtMFyไ้}Dาูผ}ผ–Iฤyl!†ฉ'O๙lื่ฟพzQตวฎฬใ~s ฆ2 ๅaz#ŸรŽž‹RN›ŠM–ฐ,H๋‰4}ก8Yำขุa๛ฝa*_‚T๓ฒ&.i]อฒ๚ล๘‹‹ฐlนaๅ†A"bd<ท5๏ๅฝHฆาy^ฺvฯo\YฤOŸูQPCอ3Y๎มแ๛฿vหgฏฟ’ํโ/ค ‚ \"H ‚ A๊ฟ\฿สuUจฒLg ส฿=ต—xFฯ๛็u!‡ฉอ_zฦฆ๊/]ืXYYa๊๕ eG7M‚I‘X Iส๓๗เwุsซฅ์*ฎฉ0eZ ้d.L้ฆ…ื~z˜บุAชyYึถฒdQพ"๏ิึ< 0H$S sไdป:O& ๆฝุป๏0ป๎๚ภ๏{ฮํm๎๔*iFฃ^Fอ]ถี โฤุdƒึ ๙%›‚Ilร€m„ˆeI xBIHศbY’-ษEถl๕^f4ฝท๋)๛วฬศ˜bd{ส=šฯ๋/|๏นง|๏นš๗sฮ๗ั๏ผ—›–/มฎฺ้่้ๅ฿๖@QฌคRษLผซo๐k_๛'–_F!„brIB๒?H=ธn9k็”ก*6. D๙ฬcคrบe๖ฏiฑ)ถ๖ฟe๗ฬ[ฃajหผส25/7๚๙ •ำ FRzโ)lฺื—รŽฯiว๗sa*ญ้ ง2t„ค5CลeWง-H-Ÿ7๗ภญ+—S[]E(่ว้pކ(M#žJา?ศ‰๓—8z๖<‰dสrc๛nYฑ ปjงณทŸ์;€ขไ๗HJ%3๑‘h=ษมปร!„bาIB๒?H=ดกf•ข*6.๔G๘๔ณวศ๊†ๅ๖ณaาฆุl31L=ดฤ7๎™_QCuไ้ฑกฎะ-H %3๔'R–ื~งฟslŽ)งงช`šฃsL %3tFำคŸ8ฝพ~V๕‚ภ๘ญyำ4ฉ}œ<‰ืNŸ#ษXvฌ่}นmeป๎~e๏๓(yzฉ„(!„b๚HB๒?H=ฒqทฬ*Eตมนพ0Ÿฺs รย็๏ู!#W4ฮ ๙vฯคqถ~๗มะ‡jžXPผ7฿ยิ›ƒ”ฮ@"ร`2mู}mšpู๑:๘ฦ&?wช &ฃท๒ %3\่oุ2ฟ๚เTฎW:“]๏tุhบN<‘คป€ำฏ๐๊ษ3dฒYห๓฿ญmฑบงรAw๒์๓yw๋g"™&SorไฟKˆB!ฆ‡)!„ ƒTใฆ•ฌฉ)Aฮ๖…๙ิžฃ–฿๓Š|f}•๏กdNk ธgาxหว0ešP[่'ไv’ัG'Jf,ฟฏ ำ$8>ว”รŽ฿iว>6ŸQFื7x๖){†aฎืtํ@<‘คฃง3—›8r๒ ู\๎†฿๗ฝg wดงรAฯภ ๒์syณnใ!*<<๒™'v6ถสฏŸB1}$H !๙คพฐyซซ‹ฑง{G๘๔ณว,ฝฟ–๐เ๚ๅT๘=$s?:z่{'šง๚๖ฉ้ถ~๗มะ}‹*ฅก"ดaบ'>ล ีKNgo˜}m&ท]ล7vKŸCU6(6”Žนกpd}Woใ็.p๔์yr9ํ†ื๗nฤ7ฏฦๅtะ;8ฤ๗์Ÿ๖u’%„Bไ RBAฉวถฎfeeงz†yh๏qK๏๏%e!>y๗2ส9>ยฯด2 c†ฉ=ต๗-ช{j:ร”aยฑ[๖าฺhŠdฒ7พ6 Ÿk4H9UeCน฿3ฅcํษ๚gฯะ4†ฯุฒuทฎมํtา78ฬž?Z<งA"™fpxไๅp2๙๐“=<ฃฮ+B!Dพ“ %„ไ๚โถ5ฌจ(ฤNt๑ศพ–฿ห+ ๙ฤ]ห(๗ป‰e4๑T3zฎh`๒ดฆwloIใp:ร”๙ssHฅ4žh’X6wร๎k0yฎฉ{รƒ๋Nๅ็~ไแวึซpเFว๏x7oฟทหIะ?ณoส็ผ“%„Bไ? RBAฉืฐผผ8ึ5Hใ“–฿++‹๘ซป–R๊sหไ๘ฮ๑f~zฑใ—^7Sริๆ'๗ฌฟgัฌฏ฿\Sฒlชย”aย"?.'ฉœFW4Iโผ์็MวS๖fBบgl^{+—‹แ0?|f/S๕๏M QB!„uHBเกฝวอื;๓๓Dmณ๑•ํkXRย^๏เ ฯŸฒ๔พฉบ˜ึ.ฅฤ็"’ฮ๑วฎฐ็rืฏ}ฝ„ฉษSฆ uE \’9ฮH‚ิ |[Hš,๎พƒmwŽวๅfpd„l๒ƒT"™ฆodค#O|๖ฌ: IDAT[?ฒ[~ี„Bˆ'AJ!€๖pย<ืๆ@s'{†๓j์Šย—ทฏaqYบa๒jว8m้}๋ฌRโŽล{]„ำYพฺežk๎yห๗˜]a—ก8wฑ~F=ฆ}*ย”9~…”I"ซัNั%HMด™ค6ฏฝ•{ึ‰วํf(ๆ๛๗ูI›C*›ำ่์%„BX)!„tร4ำšฮ`2อนพ0{ฏtsพ??š‡หฎ๒ฅmkXTD7Lท๐ฅƒึRwฬ)ใฯo_L‘วI8•ๅ๏\โPK๏uฝw&‡ฉญOํฑ~nล—๏ช-+›่0e˜P?คโYึ‘8šaะ๛S‚ิไุpอผwใ]x†#้ง{€‰๗f*™‰๗ ๅW?—฿–_1!„ยz$H !ั ำฉ*†IZำ้‹ง8ำ;ยฆ. Fฆu|N;o]อ‚’ ša๒Rk_yแฌฅ๗๗บบ ไึ…zœ งฒ|ใ• ผึถ–1ฆžฟkใLฏ“ฆL๊‹bึ‘๚ o R“ใ๎[ึ๐M๋๐y<ŒD"ใฉT2‰ฦฟั“นปฑ1ŒB!,I‚”Bวบ†ฬ9…~.ล†aBZำ้ฅ8ี3ฬ+]ดŒฤงe‚.m]อผโ ša๐BK_}ัฺAjc}%|หBn'รษ _;|#๏hY†I›bณ5๎ฝห๎™6nวริ–y•eชb{Wหฝe/@A,“ใ๊p Rm&ฉตkV๒;[7เ๓x Gฃ|๏฿6!ห•%„BX$H !๐฿}|ฯ‚jnฎ)กฎะ฿ๅภฎุ0L“Tn4L๏โู+tFSบn…'_ุฒŠ๚ข 9เ`K/๛า9K๏๏m๓ซนฆ๙ธ &2|ํ๐yํค๒35Lญ฿}0ดถฆเ๑{ๆWิใP๏d&`๊ GƒT4“ใ๊p์†฿wค&วmซ๘m๑{}Db1พ๗๏?{W“š'’i"ัฤ—%D !„7 RBl}jฏ r;ู4ฏ’f•2;ไว๏ดcWtร ‘ี้'9ึ9ฤฆnบฃษ)Yทฏ‹/lYEma€œฎ๓\s๋๐K๏๏฿ZTร‡Wฯ#่r0H๓?_:ว‰๎‰™L0iุ฿ถd&แ๕ป†>P๓ฤ‚โเฝo7L&จ6s }„Nย้,ญร๑ัJu“ 59nnXส}๏ูBภ็#๑Ÿ>K&›EU•ทตœD2M8ง๐๐ศgžุูุ*ฟTB!ฤE‚”B๐FWโuฑy^ทฮ*ฅบภ;ฆอ0Id5บข ^๏ไนๆzcฉI]ทrฟ‡ฦอ+™๒“ำu๖^้ๆฏ^ด๔ภ’ูมสz.;๑4_}๑,ง{G&๚c†ั8ีมaบ‡ฉyE฿ฟ๙ฅtรฤก*ฬ.๐›hพe$ŽM‚ิ„› Aj๕าE{ท๔๙‰&โเ๎%–Hโt\฿x”%„Bฬ ค„‚_RใJ|nถอฏโ–šRช‚ผ;vล†nšฤณํแGปูwฅ›กdfRึญ*เๅณ›V0'ไ'ฃ๋์นลG.YzฒZ>ธขฟำNo<ล฿ผp–s}“v'ฮ S{j๏[T๗TCEhรo Sšaโถ+ิGƒิH*Kˆ\!5fBZฑx๙}๏!่๗K$๘แ3๛G๐ธ]o๙พD2อเ๐ศห๑h๔%D !„7> RBมฏRใสณฐšU•ลTx๑:T› อ0‰gst„ผึ9ศ3—ปˆgrบnณ |<ฒqณ |ค5Ÿ]๊ไฏ_ถ๔เŠ:๎[V‹ื1คv:รฅIšแ!{wl99“ฦ๖๕„ฉœnเuุฉz =๙ฐ=ฟแ๗ฉษฑ|แ<เ๗PO&๘๑ณฯำ;0ˆ฿๋๙•ฏQแd๒แ'{๘ B!„˜$H !ฟ9H›๒ณi^%ซชŠจ x๑8์จ6ศ&๑LŽ๖H‚—๚ูwฅ›dN›uซ-๔๓ะ†j‚>Ršฮ\่เฉcW,ฝฟหชz~gษl<;=ฑ$_7.ปŠ ะ ƒp:K๓PŒ—๛9ะƒfผฝZTZภงึ-งย๏!™ำ๘แ™Vพบลา๛๛On]ศ๖ีธT•ถpœ/<Š๎hrฺึว4ˆ่ ป ลนk&†ฉตuO~จกฎพ2เ!เrะOsq Bะํธกท]‚ิไ˜_;›๗พขPˆd:ลฯพฤพรฏตูLณQB”B!ฦIB}ืPQศฦ๚J–•R๊sแฒ^a’ี "cWLผฺห ญ}\๏๙wiyˆฟพ{~๑ฌฦ๗Oต๐ฃณญ–฿v"ถฮฏย1ค๗Ÿค/žš๖๕šษa๊‰#—v|hลฦ€ห>ง/žๆL๏%>ื ฝอค&วู5ื฿}?%……คา้ศ3๏๚ฝ{65ส/B!~ž)!„`โ‚ิธUUEl™Wลขาล^n๛่\<`8•กi0ฦพฆnŽt ฦe5T๒‰ป–Qๆwหh|๏d3?9฿n้w,fS}%Uฅu$ฦg๗`0™ษ›๕3 "(4๎ป๋ฎ™๖]0LsGO,ีxฒ{xNeะsCoซฉษQ[]ล๗} RZฺ•สdv๙<ž0B!„ฟ@‚”B0๑AjญณJูT_ษ‚’ …ฎ๑0ฅ†ฉ a^ํๅตฮม_ปŒUUEๅK)๕น‰er<}ผ™ธุa้—w.e]]Uกy8ส#{ONg๓n= “6ลfk{–3ํ;๑ํื.jIy่QCฝa๏“ 5_"5•ป>๚ก์ช()‘%„Bˆ_K‚”B0yA ภUWฮ]ตๅ,() ะใฤกŒ†ฉฌฎ3˜ฬpพ/ฬก–^Žv า๛ืT๓๑ตK)๑นˆfr๚pCอ Šƒ๗ˆaJ‚ิ„{:cืุุ(!J!„ฟ๙๏$ RB1นAฺ๊ X?ท‚;kหYP$ไvโT 2บฮ`"รน0๛›บ9ำ;rํ}ทิ”๐ฑตK(๖บงณ<๙๚๖7u[z?ธn9k็”ก*6.F๙๔ณวIๅดผ_oรค ำุ1ีcบจaJ‚ิ„yฺฆiO์ll•_!„B\๗฿Gค„bj‚ิ8ลfc]]9๋+ฉ+ Pเ Sฆi’ส้ $าœํa_S7๚#1ปŒ?ฟc1E'แT–ฟ?r‰C-ฝ–฿mhเถYฅจŠ‹sŒฌnXi†ั8ริ,ญ๎ขาะ{}NปๅทG‚ิป&!J!„๏˜)!„`jƒิ8งชฐฑพ’ปjหฉ+๔p;q*6 RšN<ลฉž†’i~g้ =NFRY๎ี‹ผฺุg้ศฦ2ซี็๚ย|jฯ1 Uผ‡>ปiEใขภม™๔}Yฟ{Oํ}‹๊žjจmฐr˜’ ๕ฮ˜†yศPlO>๖๐Œ๗B!„˜Xค„‚้ Rใ<•ญ๓ซนmV)s \ca*ำˆg5B'.Ua(•ๅ๋‡ฯ๓J๛€ฅ๗wใๆ•ฌฉ.Aฮ๔๐เžc–Žš/l\ษ์฿กดฆ๏๐9ํญ3้{c๕0%A๊ํ‘%„Bˆ‰$AJ!˜ 5ฮ๏rฐu^ทิ”P[ไ'เt` SŠ ฐูHๅ4พyไฯX|R๓GทฌbUU16เT๏ŸyึšAjNศฯรจ)๐‘ึt~zฑใ้'Ž_j<ธc{๋L๚ฌ฿ฝง๖๓f๔ๆš’eV Sคฎ„(!„BL RBA~ฉqE๋+น}v)5>‚.ชbระ4้Š&9ิาวพฆnบฃIK๎๏วทฎfEe'{†yx๏qKnG]กŸ‡6ฌ :่%ฅ้๛…vvkย0yZSฆ6?นg=‹f}*aJ‚ิ[3 อfš฿z‘๒+!„Bˆ‰&AJ!ศฏ 5ฎฤ๋b๓*ถ/จฆา๏มfRบaฯๆ่Œ$yฝs็š{่‹ง,ตฟwn[ร๒ŠBLเXืŸย’ใฆพ(ภg64P๐’สi๋๙vพ{ข๙ฺืเ๓†โupว๚๐L๚>Y%LI๚ี$D !„b*HB๒3H๛ƒ•s๙`CNปŠišืnแำ “DNฃm$ฮฑฎ!žนE$อ›/o_ราฒpดsฯ?wา’ใfAIื5P๐สi่\x๒๊›^cDt…]35LmœWฯwี–•ๅc˜’ ๕ "ฆอ|@B”B!ฆไ๏ RB‘฿A๊๗–ี๒มu๘v’YกT†B ฏรŽbm์ŠฉŽp‚W;x๖r๑ฌ–ท๛ฺฎ(์พ†%e่&iเฑง,9n•๐ฉuหฉ๐{Hๆ4~xฆ•๏Ÿn๙•ฏษaj๋S๛vฌŸ[๑ๅ| SคฦDtƒ]š[ตปฑqFM!„BL RBA~ฉ6ิq฿๒Zผ;ฝ๑฿=ัL}Q€••ETข™n's‹”๙o SัLŽ–แ/ท๕๓\sMฯ›ํ๑9ํ<พu5 J‚h†ษKญ}|ๅ…ณ–7ห+ ๙ฤ]ห(๗ป‰e4๑T3zฎ๚Z€I›bณ5ฮฤ0๕ะ฿ธg~๕G=ี1๋1“ƒ”ฎ๑y QB!„˜Nค„‚Rดบž,™ƒวฎาK๒Rk%^๗Xš&UanQ€RŸท]EฑAฮ0‰ฆณ\Œ๒jวฯ5๗ำi฿žทƒวถฌฆพ8ˆfj้ๅผxฮ’ใfEeu็Rสnb™฿=ัฬฟ_่x[ห˜ฉaj๎ƒก7ิ<ฑ 8x๏t…ฉคžถiZใ;[ๅฬ/„Bˆ้$AJ!ศ๏ uš๙ผw๑,‚”i˜‡C฿!!J!„๙F‚”B฿A๊On]ศ๖ีธT•ถpœŽH‚ฌ๖›oฝS>‡Y!E.ปr-L '3\ SG:ฆt{*>ทi%sB~2บฮณ—ป๙๛#-9nnฉ)แck—P์uNgy๒๕+์o๊žจลฯะ0ตง๖พEuOME˜บ‘ƒ”i˜‡ ลึ๘ไcฯจ๑#„B๋ %„ไw๚ณฑu~Uฅ=ง+’$ปIสvNMศKก…SUฐู ฃ %3\่sจฅ—ื:งd{j ||vใ f๘Hk:?ปิษท_ฟlษqs๛์RSไuNeyโตKผฺ;กŸa˜<ญฉFใม[gาwr*ยิค$D !„ย*$H !๙คโŽลlชฏฤกชtDโt„“๏่ฉy‡ŠSU™๒๒8qช ฆ ]g(™แ|_˜W{9=4ฉS[่็ก ิ}ค4ธะมSวฎXrY[ฮŸถˆB“‘T–ฟ{๕"/ถ๖Mสgอิ0ตu๗พ•็U}๗ๆš’eฆnค e@› ;$D !„ย*$H !๙ค>~็RึืUเP:# ฺย๑๋บe๏ื๑:UN'7Nล†aBZำLฆ9฿แูห]œ๏Ÿœงมื๘ฬ†ช^R9Ÿ\h็;ว›-9nึีU๐งท.$ไq2œส๒ฟ_นภแIž›kฆ†ฉอO๎YฯขY_Ÿศ0u#)ฺlฆู๘ญวู-gr!„BX‰)!„ ฟƒิ'๎Zฦuๅุ…ฎh‚–แ89xWหดู ฤ็&ไqt9๐9cWL™คr:‰4g{Gุิอฅศ„nฯ‚’ ฎk 2เ!™ำ๘๑ู6๑ิUKŽ›๕•๑- น '3|ํ๐…)™“ห4ˆ่ ป ลน๋เŽ๕แ™๔]ศ0eๅ %!J!„V'AJ!ศ๏ ๕ษป—qgm9vลFw4E๓pMŸ˜ีUUทŸCล๋tเPl˜&ค4พxŠS=ร์ฝาอีแุ„|ๆ’ฒŸผ{ๅ~‰œฦNท๐ƒ3ญ–7[ๆW๑‘›Pเv0”ฬฐ๋ๅ๓ผ>Esqม Sg๏6ึW~๓ฎฺฒฒwฆ,ค "บมฎ'w>(gn!„BX™)!„ ฟƒิงื7pว์Rิฑ ี4E7&vuUลFuะ‹ฯๅภ๏ดใuุฑ+6Œฑ+ฆ๚โ)Žwณ๏J7mแ๘ป๚ฌeๅ…๕ห(๗ป‰e4้ิUๅ\›%วอ{T๓_ošOะๅ`0‘แo_>วฑฎก)_™ฆถ>ตoว๚น_~'aสRAj,Dinmื๎ฦฦuŒ…Bqc’ %„ไwzhCทอz#H]Œ2Y็n‡ชP๔t9๐:ฦย”ช &ษœFo,ลฑฎ!๖5uัIพฃฯXQYฤ'๎ZJฉฯM,“ใ;'š๙้…KŽ›๗.šล‡Wืp9Hค๙Ÿ/ใD๗๐ดญaาฆุl{๏฿ฒ{ๆ}‡Gริ–y•eชbปฎ๗X"HIˆB!ฤ J‚”B฿A๊ณWp๓ฌR ;–ไ๒`&ymv•ช€‡ ‰หฎเwฺฑ+ ša’ศjtG“ํไนฆบco/Lญฉ.ๆตK(๕น‰frรั+‡พxŠฏผp–๓yy1ห!ร0ง:พLท฿ฆ๒0HIˆB!ฤŒ!AJ!ศ๏ ๕๘ึีฌจ,FƒTำ4ฉk?6Mg^q€ช รŽjƒœaฯไ่ˆ$xนญŸMฤณฺฏ\ฦuๅ้ญ‹(๔8Ie๙ฦซyฉตฯ’ใๆC+๊๘ฝeตxvzใ)v:รฅH>ฏ๒ S{j๏[T๗TCEhรฯO|ž/Aส4ฬC†bk|๒ฑ‡gิqB!ฤฬ&AJ!ศ๏ ๕ฅmkhจ(ฤบ" š†bำพNYรภก(ฬ- Pแw†)ลFV7ˆerดฤyกตƒW{Iๆฆ6ึW๒วท, ไv2œฬ๐ฟ^นภซํ–7ธฒž{—ฮฦใฐำK๒ลƒghŠZaีกุุปcหษ™๔=S๓ŠJ}๎iRข„B1“IB๒7Hู€obYy่ฮ“ 5.gธ์*u…~สv๕ฺSัt–ๆแฏดฐฏฉ›œnŒ๎๋๙Uท›Pเv0˜ศ๐ตร็yฝsะ’ใๆรซ็๑Kfใฑซtว’<~เ4W‡c–YรไiM5๎ุ:“พ๏๋w๏ฉต๋J#ฆฑ{:‚”b˜ข„B1ำIB๒7H)6_พ†ฅๅ!ts๔ ฉๆก เP๊Š”๙ธ์*6@3 "้MรQ^iเนๆถฮฏbวšy]i๖ฅ๓๏ฒไธนอ|ปxปJg4มใNำ:ทvธ์๊ำ_ฺพบqYYaซœ „B!ฤT %„ไorจ ;ทญaqYบaาMru(ฏภ1ท]aNแa ซDาYš‡bฤณทฮ*!เrะO๓?^:วฉžaKŽ›ผ€{ึเถซtD<๚):" หm‡฿i็๑ญk˜_|Z3Œv5,g!„B1™$H !๙ค<•/n]รยาเhŠ$-qK˜อfร๋T™]เฃุ๋บvลTF7ศ๊^‡ŠjณัO๑ีฮrถฯšใปe!๏YXKUi$๘ยs'้Š&-ทn'mYE}qœnDถ๔๎๚๒แ+ป๎X/aJ!„BLฮ฿ ค„"ƒ”฿i็ฑญซYPR€ftF’ดXhŽ"ปjรcท3;ไฃะใผฆฐูH็4พ{โ*?:jษq๓฿o[ฤึ๙UธT•ถpœฦ็NาKYn;Šผ.พฐys‹ไtƒW{๘ฯGt…]†โ”0%„B!&œ)!„ ƒTมc[VS_ R Z†ญ7G‘กโTUf…ผ”๙ุำ4้Oค9ู3ฬ -}–›ฯo_ฬ–y•8T•ึ‘8ฯ ?žถ๑)๕น๙ๆ•ิศ้:๛›{๘๚แ ฃวศ ข+์zญrฆB!„E‚”BฟAชศใโ [ธrฅ+jอ 5ฮiWฉ/ P๔`ปJJ7L2บฮP2ร๙พ0๛›{,3งิว๎Xยฦ๚JชBหpŒฯ๎?มP2cนใR๎๗ะธy%sB~rบฮณWบ๙ปW/พ้5†I›bณ5๎ฝหn9c!„BˆwK‚”BฟA๊ฏ\้Œ&iตp๐9ํฌฉ.ฦiWั ร4qจ ฆi’ึtโiฮ๕‡ู{ฅ›๓๙}งุว๏\ส๚บ ชB๓p”ฯ๎;มH*kนcR๔๒ู+˜๒“ัu๖\๎โ‰#—~ๅk%L !„Bˆ‰ AJ!ศ฿ U๐ะธiๅตPะuฉวษา๒.Ue$•ก;–ข2เ!่vโTl&ค5พxŠำฝ#์o๊ๆ๒`4/ทๅw-ใ๎บr์ŠBำP”‡๗'šษY๎˜ฬ*๐๑ศฦฬ*๐‘ึt~vฉ“oฟ~๙-฿c˜ดa;๖d๛A9ƒ!„BˆทK‚”BฟAช:่ๅณ›V2ปภwรฉRฟ›…%AชJ{8ฮ—กกขf•RW เrเS9xŠS=#์k๊ฮป' ~๒๎eY[Ž]ฑqyp4HลณšๅŽImกŸ‡64P๔‘าt~zกƒ8vๅz฿~ศ0ŒF SB!„โํ %„ไoš๒๑ศ†ิŒ]นาณ~ช*๐2ทะ?6xŒฟ;r ]%‘ีhจ,dEEu…~.vล†nšคrฃWL๋โู+tFyฑ-ฎ[ฮฺ9eจŠKQ>ณ๗ฉœnนcR_เ3จ xIๅ4~rก๏o~ป‹‘0%„B!ฎ›)!„ ƒT]กŸ‡6ฌ :่%ฅ้๔D“ดŽX;Hอ)๔3ซภ‡CUธ:ใ๏\ฤ๋ฐฟ๑ๆ•YS]ฬฌ~ง‡ช &ษœFO,ษฑฎ!๖7uำIN๋ถ<ดกf•ข*6.Dxpฯ1ฒบaนcฒ $ศƒ๋จ xHๅ4~tฎ๕ฬQฌ๘ซบด<ฤ_฿ฝŒ ฟ‡xVใ๛งZ๘ัูึ Yถ„)!„B๑+/AJ!`็ก3ๆก–^๔<ปบeIYˆOฝŒŠภh(่ป‚ิา๒ล^ืตพyไAท๓7พฯญช,( าP>zล”ืaGฑf˜ฤณ9:#I^m`ฯ•.โS๔คปฦอ+YS=คฮ๔๐เžc–<& …|โฎe”๙ฤ2฿;ูฬOฮทO์‡˜ๆืฒชซ๑เŽ๕a9ใ!„B RB๔ฦRfำP”W:8xต—\žฬดฌ<ฤ_฿ฝœ๒ฑPะOา>’ฐ๔พ^VQH‘ื…jƒs}aพ๕๚eBืคฒบAฮ0ธcvซ*‹จxFร”bCำ ข™‘/ต๖๑|sฯค?๑๎ั-ซXUUŒ 8ี;ยgžตfZYYฤ_ต”RŸ›X&วwŽ7๓ำ‹9ฆADWุe(ฮ]ฆ„B!f6 RBฆifuƒH:Kหpœ๚8tตwฺ'จ^QYฤ_นt์ส•=ฑakฉฅๅ!Š}nเl_˜oฟ~‰B๋m-#–ษแPึฮ)ceee>7๎ฑ[๙rcaชu$ฮหmฃajฒž|๗๘ึีฌจ,เTฯ0ํ=nษcฒฆบ˜ฏ]J‰ฯE4“ใŽ^แ™ห]“๖yฆ„B!„)!„R9t`๔*œp:K๋HœZ๚xกตŒฆOหzญฎ*ๆใw.กิ็&šษัMN๛“ๅญ%ๅ!J}nlภ้ž:vๅบฏ๚EฑLรฮํณKiจ(คฬ๏มmWQl3Lข้,MC1Žt ฐฟฉ{ยใทญaEE!&pข{ˆG๖ฐไ1นนฆ„ึ.กุ๋"’ฮ๑ไ๋—ูืิ=้ŸkDPhwึ]rB!„˜Y$H !pจฅืœ_คะใฤฉŽฌaIei‰๑J๛ฎN•6o >vวJ|ฃก ;šค;jํ ตธ,D™฿ ภษžaพsผ‰‚wคฦล29.ทอ.eyy!e~7.UลfƒX`ผ:็ฅึ>\ํ™ฐ'แํพ†ๅๅฃA๊XืŸoอ u์Rโ๖ลy]„SYพ๙ฺ%\ํฒฯ7Lฺ›ญq๏[vหูH!„bf %„ภถง๖šwฬ)ใฮฺr–Pไ}#L฿vu8ฦ๋ƒ์k๊!•ำฆdฝnUสว๎ ้,]ั$ฝั”ฅ๗๕ขฒ*Lเxื฿;u•เ[๚TพH:;iกเOn]HแX(่Š$่[7Hูlฐฐด€ส€็Zฤ๙๑ู6|N๛ค|^,“ฃ*่แ–šR•P่qแTl6ศhฃa๊|˜Z๚8า๑๖ย”SUุน} ‹J ะ “W;xภiK—๕•๑- น '3|ํ๐…ทฝ?&ษ!ร0%L !„Bx$H !ฟ:H[YYฤๆyU,*ฝbสmWQฦžๆ6zลTœื:9pต‡‘ิฤ†ฉ_ ั$RŠbc~qช 0yฅ}€ฟะŽวaŸิฯerฬ)๔ฑบชd,L9qช ฆ Y]g(™แ|„W{8ึ5t]หtUพดm Kƒ่†ษหm์nƒ‰ _;|žื;๓i%L !„B`$H !oคฦญฎ*ๆ๎บr––…(๖นฏ=อM3Lโ™ญแ8วป†ุ{ฅ›๐]1ตe~นinCษ ‘C‰Œe๗ณชุ˜_ค2เA‹8?ปิ‰ฎNษ็ว29j :ซ”yลB๎ฑ0ค5มฤ่S{ฏtqถ/–ห๒9ํ<พu5 J‚h†ษKญ}|ๅ…ณ–<.๗,ฌaวšy]i๖ฅ๓๏สป๕41-ง˜ฑฝUฮZB!„ึ&AJ!ธพ 5nEe๋๊สYZ^H้ฏSั$ฏwฒ๗Jืปพb๊CAg4ษฐ…ƒ”CUจ/P๐ข‡Zzู฿ิƒSUฆt=9ฺŸg—RW ภใฤฉุ0TNง?‘ๆl฿๛›บนะ๙•หบ<ถu5๓Šƒh†ม -}|๕Ekฉ๗/žลYUOภๅ ?žๆผtŽS=รyปพ†ษำšj4J˜B!„ฐ. RBม Rใ–”…ุT_ษา๒e~žฑ[๙4 –อัM๑Z็š{้‹ฟณ'ใฝo๑,h, $าtF’ 'ญคœv•๚ข9เภี^ํล1ลAj\&ง3ฏ$ศš๊b๊ NŠ ำ„”ฆำOqชg„}M]4 ล๔ษฃ[WQ_$งฦต๙า9K—{—ฮแC+ๆpู้‹ง๙›ฮrถo$๏ื[ย”B!„uIBYทธฌ€อ๕Uฃaส็ฦํฐc›c*žี่Š&9ฺ9:วTO์ํ…ฉ฿Y:›ผขZ(่Œ$งฒ–ฯn‡สขๅ~9]gs/ต๖aW”i]ฏŒฆณธ,DCE!s‹\์Š ร4Iๅt๚โiNtฑฏฉ›ึ‘8ล^_ุผŠบข9เ๙ๆพv๘ผ%ห}หk๙†:|;}๑_yแ,็๛ร–Y >o(ฮ]wฌหูL!„ย$H !๏.H[XRภฆy•,ฏ(คฬ็ฦใPQm64ร$‘ำ่Œ$9ั=ฤฆ๎๋Sใกภ๏ดำOัIฑp๒\ส่:{ฏt๓jวชอ–๋g˜&๓Šƒฌฎ*fNก€ำ]Uะ “dNฃ/–โX๗๛ฎt“ึt>ฟyต…ัธึิรื_น`ษใ๒กuฒZผŽัqถ๓ะ. D,ต ฆADWุ%aJ!„ย$H !คฦอ/ ฒฉพ’ๅๅ…TฆบฃINtณ๏J7ฑไ[.๋ƒ+๊ธ๏็BAg$I4m ๅuฺฉ+ P๎s“ึtžนษ๑ฎ!lyคฎ8bcAiีUET}๘vช‚6ฆบฃI.DXS]LUภ{-ฎซ-y\pe=๗.วaง'–ไ‹ฯะ4ตไถH˜B!„ฐ RBมฤฉqs‹lชฏคกฒช€รŽj4Id5zc)Žwณฟฉ›ŽHโบBAg4I,ณ์~๖น์ิ๚)๓yHk:?ฝุม™ซศ†%ๅฌฌ,ข*่ล7v;ฆ>v+ŸCUpฉ iอเ™หป”ีUลิ๘๐9ํŒ฿l˜ส้\ŒpธฝŸM=ฤ,ฆ>z๓๎YXƒฎาI๐…็Oั๙kฎฺณ* SB!„๙E‚”B05Aj\uะห–๙Uฌช,ฆ*8zล”:๖DทdNง/–โT๏่S๋+yฯ‚ัPะIะM’ดp*๐8ฉ ๙(๖บIๆ4~tถอrsล29tำไ‹gณnnN๕'ๆtƒh&G[8ฮKญธฺc‰€๘งท.b‚*\ชJ[8ฮž;๕็7ณฐC†a4๎ศ๖ƒrๆB!„˜>ค„‚ฉ Rใช‚^6ฯซdUe15^|N๛ุSฃ๓๕'า˜&ิxq( mแ8ั$ฉœnู๒8™S่งฤ๋"–ั๘ม™ZGฌwkXw4‰วa็s›VPๆsc9รDตูPmฃ;šษั<ๅีŽ๖_้&ฃyป=~๛bถฬซฤ1ค๗Ÿค/žบัฟ๖‡ฒŠฑใเŽํญrB!„˜zค„‚้ Rใช‚^6ฬญเฆ๊ช ผ๘v์ส่LำฤฎŒึMั2#mแ U่u1งภGฑฯE,“ใŸN]ฅ#bฝ+qzbI|ŸZทœส€‡Œฎำ6’ภeW(๗{pูU่S‘tŽซร1ท๗๓\sน< Sqวb6ีฉ–แŸ‚มdๆ††ฑAฎ”B!„˜ค„‚้ Rใส6ึWpKM)Uมฑ0ฅ*ืๆ(สh:W‡c„ำYโkถWไs1ปภGฑืE4“ใ{'ฏาต^๊ฅ๐ป์|๊๎ๅ”๛=$r?8ยน0›๊+Y^^Hฉ฿ฎฃa*œฮru8ฮก–^ต๔ข๙๓๛๛๑;—ฒพฎ‡ชะ<ๅณ๛N0’ส๐฿{ RB!„ำG‚”BAj\กวษถี\]ย‚’ ฎฑจaษฌFค„‚ Rฎ[ฮฺฺrŠTN'ฃ๋๘ช‚a˜ค5มdšŽp‚ดfไ}˜*ธฉ ๚yœŒคฒŸฃWˆคญwkุ@"Mศํไฏ๎ZJฉฯM,“ใ;ว›๙้ลŽ_๙๚ตsสธซถœEฅz\ืžฬ—ีu†’Y.๔‡yกต#ำsลิงื7pว์RTลฦล(Ÿy๖ุ่x78 RB!„ำG‚”BฟA๊3๋ธ},\Œ๑bkทฬ*กฆภGะ5ฆLs4L $าtDd4รศฯs{EะCuภKศใd8™แG/[r>ฌมDšBฏ‹ฏ]BฉฯM4“ใŽ^แ™ห]o๙พป๋สนซถœ…%zœcว2บฮp2ร๙๛›บ9ู3<ฅ๓ะ†n›5:ฮ.๔Gx๐ูcy9๙๚D“ %„B1}$H !๙คูธ‚[f•ขฺเ|„๗ลeWู2ฏŠf•2ปะOภๅภกุฎ…กd†ถpM7๓.*TฝTฝธ %3|๋ตห$sึ RCษ ล^ฌ]BฑืE$ใฝฬ+ฟ๙‡ธณถœM๓*ฉ/ ปb๊็รโ๙{ฏtqฎ/<ฅใLฮ๗‡๙ไžcฬ„HB!„˜>ค„‚ RŸด’›jJP€sa>๙ฬัkอ๋ฐณu~7ี”0ทะOภํฤกุ0ฬั[ม†“:" ฒบAVห0U๔R]เ%่r0H๓ํื/“สY๏ึฐ‘T†bฏ›ฑ˜"ฏ‹p:หท^ปฬ๓อ=ืฝ ‡ชฐ~n๋๊*จ+๔t;qŽฟดฆำŸHsบg˜M=\ŒL๊๖4n^ษš๊ัqvถ/ฬง๖฿{ RB!„ำG‚”BฟA๊๓›Wฑฆบpฆw„Ÿ=๖Kฏ๑:์lจฏเถYฅฬ- ผ)lduแT–ฎH’”ฆM{˜ช zฉ)๐p9่ง๙ึ๋—ษhV RYสn๖ลyœ„SYศ%ต๔พํe_๑v๛์R๊~!,ฆ5XŠำฝร์ปาM๓plRถ็ั-ซXU5:ฮN๗Ž๐้_1ฮnDค„B!ฆ)!„ ƒิฯ‡‚“=ร<ด๗๘ฏ}ญฎrw]9wืU0'ไ#ไvŽ dดังบuG“$sำฆFƒ”€หN_<อ7_ปdษนŠ"้,ๅ~ถEŽMะW/๒Rk฿;^ๆ๘oทฬ*ฝโอๅภฎุะMHๅ4๚โiNt๑์•.ฺร‰ žวทฎfEeง~ร8ป‘HB!„˜>ค„‚ Ro]อสส"LเD๗์;๑฿ใRึฯญไฮฺ2j Œ…) S#้ แ$Y]'ฅ้0…[^๔2;ไร๋ฐำO๑อื.กึ๛ŠฆsT=ษ- G'hOe๙๚แ๓ผา๎Ÿ’Wเvฒฑพ‚;f—1+ไ#0๖TEอ0Iๆ4zc)Žw ฑทฉ›ฮศฤ„ฉ/n[รŠŠยท5ฮnค„B!ฆ)!„ ƒิฮmkX> Žu ๑นื œชย†น•6ปtl๒l'e4Le๕ั0ีI‘ีu’9ฉ๘9จ z™๒แqุ้‰)ร‚ฟCัLŽš —ฒ€ษP2ร:|#๖E›ๆUr๛์2ชƒ^N;vUA7LYฎh‚c]C์o๊ฆ'–zwใl๛–—bวบirF|๏%H !„BL RBA~)›อฦ—ทฏaiY8ฺ9ศ็Ÿ{๛กภฎุXWWมsส˜_$ไy๓S‘t–žX’TN'™ำ'๕้jUA/s x์*ฑ$Oนdษ๑ฯๆ˜]เ็›ๆSเv0˜ศ๐ตร็yฝspย?+ไv๒[‹jธนฆ„ช ŸรŽชุFรTNฃ3’เhื{.u1œสผฃq๖•ํkX26ฮ^๏เ ฯŸš฿{ RB!„ำ๘๗Ž)!„ศฯ eWl์~Kส ะMxญc€G฿E(ฐwึ–ณ~n๕ลŠ<.\๊WL…ำYzb)ูฉœ1)aชชภKmศฎาM๐อื.cณเxId5j ์X3๏ฺ๖ๅ๓๏šดฯฌxุ6ฟšUUEิŒ๖จุ@7Lโูั0๕jวฯ^๎"šษฝqฆฐs๛šัqf˜้ไฑค„B!ฤไ’ %„ไgrฉ _ฺพ†Eฅฃกเ•๖พx๐๔ป^ฎbณฑvNูต0U่vแฒ+ุxs˜2L“‘Tc็xช.๐2g,HตG|๓ศEิฑญ$™ำ˜[เVี R_}๑งz†'ณg‡|lžWล๊ชb*ๆลณ“๒Yk็”ฑvN‹JC{฿ฆ9dV#–ั่‰%฿U˜ช RUฅu$ฮ“ฏ_FUฌwำ^ZำYXRภWิแwฺ้ง๘›ฮrฎ/<ๅ๋2ฟ$ศฆ๚JVVQๆ๗เถซ(6ศ&ฑt–ฆกฏv ฐฟฉ›ฌnา๛.o]อผโัq๖BK_คq–o$H !„BL RBA~ฉษฃ[WQ_$งj้์wt\i^เ๏M•ซTฅR*IVvฮvปsป: /ณ K\e†ษMำ“ ห ;ภ–xู]Xย๎พคmwปฃsJถ%U)”Jชœ๎ฝ๏Uฒ3ำูvU๕>็ฬ9sfฌาญ็+ฉพ็yžหz๑์mžบ[ุ5aeK€ฐว‰Sื*K๙,›lฑDถd’ฬ—ˆฅs”ฟEx;• ๅวะTF็SูัaqฉBูdu[ฌ๏รc่Lงs|ๅะiฮฯ,ึ์˜Vท5ฑgฐ“๕!Zฝ.\๚จธ˜/1:Ÿโล+ำ์‰Qถฌš^g๕B‚”B!DํHB๊3H…=Nžฺณ™f?%ำbH”ฏฝt๎Ž|๏-]a๖ FXฺูT Sš†ช*฿0cชD,•ฃ๔.ยTwะKศ‡ฎช ว“ท#X ๘kจhZฌm๒แตฝx h*ว—žโา\ฒๆวถฎ=ฤ#ห;YำึD›ืu#*V๗‰งุ?ๅ…๑lฎ้uVkค„B!jG‚”BPŸAชอ็โษ›้ ๙(™&ฯ G๙๚ห็๏่1ีๆแVทฏฯ˜R([6ูR™tฑLถXb*๙ฮยิฒ —พ]Uธ4—ไžWAซ^”L‹๕!พoMnC'šส๒ล็O1Oีอ1nŒ4๓่๒NV/Eล›ยิ|ฎศๅน$๛Gฃ ว“5ฟฮjE‚”B!DํHB๊3HEn~u๗&zƒ> ฆษ3—ฆ๘W/ิไXถt…ูืฮฺถ -ีY7K๛ๅJerฅสSSษ์[ฆžjาT… ณIๆLc)ำฒู ๑=ซ{p๋“ษJOีฑณฌ•ฌlmzรฦ๕…ฒE"W`<‘f0์งลใช๙uvงIB!„จ RBA}ฉ๎&Ÿต‰ž&/๙ฒษ?_šเ_ปTำcZืb๗`„5ํAฺช๛ฝ1L™$ซK๙ e๓›พพ'่ฃฏู‡ฆภน™E๊ิ(ธ‰”ilŠ4๓ซ–แา5&3|ภ)ฎ,ค๋๖˜๊kฏl\฿ึDณ‰CSฑํสŒ)]Uะ5•Bูไ.\ซ๙uvงHB!„จ RBA}ฉ ฯ์@w5H…kษ‘หuqlkƒ์์dM{Vฏ ทฎกU๗˜สVรTชX S๙า0ี๒ั๒กgg๘๓cรธ ฝแฎถูๆ;Wvใา5ฎ-fxjI&3u์๕ตณc ƒๅแ!ท‡ฆ‚ข T฿ื๘B†ฏฟ|žำฑฤพ— %„BQ;ค„‚๚ Rอ~>ตc]นฒษw๎*zlธฎŽqM[]ƒึ-อ˜2tด๊Sน’IถT&S,3•ส’/™๔Uƒ”œŠ%๘ำฃร\FC^3[ปย<พข งฆqe!อS๛N2•ส6ฤฑซŠย๖vv DXัR SŠR™ฉV2-&“YฮN/๐๐็f>ฐ๗ฝ)!„Bˆฺ‘ %„ิg ๘ิŽ๕Drฅ2w๖*qbค.วoUk;"ฌ๏ัแsแ24TEมฌ†ฉtฑDฎlข) 'ฃ๓๑๋—h๑บ๒šูึยcห;1ชA๊ษฝ'˜N็๊=84•kc??ฐฎ—ฎaุ6– ๙ฒษt:วฉX‚ฝรSu๑ม[M‚”B!DํHB๊3Hญlmโฏงร็&[*๓ืgฦ๙ห“cu=ŽCแป#l่ัแwใพiฦTฆTฦPU††iู›Š๓ฏ]$โ๗4ไ5soO+{#šฦx"ล{O0—ษ7๛Xูาฤ'wฌ'โwcฺP4MtUEW•J˜*•™ฮไ9>g๏pด.7nฏ$H !„BิŽ)!„ >ƒิšถ ฟ๒๐::|nาล2ujŒฟ>=ใ9๖ณ{ฐ“ !ฺn<†Žฎ*ุถ Šถอd2วŸ?…ฎ*๘ทl๏6vD04•ั๙O์=ฮ|ถะp๏cM[_ูพŽ๖j๘<=ภฅk๔}๘บฆVgบ•™N็865ฯณ—'นบi๘๛^‚”B!DํHB๊3Hญ๏๑หญฃ็"U(๓฿OŽ๐ฟฮ^mจqํ๙ุ3ิษฆH3m>7ง†ฝІใ)NF็yebหฒ*Lm๏kg{†ฆ2O๒™็Žณ˜/6ตฟฎ=ฤวถ฿ธฮวษQ๖Dู5แ6บ›<๘†ฆRถlฒฅ2ัd–ฃ“q๖ŽL1ฑ˜mุ๛^‚”B!DํHB๊3HmŒ4๓หญฅี๋"U(๑็วG๘๛๓ืr|{‚^[ล‡Vu_ชž ˜–EถdMf9K๐สีส ฆv๔w๐P;บชry.ษgž;FชPjธsณ1าฬ/=ธ–6_ๅ:๛‹ฃŸs•๐t9x|Ew/kฅปษƒท:ำอดm2E“‰ล วฆโƒิถ๎~แ5„=N๓%๘ศ%žป<ีฐc์w|แั- …˜ถอฤb†Bูค+P บZYVูซ(ฯ้X‚—ฏอ/™uฆ๖ urO+šชpa6ษ'Ÿ9Jกl6yนซ+ฬ/<ฐ–oๅ:๛“#—yๆ๒ไทท]{†:ูาฆซษƒ็ฆM๋Sล“‹Y_™a฿ศ‹๙๚Sค„B!jG‚”BPŸA๊พžV~พี4{œ,ไ‹มซ90kุ1น<๕ศf›”L‹็วbJ&A/aƒ ห‰ฯกฃk*V5Lลชa๊•kณไJๅบ S.๏ไe• u~f‘OŠฆษ3—งะT—ฆ]7N]ฃ3เฦ๋ะ๑•h7…ฉูl3ำ ^ป6วbพXำ0๕]+ปฺู‚ œ™^เใOiศ๓๒P_;?w๏*Bี๐๙ปฏ\เลw>Wถ4ฑ{(ย†Žm>7.]C] S๙"ฃ๓)^พ:หแ)Šu4‹L‚”B!DํHB๊3HํŒ๐ำwฏ ่r0Ÿ-๐ต—ฮ๓๊ตู†ใฎ€‡'voขงษKม4y๚า$>‡๒-ญำะ่๐น๑;o„ฉ๋K๙ส&๑Lžณ3 ผte–dก6a๊Cซ–ฑต+ŒœŽ%๘ฤ3G๒ผ์่เg–ยgถภ๏ผ|žWฎพท๋lu[{†:Y฿ขอ็ยฅWbcษดXฬWfL‹qp,Fูช-'AJ!„ขv$H !๕ค[ลOตœ&—ม\ฆภื^:ว๋s ;ฦฝAŸูน๎&/๙ฒษแ)<†–_ใะ5"~~งปฆŒj˜ส—Mๆ2yฮฯ.๒๒ีYนย S฿ณz›;+A๊DtžO?{ฌ!ฯหžกN~jr‚.๑lฏ>วk๏๓:[฿โัๅฌn า๊uโิ5EกX6IไŠŒฬงxnxŠ—ฎิvฦŸ)!„Bˆฺ‘ %„ิg๚ฮ•ุึ!NƒูLž฿~๑วฆโ ;ฦอ~>ตc]นฒษกั†ฆพฃฏ54•Hภ฿aเq่ธ CUฐm(˜&ณ™<็f921วl&Gยิ๗ฎ้acคจฉฯ4hz|E%|œ•๐๙‡ฯrt๒ึ\g[ปย์Œฐช5Hุใภกi( M‹๙\‹ณIล8\ฃ0%AJ!„ขv$H !๐งG‡ํ็วbL%ณusLbMjำ~งมL:ฯoฝx–“ั๙†ใๅ->๙๐z"~นR™—฿รฒ0]S่ x๐:Œ๋๛Lี=ฆŠฆI<[เฬ"G&็ˆฅrท5L}ฺ^6t„ฐc“qžุ{ผ!ฯห‡V-ใ_oฤ_ Ÿ้ลณŸบตืู=m<ิ฿ฮชึ&šNีนฆฮอ,๐ย๘๔{^*๘^IB!„จ RB,ไ‹๖T2ห‰่<Fc\Iคk~L฿ฟถ—8€฿ฉ3ฮ๓‡ฮpf:ัฐcผชต‰?ผžˆ฿MฆXๆ๕‰9ฬ๗ธฎฉDn|ฝฆŽj˜*˜&๓ู"—ๆyๅฺ์m S?ฐฎuํA,เ่ไO๎=ั็ๅ{ื๔๐/7 โw๊ฬค๓ๆ g8ป=ืูC}ํl๏ogEKอn†ฆbCL7ฝc3%H !„BิŽ)!„,ถ-&[2‰ฆฒœ‰-๐XŒ‹ณ‹5;ฆฌ๏ใ#๚๑9tbฉฟ~่ ็fvŒืถ๙ุ๖ut๘ค‹eŽผ ตDืฺ}nี=ฆ|N‡ฆb6…ฒI<[ไr<ษKWf˜N฿บ0ฅ( ?ธฎ—5m• ๕๚ตYžฺฒ!ฯห‡ืUยงฯกK็๘อCg83}{ฏณ๛ฺู3a0์'่rเะ5l{i_ฐสŒฉg/O๖+AJ!„ขv$H !,”lกก* –m“+U๖%:;ณภกฑ้š,•๛่ฦ>ผฎกMๅ๘สมำ\œ[lุ1iๆ—\K›ฯEชPโ่Tำผ5ฟƒ4Uกร๏ฦ๏4๐:>‡~}ชByiฟขEŽMลนบy฿aJS~`}kฺš0mx๕๊,Ÿ?ะ˜A๊‡6๔๓‘๕}x ้tŽฏ:อ๙™้ชสށ๎oง?ไงษU‰‰KณฆำyNว์žโยm รค„B!jG‚”Bไจฝ)าLgภƒฯกฃฉ7žไฯVbฦั่-์๙๘‘อƒ|฿š†ฮT*ห—ž?ลp<ีฐcผฅ3ฬ/>ธ†Vฏ‹dกฤฑษ๘๛ž!๕TUฉ.ๅ3p~งกุฏ(‘+pi.ษ๑hœ+ ผo๓”ฟ7cจ• ตชต ำฒy๙๊,_|TCž—nเรko„ฯ/<ลฅนไ๛NMe๗P'๗๗ถั๒p9ฎ๏ –/›LงsœŠ&ุ;2ลๅ[|\ค„B!jG‚”BPyส^gภรŽ๎๊ำ้๗เsี=nn,Ž'94>อแ๑in๗Oฯ:ฤ๗ฌ๎มญkL&ณ|แภIฦ๊`oซ๗j[w แ5ดx,ๆKœˆฦ)›ทgUU!ไv`ะไ!่บyฟ"‹…|ๅ\™œใ๊Bฯป SN]ใึ๕ฒข%€iูพ2ร—žnศ๓๒แ๓‹N12็รงะxdจ“{–ต2ะ์ฏฦDำ†\ฉฬt:ฯ‰hœg.Oฒ=$H !„BิŽ)!„ ค–{ศํ`็@„{–ตฒฌษS๙`|ำ๒ฏ…|‘หsI^Ÿ˜ใ๙ั(ำบ-ว๔SV๐]+ปq้‹>wเ$W2 ;ฦ๗๕ด๒๓๗ญฆูใd!_ไT,Aฉlึ๏ฉ(เ24ผ†Nฤ๏!่vเ\:—ฆลBฎศ่|Š—ฏอpm1๛ŽgLน ฏํeyK€ฒe๑ย๘4ฟq่LCž—฿บœ๏^ฝฌnยgภiฐk0ยฝm,k๒^ฟส–MฎT&–สqt*ฮแฉ๗}?HB!„จ RBมƒิกณ{(ยถฎ๚›}4นื—•,‹ล|‘๑Dšื'ๆุ7%S,฿าc๚™ปW๒+ปpjW3<ต๏“ษlรŽ๑C}ํฝซน$rEฮN/P(›wๆ—.]รm่t*3ฆœบŠยอ3ฆR›Œ3šHใ1ดท|=ฏฃคร• up,ฦoฝpถ!ฯKฝ†ฯ หมžกN๎๏mฅ+เล็ะัซ3ฆฒฅ2‹ŽNฦู7e*๕๎ RB!„ต#AJ!๘ึAj‰กU6_พwY+Cแภ๕˜PฒlR…‹^น:หมฑ๑lแ–ำฯปŠG—wโิ4ฎ,คyr฿ bฉ\รŽ๑ฮ~ๆ๎•ๆsE.ฬ.+šw๔–fL95ฎ&!—‡ฆข(Pบ)LŠฮ3Oแ|“0ๅw|บ^š”L‹ฃQพz๘\Cž—zŸM.ƒ๏ZนŒปบ[่n๒เ5t4ต๒๐Lฑฬตล,G'็x๚าไปพ๗$H !„BิŽ)!„เญƒิอ๎๋ie็@„กฐŸฐว‰SืP…’i‘.–˜Jf92็เh์}จ๙๛Vณg(‚กiŒ'R๊ฬf๒ ;ฦ,๏ไ'๏ZNะๅ ž-0O’.”ks0ีS†ชา๔rฟ1L-ๆKŒ%RŸšg8žฤฉฟ1L59+Aช/ไงdš์‰๒;/oศ๓า(แณ็ๆัๅlํ ำ๐โuhhŠBูฒIหL&+Q๘™หS,ๆ‹๏่5%H !„BิŽ)!„เฉ%wu…ู1aek€ฐว…Kืะ0ซณ6&“YNF2๗ใ๙…ึฐs ‚กฉŒฮงxb๏qๆoั์ซZ๘Ž]๘]ห 8 f3yฦi’๙RอหะU\šFw“ทซK๙,›ไMห2วiีงBn฿ทถ— ’i๒p”ฏฟ˜A๊แ3อฏ๎=^ืแsY“—=Cl้j&โ๗เฉฮ˜*›ฉB‰k‹^บ:ร‘(‹os}IB!„จ RBมปRK6FšyจฏuํAZฝ.††ชT–eK&ฑTŽ3ำ žqav๑]ฝ๖/=ธ–‡:ะU•‘x’ฯ๛ญhZค๒EฦiŽOอs~v๎€็z*˜&ฯ\šโ๗_ฝะ็ๅๆ๐96Ÿโณ >{C>๊dsg˜vŸ ๗7„ฉ๑Dš—ฎฮผๅnค„B!jG‚”B๐ƒิ’-v tฐพ#Tตq#LๅK&ณู็ฆx~,ฦษ่;zอm_วC}ํ่ชสๅน$Ÿ~๖(้bนaว๘๛ื๖๒ร๐;uฆำy&๋+H-14‡ฎฒฌ:cสกiจสยฦๆS วS฿JwภKม4๙ง‹แk—๒ผาƒkู฿ก-…ฯใ๏xษ[=l๖๓ศ๒N6Eši๓บp:ชeห&Y(1O๒๊ต9žป<๙MOฤ” %„BQ;ค„‚๗ค–๔…|์่๏`Sg3สฬบฆbY6๙ฒI<[เโ"Fข™Œฟๅk}โแ๕<ะ†ฆ*\œM๒ฉg’+™ ;ฦ?ธพฺะฯก3ฮqm1หbฎ~ร‡ฎ)่ี=ฆZฝ.œšVูcสฒษหธ งฆ’+›ร… ๘๕ฦ R{h๕฿>Ÿ;Fบะx3๑Vถ6ฑg0ย†ฅ0ฅkื๗KJŒฮงx๑Je)_ฉฆ$H !„BิŽ)!„เึฉ%~ดsWW ]>งก*ุ@กlฯŽ'ya|šฏฬ๐ญ~z็๎]ึŠฆ*\˜]ไOฅ๘ 3<ษG7๐แuฝxŒส ฉk‹’ 0GQบJo“—V_%Lฉ  ((€iูœŒอ๓๋ฯ0Ÿkผ=พnŸ—ๆ’|๒™cไJ;om{G†:YคีW S@ฑ๚ลั๙4๛Gขผ0>Mู4%H !„Bิ๊๏l RBq๋ƒิ’&—ƒƒ๎^ึBO“ฟำภจnŒ]4-นJ˜zmbŽ็Gcส7f@}vืF๎^ึŠฆภู้>๔Qฌ™#›๙พ5=ธส ฉซ R4GงฎัไฅอWYฆ6/™\š[ไต‰9Œฦ˜k ง!~jว๎๋น>?๙๔ัoZฺึˆ6t„xtykฺšn<ณz฿ลsŽNฦ๙ำใ;๔z^~ !„Bิเฯk RBq๛‚ิทกณg0ย]- 4๛hr90ิJ˜*Yษ|‰๑…สำ๖O‘)–yr๗&ถvท gฆ๘๘ำGzŒb๋r>ดzn]#–ฮqu!Mบะx3qL&่vฐต3|=.ฺPูLปXโฺB†ฃSq๖G"L>ฯอ,๒‰งPถ>8ยށVทiv;ph60ฮ๑Wงฦvวื>/?…B!๎< RBมํRKtUeว@๗๕ด2๖r9q๊jeๆFuำ์‰ล ฏ^›e[W ๋:B(ภฉX‚O>sดกว๘งทญเ;Wvใชฉ๑Dšlƒnา~e!อ๗ฎ้!์qbSูWJS”๋›iง‹%ฆ’YŽLฦy๚าd]?ต๎Wwoโฎj๘<;ณภว๙ฤฟ ่mใกพvVถ6แu่ฤณๆ๔๘ฮ?ผy๙ („Bq็IB๎\บู=หZู5ayุOณวY„Yน>ำฦPU<†Žeœˆฮ๓้g5๔์=ซxlE'NM#šส2žศ4์^Eษ,ญ่คร็&W2Kคq้ีฅ|ฺ๕0U&šส๒๚ฤ๛FขฤRนบ{/ฟถg3[ปย(ภ้X‚O4x๘|;๖ตำ๔ฯๆ9—%{B!„5"AJ!จMZrWW˜‡:XีฺDุSู„YS–~J+`DS9~m฿ ฦ้†ใw฿jŠ`hฑTŽฑDŠ|ƒ>50–ฮฑgจ“vŸ‹Tกฬ89ส™้ป;Y฿ข็ยm่่ชBูฒHหL%ณ›Šณ$ฦฤbฆnห็ูฬๆฮJ:K๐ฉxบ™บ••ญLหๆ๐•พ|๐๔ทอ}/AJ!„ขv$H !๕คBnO=ฒ™มๆ%ำโ๙ฑฟโู7]Uyธฟ๛zฺj๑r9q๊* PดlR…“‹^ฝ6วมฑณ™|M฿ำฏํูฬึฎ0 Kๅธ0ธAJQ6w6r;˜ฯ๙๚ห็Iๆ‹ธ ]ฟ–ฎ)ฌi ฒฆ-Hw“ท:cชฒ๙yพd2“ษs*–เ๙ัุ-บขฯก๓๙Goฬฤ{a|š฿8tๆๆพ— %„BQรฟฉ%H !Dฉ“งูL_ศOษ4ู7ๅw^:ฆžeญ์่`EK€fงฆกช eำ"U,1•ฬql2ฮ๓c1&j๔Dทฯ?บ…M‘fฆำ9.4๐ )CSู t9˜ฯ๘—ฯ“+™84๕}ฝๆ๒p€๕!–Uร”ฎU–๒ๅKefณฮN'ุ;ฝeaชษe๐๙Gถ0X‰wp,ฦoฝp๖ๆพ— %„BQ;ค„‚๚ Rํ>7O๎ูD_ะGั4y๖๒ฟ๛ส…ทบญ]av๔wฐชต‰ฐื…KืะชOt+3•ฬq26ฯ‘(cw๘‰n_|l+;Bุ4~rฺ๋C4น โู_=|ำฒัTๅ}ฟถฎ* …lŠ4ำUฤๆง+ฮd๒œŸY`๏p”3ำ‰๗๕ฝš•๐9ะ์งdZ๒ีร็พm๎{ RB!„ต#AJ!จฟ ี้๗๐ฤ๎๔}L“ง/M๒_^ฝ๘Žฟ~}Gˆ‡๚ฺY฿ขอ๋ยmhจJeXถd2“ฮq:ถภมฑ[ฟ ์[ฒพ๔๘Vึท‡ฐ€ูtŽ๓ ค|NีmANƒนL฿>|…สRพ[ฦ†ญMlŠTfLy—žฎhCพl2—ษsvฆrOอฟงoัโu๑ิžMืgโํ‰๒Ÿ฿b&)!„Bˆฺ‘ %„ิ_๊n๒๒ฤฎ,k๒’/›ำล ่๕K๏๚u›์Œฐก#D‡฿ƒืกก) fuใ์นls๏3jผชข๐•วทฒถ=ˆi7~jr;Xูภ๏4˜อไ๙ํฯขฉ๊m๙^ฆmณบ5ศ๚๖ ฝ!พj˜ฒl(”Mๆฒyฮอ,๒ยXŒ#“๑w๕ฺK3๑zƒ>Jฆษ3—ง๘ฝw0๏ƒB‚”B!DํHB๊/H๕†||f็บ^re“8?9z๙ฝฟ^ะวรํlŽ„้ xฎ๏Oดด l>Wเา\’#Q^Ÿ˜ปๅ๏วะTพุVVท5aZv%„M/4์๕า์q2๖ใwฬค๓|๕๐น[ฒ\๏ญุถอPK€ อ๔…|n SK็๐ย์"Fข๏8Lu<<ฑ๋ฝฯฤktค„B!jG‚”BPAj ูฯงwn ำ๏!W6๙฿็ฎ๐็วF๗๋ถ๛์่`[w ]>งCUฐBูb>W`8žไ๐•Mcข฿nCใ‹neekำฒ‰g œmเ ี๊sั๒ใw๊LWƒ”~›ƒิำถ๙ฺูฆ7่ล๏rเะTlฆP6‰g \œKฒox๊mริฒ&/Ÿฝ3๑•)!„Bˆฺ‘ %„ิ_Zเ“o โw“+•๙ณW๘o'Foู๋๛{†"JOะKภi W—œ•,‹…\‘แx’#“q๖Dษ—อ๗๕|Nƒ/<ฒ…ๅ-•งนล3…;ฒwีําpำ๔แs่ฤR9พ๖า๙;ค–M‹กฐŸญ]-๔VฯกQ}ส_%Lน0ปภ‹ใ3ผtuๆ[พฦญž‰ืh$H !„BิŽ)!„ ‚ิชึ&>๐z"~7™b™ฟ>=ฮ_žปๅ฿วmh์ŒpWW Ca?M.ฦMa*U(1žH๓๚ฤ{‡งHห๏้๛]>๗่f›”L‹™LŽKณษ†ฝ^บš<,k๒โ1tb้_{้๕qปำŠฆษ๒p#!๚C~š\7โbั4™ฯน8ปศแ+3ผxe†›๏ใLผs๎*vl๘ๆพ— %„BQ;ค„‚๚ Rkƒ|l๛::|nาล2yrŒฟ93~พŸฎ*l๏๏เž6†ย~Bn'N]EŠ–MบPbb1รks‹1“ฮฟซืo๑8๙ต=›้oฎ<อm*•c4žjุ๋ฅ'่ฅ+เมm่DSY๓ห็ฏG Z)”Lร~6Fš ๛ บšŠmWยT"_dx.ลก๑/Žฯ`ูvu&z"~ฯm™‰W๏$H !„BิŽ)!„ ‚ิ†Žฟะ:ฺ|.R…2ํฤ๛ี;๒ฝ๏Yึสฮ–ท{œ85 UU(›้b™ษd–ใSqžqm1๓Ž^s้in}AEำไ๊B†ซ ™†ฝ^๚š}DบฦT*หื_:ฎฉuqlน’IoะหหZh๖ฌ๎1/[$๒Fโ)๖DIไŠืรgถt๛fโี+ RB!„ต#AJ!จฟ ตนณ™๘เZZฝ.R…vl„ธpํŽร–ฎ0;๚;XฺDุใฤe่hJeSํLัd*™ๅ๔t‚#QF็฿zถำ7>อm,‘fj1ฐืห`ุO›ฯ[ื˜Hf๘W.ิlษ›ษ^S- „•0ฅณฒOุt:GOะG“ห ],๓Wงฦ๘๋ำใ฿6๗ฝ)!„Bˆฺ‘ %„ิ_ฺฺๆXK‹ืIฒPโOŽ\ๆ้K“59–uํ!ถ๗ทณฎ=Dปฯ…ะPหถษ–Lfา9Nว84{ำ'็๕ฝ|v็FบซOsปO2๛.—ี“ก–m^.]cb1ร๏ฝzกๆK๖Lถdา๔ฒฅ+ฬP8p}ฦ”ข@ูฒัUUศ—L฿ใ#ทuihฝ‘ %„BQ;ค„‚๚ R๗,kๅ฿฿ฟšฐวษBพศฟ~™ฝรS5=ฆf?ป#l่๑{๐:44Eมด!_*3—-p~v‘ƒฃ1ŽMล฿๐ต!Ÿน‘ฎ@e๓์๓ณ ,d‹ {ฝ …ด๛]85ซ‹~• ืŸpWฏ2ล2}!/#aVถนช๛„)ชjPผ IDAT `ู6W2ู+ตใฦๆูงงค ๅ†ฝ^ร:ชA๊สBš๒๊ลบRKR…M๎Yึสฺ๖aฅ๚•-‹๙l‘๑…4‡ฦb›ฆP6?ฐ๗ฝ)!„Bˆฺ‘ %„ิ_ฺ฿ฮฯณАA"Wไw_นภ‹ใำu5fํ>7๗ทณญป…๎&/>งCUฐBู"‘+p9žไฅ+3Lง๓|l๛:"~7™b™ฃSqJๅฦ3๖๑ป14๑Dš?|ญq‚ิ’Tกฤถ๎>ฒพงฎaุ6( Eำ"™/2–Hsx|šƒcำdKๅ}/AJ!„ขv$H !๕คv F๘7Vt;˜ฯ๙ฯ/ใๅซณu9v>งม#Cถuทา๔pื๗S*Y•อณg2yzƒ^ฮสๆูฏึ้{yงnRc๓)๐ต‹8tญแ‡ฎ*}•ฅกEห&S(แ24œš†ข@ษฒIๅ‹Œฬงx}bŽฝรัT˜’ %„BQ;ค„‚๚ R u๒“–t9ˆg |ํ๐น๋ห฿๊•Kืุ5แฎฎ0ห[4U7ฯถํส“๙–6ฯ.”M๑ยงัฐืหPK€ŸCSO๑Gฏ_ฤก5^r๊?w๏*šซ3๑๖Ot;Xำ$์q~S˜บฒแีkณ<7ฯKWfฺุfGซ‚ดxธt (Z6ฉB‰k ^พ:รั(‹๙Rร๗ค„B!jG‚”BPA๊Cซ–๑ฏท โwฬf๒งฯr|jพแฦ๕๎๎v FX฿ขลใDฉFฉฒe“.–ศห,ไ‹,ไŠ 5ใๆๆ uy.ษ=r g.ู๓; อ+บฬg |ํฅ๓ผzํฦrส‘f๖ EXีฺD‹ว…KืPU…’iUยิb†Wฎฮ๒XŒ๙lกแฟ)!„Bˆฺ‘ %„ิ_๚5=หMƒ๘:3้<ฟ๙ยNล ;พjำ ูPู<ฦฒAU*K๙2E“™Lމล,ูR‡Z›ƒUƒ”ฆ*\œM๒ฦฉซ w^Bn?yื š\๑lฏ>ว๋฿bi่ฦH3ป#ฌim"์ญ„)ญบ”/](qe!รัษ9๖D‰7P˜’ %„BQ;ค„‚๚ R^ืหoภ็ะ‰ฅsฦก3œ^hุ๑}จฏŸฝwaทƒ‚i‘ศ๑:t<††ช(XถMถd2“ฮse!iู(u~†ย:n4Uแย์"ztธ!gHตz]ุึ!ี™xฟ}๘วbi่ฦH3๛ฺYืขตฆTๅฦŒทk ŽNลy๚โ$ ๙bฟ RB!„ต#AJ!จฟ ๕C๚๙ศ๚>ผh*วWžๆย์bรŽ๏ฮ~ๆ๎{‰%๐:tZฝ.Bn'^‡†ฆ(˜6ไKeโูW3M หชฟ฿SCแM4ฮอ,๒gว†q5`๊๐ป๙ัอ7–†ๆ g9}๛ฅกkฺ‚์่`}Gˆ6Ÿw5L•,›Lฑฬd2หkณ์Ž2›ษืํ๛— %„BQ;ค„๘่_ฒ็๊่ƒ๓G7 ๐k{q• ๕ๅƒงธ4—lุ๑ฦงŽ%า”M CS นx^Cวmh่šŠeูL“xถภไb–|ูคdZu๓~†ย~:›ผจภน™ุ.ฃ๑‚Tw“‡nฌ, ฎ. =.–†ฎjmb๗`„u!ฺฝ.†ŽVc*],3•ฬrdrŽ#1ฆRูบ{ค„B!jG‚”BTข‚}t2ฮพแh]|pั-ƒ|๏๊†ฮT*หœbd>ีฐใ{๓S็ณEฎ.ฆ)šํฦพKM.Cรm่ธ ‡ช`…ฒE"W`2™EQrjซkจ%@gภƒ œžN๐฿OŒ6ไ’ฝพฺะ}i่ฏ<รน™wฟ4tEK€=C•S7…)ำฒศหL,f96gH”ษd„) RB!„ต#AJ!€ฒeู™b™ฉd๎๚็k‹™šฯOl]ฮ‡V/รญkL$3|แภ)ฦ้†฿›Ÿธ/26Ÿฦดmt๕;Eู6\n]รkธFu“๓’Uyฒ[ถT&[,Mๅ0kดœoy5H)ภฉX‚ฟ:5†ก5ฆๆƒa??ธฎQ R_>xš‹๏ci่P8ภฎม6Eši๓นฏ๏fZ6ูRe)฿‰่<ฯ^žbชย”)!„Bˆฺ‘ %„T‚TeฃสS฿&“ND็ู7\›0๕ำV๐+ปq้ื3|nษšฒ๗๋ๆง.ๆK ว“(Š‚ชผY(ฐ๑ป šœ<†ŽำPo„)ำ"S*“+™ค‹%ขษ_ฮทคNF็๙ณWะฅแฮหส–&พmOuih–/<อๅ[ฐ4ด?ไcฯP'"!"~ฯ7…ฉX*วัฉ8{‡ฃLิ๐บ– %„BQ;ค„ธ8ทhw๘8{V๒๘Š.œšฦ•…4Oํ?Y3Jซ\฿w}iุbพศp<…ฆพ}ภ)[6>‡Nุใฌ,ใำTšŠBeํlฉLพd’*–ˆฅrไKๆy?ห[t<ุภ๑ฉ8็U” Rkฺ‚‹5=ธuฉT–/8ล่-\ฺ๒ฑs0ยๆH3‘€ฯ–๒™Lงsี๐;ลX fJB!„จ RBิ฿ถ๗ vฒ1าLgภsำS฿์7|p~nxŠ+wเƒ๓}๏*]‰Cำ_H๓ไLงs ;พ?ผฑŸจ. [,y‡AjIูดqญ^Cรกi84UU(›ึ๕ูR‹๙"‰\‘Lฑ|[฿ฯŠ๊ )8:็๏ฯ7fZ฿zราะฯ8u[ฎ๏ ƒ6w6ำแwใu่hช๚†S'ฃ๓์‰ัฝา$H !„BิŽ)!„“gm€ž —]6w†้ T>8๋ชJน๚มy&ใd4มพ‘)†ใท๏ƒ๓ฟฟ5ป#šฦx"ลฯg.[hุ๑‘อƒ|฿šสาฐ๗ค–”Mงฎ๖ธ๐:tœšŠCW฿g39ฎ-fษ•สื—๙j+Zt๚=Xภ‘‰9ยต†\ฒท)า†ฅกOํ?y[—ะu๚=์Yakg ๗๕S– นj˜:Kฐo$สp๖?UR‚”B!DํHBnฉ%]ป‡*œ#7^CGื*3:๎ฤ็_|p-;๚;04•‘๙$O8๛7ftไซ3:Nลr~ๆp•ํ๋xฐฏ]Uธ4—ไำฯปํ๛"N7oาžศธฒแVฎp[ Sอ'>‡KW๑Tใ†mไห&๓ู‹Y๒e๓}=•OS†n R/]ญ)ทัxA๊พžึบXฺ๎sณkฐƒmญt๘บฆbY6นฒษL:วู™๖ G97ณpหพฏ)!„Bˆฺ‘ %„ผ}บ๙ƒ๓ฮถuท\S7ฯ่˜อ83x฿œ?นc๗๗ดขฉ f“|๊™ฃไหfรŽ๏า&ํ†V™!uๅอz3MN‡†หะ๑:FuฟชBู"‘/0น˜EUน"๏๖๗`%H๙้ x([6‡ฏฬ๐๗็ฏแu่ w^์kฟพ4tt>ลgŸ;Vำฅกa“ƒ๎YึJW“Ÿร@W์๊lทสŒฉฯฦ8Kผ๏๏'AJ!„ขv$H !๏๑๔ั†฿m_วC}•Mฺ๏tZRถlผทกใะTšŠข(ืรTพฆฆS9rฅoฝDากkt<๔…*A๊ภh”ธ0AศํhจsbZ6.๏ไ64Uแโl’Oึ้าะ หมรืำJOะGภi`ด?ุ\ฆภลูE๖D฿U˜’ %„BQ;ค„‚๗คnเผk0ยo>ฃใย์"๛Fฆ8>๕ๆ3ฆ>๗ศf6w†Q€“ฑŸzฆฑƒิ'^=|ฬg‹Œ/คั๏pZR6m\†Fซทฆœš†CSQU…ฒi‘/›ค %๓Eนโ7อrฟ›พŸ’iฒw$ส?^˜ ์q6ิ9)™฿ตช๛ KC?๑ฬั๗ตแ๛ํๆs่์Œ๐@O=A/~—‡ช`S ฟูf94>อซืf฿๖๕$H !„BิŽ)!„เึฉ%M.ƒ‡๛;ธฟทž&/—ใ]อ่๘ยฃ[ุiเDtžฯ<{ฌกว๗ๆ=ฑๆณEฦitMฉ้1•Mงฎ๖ธ๐:tœšŠCWQ•สำ3E“ูLމล,นฒ‰^…eht<๔}M“g.O๑ฯ'h๑บ๊œM‹๏YฝŒป—ตข)pvz?}ซ.p: U6?๏ UfL้ีe˜Esi)฿"/]™แ๐•™7} RB!„ต#AJ!ธ๕Aj‰วะู=แž6zƒสŒ)Mลถo|p>7ณศ ฿0ฃใKmeCG86็‰ฝวz|o+‘+0–ศ`ิ8H-)›6บฆ๖8๑.CรuS˜ส•*{%า85…Nฟ› ‚i๒๔ฅI๙โ$mพฦ R๙ฒษ‡ื๖ฒตปฒ4๔ฬ๔๚HCฝ—ฎฑgจ“m- …•๐[รญhšฬ็Š\žKrh|šรใำ|ใM.AJ!„ขv$H !ท/H-q:{ช3:๚฿bFวแ+3ผ|e†/?~ฺ๋ƒXภั‰9žwขกว๗ษ=›ธซซ˜ฯฏฃ ตคlฺ( ดTgLน ทกกฉ*ฆU™ู–ศศหZ<.๒e“บ8มำ—&้๐ป๊œไJe>ฒพŸ-]•ฅกงb >ู KC]บฦ๖v๊kง/ไ#่rเะิ๊R>‹Dฎภp<ล‘(/_นฆ$H !„BิŽ)!„เ๖ฉ›?8๏ŠpOw+ƒีŽ๊SK3:†ใIzƒ>บ›<˜6ผvm–ฯํ?ูะใ{๓žX๓นW™š/ู{3Kaชูใฤ็0p้*CGฏ.น,”-tUAืTŠe“u๎*ฯ\šค3เiจs’)–๙่ฆ6U—†žŒฮ๓้_jh*๗wฐฝฏfM.GeVP0-ๆณŽLฦ๙฿็ฎ0ฑ˜• %„BQCค„‚;ค–8uํ}ํl๏ึ3:TUมก*”-›—ฎฮ๐…งz|ฟ๐่6Ešฑฉ,ูปฒฉูฆๆ๏Vภ้ภ๋ะp:CวPEAlๆฺb–ฟ=s…k‹†ึ0็$],๑#›‡ุX]z|*ฮgŸ;ธŸ Meว@๔ด1ะ์'่v`จ•๛k:ใžใŸ/MJB!„จ! RBมRKี4ฃรกฉ7‚0“ฮ๑วฏ_ๆะXŒF‰ๅวทฒพ=t=H]]ศ 5Hฐmธ \บ†ฯat(ีMฮห–E<[`l>อ้X‚ห๑$FuVN=KJ๘ึก๋็ๅศไO๎=๑บฏuUaว@„{–ตฐผ%€วะ‰g ™+<7<%AJ!„ข†$H !ต RKี๗๗ด1๖ำ๊uกVƒ‡iูLgr\žK๒๒ีYล([๓ณ[Uพ๒๘Vึถ1mHd \[lฌ ตฤฒlŠฆลๆฮf\†^๙m(™ฉB‰ซ‹NF ฯ%กŽ฿bฒPโ'๏Zฮฺถส^eฏ_›ๅฉ_๚ฆ์๛;่ z‰g œŠอห’=!„BˆZ&AJ!jค–่ชยwฌ์ๆวถ แsืgI-…ล|‘‘๙/ŽOsplšฒeีุ๊ชสW฿ส๊ถ&Lห&‘+2ฑ˜Amภ Mๅุiฆร๏ฦฒm2E‡ฆเิ5EกdZค %&“YNO'ธ8“คh™ืcฝHJิถฌikยดแีซณ|ภษoซ๛^‚”B!DํHB๊'H4ป<๕ศf†ย~l f%fผ9๓bพศ่|šฃQ^Ÿฎ๋S.]ใKmeekำฒ™ฯ™Lfiะล\ฆภบŽ >7้b™รWfะU…•-M„=N\บ†ช*”M‹LฉฬT2ห‰่<วฆโห~งQ๏#](๑ำwฏdU๕ผผ|u–/>๊๊พ— %„BQ;ค„‚๚ R-^OํูD_ศOษ49;ณHฎTf0์'ไrโิU hZ,Tริ‹Wฆ98ฃhึ฿Œ)ŸC็ neyK€ฒe1Ÿ+MๆP4H-ไŠฌn า๎s‘*”929‡Kื๐์yt%w}็OํUwืฺRki๕ฆnป๏ฦX†8!&ยd&v ž $$มa˜็<99Cกำฃ์CN†yf’ฦ6  ุํ}มฝช7ต๖ๅ๎ทถ_ี๓วฝ’ป—n[Rj}^็p’CิาญบUาอ๛~฿าUคu iCCJWกซ2dIZZE5Yฎโนษ<›ํ‰ศรTล๕๑๋1ุž„๘—Sำ๘โรฯฎฉ๛žAŠˆˆˆ(: RDDhฎ ต.eแ๗^‰ น!๐ภัqำ gp๙บ,ฎํiวึถ Z,บข@’๊ณ‹๒ถ‹ใs%<:6‹๏Ÿ€ใ‹ฆ9ทYSว็nฟ ›2๐D€…šƒษr-ถืJล๕1ุžAGาDษ๑pdถO05พกศฺ“’šSS`žฆjžภLลฦ๓Syป๗*llMร๕'าMWโคl_`sk}๐|ั๑plฎOชฒ๔5พ!I๕cO้,Mฅ)Pd"a๛sU‡gŠxไฬ f*๖ช‡)๘ภ ƒุZ…Ÿ˜ฤ๐๙5u฿3HE‡AŠˆอค๚sI|jฯ่อ&a๛็๐:6ŽŽคน๔5%วCWฺย }ูEz}จ67Qด]œสW๐ร“S๘่$jžู๑tฆLUุุ’‚'fชf*vlฏ?1Kก=i `{8ฑP‚+่_็}ญฅ0ฅยTXš M‘†!_`ฎ๊เ่\ Žอเlกบjaส๑~๚mKก๐{ฃ๘“๙ษšบ๏คˆˆˆˆขร ED„ๆ R๘ไž+ะ“I ๆ ๏Nใ๛'ฆะ–0~๊kKއŽค‰7m่ฤถ๖ ZCต%I‚+”l'๓๋ฉi<|b%ว[๕ใYŸNเำท] น\!0]ฑ1Wuโ{ฑ„๕hุš0ท]œZจภ4Ezญ†ฌก#ฉ+05 Uถ8จฏoป<:[ฤ3“๓81_†ฉ)+zŽ/๐มถc  :6/่…5u฿3HE‡AŠˆอคถดฅ๑๛Cปฑ>@อ๓๑?r?:=ƒKลSr60 ŒQŸ/•ิT$tš\S^P‚โษ…2ž™\ภัูโRดZnž๐มถ7ซ์O|hM๗ RDDDD~ฎf""jฎ 5ุžม}ท๎FwฺBี๓๑?ž;…วฯฮ!cพ๖Vฎล0uMO.๏ฬข=aย8'L•ง๓e[จไxhต \฿ืŽ9ด'อ๓VL•c… ~tz฿=>ผํฎุ๑œปโห๑&ส5V๐็ญ†\ 9KGฎ>?q‘=j‰/B$tํ –ฆภP่Š I’เ‰eวรxฉŠg'๓84S€+dIzรว`๛ฟyำeKณส๙ะลcGืิ}ฯ EDDD)""4Wฺน.‡฿}ห.ฌK™(9>้Qš) ฉซฝJއฌฉใฆ\™Cวb˜’%๘Kซp*xไฬ,FF'0ปณถตg๑๑[wa}ฦBอ˜(UQŒ`–ีrhI!g๊(ิ๊3บ‚ฐTฝ7ย! UFGาDBWก+๕0ฅ4ซŠ็cผXลำ“๓x์\?xCะkžผใผYeร[S๗=ƒQtคˆˆะ\A๊Š๎V|์อ;ะ™2Qr<อSฃ8>W‚๕†\—CรMุฑ๎ลSŠTj\ษ๕p&_มcg็๐ใ˜]ฦงเ]™ร๏ฝe'บาชžภxฑŠฒ๏ ตฑ%ฌฉ!_sq2_^ึ๏ํ‹บ*ฃ-a ฉฉ0T†*C–$aˆj#๊=7™วฃcณจy๋ Sืรวnูyฌฒฟ~๒๘šบ๏คˆˆˆˆขร ED„ๆ RWฏoรoฝ๙rt$M๙ฤ1Œชะ—aธ๕b˜บฎทปบZะ™4aj*ไF˜*ปฦ UwVู฿>5Š๏'งืิ}ฯ EDDD)""4Wบuc>xร6ดX:ๆk.๔ว/ภ๑W'เ”š"ใฦพ\ูŠui MYš[ดฆžš˜วGว/hลิ-๋๐กF`+;ฺไ•@ IDAT>Nๆห๐ƒ๘)SUะ—M"กฉ(:๕RRDฏ% ดQ฿ฦ—ิT$tZckง(4f\=;น€ฃsลฅmŸsUYSรoฟyวyณสฯกฑ5u฿3HE‡AŠˆอคn๗_7ˆœฅcพ๊เห?>ด๊+ŠJŽE–pS'ฎ่jAw&ฑฆDข๊๙˜,ี๐ฤ๘<<6ŽฑBๅฟืะฆ.๚๕๕ภVr}œ˜/!ˆ๑฿ž„ฆข'“@BSQp<œ\แRB!R††S‡ฅฉ0Tyiพ'”\c… žŸว 3yฬUฌK™K[) ถ‡ลทœ]S๗=ƒQtคˆˆะ\A๊Žญ๋๑kื"kj˜ซ:๘ฏ?:ูŠขล0uCoฎ\฿Š๕๓โV>ฉr OMฬใ;วฦqโež8ทwหzผ๏บญศ™:Jއั๙2Bฤ๗oOJืฐ>mมาTฃ๓eHRsผ6_„H่ ฺ&,Mก*ะ’TŸ Vv<Œ—ชx์์๒5ฟtๅ&ด5f•ํ;x_S๗=ƒQtคˆˆะ\A๊ฮmฝธ๗š-ศf*6พใCAด/ฏไxะื๖ถใช๎Vtฅ-$uŠ,Ÿทb๊้‰y|๗๘Žฯฟ8่๛mƒ=๘ีkถ"kjซ>|%คu ™,UAม๑pbพิ4Aj‘/BชŒŽค‰„ฎBW๊a๊™`EวC{ย€ฉ*Xจน๘ณGใแ“k๊พg""""Šƒš+Hฝ๓ฒ>๒U›‘^ R?:ั$ฟซƒ0„.+ุั•ลฎuญXŸฑ–VL!P๓|L”jxfr฿9VS?ปฝ๗\]lEวร๑นๆ 8#k๊่JY0Uวล‰ˆ†š__„ะUm IM…ก*0Tฒ$! Y Iฐ=ฟz™k๊พg""""Šƒš+HŽ~ป+6#mจ˜*ืWH5ใ๏jM‘qyg—wถ 7›@r)L…จySeฯLฮC!๎ฺุƒดกขไx8:[„,วทHต˜::ฯ RMดe๏•๘"„"KhOH4ยิโ๛abบbใเุ,พb OMฬฏ‰๛žAŠˆˆˆ(: RDDhฎ u๗ฎผg๗Fคt“ๅพฃCM}๎ Eฦ`G;ืๅะ›M"uฮVพš็รา†M–Ph)%ฦAชี2ะ™2a( vc…TLว! Ckฌ๔2!5^bHœซ:8<[ภ๗ŽOเฑณs—๔}ฯ EDDD)""4W๚ล+6โ๎H๊*&J5|๙ว‡‡ก+2ถถgฐปซ=™Rบ M‘๋ซปัร๖N,”Pv|จJ<ฃT{ยDGา€ฎฤg…ิK๙ข ฏ๏๋€ฉ)รAB‘๋๏—ใ ฬU]Ÿ/โ๛'ฆ๐ร“Sธ?-0HE‡AŠˆอค•›qืŽ~XšŠ‰R_๑aฤiA‘"I์ศโส๎ฌฯ$5๔๓ถ†EวรLล†/ยุ…ฉŽค‰๖„MQPlากๆยB ถg14T]“ๅrฆ^_อฆศWXจน86Wฤฃcณ9>G—ฬ}ฯ EDDD)""4Wบ๗๊-๘นห๛aฉ ฮซ๘ณGAŽa๑ร:ฒx๛`:“๕ญa!„pEว(ปๆช\?ˆM˜๊LZhKะล˜mู;ถดฅ‘648~€C3(ฒ„ดก!khH่*4นฆผ @ั๖p2_ฦมฑYฆ›ฮซxfb฿ภ‰…rlฮƒQtคˆˆะ\A๊บ้2พฅšขเไB~๐๐า,Ÿธ™ฏ:๘ูํฝธm๓โ๑”๐ง?>KSQq}์๊jมญh„ฉ๚S๙๊3‹ฟพ•oฎ๊ ๆ‰ฆ S๋ำ ดX๕AํEวํPsU–ฑฉ%ซคž™ZXฺš๗r|BWdด% $tฆชภPeศ’„ Q๕ฆJ5<;ต€๏Ÿ˜ยOฆ๓Mคˆˆˆˆขร EDเ๖}ไ%ูfx-yำๅx๋ๆnhŠŒ๓%|ํเ‘ุฎ*ุ.~v{†6vASdŸ/โฯ~|I]]๚WุึžมฎฎlnM#m๊ะd้œ0 ๊๙˜ญฺจนั‡ฉžL9ณคJއั˜ฎา-iXชาุฒทpAื™/Bศฒ„Žค„ฆยTUXš E’ ยถ'0Su๐ยtŸ˜ฤ“ใ๓M{คˆˆˆˆขร ED`hx$'๎G•:L๖›wเึsฮพƒG_uๅJ3+ปฑญทl\U–qtถˆฏ{0HE‡AŠˆ่e P…|ฟ,แžีูฟu8Gf‹ุ๘11 RŽ/๐ฮห๚pc_Yยก™พ๒ศadM‚ํฆึ4ฎ^฿†ญ)dM}i––ิรTล๕ฑPsPv}จซT…๚ฒIdM€ผํโtพหR)]Co6CQP๕|™-.หqd  Mตฆืฎ+ไk.Žฮ๑ุุ,พ;: ว‘ž)"""ข่0HฝŠ(ยิ'†vใฆฮ_>~ บชฤ๒๙A€wl๏ร๕}P$เ๙ฉ<พ๚่ดX๚ุิ–ฦ๎ฎ ถg]ฺส๗b˜ชy๕แ็ืฟจmgฏG6‰L#Hอื\œ-Vb๙d =™4EAอ๓qxถ๐บWHฝTึฟฟฉสH้บz^L,ฺNๅห886‹‡Žฃ์๚‘œ)"""ข่0H]€กแz ธuฅึ์น7๔ืฮOฆ ๘›'วvห^†x็๖>\ ภณS ุw๐Z,ใขฟWล๓ฑ1—ย5=mุา–A‹ฅŸท•ฯjžภlี^ัSนิา–รนชƒ‰R5–๏MฮิัN@Sไ๚ ฉ™ไe>g"‘าUไ, Mก*ะ7Qr<œ-T๐ศ™YŒœ˜ฤlล^๋“AŠˆˆˆ(2 RDDa๏พCฒ,฿ SŸนํJ\ภ๓ำyํSฃฑj?wYฎZ฿ ภำ“ ๘oEฮิ_๗๗ซธ>zณ ิ฿‰MญiดX๚า–F7เxj~}ลิJ„ฉ  ˜ญุ˜,ืb๙พดZึฅ,hŠŒŠ็แศLqลV—๙"„ฅ)hOš็‡)I‚/”\ใลž8;‡‘“+ฌฮช3)"""ข่0Hฝ+ฆp๏Uธฆงpž\ภืŸ=UŽ๏ ฉปvlภ•ญ<51ฟzโุอz-ฯว†\ื๖ดcK[นฦV>Iช?ฑฏโ๚๕x>JŽทlฑe1Hฆห5L—ํXฮjKิƒ”*หจธŽฬW|ปฃ/BชŒŽค‰„ฆBo„)EDขโ๚/ึ๐๔ไช๎๋S2$lhI!mจ!0^จbฎj/๛์ฅีฐ.eก=a@‘%”GๆV/H-๒EYฺf#Lฉฐ4Š$A„!lO`ถ๊เ'ำ๕0๕ไ๘ฒ|)"""ข่0H-ฃๅS๙mืœทข่Ÿล7H9พภ/_ฝ—wf!Bเ‘ำ3๘ฦ๓งิี๛™%วรบ”…7m่ฤึถ ฺ: U$IpE€ฒใกๆีท๓•]ขถษ’„\ )C…BŒช˜ฏ9+พีm%tฅhKP$ ไ๘8:[„ชDsพ!K@kย@Rื`ฉ ,Mชศ‚ถ/0_spdถˆ๏Ÿภมฑูe๙น RDDDDัa""Zท๏เฃpฟ$#{Qฟ”% _|๛5็ญ(๚ๆแฑุžGธ๗๊-ุ‘B๋้ำ งai๊Š์’ใก+mแฦพ ถgัšะa( dฉ„ทช๋ก์๚จy%วป 0ฅH6œคNๅ+ศ๎ชฏ,zฃB=้Zd%ืรัูbS<อQ‚„ฌฉ!กฉ05 M…&K8~€๙šƒcsE๐ไ4~pr ม๘ร EDDDแ็>)"ข•14<’“๗ฃJ€^h˜Rd _|๛ตK+Š=3ƒGฮฦ๖ธBเWฎูŠม๖, ภONแ[GฮยT•U{ ‹aฺ๊žv\ึ™E[ย€S^#LU\5_ hฟz˜R% -)คt ~เฤBEƒฆฤ,H…@_6‰œฅCPt<›+5ีq„2F=LYš‚คฆ. ๗๗‚๙š‹ฃsE<~v฿=>3คˆˆˆˆขร EDดย.&LŠŒ/ผl๏ศB!~tzํฑปBเ}ืbs[~เแัI1‰้ฒš฿“AŠˆˆˆ(: RDDซlh๘ภ€*ไ๛_.Lฅ Ÿฟjlmฏฏ(๚ม‰)Œœ˜Œํฑz"ภฎ฿†-)ธBเกc๘แฉฉฅญWQ*9ฒฆŽ›7t6ถ๒™ฐTฒ,มo„ฉ๚S๙๒ถ ItEF.…„ฆย‡f ฐ}cท .‡ล •3u„ ถ‹“ ๅX g๗EKSะž4—ย”&หK๏[ู๕qถXล“ใsฤ™BๅฟƒQtคˆˆ"๒ra*k๊๘Waskž๐๐‰IเไTlั>xร6lศฅเoว#c3Pคๆ ‹+ฆฎ๋mรฮu-hOึWLษเ!jž…š‹’ใกโ๙ุั™ƒีRฯOเ ฑช3ฑ–รนA*Pจน8•/ว๊iพaจ2ฺ“&’š ]‘กซ  aˆŠ+0^ฌโ™ษy|otฃ๓ฅŸ๚ RDDDDัa""Šุร^‰ ทถ% |v๏Uุุุโ๖ใ๘ืำำฑ=6๘เ ั—Mย๖พyx OŽฯA’š/|”Cว }ํุฑ.‡ฮ”u^˜*ปfส6:’&ฒฆG<;™‡˜Zผ‚”„ุุ’B‹ฅC„@พๆเLพ9fO ๊aJSdด% $u†ชภTeศ’„ Q๕ฆห5<7•วร'&๑T~้฿2HE‡AŠˆจI์w`จ+“ธฎบuq‹ƒว&๐ศ™™ุ“C|่†m่อ$Q๓๙…3xvjกฉ_sษ๑ิUุื]-X—ฒฮส'Kาาฃ3๕ี^–ฯR-–„ศ.ฮชˆaZโ‹’t$M$uฆชยาd(’ถ็cถ๊เ…™ฤใs RDDDDb""j2ว็KCนิŽท~๛ศ8;;฿ƒ ธ ๋ำ ิ<๋…ำ๘ษt!/ฝไxฐ4o๊๏ภฎฎt&MXšบดญ-0WฑqถTยz|‹‹—ฉ๙š‹๑b’๛g1LีWLiฐT–ฆ@UdAGฬU-โ;วฦ๗|mืŒ๐ทั๊c""jRืื7ษNNmHZ<ศ๘เ ฑ>cกโ๚๘ฦ๓งpdถซc(9LMม}ธฎท=™ไFน ย๚Vพš ๆ๙(ุn,ยิ‹Aส€˜ฏ:˜(ี.‰ u๕'!cjHh ,M…ฅฉะe !ว0Wต๗๔f“#mCDDDมg5)"ขๆvว๏Mฺg{2V_ย”,บa;บาสฎ|๖$Žฟฬp้8(9ึฅ,ๆ›.Cช๑>, AจyUฯGพๆ"@๓}RนFšญุ˜ฎุ—์=ศ,MERS‘ะจฒŒแU–G๘[†ˆˆˆh๕1Hลฤb˜ฺิš๊ำ9ฏY‘d|่ฦmX—2Qr|รณ'prก๗ oป๘[wa}&?Qt<่ŠŒ„ฆ.=ญๆ ุพ@ู๕1[ฑ†€ช4ืาฃ 1ะ’Fฮาแ‰ณU3—pZ:๎0Dฺะ5uXชŠ0 ๗ไ,}„ฟ]ˆˆˆˆVƒQŒ ไฒZ๘;™ไGR†šj๖ืซส2~ใฦm่Hš(9๎้QŒชฑ=Eวร}ท๎BWสBี๓๑ƒ“Sธผ3Irf)oช,มร”฿”a*B ดฆ3 xB`บbcฎ๊ฌ™๛ศB$4Oœ๓หWoแo"""ขีว EDCCร#นžคr_ปi|ธ™ร”ฉส๘ภ๕๕ Ut<อ“ว1Qชล๖ผW\ฟ๗–K[ŽอยT่ŠŒึ„”ฎ"กi็?อจy>*ฎ™& Sa ดค3uธB`ฒTร‚ํฎฉ{จๆ |ๆม'๘”="""ขˆ0HลXณ‡)KS๑๋ืข-a oป๘ซ'ŽวzkXอ๘ท์\ฺ‚๘ิภิ€,Ihฑ๊ม,Mฉ*Pd Aย๖ุž@ู๕0[ฑDฆฮ RŽ˜(ีP`""""ขUฤ EDt >0ะe&จ3iผท™Ÿง ๏ฟnญ–Ž|อลวaก฿ญaฎ๐ฑ7๏@gชพ๑๘| Ž/`จสy_งHR}NQc€ถฅ)S!?hฬ˜๒0Sฑซฆฮ Rถ/0^ฌขไzk๊žฑ=O3HE†AŠˆ่าla*c่x฿u[ัb้Xจน๘‹วŽฦz%Ž๘ํ7๏Xš‰5:_†ใ ่๊ห™_ S ญพbสj<อB8~}๘๙โV>„ซฆฮ R5_เlฑŠ ƒญ")"ขKPณ„ฉVหภฏ\ณ9Kว|ีมื;‚ฒใว๖ผaˆ฿บyฺ“ŠŽ‡S eุพ€๖O=”% YCGBWaฉ‹aJBŽ/P[ๅ0๕า 5Vจ ๊๙k๊ฑ}O?ภ EDDD)"ขKุ}†ฺ’ๆ็ึฅฌ›ฃSํI๗^ฝYSรlลมื‰u๘|ไๆหั–0Pฐ=œษW`๛โ‚’ YS;gล”บฆ\ภ๖|Tผ๚S๙<ฑra*M-)dM5ฯว้|ถk๊p|O1HE๗ูšAŠˆ่าU˜๊L™ธ็๊-ศf*6พ๖่ิ๘†U–๐oบ ญ!ํc…j=HษŽร”ฅ)Hh*šฺ~^SŽ/Pq=ฬTx"X๖0„ภฆึฒ†Žช็ใdพ Ok๊žpŸzเq)"""ขˆ0Hญ!{๗ส%ฬ/ญO[;W#Lญฯ$๐๏ฏ„ดกaบlใฯใ eจ ~ใฦํKCฺฯ*pEE~}มH†„ดก!กฟฆTEF„pDืจx>ฆห๖ฒ†ฉ 6ทึWH•]'ส๐)""""Z= RDDkะ๛ผ7mhŸํษX}+ฆDbcK ๏ฝbRบŠษr ่‘XฏฤIh*>xร6ดX:๒ถ‡ำ๙2 „;‘ิSI]i<™O…ึSK+ฆผ๚Œ)ืใa*€อmidM %วว‰…‚5๖yภเ RDDDDQa""ZรริฆึTŸƒน/–lฯโ฿๎@RW1Qชแซ†ˆ๑฿ดกแื "g้(ุ.Nๅ+Ai™vิ…!OๅS`้0%KCภ ‚ฅง๒อVl8o LีWH-)ว็Kk๎ฺg""""Šƒแ=๐๛ปRึง 5ตœฮu9ผ{วXšŠ‰R_}๔HฌWโไLฟvVไLรษ…Vโhยศ˜,UAาะ–ยTžเŠF˜ช:ฐ=qQa* Hภฦ\ 9KGั๑0บƒ”'|’AŠˆˆˆ(2 RDDษ๕$•๛ฺMใรหฆjžภ5=m๘7—๗รRœ-V๑ีGว๚ต& ๊5[‘55l'V6ไ„าบ†„ฆ ฉซH่๔ล0ิท๒U=™Š}มa*Bศฒ„ นZ,}UŽฃy"ฤ'ฟƒQDคˆˆ่<หฆชž๚:๐ฮํ}0Uc… พ๚่‘e…sŸXด=ŒฎRศ C ฅซีR ’บ]‘†/ฮ˜บะ0ๅ!tEF_6ู˜…ๅโิBkํำ€/B|‚AŠˆˆˆ(2 RDD๔ฒรT›ฉ๕ >/ปnX‡;ท๕ยPœ.T๐•GA•ๅุž“๎ด…pีfคW9H- ‚iSCRSai*Rz}๘9P฿"้Šืร\ีEี๕_6Ly"„ฉส่ษ&—žxชPมZ๛<เ!>q€AŠˆˆˆ(* RDD๔ช†† t™‰?๊L๏ฝ˜0Ut<ถนwl]CQp2_ฦW= -ฦAช/›ฤ{ฏุ„ดกข`ืg/Eฑโk1LYชŠคฎ ฅkKกฯ ๊aช๊๚˜ฏ9(ป>ิs่Šบ"/mู›ฏน8/ฏน๋šAŠˆˆˆ(Z RDDtA.6Lๅmo์มอะ'J๘ฺมฃ็ล‘ธhIแ=ป7"ฅซ(8.F็ส‘nAAˆดก!ีx"_RW๋[๙๐โŠฉšW~^i„)วPe›าศ™:ๆซŽฯ—–VZญ"๑๛ RDDDD‘a""ข‹RSๆ—;“ึฏฆๆซyY๖l๊†ฆศ8>_ฤ_>~ RŒ‡HmnMใ๎]Hh๊าำ้šแpD"ฉซhKฐTฆ&/ญDsƒŽภ๖|LWmŠ,aฐ=‹ฌฉaฎ๊เ๐L ]]Sื1ƒQดคˆˆ่uูป๏ภP[าบ”u๓ห…ฉูŠw๏ภ[6ฎƒ*ห86Wฤ฿ำ+ แ!สއช็ฃ๊๙(9d้า S!€“AŠˆˆˆ(* RDDดข\|T๎fr!๛็Oล๚Xฎํiวืว6H@อ๐ƒื๗vภิˆ €„ะd’xAˆŠ๋กโ๚จyEวฝdริ๏1HE†AŠˆˆVœ+‚“๓ป'G?–2ิT\ใ๚vฑu=4EA1ฆAส๖„ธฒป –ช`กๆเ่\ ฝฺูtEฉ‡) ๊๙จx5ฯGั๖pฉu))"""ข่0Hัชษ๕$•๛ฺMใรq S7๕wเถอKAjtฎปyKถ/ CยฮฎXช‚๙šƒcsEช‚Vห@สะj @—ธAˆช็ฃๆ๚(ป>สฎwษ\ RDDDDัa""ขU74<’๋ตค/ด&ฬฆ -6ฏ๛ๆ ุณฉš"วv…”ใ hฒŒห:s0A๊๘\ a}š,MEgาDBWa( tE†,KK+ฆชฎJcฦT1HE‡AŠˆˆ"34|` หLQgาxoยิ[ึแึM]Peน๑”ฝR์ๆ+น~CUฐญ#CQ0WupฆP9/ฌ-?oOšH6ย”ฆศ%ภBิ<ฟ>cส(ุnlฏ?)"""ข่0HQไโฆ†6uแ–u็ฉ2bถcฎะTlmKCWฬีœษW^vฅ—/B่ชŒ๖„คฎมT:LีŸสฯ0ล EDDD)""jw ?xe‹กืu)๋ๆf Sทm๎ฦอ:กศŠŽ‡ัน2d9^็ุ!R†ŠM-)hŠ‚๙ชำ…๊ซ†5_„P IIM…ฅีgLษง๒ีผ๚๙*ž’ํ!@<>[0HE‡AŠˆˆšฮ}†ฺ’ๆ็š-Lพe=n๊๏h)ฃsลุ 5๗ƒCร†\ š"ืท์ๅหt/ SฆฆยPd(S5O ๆืท๓A“ฦ`""""Šƒ5ญf Soฺƒ๚; H@ั๑p|ฎ%fAJ!r–Žพlช,cฎjใtพrQวแ‹Š,กณ1cสT่ชฅฑ•ฯ๖ลy3ฆš5L1HE‡AŠˆˆšฟxเ]YS{2V_”a๊g{p]_d%ืร๑น2”˜mู ‚- =™TYฦlลฦ™Bๅu…ตล0ีž4jฬ˜2Tฒ$A4ยTีจy>๒Mฆคˆˆˆˆขร EDDฑqว๏Mฺgฃ S?ปญื๔ถCF}…ิ‰๙R์ถ์!ะž0ะถ ศf+N*P฿ภq๘"„,)IMƒฅ)0^ฒbชๆ T=๙š43ฆคˆˆˆˆขร EDDฑณฆ6ตฆ๚๔U\ข๔ฮํ}ธบง €ผํโ๔+<ฎ™…!ะ‘4ฑ.eพค๒จส?_„e4fLฝฆTY‚ิgLูพ@ู๕0[q†X–Ÿ๛z1HE‡AŠˆˆb๋฿ํ๗>7I~$eจฉU๙y—๗ใส๎V@พๆโLฑ)†็ญ3iข#eA‘€™Šำ๙*ดe Cพ!I๕0•า5ชK“กศ2D ๆจy~=LU ข0ล EDDD)""Šตกแ‘\ORนฏ4>ผาa๊] ˜ฏ:/UcyฮบSZ“&dณUง๒eฌฤJ3_„€t$Lค ฆชยT๋Oๅ ย5/€x*฿tล^๕0ล EDDD)""บ$ฌF˜z๗Ž ุีี‚ภlลฦdนหsี“N %a@jว้Bฺ n}๔Eˆ!:“’บ KSai๕แ็Aย๖ุ^}+฿Lล^ตญ| RDDDDัa""ขKสะ๐.3๑GIใฝห9๘\๐๎ุน.‡ภLูฦt%~A* พ\9S€ฦS๖–wห+๑E3วโS๙,Mฅ)ญ|๕แ็Žb˜ ‚• S RDDDDัa""ขKาr‡)Y’๐ ;7`วบDL•j˜ญฺฑ;/A ไ’ศš๚าJฏฑb๕ =e๏b-nๅkOผฆLUชิร”๋ ิฮ~.‚pEยƒQtคˆˆ่’ถ\aJ‘$ฝk—uf!‚ฅๆkN์ฮG„ุุšFฦะ" R็}ณ $4–ชภิ๊Oๅ B,…ฉŠ๋cฆb/{˜b""""Šƒญ {๗jKšŸ[—ฒn~=aJSdยฮl๏ศ@!ฮซศn์ฮƒBljฉภ\ลฦูbŠํ๓eHศZ:,Uฉว)MY~๎๚l_ ์๚˜ญฺ๐ล๒„))"""ข่0Hัš๒zร”ก*๘…0ุž„8Sจ ไxฑ;ล •55ˆฐy‚ิ"Y’1๊๘’Zžป•ฯ*ฎ‡้สS RDDDDัa""ข5้bร”ฅฉx๗Ž ฺุž8ตPFล๓cw็ฉ ฤ\ีiช ตH–$ค ษฦjฉ„ฆBUdAWิWL-nๅ๓D๐บยƒQtคˆˆhMปcƒ๗ฆ ํณ=ซ๏ียTRฏฉอmx"ภษ…jพˆ๑พ\/V!7Y:Wึะ‘ะ๋QสิThฒ„0ฤyajถjร๕/.L1HE‡AŠˆˆ/†ฉMญฉ>]‘๊ž64ตc6ตฆแ‰ว็‹pEปใ|1H้๐ƒ๓UใฅšธGB9Cฏฏ–าUX0„€'8B ์๘˜ซฺp.0L1HE‡AŠˆˆ่๏๙๛‡฿฿•ฒ8eจฉs๛ฌฉใฎhIรGๆŠA†žค<`กๆ`ขTƒ$ล็า†KS‘ิU$ฯ SฎเŠท๒ฝV˜b""""ŠƒัK ไz’ส}ํฆ๑แล0ีb้๘๙0Kม/L)~ว„ภฦ–ิRšซ:˜*ว+H-J้Rzcฦ”ฎAWd„a}ฦ”ใ T=ำๅWS RDDDDัa"""z‹a*ญชฟ1ุžIฟkG?6ไRp„ภ๓S๙ฆ~!ฮRณUำ;Žm †@ฺP‘ะ5$รฯuEF,…ฉŠ็cฎโ ๆ‰๓ยƒQtคˆˆˆ^ระ๐HnCZ๛่พ๕Šฯ๔e“ฐ}g&`จr์Ž% –rฆWฬTฬVํุฟGA"ณ8cJS‘าUจr๑‚Ž โy˜ฏบ(9. Ua""""Šƒั:™/๔e’๗พธ็ฉ‰9Xšปcxišฎุ˜ซ:—ฬ{!าฆKUll้ำaXSฎPu}L•k๘o๓เฏฝm„W6ั๊c"""บHeืxุ่ฤฺฦ/&๕xEฉ๐œ-{Ž_R๓5็’{‚ Dาะั5˜šŒิโŒ)ิท๒aˆƒcณ{nุ5ย+šˆˆˆh๕1HฝNCร๎์๚U๋ฎK˜:7HูB`บdcมv.ู๗H!Rบ๚โv>]…กิท๒9"ุcชสฏd"""ขีว EDD๔ํw`่ฮํ}_บฎท}gณ‡ฉ06ถฆ5tุพภTน†ผํ^๒๏Q=LihM่ฐT†*C•ๅ=Š,๐ &"""Z} RDDDห$.aj %ฌกม๖&ห5ึ@Zดธb 5๔=๋3‰^นDDDDซAŠˆˆh™ํw`่๖มo๊๏ุุŒajqห^อ˜(ีPt5๗ซxr|nฯงzๅฏX"""ขีว EDDดB๎ุเฝC›บพxห@gg3…ฉล U๕&JU”oอฝ7๓Uyไ้=ฝ๏ํ#ผR‰ˆˆˆVƒั [ Sทo้๎Td)ฺ?6ถค‘15T]๑Rewํฉ…š‹/|AŠˆˆˆ(ฒฯฅ RDDDซใณ฿}๚‹ทm๎-KSดศ๐ุุšFฦะPu}œ-UQq5๗^lŸ.ƒQdŸKคˆˆˆVฯะ๐H๎žฝ_lหE˜’ acK SCล๓qถPEี[{Aชh{๘wŸb""""ŠƒQข Sฒ$a WReืวูB5_ฌน๓_r<ง๏0HE…AŠˆˆ(BCร#น_ฺัื;r๏Xม็Š$aC.‰Œฉฃ์๘+V`ฏม Uv}|๖ก'คˆˆˆˆ"ย EDDิ†† ฝ}ใ]น=+ฆTYBถฑBส๑qบP+ึ^ชธ>AŠˆˆˆ(2 RDDDMdฅร”&ห่ห&‘15”\g๒k3Hี<ฯ<๘ƒQDคˆˆˆšะะ๐Ÿา๗Oื๕ถ๏\ฎ0†€ก*่ษZศ:Jއำ๙ ผ Xs็ื๖>อ EDDD)""ข&ถw฿ก;ท๗}i9ยT–ฆ`}:ฌฉกh{8S(ร ึgว๘ิ RDDDDQa"""ЁๅS"‘ะT๔dศŠŽ‡ำ ๘แฺ[!ๅ๚เวคˆˆˆˆ"ย EDD#{โwฝus๗Wo่์ผุ0ๅ!š‚พliฃพB๊Tพ ฑ? ธ"ภ|›AŠˆˆˆ(* RDDD1tว๏ฺิ๕ล‹ S~ยR๔็RH*Jއ ek๐ณ€'|’AŠˆˆˆ(2 RDDD1ถฆn฿าฉศาซ~ญ'BชŒMญiคteวว่BiM)_„๘ฤทc""""Šƒั%เ“=๙ๅ;ท๖ผ฿าํ•พฦ4Yฦ`{ MEล๓1:WB€5ค‚Ÿ8ภ EDDD)""ขKฤะ๐H๎žฝ_lห๕raส๑ศฐฃ3KSQ๓}-!\ƒAJ!~ŸAŠˆˆˆ(2 RDDD—˜W SŽ/HุีีKU`๛‡g k๒aˆ๛พล EDDD)""ขKิะ๐H๎๎ํ฿ุ•“ิUุž\ั SUP๓Ž0H๐J!"""Z} RDDD—ธกแwo฿ธส๎–=๓Uoฝคฯ ญมs„ภ}฿:ศ EDDD)""ข5bh๘ภภo^w๙o\ก(จz>ŽฮืไนCเใ RDDDD‘a"""Zcชž?ค+สฝ๕Tพผ&ฯƒQดคˆˆˆึจ#ณลก๕“ำ|cวUI]]sว{฿d""""Š ƒัทw฿ก;ท๗}้บ๖k)L1HE‡AŠˆˆˆw์๐กM]_ผe ณs-„))"""ข่0HัyึJ˜b""""Šƒฝฌล0u๛–๎NE–.นใc""""Šƒฝขกแ‘อฝูฯ฿นต็–ฆh—าฑ1HE‡AŠˆˆˆ^ำะ๐H๎žฝ_lหuฉ„))"""ข่0Hั[ S[Zำ๏‰๛|))"""ข่0HัE>0p๗๖๛wwๅ๖ฤ5L1HE‡AŠˆˆˆ^ท8‡))"""ข่0Hั64|`เฎมฏ_ตพํ๚ธ„))"""ข่0Hัฒูป๏ภะ๛พt]o๛ฮfS RDDDDัa"""ขe‡0ล EDDD)"""Z1{๗บ}ฐw›๚;66[˜b""""Šƒญธ;๖?x๏ะฆฎ/2ะูู,aŠAŠˆˆˆ(: RDDDดjริํ[บ;YŠ๔ต0HE‡AŠˆˆˆV๙sŸผyC็g,Mัขz RDDDDัa"""ขH ไ๎ู๛•มถฬ]Q„))"""ข่0HQคข S RDDDDัa"""ขฆ04<’๛ฅฝฝ#๗Žี|ฮ EDDD)"""j*Cร๎พq๎ฎž• S RDDDDัa"""ขฆดาaŠAŠˆˆˆ(: RDDDิิ†† –พบฎท}็r†))"""ข่0HQ,์w`่ฮํ}_Zฎ0ล EDDD)"""Š•ๅ S RDDDDัa"""ขXฺป๏ภะ[ท๔|–ฮฮืฆคˆˆˆˆขร EDDDฑvว๏ฺิ๕ล‹ S RDDDDัa"""ขKยb˜บ}Kwง"Kฏ๙๕ RDDDDัa"""ขKส'z๒หwnํyฟฅ)ฺซ}ƒQtคˆˆˆ่’34<’ปgw๏W2wฝR˜b""""Šƒ]ฒ^-L1HE‡AŠˆˆˆ.yCร#นปทwcwWnฯโเs)"""ข่0Hัš14|`เ๎ํ๗฿ิ฿พ‡AŠˆˆˆ(: RDD;vL 0๐ฅ …ฑ2† Fค ฅ.ฒp'!c€฿9๏็˜™yฏ๕ฉะ3คHRค )R†)C €”!@ส eH2คHRค )Rj%ุ๚^fhIENDฎB`‚sparse-0.17.0/docs/logo.svg000066400000000000000000000545651501262445000155230ustar00rootroot00000000000000 sparse-0.17.0/docs/operations.md000066400000000000000000000176131501262445000165400ustar00rootroot00000000000000# Operations on [`sparse.COO`][] and [`sparse.GCXS`][] arrays ## Operators [`sparse.COO`][] and [`sparse.GCXS`][] objects support a number of operations. They interact with scalars, [`sparse.COO`][] and [`sparse.GCXS`][] objects, [scipy.sparse.spmatrix][] objects, all following standard Python and Numpy conventions. For example, the following Numpy expression produces equivalent results for both Numpy arrays, COO arrays, or a mix of the two: ```python np.log(X.dot(beta.T) + 1) ``` However some operations are not supported, like operations that implicitly cause dense structures, or numpy functions that are not yet implemented for sparse arrays. ```python np.linalg.cholesky(x) # sparse cholesky not implemented ``` This page describes those valid operations, and their limitations. **[`sparse.elemwise`][]** This function allows you to apply any arbitrary broadcasting function to any number of arguments where the arguments can be [`sparse.SparseArray`][] objects or [`scipy.sparse.spmatrix`][] objects. For example, the following will add two arrays: ```python sparse.elemwise(np.add, x, y) ``` !!! warning Previously, [`sparse.elemwise`][] was a method of the [`sparse.COO`][] class. Now, it has been moved to the [sparse][] module. **Auto-Densification** Operations that would result in dense matrices, such as operations with [Numpy arrays][`numpy.ndarray`] raises a [ValueError][]. For example, the following will raise a [ValueError][] if `x` is a [`numpy.ndarray`][]: ```python x + y ``` However, all of the following are valid operations. ```python x + 0 x != y x + y x == 5 5 * x x / 7.3 x != 0 x == 0 ~x x + 5 ``` We also support operations with a nonzero fill value. These are operations that map zero values to nonzero values, such as `x + 1` or `~x`. In these cases, they will produce an output with a fill value of `1` or `True`, assuming the original array has a fill value of `0` or `False` respectively. If densification is needed, it must be explicit. In other words, you must call [`sparse.SparseArray.todense`][] on the [`sparse.SparseArray`][] object. If both operands are [`sparse.SparseArray`][], both must be densified. **Operations with NumPy arrays** In certain situations, operations with NumPy arrays are also supported. For example, the following will work if `x` is [`sparse.COO`][] and `y` is a NumPy array: ```python x * y ``` The following conditions must be met when performing element-wise operations with NumPy arrays: * The operation must produce a consistent fill-values. In other words, the resulting array must also be sparse. * Operating on the NumPy arrays must not increase the size when broadcasting the arrays. ## Operations with [`scipy.sparse.spmatrix`][] Certain operations with [`scipy.sparse.spmatrix`][] are also supported. For example, the following are all allowed if `y` is a [`scipy.sparse.spmatrix`][]: ```python x + y x - y x * y x > y x < y ``` In general, operating on a [`scipy.sparse.spmatrix`][] is the same as operating on [`sparse.COO`][] or [`sparse.GCXS`][], as long as it is to the right of the operator. !!! note Results are not guaranteed if `x` is a [scipy.sparse.spmatrix][]. For this reason, we recommend that all Scipy sparse matrices should be explicitly converted to [`sparse.COO`][] or [`sparse.GCXS`][] before any operations. ## Broadcasting All binary operators support [broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html). This means that (under certain conditions) you can perform binary operations on arrays with unequal shape. Namely, when the shape is missing a dimension, or when a dimension is `1`. For example, performing a binary operation on two `COO` arrays with shapes `(4,)` and `(5, 1)` yields an object of shape `(5, 4)`. The same happens with arrays of shape `(1, 4)` and `(5, 1)`. However, `(4, 1)` and `(5, 1)` will raise a [`ValueError`][].If densification is needed, ## Element-wise Operations [`sparse.COO`][] and [`sparse.GCXS`][] arrays support a variety of element-wise operations. However, as with operators, operations that map zero to a nonzero value are not supported. To illustrate, the following are all possible, and will produce another [`sparse.SparseArray`][]: ```python np.abs(x) np.sin(x) np.sqrt(x) np.conj(x) np.expm1(x) np.log1p(x) np.exp(x) np.cos(x) np.log(x) ``` As above, in the last three cases, an array with a nonzero fill value will be produced. Notice that you can apply any unary or binary [`sparse.COO`][] arrays, and [`numpy.ndarray`][] objects and scalars and it will work so long as the result is not dense. When applying to [`numpy.ndarray`][] objects, we check that operating on the array with zero would always produce a zero. ## Reductions [`sparse.COO`][] and [`sparse.GCXS`][] objects support a number of reductions. However, not all important reductions are currently implemented (help welcome!). All of the following currently work: ```python x.sum(axis=1) np.max(x) np.min(x, axis=(0, 2)) x.prod() ``` [`sparse.SparseArray.reduce`][] This method can take an arbitrary [`numpy.ufunc`][] and performs a reduction using that method. For example, the following will perform a sum: ```python x.reduce(np.add, axis=1) ``` !!! note This library currently performs reductions by grouping together all coordinates along the supplied axes and reducing those. Then, if the number in a group is deficient, it reduces an extra time with zero. As a result, if reductions can change by adding multiple zeros to it, this method won't be accurate. However, it works in most cases. **Partial List of Supported Reductions** Although any binary [`numpy.ufunc`][] should work for reductions, when calling in the form `x.reduction()`, the following reductions are supported: * [`sparse.COO.sum`][] * [`sparse.COO.max`][] * [`sparse.COO.min`][] * [`sparse.COO.prod`][] ## Indexing [`sparse.COO`][] and [`sparse.GCXS`][] arrays can be [indexed](https://numpy.org/doc/stable/user/basics.indexing.html) just like regular [`numpy.ndarray`][] objects. They support integer, slice and boolean indexing. However, currently, numpy advanced indexing is not properly supported. This means that all of the following work like in Numpy, except that they will produce [`sparse.SparseArray`][] arrays rather than [`numpy.ndarray`][] objects, and will produce scalars where expected. Assume that `z.shape` is `(5, 6, 7)` ```python z[0] z[1, 3] z[1, 4, 3] z[:3, :2, 3] z[::-1, 1, 3] z[-1] ``` All of the following will raise an `IndexError`, like in Numpy 1.13 and later. ```python z[6] z[3, 6] z[1, 4, 8] z[-6] ``` **Advanced Indexing** Advanced indexing (indexing arrays with other arrays) is supported, but only for indexing with a *single array*. Indexing a single array with multiple arrays is not supported at this time. As above, if `z.shape` is `(5, 6, 7)`, all of the following will work like NumPy: ```python z[[0, 1, 2]] z[1, [3]] z[1, 4, [3, 6]] z[:3, :2, [1, 5]] ``` **Package Configuration** By default, when performing something like `np.array(COO)`, we do not allow the array to be converted into a dense one and it raise a [`RuntimeError`][]. To prevent this, set the environment variable `SPARSE_AUTO_DENSIFY` to `1`. If it is desired to raise a warning if creating a sparse array that takes no less memory than an equivalent desne array, set the environment variable `SPARSE_WARN_ON_TOO_DENSE` to `1`. ## Other Operations [`sparse.COO`][] and [`sparse.GCXS`][] arrays support a number of other common operations. Among them are [`sparse.dot`][], [`sparse.tensordot`][] [`sparse.einsum`][], [`sparse.concatenate`][] and [`sparse.stack`][], [`sparse.COO.transpose`][] and [`sparse.COO.reshape`][]. You can view the full list on the [API reference page](../../api/). !!! note Some operations require zero fill-values (such as [`sparse.COO.nonzero`][]) and others (such as [`sparse.concatenate`][]) require that all inputs have consistent fill-values. For details, check the API reference. sparse-0.17.0/docs/quickstart.md000066400000000000000000000031351501262445000165410ustar00rootroot00000000000000# Getting Started ## Install If you haven't already, install the `sparse` library ```bash pip install sparse ``` ## Create To start, lets construct a sparse [`sparse.COO`][] array from a [`numpy.ndarray`][]: ```python import numpy as np import sparse x = np.random.random((100, 100, 100)) x[x < 0.9] = 0 # fill most of the array with zeros s = sparse.COO(x) # convert to sparse array ``` These store the same information and support many of the same operations, but the sparse version takes up less space in memory ```python >>> x.nbytes 8000000 >>> s.nbytes 1102706 >>> s ``` For more efficient ways to construct sparse arrays, see documentation on [Construct sparse arrays][construct-sparse-arrays]. ## Compute Many of the normal Numpy operations work on [`sparse.COO`][] objects just like on [`numpy.ndarray`][] objects. This includes arithmetic, [`numpy.ufunc`][] operations, or functions like tensordot and transpose. ```python >>> np.sin(s) + s.T * 1 ``` However, operations which map zero elements to nonzero will usually change the fill-value instead of raising an error. ```python >>> y = s + 5 ``` However, if you're sure you want to convert a sparse array to a dense one, you can use the ``todense`` method (which will result in a [`numpy.ndarray`][]): ```python y = s.todense() + 5 ``` For more operations see the [operations][operators] or the [API reference page](../../api/). sparse-0.17.0/docs/roadmap.md000066400000000000000000000077701501262445000160030ustar00rootroot00000000000000# Roadmap For a brochure version of this roadmap, see [this link](https://docs.wixstatic.com/ugd/095d2c_ac81d19db47047c79a55da7a6c31cf66.pdf). ## Background The aim of PyData/Sparse is to create sparse containers that implement the ndarray interface. Traditionally in the PyData ecosystem, sparse arrays have been provided by the `scipy.sparse` submodule. All containers there depend on and emulate the `numpy.matrix` interface. This means that they are limited to two dimensions and also donโ€™t work well in places where `numpy.ndarray` would work. PyData/Sparse is well on its way to replacing `scipy.sparse` as the de-facto sparse array implementation in the PyData ecosystem. ## Topics * More storage formats * Better performance/algorithms * Covering more of the NumPy API * SciPy Integration * Dask integration for high scalability * CuPy integration for GPU-acceleration * Maintenance and General Improvements ## More Storage Formats In the sparse domain, you have to make a choice of format when representing your array in memory, and different formats have different trade-offs. For example: * CSR/CSC are usually expected by external libraries, and have good space characteristics for most arrays * DOK allows in-place modification and writes * LIL has faster writes if written to in-order. * BSR allows block-writes and reads The most important formats are, of course, CSR and CSC, because they allow zero-copy interaction with a number of libraries including MKL, LAPACK and others. This will allow PyData/Sparse to quickly reach the functionality of `scipy.sparse`, accelerating the path to its replacement. ## Better Performance/Algorithms There are a few places in scipy.sparse where algorithms are sub-optimal, sometimes due to reliance on NumPy which doesnโ€™t have these algorithms. We intend to both improve the algorithms in NumPy, giving the broader community a chance to use them; as well as in PyData/Sparse, to reach optimal efficiency in the broadest use-cases. ## Covering More of the NumPy API Our eventual aim is to cover all areas of NumPy where algorithms exist that give sparse arrays an edge over dense arrays. Currently, PyData/Sparse supports reductions, element-wise functions and other common functions such as stacking, concatenating and tensor products. Common uses of sparse arrays include linear algebra and graph theoretic subroutines, so we plan on covering those first. ## SciPy Integration PyData/Sparse aims to build containers and elementary operations on them, such as element-wise operations, reductions and so on. We plan on modifying the current graph theoretic subroutines in `scipy.sparse.csgraph` to support PyData/Sparse arrays. The same applies for linear algebra and `scipy.sparse.linalg`. ## CuPy integration for GPU-acceleration CuPy is a project that implements a large portion of NumPyโ€™s ndarray interface on GPUs. We plan to integrate with CuPy so that itโ€™s possible to accelerate sparse arrays on GPUs. [](){#completed} # Completed Tasks ## Dask Integration for High Scalability Dask is a project that takes ndarray style containers and then allows them to scale across multiple cores or clusters. We plan on tighter integration and cooperation with the Dask team to ensure the highest amount of Dask functionality works with sparse arrays. Currently, integration with Dask is supported via array protocols. When more of the NumPy API (e.g. array creation functions) becomes available through array protocols, it will be automatically be supported by Dask. ## (Partial) SciPy Integration Support for `scipy.sparse.linalg` has been completed. We hope to add support for `scipy.sparse.csgraph` in the future. ## More Storage Formats GCXS, a compressed n-dimensional array format based on the GCRS/GCCS formats of [Shaikh and Hasan 2015](https://ieeexplore.ieee.org/document/7237032), has been added. In conjunction with this work, the CSR/CSC matrix formats have been are now a part of pydata/sparse. We plan to add better-performing algorithms for many of the operations currently supported. sparse-0.17.0/examples/000077500000000000000000000000001501262445000147115ustar00rootroot00000000000000sparse-0.17.0/examples/__init__.py000066400000000000000000000000001501262445000170100ustar00rootroot00000000000000sparse-0.17.0/examples/elemwise_example.py000066400000000000000000000040051501262445000206070ustar00rootroot00000000000000import importlib import operator import os import sparse from utils import benchmark import numpy as np import scipy.sparse as sps LEN = 10000 DENSITY = 0.001 ITERS = 3 rng = np.random.default_rng(0) if __name__ == "__main__": print("Elementwise Example:\n") for func_name in ["multiply", "add", "greater_equal"]: print(f"{func_name} benchmark:\n") s1_sps = sps.random(LEN, LEN, format="csr", density=DENSITY, random_state=rng) * 10 s1_sps.sum_duplicates() s2_sps = sps.random(LEN, LEN, format="csr", density=DENSITY, random_state=rng) * 10 s2_sps.sum_duplicates() # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) s1 = sparse.asarray(s1_sps.asformat("csc"), format="csc") s2 = sparse.asarray(s2_sps.asformat("csc"), format="csc") func = getattr(sparse, func_name) # Compile & Benchmark result_finch = benchmark(func, args=[s1, s2], info="Finch", iters=ITERS) # ======= Numba ======= os.environ[sparse._ENV_VAR_NAME] = "Numba" importlib.reload(sparse) s1 = sparse.asarray(s1_sps) s2 = sparse.asarray(s2_sps) func = getattr(sparse, func_name) # Compile & Benchmark result_numba = benchmark(func, args=[s1, s2], info="Numba", iters=ITERS) # ======= SciPy ======= s1 = s1_sps s2 = s2_sps if func_name == "multiply": func, args = s1.multiply, [s2] elif func_name == "add": func, args = operator.add, [s1, s2] elif func_name == "greater_equal": func, args = operator.ge, [s1, s2] # Compile & Benchmark result_scipy = benchmark(func, args=args, info="SciPy", iters=ITERS) np.testing.assert_allclose(result_numba.todense(), result_scipy.toarray()) np.testing.assert_allclose(result_finch.todense(), result_numba.todense()) np.testing.assert_allclose(result_finch.todense(), result_scipy.toarray()) sparse-0.17.0/examples/hits_example.py000066400000000000000000000036731501262445000177560ustar00rootroot00000000000000import os from typing import Any import graphblas as gb import graphblas_algorithms as ga import numpy as np import scipy.sparse as sps from numpy.testing import assert_allclose os.environ["SPARSE_BACKEND"] = "Finch" import sparse # select namespace xp = sparse # np jnp Array = Any def converged(xprev: Array, x: Array, N: int, tol: float) -> bool: err = xp.sum(xp.abs(x - xprev)) return err < xp.asarray(N * tol) class Graph: def __init__(self, A: Array): assert A.ndim == 2 and A.shape[0] == A.shape[1] self.N = A.shape[0] self.A = A @sparse.compiled() def kernel(hprev: Array, A: Array, N: int, tol: float) -> tuple[Array, Array, Array]: a = hprev.mT @ A h = A @ a.mT h = h / xp.max(h) conv = converged(hprev, h, N, tol) return h, a, conv def hits_finch(G: Graph, max_iter: int = 100, tol: float = 1e-8, normalized: bool = True) -> tuple[Array, Array]: N = G.N if N == 0: return xp.asarray([]), xp.asarray([]) h = xp.full((N, 1), 1.0 / N) A = xp.asarray(G.A) for _ in range(max_iter): hprev = h a = hprev.mT @ A h = A @ a.mT h = h / xp.max(h) if converged(hprev, h, N, tol): break # alternatively these lines can be compiled # h, a, conv = kernel(h, A, N, tol) else: raise Exception("Didn't converge") if normalized: h = h / xp.sum(xp.abs(h)) a = a / xp.sum(xp.abs(a)) return h, a if __name__ == "__main__": coords = (np.array([0, 0, 1, 2, 2, 3]), np.array([1, 3, 0, 0, 1, 2])) data = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]) A = sps.coo_array((data, coords)) G = Graph(A) h_finch, a_finch = hits_finch(G) print(h_finch, a_finch) M = gb.io.from_scipy_sparse(A) G = ga.Graph(M) h_gb, a_gb = ga.hits(G) assert_allclose(h_finch.todense().ravel(), h_gb.to_dense()) assert_allclose(a_finch.todense().ravel(), a_gb.to_dense()) sparse-0.17.0/examples/matmul_example.py000066400000000000000000000031241501262445000202750ustar00rootroot00000000000000import importlib import os import sparse from utils import benchmark import numpy as np import scipy.sparse as sps LEN = 100000 DENSITY = 0.00001 ITERS = 3 rng = np.random.default_rng(0) if __name__ == "__main__": print("Matmul Example:\n") a_sps = sps.random(LEN, LEN - 10, format="csr", density=DENSITY, random_state=rng) * 10 a_sps.sum_duplicates() b_sps = sps.random(LEN - 10, LEN, format="csr", density=DENSITY, random_state=rng) * 10 b_sps.sum_duplicates() # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) a = sparse.asarray(a_sps) b = sparse.asarray(b_sps) @sparse.compiled() def sddmm_finch(a, b): return a @ b # Compile & Benchmark result_finch = benchmark(sddmm_finch, args=[a, b], info="Finch", iters=ITERS) # ======= Numba ======= os.environ[sparse._ENV_VAR_NAME] = "Numba" importlib.reload(sparse) a = sparse.asarray(a_sps) b = sparse.asarray(b_sps) def sddmm_numba(a, b): return a @ b # Compile & Benchmark result_numba = benchmark(sddmm_numba, args=[a, b], info="Numba", iters=ITERS) # ======= SciPy ======= def sddmm_scipy(a, b): return a @ b a = a_sps b = b_sps # Compile & Benchmark result_scipy = benchmark(sddmm_scipy, args=[a, b], info="SciPy", iters=ITERS) # np.testing.assert_allclose(result_numba.todense(), result_scipy.toarray()) # np.testing.assert_allclose(result_finch.todense(), result_numba.todense()) # np.testing.assert_allclose(result_finch.todense(), result_scipy.toarray()) sparse-0.17.0/examples/mttkrp_example.py000066400000000000000000000026401501262445000203210ustar00rootroot00000000000000import importlib import os import sparse from utils import benchmark import numpy as np I_ = 1000 J_ = 25 K_ = 1000 L_ = 100 DENSITY = 0.0001 ITERS = 3 rng = np.random.default_rng(0) if __name__ == "__main__": print("MTTKRP Example:\n") B_sps = sparse.random((I_, K_, L_), density=DENSITY, random_state=rng) * 10 D_sps = rng.random((L_, J_)) * 10 C_sps = rng.random((K_, J_)) * 10 # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) B = sparse.asarray(B_sps.todense(), format="csf") D = sparse.asarray(np.array(D_sps, order="F")) C = sparse.asarray(np.array(C_sps, order="F")) @sparse.compiled() def mttkrp_finch(B, D, C): return sparse.sum(B[:, :, :, None] * D[None, None, :, :] * C[None, :, None, :], axis=(1, 2)) # Compile & Benchmark result_finch = benchmark(mttkrp_finch, args=[B, D, C], info="Finch", iters=ITERS) # ======= Numba ======= os.environ[sparse._ENV_VAR_NAME] = "Numba" importlib.reload(sparse) B = sparse.asarray(B_sps, format="gcxs") D = D_sps C = C_sps def mttkrp_numba(B, D, C): return sparse.sum(B[:, :, :, None] * D[None, None, :, :] * C[None, :, None, :], axis=(1, 2)) # Compile & Benchmark result_numba = benchmark(mttkrp_numba, args=[B, D, C], info="Numba", iters=ITERS) np.testing.assert_allclose(result_finch.todense(), result_numba.todense()) sparse-0.17.0/examples/sddmm_example.py000066400000000000000000000034611501262445000201060ustar00rootroot00000000000000import importlib import os import sparse from utils import benchmark import numpy as np import scipy.sparse as sps LEN = 10000 DENSITY = 0.00001 ITERS = 3 rng = np.random.default_rng(0) if __name__ == "__main__": print("SDDMM Example:\n") a_sps = rng.random((LEN, LEN - 10)) * 10 b_sps = rng.random((LEN - 10, LEN)) * 10 s_sps = sps.random(LEN, LEN, format="coo", density=DENSITY, random_state=rng) * 10 s_sps.sum_duplicates() # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) s = sparse.asarray(s_sps) a = sparse.asarray(np.array(a_sps, order="F")) b = sparse.asarray(np.array(b_sps, order="C")) @sparse.compiled() def sddmm_finch(s, a, b): return sparse.sum( s[:, :, None] * (a[:, None, :] * sparse.permute_dims(b, (1, 0))[None, :, :]), axis=-1, ) # Compile & Benchmark result_finch = benchmark(sddmm_finch, args=[s, a, b], info="Finch", iters=ITERS) # ======= Numba ======= os.environ[sparse._ENV_VAR_NAME] = "Numba" importlib.reload(sparse) s = sparse.asarray(s_sps) a = a_sps b = b_sps def sddmm_numba(s, a, b): return s * (a @ b) # Compile & Benchmark result_numba = benchmark(sddmm_numba, args=[s, a, b], info="Numba", iters=ITERS) # ======= SciPy ======= def sddmm_scipy(s, a, b): return s.multiply(a @ b) s = s_sps.asformat("csr") a = a_sps b = b_sps # Compile & Benchmark result_scipy = benchmark(sddmm_scipy, args=[s, a, b], info="SciPy", iters=ITERS) np.testing.assert_allclose(result_numba.todense(), result_scipy.toarray()) np.testing.assert_allclose(result_finch.todense(), result_numba.todense()) np.testing.assert_allclose(result_finch.todense(), result_scipy.toarray()) sparse-0.17.0/examples/sparse_finch.ipynb000066400000000000000000000407441501262445000204310ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Finch backend for `sparse`\n", "\n", "\n", " \"Open\n", " to download and run." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#!pip install 'sparse[finch]==0.16.0a9' scipy\n", "#!export SPARSE_BACKEND=Finch\n", "\n", "# let's make sure we're using Finch backend\n", "import os\n", "\n", "os.environ[\"SPARSE_BACKEND\"] = \"Finch\"\n", "CI_MODE = bool(int(os.getenv(\"CI_MODE\", default=\"0\")))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import importlib\n", "import time\n", "\n", "import sparse\n", "\n", "import matplotlib.pyplot as plt\n", "import networkx as nx\n", "\n", "import numpy as np\n", "import scipy.sparse as sps\n", "import scipy.sparse.linalg as splin" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tns = sparse.asarray(np.zeros((10, 10))) # offers a no-copy constructor for NumPy as scipy.sparse inputs\n", "\n", "s1 = sparse.random((100, 10), density=0.01) # creates random COO tensor\n", "s2 = sparse.random((100, 100, 10), density=0.01)\n", "s2 = sparse.asarray(s2, format=\"csf\") # can be used to rewrite tensor to a new format\n", "\n", "result = sparse.tensordot(s1, s2, axes=([0, 1], [0, 2]))\n", "\n", "total = sparse.sum(result * result)\n", "print(total)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example: least squares - closed form" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "y = sparse.random((100, 1), density=0.08)\n", "X = sparse.random((100, 5), density=0.08)\n", "X = sparse.asarray(X, format=\"csc\")\n", "X_lazy = sparse.lazy(X)\n", "\n", "X_X = sparse.compute(sparse.permute_dims(X_lazy, (1, 0)) @ X_lazy)\n", "\n", "X_X = sparse.asarray(X_X, format=\"csc\") # move back from dense to CSC format\n", "\n", "inverted = splin.inv(X_X) # dispatching to scipy.sparse.sparray\n", "\n", "b_hat = (inverted @ sparse.permute_dims(X, (1, 0))) @ y\n", "\n", "print(b_hat.todense())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Benchmark plots" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ITERS = 1\n", "rng = np.random.default_rng(0)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.style.use(\"seaborn-v0_8\")\n", "plt.rcParams[\"figure.dpi\"] = 400\n", "plt.rcParams[\"figure.figsize\"] = [8, 4]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def benchmark(func, info, args) -> float:\n", " start = time.time()\n", " for _ in range(ITERS):\n", " func(*args)\n", " elapsed = time.time() - start\n", " return elapsed / ITERS" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## MTTKRP" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"MTTKRP Example:\\n\")\n", "\n", "os.environ[sparse._ENV_VAR_NAME] = \"Numba\"\n", "importlib.reload(sparse)\n", "\n", "configs = [\n", " {\"I_\": 100, \"J_\": 25, \"K_\": 100, \"L_\": 10, \"DENSITY\": 0.001},\n", " {\"I_\": 100, \"J_\": 25, \"K_\": 100, \"L_\": 100, \"DENSITY\": 0.001},\n", " {\"I_\": 1000, \"J_\": 25, \"K_\": 100, \"L_\": 100, \"DENSITY\": 0.001},\n", " {\"I_\": 1000, \"J_\": 25, \"K_\": 1000, \"L_\": 100, \"DENSITY\": 0.001},\n", " {\"I_\": 1000, \"J_\": 25, \"K_\": 1000, \"L_\": 1000, \"DENSITY\": 0.001},\n", "]\n", "nonzeros = [100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000]\n", "\n", "if CI_MODE:\n", " configs = configs[:1]\n", " nonzeros = nonzeros[:1]\n", "\n", "finch_times = []\n", "numba_times = []\n", "finch_galley_times = []\n", "\n", "for config in configs:\n", " B_shape = (config[\"I_\"], config[\"K_\"], config[\"L_\"])\n", " B_sps = sparse.random(B_shape, density=config[\"DENSITY\"], random_state=rng)\n", " D_sps = rng.random((config[\"L_\"], config[\"J_\"]))\n", " C_sps = rng.random((config[\"K_\"], config[\"J_\"]))\n", "\n", " # ======= Finch =======\n", " os.environ[sparse._ENV_VAR_NAME] = \"Finch\"\n", " importlib.reload(sparse)\n", "\n", " B = sparse.asarray(B_sps.todense(), format=\"csf\")\n", " D = sparse.asarray(np.array(D_sps, order=\"F\"))\n", " C = sparse.asarray(np.array(C_sps, order=\"F\"))\n", "\n", " @sparse.compiled(opt=sparse.DefaultScheduler())\n", " def mttkrp_finch(B, D, C):\n", " return sparse.sum(B[:, :, :, None] * D[None, None, :, :] * C[None, :, None, :], axis=(1, 2))\n", "\n", " # Compile\n", " result_finch = mttkrp_finch(B, D, C)\n", " # Benchmark\n", " time_finch = benchmark(mttkrp_finch, info=\"Finch\", args=[B, D, C])\n", "\n", " # ======= Finch Galley =======\n", " os.environ[sparse._ENV_VAR_NAME] = \"Finch\"\n", " importlib.reload(sparse)\n", "\n", " B = sparse.asarray(B_sps.todense(), format=\"csf\")\n", " D = sparse.asarray(np.array(D_sps, order=\"F\"))\n", " C = sparse.asarray(np.array(C_sps, order=\"F\"))\n", "\n", " @sparse.compiled(opt=sparse.GalleyScheduler(), tag=sum(B_shape))\n", " def mttkrp_finch_galley(B, D, C):\n", " return sparse.sum(B[:, :, :, None] * D[None, None, :, :] * C[None, :, None, :], axis=(1, 2))\n", "\n", " # Compile\n", " result_finch_galley = mttkrp_finch_galley(B, D, C)\n", " # Benchmark\n", " time_finch_galley = benchmark(mttkrp_finch_galley, info=\"Finch Galley\", args=[B, D, C])\n", "\n", " # ======= Numba =======\n", " os.environ[sparse._ENV_VAR_NAME] = \"Numba\"\n", " importlib.reload(sparse)\n", "\n", " B = sparse.asarray(B_sps, format=\"gcxs\")\n", " D = D_sps\n", " C = C_sps\n", "\n", " def mttkrp_numba(B, D, C):\n", " return sparse.sum(B[:, :, :, None] * D[None, None, :, :] * C[None, :, None, :], axis=(1, 2))\n", "\n", " # Compile\n", " result_numba = mttkrp_numba(B, D, C)\n", " # Benchmark\n", " time_numba = benchmark(mttkrp_numba, info=\"Numba\", args=[B, D, C])\n", "\n", " np.testing.assert_allclose(result_finch.todense(), result_numba.todense())\n", "\n", " finch_times.append(time_finch)\n", " numba_times.append(time_numba)\n", " finch_galley_times.append(time_finch_galley)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(nrows=1, ncols=1)\n", "\n", "ax.plot(nonzeros, finch_times, \"o-\", label=\"Finch\")\n", "ax.plot(nonzeros, numba_times, \"o-\", label=\"Numba\")\n", "ax.plot(nonzeros, finch_galley_times, \"o-\", label=\"Finch - Galley\")\n", "ax.grid(True)\n", "ax.set_xlabel(\"no. of elements\")\n", "ax.set_ylabel(\"time (sec)\")\n", "ax.set_title(\"MTTKRP\")\n", "ax.set_xscale(\"log\")\n", "ax.set_yscale(\"log\")\n", "ax.legend(loc=\"best\", numpoints=1)\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## SDDMM" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"SDDMM Example:\\n\")\n", "\n", "configs = [\n", " {\"LEN\": 5000, \"DENSITY\": 0.00001},\n", " {\"LEN\": 10000, \"DENSITY\": 0.00001},\n", " {\"LEN\": 15000, \"DENSITY\": 0.00001},\n", " {\"LEN\": 20000, \"DENSITY\": 0.00001},\n", " {\"LEN\": 25000, \"DENSITY\": 0.00001},\n", " {\"LEN\": 30000, \"DENSITY\": 0.00001},\n", "]\n", "size_n = [5000, 10000, 15000, 20000, 25000, 30000]\n", "\n", "if CI_MODE:\n", " configs = configs[:1]\n", " size_n = size_n[:1]\n", "\n", "finch_times = []\n", "numba_times = []\n", "scipy_times = []\n", "finch_galley_times = []\n", "\n", "for config in configs:\n", " LEN = config[\"LEN\"]\n", " DENSITY = config[\"DENSITY\"]\n", "\n", " a_sps = rng.random((LEN, LEN))\n", " b_sps = rng.random((LEN, LEN))\n", " s_sps = sps.random(LEN, LEN, format=\"coo\", density=DENSITY, random_state=rng)\n", " s_sps.sum_duplicates()\n", "\n", " # ======= Finch =======\n", " print(\"finch\")\n", " os.environ[sparse._ENV_VAR_NAME] = \"Finch\"\n", " importlib.reload(sparse)\n", "\n", " s = sparse.asarray(s_sps)\n", " a = sparse.asarray(a_sps)\n", " b = sparse.asarray(b_sps)\n", "\n", " @sparse.compiled(opt=sparse.DefaultScheduler())\n", " def sddmm_finch(s, a, b):\n", " return s * (a @ b)\n", "\n", " # Compile\n", " result_finch = sddmm_finch(s, a, b)\n", " # Benchmark\n", " time_finch = benchmark(sddmm_finch, info=\"Finch\", args=[s, a, b])\n", "\n", " # ======= Finch Galley =======\n", " print(\"finch galley\")\n", " os.environ[sparse._ENV_VAR_NAME] = \"Finch\"\n", " importlib.reload(sparse)\n", "\n", " s = sparse.asarray(s_sps)\n", " a = sparse.asarray(a_sps)\n", " b = sparse.asarray(b_sps)\n", "\n", " @sparse.compiled(opt=sparse.GalleyScheduler(), tag=LEN)\n", " def sddmm_finch_galley(s, a, b):\n", " return s * (a @ b)\n", "\n", " # Compile\n", " result_finch_galley = sddmm_finch_galley(s, a, b)\n", " # Benchmark\n", " time_finch_galley = benchmark(sddmm_finch_galley, info=\"Finch Galley\", args=[s, a, b])\n", "\n", " # ======= Numba =======\n", " print(\"numba\")\n", " os.environ[sparse._ENV_VAR_NAME] = \"Numba\"\n", " importlib.reload(sparse)\n", "\n", " s = sparse.asarray(s_sps)\n", " a = a_sps\n", " b = b_sps\n", "\n", " def sddmm_numba(s, a, b):\n", " return s * (a @ b)\n", "\n", " # Compile\n", " result_numba = sddmm_numba(s, a, b)\n", " # Benchmark\n", " time_numba = benchmark(sddmm_numba, info=\"Numba\", args=[s, a, b])\n", "\n", " # ======= SciPy =======\n", " print(\"scipy\")\n", "\n", " def sddmm_scipy(s, a, b):\n", " return s.multiply(a @ b)\n", "\n", " s = s_sps.asformat(\"csr\")\n", " a = a_sps\n", " b = b_sps\n", "\n", " result_scipy = sddmm_scipy(s, a, b)\n", " # Benchmark\n", " time_scipy = benchmark(sddmm_scipy, info=\"SciPy\", args=[s, a, b])\n", "\n", " finch_times.append(time_finch)\n", " numba_times.append(time_numba)\n", " scipy_times.append(time_scipy)\n", " finch_galley_times.append(time_finch_galley)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(nrows=1, ncols=1)\n", "\n", "ax.plot(size_n, finch_times, \"o-\", label=\"Finch\")\n", "ax.plot(size_n, numba_times, \"o-\", label=\"Numba\")\n", "ax.plot(size_n, scipy_times, \"o-\", label=\"SciPy\")\n", "ax.plot(size_n, finch_galley_times, \"o-\", label=\"Finch Galley\")\n", "\n", "ax.grid(True)\n", "ax.set_xlabel(\"size N\")\n", "ax.set_ylabel(\"time (sec)\")\n", "ax.set_title(\"SDDMM\")\n", "ax.legend(loc=\"best\", numpoints=1)\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Counting Triangles" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"Counting Triangles Example:\\n\")\n", "\n", "configs = [\n", " {\"LEN\": 10000, \"DENSITY\": 0.001},\n", " {\"LEN\": 15000, \"DENSITY\": 0.001},\n", " {\"LEN\": 20000, \"DENSITY\": 0.001},\n", " {\"LEN\": 25000, \"DENSITY\": 0.001},\n", " {\"LEN\": 30000, \"DENSITY\": 0.001},\n", " {\"LEN\": 35000, \"DENSITY\": 0.001},\n", " {\"LEN\": 40000, \"DENSITY\": 0.001},\n", " {\"LEN\": 45000, \"DENSITY\": 0.001},\n", " {\"LEN\": 50000, \"DENSITY\": 0.001},\n", "]\n", "size_n = [10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000, 50000]\n", "\n", "if CI_MODE:\n", " configs = configs[:1]\n", " size_n = size_n[:1]\n", "\n", "finch_times = []\n", "finch_galley_times = []\n", "networkx_times = []\n", "scipy_times = []\n", "\n", "for config in configs:\n", " LEN = config[\"LEN\"]\n", " DENSITY = config[\"DENSITY\"]\n", "\n", " G = nx.gnp_random_graph(n=LEN, p=DENSITY)\n", " a_sps = nx.to_scipy_sparse_array(G)\n", "\n", " # ======= Finch =======\n", " print(\"finch\")\n", " os.environ[sparse._ENV_VAR_NAME] = \"Finch\"\n", " importlib.reload(sparse)\n", "\n", " a = sparse.asarray(a_sps)\n", "\n", " @sparse.compiled(opt=sparse.DefaultScheduler())\n", " def ct_finch(a):\n", " return sparse.sum(a @ a * a) / sparse.asarray(6)\n", "\n", " # Compile\n", " result_finch = ct_finch(a)\n", " # Benchmark\n", " time_finch = benchmark(ct_finch, info=\"Finch\", args=[a])\n", "\n", " # ======= Finch Galley =======\n", " print(\"finch galley\")\n", " os.environ[sparse._ENV_VAR_NAME] = \"Finch\"\n", " importlib.reload(sparse)\n", "\n", " a = sparse.asarray(a_sps)\n", "\n", " @sparse.compiled(opt=sparse.GalleyScheduler(), tag=LEN)\n", " def ct_finch_galley(a):\n", " return sparse.sum(a @ a * a) / sparse.asarray(6)\n", "\n", " # Compile\n", " result_finch_galley = ct_finch_galley(a)\n", " # Benchmark\n", " time_finch_galley = benchmark(ct_finch_galley, info=\"Finch Galley\", args=[a])\n", "\n", " # ======= SciPy =======\n", " print(\"scipy\")\n", "\n", " def ct_scipy(a):\n", " return (a @ a * a).sum() / 6\n", "\n", " a = a_sps\n", "\n", " # Benchmark\n", " time_scipy = benchmark(ct_scipy, info=\"SciPy\", args=[a])\n", "\n", " # ======= NetworkX =======\n", " print(\"networkx\")\n", "\n", " def ct_networkx(a):\n", " return sum(nx.triangles(a).values()) / 3\n", "\n", " a = G\n", "\n", " time_networkx = benchmark(ct_networkx, info=\"SciPy\", args=[a])\n", "\n", " finch_times.append(time_finch)\n", " finch_galley_times.append(time_finch_galley)\n", " networkx_times.append(time_networkx)\n", " scipy_times.append(time_scipy)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(nrows=1, ncols=1)\n", "\n", "ax.plot(size_n, finch_times, \"o-\", label=\"Finch\")\n", "ax.plot(size_n, networkx_times, \"o-\", label=\"NetworkX\")\n", "ax.plot(size_n, scipy_times, \"o-\", label=\"SciPy\")\n", "ax.plot(size_n, finch_galley_times, \"o-\", label=\"Finch Galley\")\n", "\n", "ax.grid(True)\n", "ax.set_xlabel(\"size N\")\n", "ax.set_ylabel(\"time (sec)\")\n", "ax.set_title(\"Counting Triangles\")\n", "ax.legend(loc=\"best\", numpoints=1)\n", "\n", "plt.show()" ] } ], "metadata": { "kernelspec": { "display_name": "sparse-dev", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.14" } }, "nbformat": 4, "nbformat_minor": 2 } sparse-0.17.0/examples/spmv_add_example.py000066400000000000000000000032541501262445000205770ustar00rootroot00000000000000import importlib import os import sparse from utils import benchmark import numpy as np import scipy.sparse as sps LEN = 100000 DENSITY = 0.000001 ITERS = 3 rng = np.random.default_rng(0) if __name__ == "__main__": print("SpMv_add Example:\n") A_sps = sps.random(LEN - 10, LEN, format="csc", density=DENSITY, random_state=rng) * 10 x_sps = rng.random((LEN, 1)) * 10 y_sps = rng.random((LEN - 10, 1)) * 10 # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) A = sparse.asarray(A_sps) x = sparse.asarray(np.array(x_sps, order="C")) y = sparse.asarray(np.array(y_sps, order="C")) @sparse.compiled() def spmv_finch(A, x, y): return sparse.sum(A[:, None, :] * sparse.permute_dims(x, (1, 0))[None, :, :], axis=-1) + y # Compile & Benchmark result_finch = benchmark(spmv_finch, args=[A, x, y], info="Finch", iters=ITERS) # ======= Numba ======= os.environ[sparse._ENV_VAR_NAME] = "Numba" importlib.reload(sparse) A = sparse.asarray(A_sps, format="csc") x = x_sps y = y_sps def spmv_numba(A, x, y): return A @ x + y # Compile & Benchmark result_numba = benchmark(spmv_numba, args=[A, x, y], info="Numba", iters=ITERS) # ======= SciPy ======= def spmv_scipy(A, x, y): return A @ x + y A = A_sps x = x_sps y = y_sps # Compile & Benchmark result_scipy = benchmark(spmv_scipy, args=[A, x, y], info="SciPy", iters=ITERS) np.testing.assert_allclose(result_numba, result_scipy) np.testing.assert_allclose(result_finch.todense(), result_numba) np.testing.assert_allclose(result_finch.todense(), result_scipy) sparse-0.17.0/examples/triangles_example.py000066400000000000000000000024641501262445000207740ustar00rootroot00000000000000import importlib import os import sparse import networkx as nx from utils import benchmark import numpy as np ITERS = 3 if __name__ == "__main__": print("Counting Triangles Example:\n") G = nx.gnp_random_graph(n=200, p=0.2) # ======= Finch ======= os.environ[sparse._ENV_VAR_NAME] = "Finch" importlib.reload(sparse) a_sps = nx.to_scipy_sparse_array(G) a = sparse.asarray(a_sps) @sparse.compiled() def count_triangles_finch(a): return sparse.sum(a @ a * a) / sparse.asarray(6) # Compile & Benchmark result_finch = benchmark(count_triangles_finch, args=[a], info="Finch", iters=ITERS) # ======= SciPy ======= def count_triangles_scipy(a): return (a @ a * a).sum() / 6 a = nx.to_scipy_sparse_array(G) # Compile & Benchmark result_scipy = benchmark(count_triangles_scipy, args=[a], info="SciPy", iters=ITERS) # ======= NetworkX ======= def count_triangles_networkx(a): return sum(nx.triangles(a).values()) / 3 a = G # Compile & Benchmark result_networkx = benchmark(count_triangles_networkx, args=[a], info="NetworkX", iters=ITERS) np.testing.assert_equal(result_finch.todense(), result_scipy) np.testing.assert_equal(result_finch.todense(), result_networkx) assert result_networkx == result_scipy sparse-0.17.0/examples/utils.py000066400000000000000000000011011501262445000164140ustar00rootroot00000000000000import os import time from collections.abc import Callable, Iterable from typing import Any CI_MODE = bool(int(os.getenv("CI_MODE", default="0"))) def benchmark( func: Callable, args: Iterable[Any], info: str, iters: int, ) -> object: # Compile result = func(*args) if CI_MODE: print("CI mode - skipping benchmark") return result # Benchmark print(info) start = time.time() for _ in range(iters): func(*args) elapsed = time.time() - start print(f"Took {elapsed / iters} s.\n") return result sparse-0.17.0/mkdocs.yml000066400000000000000000000051111501262445000150740ustar00rootroot00000000000000site_name: sparse repo_url: https://github.com/pydata/sparse.git edit_uri: edit/main/docs/ #use_directory_urls: false theme: name: material palette: primary: custom accent: cyan font: false #avoid Google Fonts to adhere to data privacy regulations logo: assets/images/logo.png favicon: assets/images/logo.svg features: - navigation.tabs - navigation.tabs.sticky - navigation.tracking - navigation.instant - navigation.instant.progress - navigation.prune - navigation.footer - navigation.indexes - navigation.expand - navigation.top # adds a back-to-top button when user scrolls up - content.code.copy markdown_extensions: - tables - admonition # This line, pymdownx.details and pymdownx.superfences are used by warings - pymdownx.details - pymdownx.superfences - codehilite - toc: toc_depth: 3 - pymdownx.arithmatex: # To display math content with KaTex generic: true - attr_list # To be able to link to a header on another page, use grids - md_in_html # Used for grids extra_javascript: - js/katex.js - https://unpkg.com/katex@0/dist/katex.min.js - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js extra_css: - https://unpkg.com/katex@0/dist/katex.min.css - css/mkdocstrings.css plugins: - search - section-index - autorefs - gen-files: scripts: - scripts/gen_ref_pages.py - literate-nav - mkdocstrings: handlers: python: import: - https://numpy.org/doc/stable/objects.inv - https://docs.python.org/3/objects.inv - https://docs.scipy.org/doc/scipy/objects.inv options: inherited_members: yes show_root_members_full_path: false show_if_no_docstring: true members_order: source docstring_style: numpy show_source: true filters: ["!^_"] group_by_category: true show_category_heading: true - mkdocs-jupyter: include_source: true execute: true ignore: ["__init__.py", "utils.py", "gen_logo.py"] nav: - Home: - index.md - Introduction: - introduction.md - Install: - install.md - Tutorials: - examples.md - examples/* - How to guides: - how-to-guides.md - quickstart.md - construct.md - operations.md - API: - api.md - api/* - Contributing: - contributing.md - roadmap.md - completed-tasks.md - changelog.md - conduct.md sparse-0.17.0/pixi.toml000066400000000000000000000042031501262445000147400ustar00rootroot00000000000000[project] authors = ["Hameer Abbasi <2190658+hameerabbasi@users.noreply.github.com>"] channels = ["conda-forge"] name = "sparse" platforms = ["osx-arm64", "osx-64", "linux-64", "win-64"] [pypi-dependencies] sparse = { path = ".", editable = true } numba = ">=0.49" numpy = ">=1.17" [dependencies] python = ">=3.10,<3.13" [feature.extra.pypi-dependencies] dask = { version = ">=2024", extras = ["array"] } scipy = ">=0.19" scikit-learn = "*" [feature.doc.pypi-dependencies] mkdocs-material = "*" mkdocstrings = { version = "*", extras = ["python"] } mkdocs-gen-files = "*" mkdocs-literate-nav = "*" mkdocs-section-index = "*" mkdocs-jupyter = "*" [feature.test.tasks] test = "ci/test_Numba.sh" test-mlir = "ci/test_MLIR.sh" test-finch = "ci/test_Finch.sh" [feature.test.pypi-dependencies] pytest = ">=3.5" pytest-cov = "*" pytest-xdist = "*" pytest-codspeed = "*" [feature.notebooks.pypi-dependencies] ipykernel = "*" nbmake = "*" matplotlib = "*" networkx = "*" jupyterlab = "*" [feature.matrepr.pypi-dependencies] matrepr = "*" [feature.finch.tasks] precompile = "python -c 'import finch'" [feature.finch.dependencies] python = ">=3.10" juliaup = ">=1.17.10" [feature.finch.pypi-dependencies] scipy = ">=1.13" finch-tensor = ">=0.2.12" [feature.finch.activation.env] SPARSE_BACKEND = "Finch" [feature.finch.target.osx-arm64.activation.env] PYTHONFAULTHANDLER = "${HOME}/faulthandler.log" [feature.mlir.dependencies] python = ">=3.10" [feature.mlir.pypi-dependencies] scipy = ">=0.19" finch-mlir = ">=0.0.2" "PyYAML" = "*" [feature.barebones.dependencies] python = ">=3.10,<3.13" pip = ">=24" [feature.barebones.tasks] setup-env = {cmd = "ci/setup_env.sh" } test-all = { cmd = "ci/test_all.sh", env = { ACTIVATE_VENV = "1" }, depends-on = ["setup-env"] } test-finch = "ci/test_Finch.sh" [feature.mlir.activation.env] SPARSE_BACKEND = "MLIR" [environments] test = ["test", "extra"] doc = ["doc", "extra"] mlir-dev = {features = ["test", "mlir"], no-default-feature = true} finch-dev = {features = ["test", "finch"], no-default-feature = true} notebooks = ["extra", "mlir", "finch", "notebooks"] barebones = {features = ["barebones"], no-default-feature = true} sparse-0.17.0/pyproject.toml000066400000000000000000000053621501262445000160150ustar00rootroot00000000000000[build-system] requires = ["setuptools>=64", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [project] name = "sparse" dynamic = ["version"] description = "Sparse n-dimensional arrays for the PyData ecosystem" readme = "README.md" dependencies = ["numpy>=1.17", "numba>=0.49"] maintainers = [{ name = "Hameer Abbasi", email = "hameerabbasi@yahoo.com" }] requires-python = ">=3.10" license = { file = "LICENSE" } keywords = ["sparse", "numpy", "scipy", "dask"] classifiers = [ "Development Status :: 2 - Pre-Alpha", "Operating System :: OS Independent", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Intended Audience :: Developers", "Intended Audience :: Science/Research", ] [project.optional-dependencies] docs = [ "mkdocs-material", "mkdocstrings[python]", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-section-index", "mkdocs-jupyter", "sparse[extras]", ] extras = [ "dask[array]", "sparse[finch]", "scipy", "scikit-learn", "networkx", ] tests = [ "sparse[extras]", "pytest>=3.5", "pytest-cov", "pytest-xdist", "pre-commit", "pytest-codspeed", ] tox = ["sparse[tests]", "tox"] notebooks = ["sparse[tests]", "nbmake", "matplotlib"] all = ["sparse[docs,tox,notebooks,mlir]", "matrepr"] finch = ["finch-tensor>=0.2.12"] mlir = ["finch-mlir>=0.0.2"] [project.urls] Documentation = "https://sparse.pydata.org/" Source = "https://github.com/pydata/sparse/" Repository = "https://github.com/pydata/sparse.git" "Issue Tracker" = "https://github.com/pydata/sparse/issues" Discussions = "https://github.com/pydata/sparse/discussions" [project.entry-points.numba_extensions] init = "sparse.numba_backend._numba_extension:_init_extension" [tool.setuptools.packages.find] where = ["."] include = ["sparse", "sparse.*"] [tool.setuptools_scm] version_file = "sparse/_version.py" [tool.ruff] exclude = ["sparse/_version.py"] line-length = 120 [tool.ruff.lint] select = ["F", "E", "W", "I", "B", "UP", "YTT", "BLE", "C4", "T10", "ISC", "ICN", "PIE", "PYI", "RSE", "RET", "SIM", "PGH", "FLY", "NPY", "PERF"] [tool.ruff.lint.isort.sections] numpy = ["numpy", "numpy.*", "scipy", "scipy.*"] [tool.ruff.format] quote-style = "double" docstring-code-format = true [tool.ruff.lint.isort] section-order = [ "future", "standard-library", "first-party", "third-party", "numpy", "local-folder", ] [tool.jupytext.formats] "docs/examples_ipynb/" = "ipynb" "docs/examples/" = "py:light" sparse-0.17.0/pytest.ini000066400000000000000000000003461501262445000151270ustar00rootroot00000000000000[pytest] addopts = --cov-report term-missing --cov-report html --cov-report=term:skip-covered --cov sparse --cov-config .coveragerc filterwarnings = ignore::PendingDeprecationWarning testpaths = sparse junit_family=xunit2 sparse-0.17.0/release-procedure.md000066400000000000000000000022361501262445000170260ustar00rootroot00000000000000* Tag commit ```bash git tag -a x.x.x -m 'Version x.x.x' ``` * Push to github ```bash git push pydata main --tags ``` When you open the PR on GitHub, make sure the title of the PR starts with "release". * Upload to PyPI ```bash git clean -xfd # remove all files in directory not in repository python -m build --wheel --sdist # make packages twine upload dist/* # upload packages ``` * Update the release drafter: Go to https://github.com/pydata/sparse Under the โ€œRelease" section there are two links: One is the latest release (it has a tag). The second one is +. Click on the second one so you can see the release drafter. Edit the draft by clicking the "pencil" figure. Make sure you have the correct tags. If they are not, you can create one. If the markdown page looks correct, click on โ€œPublish releaseโ€.
* Enable the newly-pushed tag for documentation: https://readthedocs.org/projects/sparse-nd/versions/ * Wait for conda-forge to realise that the build is too old and make a PR. * Edit and merge that PR. * Announce the release on: * numpy-discussion@python.org * python-announce-list@python.org sparse-0.17.0/scripts/000077500000000000000000000000001501262445000145625ustar00rootroot00000000000000sparse-0.17.0/scripts/gen_ref_pages.py000066400000000000000000000011071501262445000177170ustar00rootroot00000000000000"""Generate the code reference pages.""" from pathlib import Path import sparse import mkdocs_gen_files nav = mkdocs_gen_files.Nav() root = Path(__file__).parent.parent for item in dir(sparse): if item.startswith("_") or not getattr(getattr(sparse, item), "__module__", "").startswith("sparse"): continue full_doc_path = Path("api/" + item + ".md") with mkdocs_gen_files.open(Path("api", f"{item}.md"), "w") as fd: print(f"# {item}", file=fd) print("::: " + f"sparse.{item}", file=fd) mkdocs_gen_files.set_edit_path(full_doc_path, root) sparse-0.17.0/setup.cfg000066400000000000000000000004431501262445000147150ustar00rootroot00000000000000[flake8] # References: # https://flake8.readthedocs.io/en/latest/user/configuration.html # https://flake8.readthedocs.io/en/latest/user/error-codes.html # Note: there cannot be spaces after comma's here exclude = __init__.py .tox/ max-line-length = 120 [bdist_wheel] universal=1 sparse-0.17.0/sparse/000077500000000000000000000000001501262445000143705ustar00rootroot00000000000000sparse-0.17.0/sparse/__init__.py000066400000000000000000000030171501262445000165020ustar00rootroot00000000000000import os import warnings from enum import Enum from ._version import __version__, __version_tuple__ # noqa: F401 __array_api_version__ = "2024.12" class _BackendType(Enum): Numba = "Numba" Finch = "Finch" MLIR = "MLIR" _ENV_VAR_NAME = "SPARSE_BACKEND" class SparseFutureWarning(FutureWarning): pass if os.environ.get(_ENV_VAR_NAME, "") != "": warnings.warn( "Changing back-ends is a development feature, please do not rely on it in production.", SparseFutureWarning, stacklevel=1, ) _backend_name = os.environ[_ENV_VAR_NAME] else: _backend_name = _BackendType.Numba.value if _backend_name not in {v.value for v in _BackendType}: warnings.warn(f"Invalid backend identifier: {_backend_name}. Selecting Numba backend.", UserWarning, stacklevel=1) _BACKEND = _BackendType.Numba else: _BACKEND = _BackendType[_backend_name] del _backend_name if _BackendType.Finch == _BACKEND: from sparse.finch_backend import * # noqa: F403 from sparse.finch_backend import __all__ elif _BackendType.MLIR == _BACKEND: from sparse.mlir_backend import * # noqa: F403 from sparse.mlir_backend import __all__ else: from sparse.numba_backend import * # noqa: F403 from sparse.numba_backend import ( # noqa: F401 __all__, __array_namespace_info__, _common, _compressed, _coo, _dok, _io, _numba_extension, _settings, _slicing, _sparse_array, _umath, _utils, ) sparse-0.17.0/sparse/finch_backend/000077500000000000000000000000001501262445000171265ustar00rootroot00000000000000sparse-0.17.0/sparse/finch_backend/__init__.py000066400000000000000000000003721501262445000212410ustar00rootroot00000000000000try: import finch # noqa: F401 except ModuleNotFoundError as e: raise ImportError("Finch not installed. Run `pip install sparse[finch]` to enable Finch backend") from e from finch import * # noqa: F403 from finch import __all__ as __all__ sparse-0.17.0/sparse/mlir_backend/000077500000000000000000000000001501262445000170025ustar00rootroot00000000000000sparse-0.17.0/sparse/mlir_backend/__init__.py000066400000000000000000000016521501262445000211170ustar00rootroot00000000000000try: import mlir_finch # noqa: F401 del mlir_finch except ModuleNotFoundError as e: raise ImportError( "MLIR Python bindings not installed. Run `pip install finch-mlir` to enable the MLIR backend." ) from e from . import formats from ._array import Array from ._conversions import asarray, from_constituent_arrays, to_numpy, to_scipy from ._dtypes import ( asdtype, complex64, complex128, float16, float32, float64, int8, int16, int32, int64, uint8, uint16, uint32, uint64, ) from ._ops import add, reshape __all__ = [ "Array", "add", "asarray", "asdtype", "to_numpy", "to_scipy", "formats", "reshape", "from_constituent_arrays", "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float16", "float32", "float64", "complex64", "complex128", ] sparse-0.17.0/sparse/mlir_backend/_array.py000066400000000000000000000026461501262445000206410ustar00rootroot00000000000000import numpy as np from ._dtypes import DType from .formats import ConcreteFormat class Array: def __init__(self, *, storage, shape: tuple[int, ...]) -> None: storage_rank = storage.get_storage_format().rank if len(shape) != storage_rank: raise ValueError(f"Mismatched rank, `{storage_rank=}`, `{shape=}`") self._storage = storage self._shape = shape @property def shape(self) -> tuple[int, ...]: return self._shape @property def ndim(self) -> int: return len(self.shape) @property def dtype(self) -> DType: return self._storage.get_storage_format().dtype @property def format(self) -> ConcreteFormat: return self._storage.get_storage_format() def _get_mlir_type(self): return self.format._get_mlir_type(shape=self.shape) def _to_module_arg(self): return self._storage.to_module_arg() def copy(self) -> "Array": from ._conversions import from_constituent_arrays arrs = tuple(arr.copy() for arr in self.get_constituent_arrays()) return from_constituent_arrays(format=self.format, arrays=arrs, shape=self.shape) def asformat(self, format: ConcreteFormat) -> "Array": from ._ops import asformat return asformat(self, format=format) def get_constituent_arrays(self) -> tuple[np.ndarray, ...]: return self._storage.get_constituent_arrays() sparse-0.17.0/sparse/mlir_backend/_common.py000066400000000000000000000032211501262445000210010ustar00rootroot00000000000000import ctypes import functools import weakref from collections.abc import Iterable import mlir_finch.runtime as rt import numpy as np from ._core import libc from ._dtypes import DType, asdtype def fn_cache(f, maxsize: int | None = None): return functools.wraps(f)(functools.lru_cache(maxsize=maxsize)(f)) def get_nd_memref_descr(rank: int, dtype: DType) -> ctypes.Structure: return _get_nd_memref_descr(int(rank), asdtype(dtype)) @fn_cache def _get_nd_memref_descr(rank: int, dtype: DType) -> ctypes.Structure: return rt.make_nd_memref_descriptor(rank, dtype.to_ctype()) def numpy_to_ranked_memref(arr: np.ndarray) -> ctypes.Structure: memref = rt.get_ranked_memref_descriptor(arr) memref_descr = get_nd_memref_descr(arr.ndim, asdtype(arr.dtype)) # Required due to ctypes type checks return memref_descr( allocated=memref.allocated, aligned=memref.aligned, offset=memref.offset, shape=memref.shape, strides=memref.strides, ) def ranked_memref_to_numpy(ref: ctypes.Structure) -> np.ndarray: return rt.ranked_memref_to_numpy([ref]) def free_memref(obj: ctypes.Structure) -> None: libc.free(ctypes.cast(obj.allocated, ctypes.c_void_p)) def _hold_ref(owner, obj): ptr = ctypes.py_object(obj) ctypes.pythonapi.Py_IncRef(ptr) def finalizer(ptr): ctypes.pythonapi.Py_DecRef(ptr) weakref.finalize(owner, finalizer, ptr) def as_shape(x) -> tuple[int]: if not isinstance(x, Iterable): x = (x,) if not all(isinstance(xi, int) for xi in x): raise TypeError("Shape must be an `int` or tuple of `int`s.") return tuple(int(xi) for xi in x) sparse-0.17.0/sparse/mlir_backend/_conversions.py000066400000000000000000000121341501262445000220640ustar00rootroot00000000000000import functools import numpy as np from ._array import Array from .formats import ConcreteFormat, Coo, Csf, Dense, Level, LevelFormat try: import scipy.sparse as sps ScipySparseArray = sps.sparray | sps.spmatrix except ImportError: sps = None ScipySparseArray = None def _guard_scipy(f): @functools.wraps(f) def wrapped(*args, **kwargs): if sps is None: raise RuntimeError("Could not import `scipy.sparse`. Please install `scipy`.") return f(*args, **kwargs) return wrapped def _from_numpy(arr: np.ndarray, copy: bool | None = None) -> Array: if copy is not None and not copy and not arr.flags["C_CONTIGUOUS"]: raise NotImplementedError("Cannot only convert C-contiguous arrays at the moment.") if copy: arr = arr.copy(order="C") arr_flat = np.ascontiguousarray(arr).reshape(-1) dense_format = Dense().with_ndim(arr.ndim).with_dtype(arr.dtype).build() return from_constituent_arrays(format=dense_format, arrays=(arr_flat,), shape=arr.shape) def to_numpy(arr: Array) -> np.ndarray: if not Dense.is_this_format(arr.format): raise TypeError(f"Cannot convert a non-dense array to NumPy. `{arr.format=}`") (data,) = arr.get_constituent_arrays() arg_order = [0] * arr.format.storage_rank for i, o in enumerate(arr.format.order): arg_order[o] = i arg_order = tuple(arg_order) storage_shape = tuple(int(arr.shape[o]) for o in arg_order) return data.reshape(storage_shape).transpose(arg_order) @_guard_scipy def _from_scipy(arr: ScipySparseArray, copy: bool | None = None) -> Array: if not isinstance(arr, ScipySparseArray): raise TypeError(f"`arr` is not a `scipy.sparse` array, `{type(arr)=}`.") match arr.format: case "csr" | "csc": order = (0, 1) if arr.format == "csr" else (1, 0) pos_width = arr.indptr.dtype.itemsize * 8 crd_width = arr.indices.dtype.itemsize * 8 csx_format = ( Csf() .with_ndim(2, canonical=arr.has_canonical_format) .with_dtype(arr.dtype) .with_crd_width(crd_width) .with_pos_width(pos_width) .with_order(order) .build() ) indptr = arr.indptr indices = arr.indices data = arr.data if copy: indptr = indptr.copy() indices = indices.copy() data = data.copy() return from_constituent_arrays(format=csx_format, arrays=(indptr, indices, data), shape=arr.shape) case "coo": row, col = arr.row, arr.col if row.dtype != col.dtype: raise RuntimeError(f"`row` and `col` dtypes must be the same: {row.dtype} != {col.dtype}.") pos = np.array([0, arr.nnz], dtype=np.int64) pos_width = pos.dtype.itemsize * 8 crd_width = row.dtype.itemsize * 8 data = arr.data if copy: data = data.copy() row = row.copy() col = col.copy() coo_format = ( Coo() .with_ndim(2, canonical=arr.has_canonical_format) .with_dtype(arr.dtype) .with_pos_width(pos_width) .with_crd_width(crd_width) .build() ) return from_constituent_arrays(format=coo_format, arrays=(pos, row, col, data), shape=arr.shape) case _: raise NotImplementedError(f"No conversion implemented for `scipy.sparse.{type(arr.__name__)}`.") @_guard_scipy def to_scipy(arr: Array) -> ScipySparseArray: storage_format = arr.format match storage_format.levels: case (Level(LevelFormat.Dense, _), Level(LevelFormat.Compressed, _)): indptr, indices, data = arr.get_constituent_arrays() if storage_format.order == (0, 1): return sps.csr_array((data, indices, indptr), shape=arr.shape) return sps.csc_array((data, indices, indptr), shape=arr.shape) case (Level(LevelFormat.Compressed, _), Level(LevelFormat.Singleton, _)): _, row, col, data = arr.get_constituent_arrays() return sps.coo_array((data, (row, col)), shape=arr.shape) case _: raise RuntimeError(f"No conversion implemented for `{storage_format=}`.") def asarray(arr, copy: bool | None = None) -> Array: if sps is not None and isinstance(arr, ScipySparseArray): return _from_scipy(arr, copy=copy) if isinstance(arr, np.ndarray): return _from_numpy(arr, copy=copy) if isinstance(arr, Array): if copy: arr = arr.copy() return arr if copy is not None and not copy and not isinstance(arr, np.ndarray): raise ValueError("Cannot non-copy convert this object.") return _from_numpy(np.asarray(arr), copy=copy) def from_constituent_arrays(*, format: ConcreteFormat, arrays: tuple[np.ndarray, ...], shape: tuple[int, ...]) -> Array: storage = format._get_ctypes_type().from_constituent_arrays(arrays) return Array(storage=storage, shape=shape) sparse-0.17.0/sparse/mlir_backend/_core.py000066400000000000000000000024531501262445000204470ustar00rootroot00000000000000import ctypes import ctypes.util import os import pathlib import sys from mlir_finch.ir import Context from mlir_finch.passmanager import PassManager DEBUG = bool(int(os.environ.get("DEBUG", "0"))) CWD = pathlib.Path(".") finch_lib_path = f"{sys.prefix}/lib/python3.{sys.version_info.minor}/site-packages/lib" ld_library_path = os.environ.get("LD_LIBRARY_PATH") ld_library_path = f"{finch_lib_path}:{ld_library_path}" if ld_library_path is None else finch_lib_path os.environ["LD_LIBRARY_PATH"] = ld_library_path MLIR_C_RUNNER_UTILS = ctypes.util.find_library("mlir_c_runner_utils") if os.name == "posix" and MLIR_C_RUNNER_UTILS is not None: MLIR_C_RUNNER_UTILS = f"{finch_lib_path}/{MLIR_C_RUNNER_UTILS}" SHARED_LIBS = [] if MLIR_C_RUNNER_UTILS is not None: SHARED_LIBS.append(MLIR_C_RUNNER_UTILS) libc = ctypes.CDLL(ctypes.util.find_library("c")) if os.name != "nt" else ctypes.cdll.msvcrt libc.free.argtypes = [ctypes.c_void_p] libc.free.restype = None SHARED_LIBS = [] if DEBUG: SHARED_LIBS.append(MLIR_C_RUNNER_UTILS) OPT_LEVEL = 0 if DEBUG else 2 # TODO: remove global state ctx = Context() pm = PassManager.parse( """ builtin.module( sparse-assembler{direct-out=true}, sparsifier{create-sparse-deallocs=1 enable-runtime-library=false} ) """, context=ctx, ) sparse-0.17.0/sparse/mlir_backend/_dtypes.py000066400000000000000000000057171501262445000210350ustar00rootroot00000000000000import abc import dataclasses import math import sys import mlir_finch.runtime as rt from mlir_finch import ir import numpy as np class MlirType(abc.ABC): @abc.abstractmethod def _get_mlir_type(self) -> ir.Type: ... def _get_pointer_width() -> int: return round(math.log2(sys.maxsize + 1.0)) + 1 _PTR_WIDTH = _get_pointer_width() @dataclasses.dataclass(eq=True, frozen=True, kw_only=True) class DType(MlirType): bit_width: int @property @abc.abstractmethod def np_dtype(self) -> np.dtype: raise NotImplementedError def to_ctype(self): return rt.as_ctype(self.np_dtype) def __eq__(self, value): if np.isdtype(value) or isinstance(value, str): value = asdtype(value) return super().__eq__(value) @dataclasses.dataclass(eq=True, frozen=True, kw_only=True) class IeeeRealFloatingDType(DType): @property def np_dtype(self) -> np.dtype: return np.dtype(getattr(np, f"float{self.bit_width}")) def _get_mlir_type(self) -> ir.Type: return getattr(ir, f"F{self.bit_width}Type").get() float64 = IeeeRealFloatingDType(bit_width=64) float32 = IeeeRealFloatingDType(bit_width=32) float16 = IeeeRealFloatingDType(bit_width=16) @dataclasses.dataclass(eq=True, frozen=True, kw_only=True) class IeeeComplexFloatingDType(DType): @property def np_dtype(self) -> np.dtype: return np.dtype(getattr(np, f"complex{self.bit_width}")) def _get_mlir_type(self) -> ir.Type: return ir.ComplexType.get(getattr(ir, f"F{self.bit_width // 2}Type").get()) complex64 = IeeeComplexFloatingDType(bit_width=64) complex128 = IeeeComplexFloatingDType(bit_width=128) @dataclasses.dataclass(eq=True, frozen=True, kw_only=True) class IntegerDType(DType): def _get_mlir_type(self) -> ir.Type: return ir.IntegerType.get_signless(self.bit_width) @dataclasses.dataclass(eq=True, frozen=True, kw_only=True) class UnsignedIntegerDType(IntegerDType): @property def np_dtype(self) -> np.dtype: return np.dtype(getattr(np, f"uint{self.bit_width}")) uint8 = UnsignedIntegerDType(bit_width=8) uint16 = UnsignedIntegerDType(bit_width=16) uint32 = UnsignedIntegerDType(bit_width=32) uint64 = UnsignedIntegerDType(bit_width=64) @dataclasses.dataclass(eq=True, frozen=True, kw_only=True) class SignedIntegerDType(IntegerDType): @property def np_dtype(self) -> np.dtype: return np.dtype(getattr(np, f"int{self.bit_width}")) int8 = SignedIntegerDType(bit_width=8) int16 = SignedIntegerDType(bit_width=16) int32 = SignedIntegerDType(bit_width=32) int64 = SignedIntegerDType(bit_width=64) intp: SignedIntegerDType = locals()[f"int{_PTR_WIDTH}"] uintp: UnsignedIntegerDType = locals()[f"uint{_PTR_WIDTH}"] def isdtype(dt, /) -> bool: return isinstance(dt, DType) NUMPY_DTYPE_MAP = {np.dtype(dt.np_dtype): dt for dt in locals().values() if isdtype(dt)} def asdtype(dt, /) -> DType: if isdtype(dt): return dt return NUMPY_DTYPE_MAP[np.dtype(dt)] sparse-0.17.0/sparse/mlir_backend/_ops.py000066400000000000000000000205611501262445000203200ustar00rootroot00000000000000import ctypes import math import mlir_finch.execution_engine import mlir_finch.passmanager from mlir_finch import ir from mlir_finch.dialects import arith, complex, func, linalg, sparse_tensor, tensor import numpy as np from ._array import Array from ._common import as_shape, fn_cache from ._core import CWD, DEBUG, OPT_LEVEL, SHARED_LIBS, ctx, pm from ._dtypes import DType, IeeeComplexFloatingDType, IeeeRealFloatingDType, IntegerDType from .formats import ConcreteFormat, _determine_format @fn_cache def get_add_module( a_tensor_type: ir.RankedTensorType, b_tensor_type: ir.RankedTensorType, out_tensor_type: ir.RankedTensorType, dtype: DType, ) -> ir.Module: with ir.Location.unknown(ctx): module = ir.Module.create() if isinstance(dtype, IeeeRealFloatingDType): arith_op = arith.AddFOp elif isinstance(dtype, IeeeComplexFloatingDType): arith_op = complex.AddOp elif isinstance(dtype, IntegerDType): arith_op = arith.AddIOp else: raise RuntimeError(f"Can not add {dtype=}.") dtype = dtype._get_mlir_type() max_rank = out_tensor_type.rank with ir.InsertionPoint(module.body): @func.FuncOp.from_py_func(a_tensor_type, b_tensor_type) def add(a, b): out = tensor.empty(out_tensor_type.shape, dtype, encoding=out_tensor_type.encoding) generic_op = linalg.GenericOp( [out_tensor_type], [a, b], [out], ir.ArrayAttr.get( [ ir.AffineMapAttr.get(ir.AffineMap.get_minor_identity(max_rank, t.rank)) for t in (a_tensor_type, b_tensor_type, out_tensor_type) ] ), ir.ArrayAttr.get([ir.Attribute.parse("#linalg.iterator_type")] * max_rank), ) block = generic_op.regions[0].blocks.append(dtype, dtype, dtype) with ir.InsertionPoint(block): a, b, o = block.arguments res = sparse_tensor.BinaryOp(dtype, a, b) overlap = res.regions[0].blocks.append(dtype, dtype) with ir.InsertionPoint(overlap): arg0, arg1 = overlap.arguments overlap_res = arith_op(arg0, arg1) sparse_tensor.YieldOp([overlap_res]) left_region = res.regions[1].blocks.append(dtype) with ir.InsertionPoint(left_region): (arg0,) = left_region.arguments sparse_tensor.YieldOp([arg0]) right_region = res.regions[2].blocks.append(dtype) with ir.InsertionPoint(right_region): (arg0,) = right_region.arguments sparse_tensor.YieldOp([arg0]) linalg.YieldOp([res]) return generic_op.result add.func_op.attributes["llvm.emit_c_interface"] = ir.UnitAttr.get() if DEBUG: (CWD / "add_module.mlir").write_text(str(module)) pm.run(module.operation) if DEBUG: (CWD / "add_module_opt.mlir").write_text(str(module)) return mlir_finch.execution_engine.ExecutionEngine(module, opt_level=OPT_LEVEL, shared_libs=SHARED_LIBS) @fn_cache def get_reshape_module( a_tensor_type: ir.RankedTensorType, shape_tensor_type: ir.RankedTensorType, out_tensor_type: ir.RankedTensorType, ) -> ir.Module: with ir.Location.unknown(ctx): module = ir.Module.create() with ir.InsertionPoint(module.body): @func.FuncOp.from_py_func(a_tensor_type, shape_tensor_type) def reshape(a, shape): return tensor.reshape(out_tensor_type, a, shape) reshape.func_op.attributes["llvm.emit_c_interface"] = ir.UnitAttr.get() if DEBUG: (CWD / "reshape_module.mlir").write_text(str(module)) pm.run(module.operation) if DEBUG: (CWD / "reshape_module_opt.mlir").write_text(str(module)) return mlir_finch.execution_engine.ExecutionEngine(module, opt_level=OPT_LEVEL, shared_libs=SHARED_LIBS) @fn_cache def get_broadcast_to_module( in_tensor_type: ir.RankedTensorType, out_tensor_type: ir.RankedTensorType, dimensions: tuple[int, ...], ) -> ir.Module: with ir.Location.unknown(ctx): module = ir.Module.create() with ir.InsertionPoint(module.body): @func.FuncOp.from_py_func(in_tensor_type) def broadcast_to(in_tensor): out = tensor.empty( out_tensor_type.shape, out_tensor_type.element_type, encoding=out_tensor_type.encoding ) return linalg.broadcast(in_tensor, outs=[out], dimensions=dimensions) broadcast_to.func_op.attributes["llvm.emit_c_interface"] = ir.UnitAttr.get() if DEBUG: (CWD / "broadcast_to_module.mlir").write_text(str(module)) pm.run(module.operation) if DEBUG: (CWD / "broadcast_to_module_opt.mlir").write_text(str(module)) return mlir_finch.execution_engine.ExecutionEngine(module, opt_level=OPT_LEVEL, shared_libs=SHARED_LIBS) @fn_cache def get_convert_module( in_tensor_type: ir.RankedTensorType, out_tensor_type: ir.RankedTensorType, ): with ir.Location.unknown(ctx): module = ir.Module.create() with ir.InsertionPoint(module.body): @func.FuncOp.from_py_func(in_tensor_type) def convert(in_tensor): return sparse_tensor.convert(out_tensor_type, in_tensor) convert.func_op.attributes["llvm.emit_c_interface"] = ir.UnitAttr.get() if DEBUG: (CWD / "convert_module.mlir").write_text(str(module)) pm.run(module.operation) if DEBUG: (CWD / "convert_module.mlir").write_text(str(module)) return mlir_finch.execution_engine.ExecutionEngine(module, opt_level=OPT_LEVEL, shared_libs=SHARED_LIBS) def add(x1: Array, x2: Array, /) -> Array: # TODO: Determine output format via autoscheduler ret_storage_format = _determine_format(x1.format, x2.format, dtype=x1.dtype, union=True) ret_storage = ret_storage_format._get_ctypes_type(owns_memory=True)() out_tensor_type = ret_storage_format._get_mlir_type(shape=np.broadcast_shapes(x1.shape, x2.shape)) add_module = get_add_module( x1._get_mlir_type(), x2._get_mlir_type(), out_tensor_type=out_tensor_type, dtype=x1.dtype, ) add_module.invoke( "add", ctypes.pointer(ctypes.pointer(ret_storage)), *x1._to_module_arg(), *x2._to_module_arg(), ) return Array(storage=ret_storage, shape=tuple(out_tensor_type.shape)) def asformat(x: Array, /, format: ConcreteFormat) -> Array: if format.rank != x.ndim: raise ValueError(f"`format.rank != `self.ndim`, {format.rank=}, {x.ndim=}") if format == x.format: return x out_tensor_type = format._get_mlir_type(shape=x.shape) ret_storage = format._get_ctypes_type(owns_memory=True)() convert_module = get_convert_module( x._get_mlir_type(), out_tensor_type, ) convert_module.invoke( "convert", ctypes.pointer(ctypes.pointer(ret_storage)), *x._to_module_arg(), ) return Array(storage=ret_storage, shape=x.shape) def reshape(x: Array, /, shape: tuple[int, ...]) -> Array: from ._conversions import _from_numpy shape = as_shape(shape) if math.prod(x.shape) != math.prod(shape): raise ValueError(f"`math.prod(x.shape) != math.prod(shape)`, {x.shape=}, {shape=}") ret_storage_format = _determine_format(x.format, dtype=x.dtype, union=len(shape) > x.ndim, out_ndim=len(shape)) shape_array = _from_numpy(np.asarray(shape, dtype=np.uint64)) out_tensor_type = ret_storage_format._get_mlir_type(shape=shape) ret_storage = ret_storage_format._get_ctypes_type(owns_memory=True)() reshape_module = get_reshape_module(x._get_mlir_type(), shape_array._get_mlir_type(), out_tensor_type) reshape_module.invoke( "reshape", ctypes.pointer(ctypes.pointer(ret_storage)), *x._to_module_arg(), *shape_array._to_module_arg(), ) return Array(storage=ret_storage, shape=shape) sparse-0.17.0/sparse/mlir_backend/formats.py000066400000000000000000000344331501262445000210360ustar00rootroot00000000000000import ctypes import dataclasses import enum import itertools import re import typing from mlir_finch import ir from mlir_finch.dialects import sparse_tensor import numpy as np from ._common import ( _hold_ref, fn_cache, free_memref, get_nd_memref_descr, numpy_to_ranked_memref, ranked_memref_to_numpy, ) from ._core import ctx from ._dtypes import DType, asdtype _CAMEL_TO_SNAKE = [re.compile("(.)([A-Z][a-z]+)"), re.compile("([a-z0-9])([A-Z])")] __all__ = ["LevelProperties", "LevelFormat", "ConcreteFormat", "Level", "get_concrete_format"] def _camel_to_snake(name: str) -> str: for exp in _CAMEL_TO_SNAKE: name = exp.sub(r"\1_\2", name) return name.lower() class LevelProperties(enum.Flag): NonOrdered = enum.auto() NonUnique = enum.auto() SOA = enum.auto() def build(self) -> list[sparse_tensor.LevelProperty]: return [getattr(sparse_tensor.LevelProperty, _camel_to_snake(p.name)) for p in type(self) if p in self] class LevelFormat(enum.Enum): Dense = "dense" Compressed = "compressed" Singleton = "singleton" def build(self) -> sparse_tensor.LevelFormat: return getattr(sparse_tensor.LevelFormat, self.value) @dataclasses.dataclass(eq=True, frozen=True) class Level: format: LevelFormat properties: LevelProperties = LevelProperties(0) def build(self): return sparse_tensor.EncodingAttr.build_level_type(self.format.build(), self.properties.build()) @dataclasses.dataclass(eq=True, frozen=True, kw_only=True) class ConcreteFormat: levels: tuple[Level, ...] order: tuple[int, ...] pos_width: int crd_width: int dtype: DType @property def storage_rank(self) -> int: return len(self.levels) @property def rank(self) -> int: return self.storage_rank def __post_init__(self): if sorted(self.order) != list(range(self.rank)): raise ValueError(f"`sorted(self.order) != list(range(self.rank))`, `{self.order=}`, `{self.rank=}`.") @fn_cache def _get_mlir_type(self, *, shape: tuple[int, ...]) -> ir.RankedTensorType: if len(shape) != self.rank: raise ValueError(f"`len(shape) != self.rank`, {shape=}, {self.rank=}") with ir.Location.unknown(ctx): mlir_levels = [level.build() for level in self.levels] mlir_order = list(self.order) mlir_reverse_order = [0] * self.rank for i, r in enumerate(mlir_order): mlir_reverse_order[r] = i dtype = self.dtype._get_mlir_type() encoding = sparse_tensor.EncodingAttr.get( mlir_levels, ir.AffineMap.get_permutation(mlir_order), ir.AffineMap.get_permutation(mlir_reverse_order), self.pos_width, self.crd_width, ) return ir.RankedTensorType.get(list(shape), dtype, encoding) @fn_cache def _get_ctypes_type(self, *, owns_memory=False): ptr_dtype = asdtype(getattr(np, f"int{self.pos_width}")) idx_dtype = asdtype(getattr(np, f"int{self.crd_width}")) def get_fields(): fields = [] compressed_counter = 0 singleton_counter = 0 for level, next_level in itertools.zip_longest(self.levels, self.levels[1:]): if LevelFormat.Compressed == level.format: compressed_counter += 1 fields.append((f"pointers_to_{compressed_counter}", get_nd_memref_descr(1, ptr_dtype))) if next_level is not None and LevelFormat.Singleton == next_level.format: singleton_counter += 1 fields.append( ( f"indices_{compressed_counter}_coords_{singleton_counter}", get_nd_memref_descr(1, idx_dtype), ) ) else: fields.append((f"indices_{compressed_counter}", get_nd_memref_descr(1, idx_dtype))) if LevelFormat.Singleton == level.format: singleton_counter += 1 fields.append( (f"indices_{compressed_counter}_coords_{singleton_counter}", get_nd_memref_descr(1, idx_dtype)) ) fields.append(("values", get_nd_memref_descr(1, self.dtype))) return fields storage_format = self class Storage(ctypes.Structure): _fields_ = get_fields() def to_module_arg(self) -> list: return [ctypes.pointer(ctypes.pointer(f)) for f in self.get__fields_()] def get__fields_(self) -> list: return [getattr(self, field[0]) for field in self._fields_] def get_constituent_arrays(self) -> tuple[np.ndarray, ...]: arrays = tuple(ranked_memref_to_numpy(field) for field in self.get__fields_()) for arr in arrays: _hold_ref(arr, self) return arrays def get_storage_format(self) -> ConcreteFormat: return storage_format @classmethod def from_constituent_arrays(cls, arrs: list[np.ndarray]) -> "Storage": storage = cls(*(numpy_to_ranked_memref(arr) for arr in arrs)) for arr in arrs: _hold_ref(storage, arr) return storage if owns_memory: def __del__(self) -> None: for field in self.get__fields_(): free_memref(field) return Storage @dataclasses.dataclass(eq=True, frozen=True, kw_only=True) class FormatFactory: levels: tuple[Level, ...] | None = None order: typing.Literal["C", "F"] | tuple[int, ...] = "C" pos_width: int = 64 crd_width: int = 64 dtype: DType | None = None def is_ready(self) -> bool: fields = dataclasses.fields(self) return all(getattr(self, f.name) is not None for f in fields) def build(self) -> ConcreteFormat: if not self.is_ready(): raise RuntimeError("This factory is not ready. All fields must be non-None.") return get_concrete_format( levels=self.levels, order=self.order, pos_width=self.pos_width, crd_width=self.crd_width, dtype=self.dtype, ) @classmethod def _get_levels_from_ndim(cls, ndim: int, /) -> tuple[Level, ...]: raise TypeError(f"`{cls.__name__}` doesn't implement this method.") def with_ndim(self, ndim: int, /, *, canonical: bool = True) -> "FormatFactory": if ndim < 0: raise ValueError(f"`ndim < 0`, `{ndim=}`.") levels = self._get_levels_from_ndim(ndim) if not canonical: levels = tuple( dataclasses.replace( level, properties=level.properties | LevelProperties.NonOrdered | LevelProperties.NonUnique ) for level in levels ) assert len(levels) == ndim return self.with_levels(levels) def with_levels(self, levels: tuple[Level, ...], /) -> "FormatFactory": out = dataclasses.replace(self, levels=levels) out._check_consistency() return out def _check_consistency(self) -> None: order = self.order if isinstance(order, str): if order in {"C", "F"}: return raise ValueError(f"Invalid order, `{order=}`.") if sorted(order) != list(range(len(order))): raise ValueError(f"`sorted(order) != list(range(len(order)))`, `{order=}`.") levels = self.levels if levels is not None and len(levels) != len(order): raise ValueError(f"`levels is not None and len(levels) != len(order)`, `{order=}`, `{levels=}`.") def with_order(self, order: typing.Literal["C", "F"] | tuple[int, ...], /): out = dataclasses.replace(self, order=order) out._check_consistency() return out def with_ptr_width(self, width: int, /) -> "FormatFactory": return dataclasses.replace(self, pos_width=width, crd_width=width) def with_pos_width(self, width: int, /) -> "FormatFactory": return dataclasses.replace(self, pos_width=width) def with_crd_width(self, width: int, /) -> "FormatFactory": return dataclasses.replace(self, crd_width=width) def with_dtype(self, dtype: DType, /) -> "FormatFactory": return dataclasses.replace(self, dtype=dtype) @classmethod def is_this_format(cls, format: ConcreteFormat) -> bool: levels_self = cls._get_levels_from_ndim(format.storage_rank) levels_other = format.levels return all( dataclasses.replace(l1, properties=l1.properties | LevelProperties.NonOrdered | LevelProperties.NonUnique) == dataclasses.replace( l2, properties=l2.properties | LevelProperties.NonOrdered | LevelProperties.NonUnique ) for l1, l2 in zip(levels_self, levels_other, strict=True) ) class Coo(FormatFactory): @classmethod def _get_levels_from_ndim(cls, ndim: int, /) -> tuple[Level, ...]: if ndim == 0: return () level_base = Level(LevelFormat.Compressed) level_middle = Level(LevelFormat.Singleton, LevelProperties.SOA) levels = [] for i in range(ndim): level = level_base if i == 0 else level_middle if i != ndim - 1: level = dataclasses.replace(level, properties=level.properties | LevelProperties.NonUnique) levels.append(level) return tuple(levels) class Csf(FormatFactory): @classmethod def _get_levels_from_ndim(self, ndim: int, /) -> tuple[Level, ...]: if ndim == 0: return () level_middle = Level(LevelFormat.Compressed) level_base = Level(LevelFormat.Dense) levels = [] for i in range(ndim): level = level_base if i == 0 else level_middle levels.append(level) return tuple(levels) class Dense(FormatFactory): @classmethod def _get_levels_from_ndim(self, ndim: int, /) -> tuple[Level, ...]: return (Level(LevelFormat.Dense),) * ndim def get_concrete_format( *, levels: tuple[Level, ...], order: typing.Literal["C", "F"] | tuple[int, ...], pos_width: int, crd_width: int, dtype: DType, ) -> ConcreteFormat: levels = tuple(levels) if isinstance(order, str): if order == "C": order = tuple(range(len(levels))) if order == "F": order = tuple(reversed(range(len(levels)))) return _get_concrete_format( levels=levels, order=order, pos_width=int(pos_width), crd_width=int(crd_width), dtype=asdtype(dtype), ) @fn_cache def _get_concrete_format( *, levels: tuple[Level, ...], order: tuple[int, ...], pos_width: int, crd_width: int, dtype: DType, ) -> ConcreteFormat: return ConcreteFormat( levels=levels, order=order, pos_width=pos_width, crd_width=crd_width, dtype=dtype, ) def _is_sparse_level(lvl: Level | LevelFormat, /) -> bool: assert isinstance(lvl, Level | LevelFormat) if isinstance(lvl, Level): lvl = lvl.format return LevelFormat.Dense != lvl def _count_sparse_levels(format: ConcreteFormat) -> int: return sum(_is_sparse_level(lvl) for lvl in format.levels) def _count_dense_levels(format: ConcreteFormat) -> int: return sum(not _is_sparse_level(lvl) for lvl in format.levels) def _get_sparse_dense_levels( *, n_sparse: int | None = None, n_dense: int | None = None, ndim: int | None = None ) -> tuple[Level, ...]: if (n_sparse is not None) + (n_dense is not None) + (ndim is not None) != 2: assert n_sparse is not None and n_dense is not None and ndim is not None # assert n_sparse + n_dense == ndim if n_sparse is None: n_sparse = ndim - n_dense if n_dense is None: n_dense = ndim - n_sparse if ndim is None: ndim = n_dense + n_sparse assert ndim >= 0 assert n_dense >= 0 assert n_sparse >= 0 return (Level(LevelFormat.Dense),) * n_dense + (Level(LevelFormat.Compressed),) * n_sparse def _determine_format( *formats: ConcreteFormat, dtype: DType, union: bool, out_ndim: int | None = None ) -> ConcreteFormat: """Determines the output format from a group of input formats. 1. Counts the sparse levels for `union=True`, and dense ones for `union=False`. 2. Gets the max number of counted levels for each format. 3. Constructs a format with rank of `out_ndim` (max rank of inputs is taken if it's `None`). If `union=False` counted levels is the number of sparse levels, otherwise dense. Sparse levels are replaced with `LevelFormat.Compressed`. Returns ------- StorageFormat Output storage format. """ if len(formats) == 0: if out_ndim is None: out_ndim = 0 return get_concrete_format( levels=(Level(LevelFormat.Dense if union else LevelFormat.Compressed),) * out_ndim, order="C", pos_width=64, crd_width=64, dtype=dtype, ) if out_ndim is None: out_ndim = max(fmt.rank for fmt in formats) pos_width = 0 crd_width = 0 counter = _count_sparse_levels if not union else _count_dense_levels n_counted = None order = () for fmt in formats: n_counted = counter(fmt) if n_counted is None else max(n_counted, counter(fmt)) pos_width = max(pos_width, fmt.pos_width) crd_width = max(crd_width, fmt.crd_width) if order != "C": if fmt.order[: len(order)] == order: order = fmt.order elif order[: len(fmt.order)] != fmt.order: order = "C" if not isinstance(order, str): order = order + tuple(range(len(order), out_ndim)) order = order[:out_ndim] if out_ndim < n_counted: n_counted = out_ndim n_sparse = n_counted if not union else out_ndim - n_counted levels = _get_sparse_dense_levels(n_sparse=n_sparse, ndim=out_ndim) return get_concrete_format( levels=levels, order=order, pos_width=pos_width, crd_width=crd_width, dtype=dtype, ) sparse-0.17.0/sparse/mlir_backend/tests/000077500000000000000000000000001501262445000201445ustar00rootroot00000000000000sparse-0.17.0/sparse/mlir_backend/tests/__init__.py000066400000000000000000000000001501262445000222430ustar00rootroot00000000000000sparse-0.17.0/sparse/mlir_backend/tests/conftest.py000066400000000000000000000002131501262445000223370ustar00rootroot00000000000000import pytest import numpy as np @pytest.fixture(scope="module") def rng() -> np.random.Generator: return np.random.default_rng(42) sparse-0.17.0/sparse/mlir_backend/tests/test_simple.py000066400000000000000000000352651501262445000230610ustar00rootroot00000000000000import math import typing from collections.abc import Iterable import sparse import pytest import numpy as np import scipy.sparse as sps parametrize_dtypes = pytest.mark.parametrize( "dtype", [ np.int8, np.uint8, np.int16, np.uint16, np.int32, np.uint32, np.int64, np.uint64, np.float32, np.float64, np.complex64, np.complex128, ], ) def parametrize_scipy_fmt_with_arg(name: str) -> pytest.MarkDecorator: return pytest.mark.parametrize( name, ["csr", "csc", "coo"], ) parametrize_scipy_fmt = parametrize_scipy_fmt_with_arg("format") def assert_sps_equal( expected: sps.csr_array | sps.csc_array | sps.coo_array, actual: sps.csr_array | sps.csc_array | sps.coo_array, /, *, check_canonical=False, check_dtype=True, ) -> None: assert expected.shape == actual.shape assert expected.format == actual.format if check_dtype: assert expected.dtype == actual.dtype if check_canonical: expected.eliminate_zeros() expected.sum_duplicates() actual.eliminate_zeros() actual.sum_duplicates() if expected.format != "coo": np.testing.assert_array_equal(expected.indptr, actual.indptr) np.testing.assert_array_equal(expected.indices, actual.indices) else: np.testing.assert_array_equal(expected.row, actual.row) np.testing.assert_array_equal(expected.col, actual.col) np.testing.assert_array_equal(expected.data, actual.data) def generate_sampler(dtype: np.dtype, rng: np.random.Generator) -> typing.Callable[[tuple[int, ...]], np.ndarray]: dtype = np.dtype(dtype) if np.issubdtype(dtype, np.signedinteger): def sampler_signed(size: tuple[int, ...]): return rng.integers(-10, 10, dtype=dtype, endpoint=True, size=size) return sampler_signed if np.issubdtype(dtype, np.unsignedinteger): def sampler_unsigned(size: tuple[int, ...]): return rng.integers(0, 10, dtype=dtype, endpoint=True, size=size) return sampler_unsigned if np.issubdtype(dtype, np.floating): def sampler_real_floating(size: tuple[int, ...]): return -10 + 20 * rng.random(dtype=dtype, size=size) return sampler_real_floating if np.issubdtype(dtype, np.complexfloating): float_dtype = np.array(0, dtype=dtype).real.dtype def sampler_complex_floating(size: tuple[int, ...]): real_sampler = generate_sampler(float_dtype, rng) if not isinstance(size, Iterable): size = (size,) float_arr = real_sampler(tuple(size) + (2,)) return float_arr.view(dtype)[..., 0] return sampler_complex_floating raise NotImplementedError(f"{dtype=} not yet supported.") def get_example_csf_arrays(dtype: np.dtype) -> tuple: pos_1 = np.array([0, 1, 3], dtype=np.int64) crd_1 = np.array([1, 0, 1], dtype=np.int64) pos_2 = np.array([0, 3, 5, 7], dtype=np.int64) crd_2 = np.array([0, 1, 3, 0, 3, 0, 1], dtype=np.int64) data = np.array([1, 2, 3, 4, 5, 6, 7], dtype=dtype) return pos_1, crd_1, pos_2, crd_2, data @parametrize_dtypes @pytest.mark.parametrize("shape", [(100,), (10, 200), (5, 10, 20)]) def test_dense_format(dtype, shape): data = np.arange(math.prod(shape), dtype=dtype).reshape(shape) tensor = sparse.asarray(data) actual = sparse.to_numpy(tensor) np.testing.assert_equal(actual, data) def assert_array_equal( expected: sparse.Array, actual: sparse.Array, /, *, same_format: bool = True, same_dtype: bool = True, data_test_fn: typing.Callable[[np.ndarray, np.ndarray], None] = np.testing.assert_array_equal, ) -> None: if same_format: assert expected.format == actual.format if same_dtype: assert expected.dtype == actual.dtype assert expected.shape == actual.shape actual = actual.asformat(expected.format) carrs_expected = expected.get_constituent_arrays() carrs_actual = actual.get_constituent_arrays() for e, a in zip(carrs_expected[:-1], carrs_actual[:-1], strict=True): assert e.dtype == a.dtype np.testing.assert_equal(e, a) data_test_fn(carrs_expected[-1], carrs_actual[-1]) @parametrize_dtypes @parametrize_scipy_fmt def test_roundtrip(rng, dtype, format): SHAPE = (80, 100) DENSITY = 0.6 sampler = generate_sampler(dtype, rng) sps_arr = sps.random_array( SHAPE, density=DENSITY, format=format, dtype=dtype, random_state=rng, data_sampler=sampler ) sp_arr = sparse.asarray(sps_arr) sps_roundtripped = sparse.to_scipy(sp_arr) assert_sps_equal(sps_arr, sps_roundtripped) sp_arr_roundtripped = sparse.asarray(sps_roundtripped) assert_array_equal(sp_arr, sp_arr_roundtripped) @parametrize_dtypes @pytest.mark.parametrize("shape", [(80, 100), (200,), (10, 20, 30)]) def test_roundtrip_dense(rng, dtype, shape): sampler = generate_sampler(dtype, rng) np_arr = sampler(shape) sp_arr = sparse.asarray(np_arr) np_roundtripped = sparse.to_numpy(sp_arr) assert np_arr.dtype == np_roundtripped.dtype np.testing.assert_array_equal(np_arr, np_roundtripped) sp_arr_roundtripped = sparse.asarray(np_roundtripped) assert_array_equal(sp_arr, sp_arr_roundtripped) @parametrize_dtypes @parametrize_scipy_fmt_with_arg("format1") @parametrize_scipy_fmt_with_arg("format2") def test_add(rng, dtype, format1, format2): if format1 == "coo" or format2 == "coo": pytest.xfail(reason="https://github.com/llvm/llvm-project/issues/116012") SHAPE = (100, 50) DENSITY = 0.5 sampler = generate_sampler(dtype, rng) sps_arr1 = sps.random_array( SHAPE, density=DENSITY, format=format1, dtype=dtype, random_state=rng, data_sampler=sampler ) sps_arr2 = sps.random_array( SHAPE, density=DENSITY, format=format2, dtype=dtype, random_state=rng, data_sampler=sampler ) sp_arr1 = sparse.asarray(sps_arr1) sp_arr2 = sparse.asarray(sps_arr2) expected = sps_arr1 + sps_arr2 actual = sparse.add(sp_arr1, sp_arr2) actual_sps = sparse.to_scipy(actual.asformat(sparse.asarray(expected).format)) assert_sps_equal(expected, actual_sps, check_canonical=True) @parametrize_dtypes @pytest.mark.parametrize("shape", [(80, 100), (200,), (10, 20, 30)]) def test_add_dense(rng, dtype, shape): sampler = generate_sampler(dtype, rng) np_arr1 = sampler(shape) np_arr2 = sampler(shape) sp_arr1 = sparse.asarray(np_arr1) sp_arr2 = sparse.asarray(np_arr2) expected = np_arr1 + np_arr2 actual = sparse.add(sp_arr1, sp_arr2) actual_np = sparse.to_numpy(actual) np.testing.assert_array_equal(expected, actual_np) @parametrize_dtypes @parametrize_scipy_fmt def test_add_dense_sparse(rng, dtype, format): if format == "coo": pytest.xfail(reason="https://github.com/llvm/llvm-project/issues/116012") sampler = generate_sampler(dtype, rng) SHAPE = (100, 50) DENSITY = 0.5 np_arr1 = sampler(SHAPE) sps_arr2 = sps.random_array( SHAPE, density=DENSITY, format=format, dtype=dtype, random_state=rng, data_sampler=sampler ) sp_arr1 = sparse.asarray(np_arr1) sp_arr2 = sparse.asarray(sps_arr2) expected = np_arr1 + sps_arr2 actual = sparse.add(sp_arr1, sp_arr2) actual_np = sparse.to_numpy(actual.asformat(sp_arr1.format)) np.testing.assert_array_equal(expected, actual_np) @parametrize_dtypes def test_csf_format(dtype): format = sparse.formats.Csf().with_ndim(3).with_dtype(dtype).build() SHAPE = (2, 2, 4) pos_1, crd_1, pos_2, crd_2, data = get_example_csf_arrays(dtype) constituent_arrays = (pos_1, crd_1, pos_2, crd_2, data) csf_array = sparse.from_constituent_arrays(format=format, arrays=constituent_arrays, shape=SHAPE) result_arrays = csf_array.get_constituent_arrays() for actual, expected in zip(result_arrays, constituent_arrays, strict=True): np.testing.assert_array_equal(actual, expected) actual = sparse.add(csf_array, csf_array) expected = sparse.from_constituent_arrays(format=format, arrays=(pos_1, crd_1, pos_2, crd_2, data * 2), shape=SHAPE) assert_array_equal(expected, actual) @parametrize_dtypes def test_coo_3d_format(dtype): format = sparse.formats.Coo().with_ndim(3).with_dtype(dtype).build() SHAPE = (2, 2, 4) pos = np.array([0, 7]) crd = [np.array([0, 1, 0, 0, 1, 1, 0]), np.array([1, 3, 1, 0, 0, 1, 0]), np.array([3, 1, 1, 0, 1, 1, 1])] data = np.array([1, 2, 3, 4, 5, 6, 7], dtype=dtype) carrs = (pos, *crd, data) coo_array = sparse.from_constituent_arrays(format=format, arrays=carrs, shape=SHAPE) result = coo_array.get_constituent_arrays() for actual, expected in zip(result, carrs, strict=True): np.testing.assert_array_equal(actual, expected) actual = sparse.add(coo_array, coo_array).asformat(coo_array.format) expected = sparse.from_constituent_arrays(format=actual.format, arrays=(pos, *crd, data * 2), shape=SHAPE) assert_array_equal(expected, actual) @parametrize_dtypes def test_sparse_vector_format(dtype): if sparse.asdtype(dtype) in {sparse.complex64, sparse.complex128}: pytest.xfail("The sparse_vector format returns incorrect results for complex dtypes.") format = sparse.formats.Coo().with_ndim(1).with_dtype(dtype).build() SHAPE = (10,) pos = np.array([0, 6]) crd = np.array([0, 1, 2, 6, 8, 9]) data = np.array([1, 2, 3, 4, 5, 6], dtype=dtype) carrs = (pos, crd, data) sv_array = sparse.from_constituent_arrays(format=format, arrays=carrs, shape=SHAPE) result = sv_array.get_constituent_arrays() for actual, expected in zip(result, carrs, strict=True): np.testing.assert_array_equal(actual, expected) actual = sparse.add(sv_array, sv_array) expected = sparse.from_constituent_arrays(format=actual.format, arrays=(pos, crd, data * 2), shape=SHAPE) assert_array_equal(expected, actual) dense = np.array([1, 2, 3, 0, 0, 0, 4, 0, 5, 6], dtype=dtype) dense_array = sparse.asarray(dense) res = sparse.to_numpy(sparse.add(dense_array, sv_array)) np.testing.assert_array_equal(res, dense * 2) def test_copy(): arr_np_orig = np.arange(25).reshape((5, 5)) arr_np_copy = arr_np_orig.copy() arr_sp1 = sparse.asarray(arr_np_copy, copy=True) arr_sp2 = sparse.asarray(arr_np_copy, copy=False).copy() arr_sp3 = sparse.asarray(arr_np_copy, copy=False) arr_np_copy[2, 2] = 42 np.testing.assert_array_equal(sparse.to_numpy(arr_sp1), arr_np_orig) np.testing.assert_array_equal(sparse.to_numpy(arr_sp2), arr_np_orig) np.testing.assert_array_equal(sparse.to_numpy(arr_sp3), arr_np_copy) @parametrize_dtypes @pytest.mark.parametrize( "format", [ "csr", pytest.param("csc", marks=pytest.mark.xfail(reason="https://github.com/llvm/llvm-project/pull/109641")), "coo", ], ) @pytest.mark.parametrize( ("shape", "new_shape"), [ ((100, 50), (25, 200)), ((100, 50), (10, 500, 1)), ((80, 1), (8, 10)), ((80, 1), (80,)), ], ) def test_reshape(rng, dtype, format, shape, new_shape): DENSITY = 0.5 sampler = generate_sampler(dtype, rng) arr_sps = sps.random_array( shape, density=DENSITY, format=format, dtype=dtype, random_state=rng, data_sampler=sampler ) arr_sps.eliminate_zeros() arr_sps.sum_duplicates() arr = sparse.asarray(arr_sps) actual = sparse.reshape(arr, shape=new_shape) assert actual.shape == new_shape try: scipy_format = sparse.to_scipy(actual).format except RuntimeError: tmp_fmt = sparse.formats.Dense().with_ndim(arr.ndim).with_dtype(dtype).build() arr_dense = arr.asformat(tmp_fmt) arr_np = sparse.to_numpy(arr_dense) expected_np = arr_np.reshape(new_shape) out_fmt = sparse.formats.Dense().with_ndim(expected_np.ndim).with_dtype(dtype).build() actual_dense = actual.asformat(out_fmt) actual_np = sparse.to_numpy(actual_dense) np.testing.assert_array_equal(expected_np, actual_np) return expected = sparse.asarray(arr_sps.reshape(new_shape).asformat(scipy_format)) for x, y in zip(expected.get_constituent_arrays(), actual.get_constituent_arrays(), strict=True): np.testing.assert_array_equal(x, y) @parametrize_dtypes def test_reshape_csf(dtype): # CSF csf_shape = (2, 2, 4) csf_format = sparse.formats.Csf().with_ndim(3).with_dtype(dtype).build() for shape, new_shape, expected_arrs in [ ( csf_shape, (4, 4, 1), [ np.array([0, 0, 3, 5, 7]), np.array([0, 1, 3, 0, 3, 0, 1]), np.array([0, 1, 2, 3, 4, 5, 6, 7]), np.array([0, 0, 0, 0, 0, 0, 0]), np.array([1, 2, 3, 4, 5, 6, 7]), ], ), ( csf_shape, (2, 1, 8), [ np.array([0, 1, 2]), np.array([0, 0]), np.array([0, 3, 7]), np.array([4, 5, 7, 0, 3, 4, 5]), np.array([1, 2, 3, 4, 5, 6, 7]), ], ), ]: arrs = get_example_csf_arrays(dtype) csf_tensor = sparse.from_constituent_arrays(format=csf_format, arrays=arrs, shape=shape) result = sparse.reshape(csf_tensor, shape=new_shape) for actual, expected in zip(result.get_constituent_arrays(), expected_arrs, strict=True): np.testing.assert_array_equal(actual, expected) @parametrize_dtypes def test_reshape_dense(dtype): SHAPE = (2, 2, 4) np_arr = np.arange(math.prod(SHAPE), dtype=dtype).reshape(SHAPE) sp_arr = sparse.asarray(np_arr) for new_shape in [ (4, 4, 1), (2, 1, 8), ]: expected = np_arr.reshape(new_shape) actual = sparse.reshape(sp_arr, new_shape) actual_np = sparse.to_numpy(actual) assert actual_np.dtype == expected.dtype np.testing.assert_equal(actual_np, expected) @pytest.mark.parametrize("src_fmt", ["csr", "csc", "coo"]) @pytest.mark.parametrize("dst_fmt", ["csr", "csc", "coo"]) def test_asformat(rng, src_fmt, dst_fmt): if "coo" in {src_fmt, dst_fmt}: pytest.xfail(reason="https://github.com/llvm/llvm-project/issues/116012") SHAPE = (100, 50) DENSITY = 0.5 sampler = generate_sampler(np.float64, rng) sps_arr = sps.random_array( SHAPE, density=DENSITY, format=src_fmt, dtype=np.float64, random_state=rng, data_sampler=sampler ) sp_arr = sparse.asarray(sps_arr) expected = sps_arr.asformat(dst_fmt) actual_fmt = sparse.asarray(expected, copy=False).format actual = sp_arr.asformat(actual_fmt) actual_sps = sparse.to_scipy(actual) assert actual_sps.format == dst_fmt assert_sps_equal(expected, actual_sps) sparse-0.17.0/sparse/numba_backend/000077500000000000000000000000001501262445000171415ustar00rootroot00000000000000sparse-0.17.0/sparse/numba_backend/__init__.py000066400000000000000000000117721501262445000212620ustar00rootroot00000000000000from numpy import ( add, bitwise_and, bitwise_not, bitwise_or, bitwise_xor, ceil, complex64, complex128, conj, copysign, cos, cosh, divide, e, exp, expm1, finfo, float16, float32, float64, floor, floor_divide, greater, greater_equal, hypot, iinfo, inf, int8, int16, int32, int64, isfinite, less, less_equal, log, log1p, log2, log10, logaddexp, logical_and, logical_not, logical_or, logical_xor, maximum, minimum, multiply, nan, negative, newaxis, nextafter, not_equal, pi, positive, reciprocal, remainder, sign, signbit, sin, sinh, sqrt, square, subtract, tan, tanh, trunc, uint8, uint16, uint32, uint64, ) from numpy import arccos as acos from numpy import arccosh as acosh from numpy import arcsin as asin from numpy import arcsinh as asinh from numpy import arctan as atan from numpy import arctan2 as atan2 from numpy import arctanh as atanh from numpy import bool_ as bool from numpy import invert as bitwise_invert from numpy import left_shift as bitwise_left_shift from numpy import power as pow from numpy import right_shift as bitwise_right_shift from ._common import ( SparseArray, abs, all, any, asarray, asnumpy, astype, broadcast_arrays, broadcast_to, can_cast, concat, concatenate, dot, einsum, empty, empty_like, equal, eye, full, full_like, imag, isinf, isnan, matmul, max, mean, min, moveaxis, nonzero, ones, ones_like, outer, pad, permute_dims, prod, real, reshape, round, squeeze, stack, std, sum, tensordot, var, vecdot, zeros, zeros_like, ) from ._compressed import GCXS from ._coo import COO, as_coo from ._coo.common import ( argmax, argmin, argwhere, asCOO, clip, diagonal, diagonalize, expand_dims, flip, isneginf, isposinf, kron, matrix_transpose, nanmax, nanmean, nanmin, nanprod, nanreduce, nansum, result_type, roll, sort, take, tril, triu, unique_counts, unique_values, where, ) from ._dok import DOK from ._io import load_npz, save_npz from ._settings import IS_NUMPY2 as _IS_NUMPY2 from ._settings import __array_namespace_info__ # noqa: F401 from ._umath import elemwise from ._utils import random __all__ = [ "COO", "DOK", "GCXS", "SparseArray", "abs", "acos", "acosh", "add", "all", "any", "argmax", "argmin", "argwhere", "asCOO", "as_coo", "asarray", "asin", "asinh", "asnumpy", "astype", "atan", "atan2", "atanh", "bitwise_and", "bitwise_invert", "bitwise_left_shift", "bitwise_not", "bitwise_or", "bitwise_right_shift", "bitwise_xor", "bool", "broadcast_arrays", "broadcast_to", "can_cast", "ceil", "clip", "complex128", "complex64", "concat", "concatenate", "conj", "copysign", "cos", "cosh", "diagonal", "diagonalize", "divide", "dot", "e", "einsum", "elemwise", "empty", "empty_like", "equal", "exp", "expand_dims", "expm1", "eye", "finfo", "flip", "float16", "float32", "float64", "floor", "floor_divide", "full", "full_like", "greater", "greater_equal", "hypot", "iinfo", "imag", "inf", "int16", "int32", "int64", "int8", "isfinite", "isinf", "isnan", "isneginf", "isposinf", "kron", "less", "less_equal", "load_npz", "log", "log10", "log1p", "log2", "logaddexp", "logical_and", "logical_not", "logical_or", "logical_xor", "matmul", "matrix_transpose", "max", "maximum", "mean", "min", "minimum", "moveaxis", "multiply", "nan", "nanmax", "nanmean", "nanmin", "nanprod", "nanreduce", "nansum", "negative", "newaxis", "nextafter", "nonzero", "not_equal", "ones", "ones_like", "outer", "pad", "permute_dims", "pi", "positive", "pow", "prod", "random", "real", "reciprocal", "remainder", "reshape", "result_type", "roll", "round", "save_npz", "sign", "signbit", "sin", "sinh", "sort", "sqrt", "square", "squeeze", "stack", "std", "subtract", "sum", "take", "tan", "tanh", "tensordot", "tril", "triu", "trunc", "uint16", "uint32", "uint64", "uint8", "unique_counts", "unique_values", "var", "vecdot", "where", "zeros", "zeros_like", ] if _IS_NUMPY2: from numpy import isdtype __all__ += [ "isdtype", ] __all__.sort() sparse-0.17.0/sparse/numba_backend/_common.py000066400000000000000000003114601501262445000211470ustar00rootroot00000000000000import builtins import warnings from collections.abc import Iterable from functools import reduce, wraps from itertools import chain from operator import index, mul import numba from numba import literal_unroll import numpy as np from ._coo import as_coo from ._sparse_array import SparseArray from ._utils import ( _zero_of_dtype, check_zero_fill_value, equivalent, normalize_axis, ) _EINSUM_SYMBOLS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" _EINSUM_SYMBOLS_SET = set(_EINSUM_SYMBOLS) def _is_scipy_sparse_obj(x): """ Tests if the supplied argument is a SciPy sparse object. """ return bool(hasattr(x, "__module__") and x.__module__.startswith("scipy.sparse")) def _check_device(func): @wraps(func) def wrapped(*args, **kwargs): device = kwargs.get("device") if device not in {"cpu", None}: raise ValueError("Device must be `'cpu'` or `None`.") return func(*args, **kwargs) return wrapped def _is_sparse(x): """ Tests if the supplied argument is a SciPy sparse object, or one from this library. """ return isinstance(x, SparseArray) or _is_scipy_sparse_obj(x) @numba.njit def nan_check(*args): """ Check for the NaN values in Numpy Arrays Parameters ---------- Union[Numpy Array, Integer, Float] Returns ------- Boolean Whether Numpy Array Contains NaN """ for i in literal_unroll(args): ia = np.asarray(i) if ia.size != 0 and np.isnan(np.min(ia)): return True return False def check_class_nan(test): """ Check NaN for Sparse Arrays Parameters ---------- test : Union[sparse.COO, sparse.GCXS, scipy.sparse.spmatrix, Numpy Ndarrays] Returns ------- Boolean Whether Sparse Array Contains NaN """ from ._compressed import GCXS from ._coo import COO if isinstance(test, GCXS | COO): return nan_check(test.fill_value, test.data) if _is_scipy_sparse_obj(test): return nan_check(test.data) return nan_check(test) def tensordot(a, b, axes=2, *, return_type=None): """ Perform the equivalent of [`numpy.tensordot`][]. Parameters ---------- a, b : Union[SparseArray, np.ndarray, scipy.sparse.spmatrix] The arrays to perform the `tensordot` operation on. axes : tuple[Union[int, tuple[int], Union[int, tuple[int]], optional The axes to match when performing the sum. return_type : {None, COO, np.ndarray}, optional Type of returned array. Returns ------- Union[SparseArray, numpy.ndarray] The result of the operation. Raises ------ ValueError If all arguments don't have zero fill-values. See Also -------- - [`numpy.tensordot`][] : NumPy equivalent function """ from ._compressed import GCXS # Much of this is stolen from numpy/core/numeric.py::tensordot # Please see license at https://github.com/numpy/numpy/blob/main/LICENSE.txt check_zero_fill_value(a, b) if _is_scipy_sparse_obj(a): a = GCXS.from_scipy_sparse(a) if _is_scipy_sparse_obj(b): b = GCXS.from_scipy_sparse(b) try: iter(axes) except TypeError: axes_a = list(range(-axes, 0)) axes_b = list(range(axes)) else: axes_a, axes_b = axes try: na = len(axes_a) axes_a = list(axes_a) except TypeError: axes_a = [axes_a] na = 1 try: nb = len(axes_b) axes_b = list(axes_b) except TypeError: axes_b = [axes_b] nb = 1 # a, b = asarray(a), asarray(b) # <--- modified as_ = a.shape nda = a.ndim bs = b.shape ndb = b.ndim equal = True if nda == 0 or ndb == 0: if axes_a == [] and axes_b == []: if nda == 0 and isinstance(a, SparseArray): a = a.todense() if ndb == 0 and isinstance(b, SparseArray): b = b.todense() return a * b pos = int(nda != 0) raise ValueError(f"Input {pos} operand does not have enough dimensions") if na != nb: equal = False else: for k in range(na): if as_[axes_a[k]] != bs[axes_b[k]]: equal = False break if axes_a[k] < 0: axes_a[k] += nda if axes_b[k] < 0: axes_b[k] += ndb if not equal: raise ValueError("shape-mismatch for sum") # Move the axes to sum over to the end of "a" # and to the front of "b" notin = [k for k in range(nda) if k not in axes_a] newaxes_a = notin + axes_a N2 = 1 for axis in axes_a: N2 *= as_[axis] newshape_a = (-1, N2) olda = [as_[axis] for axis in notin] notin = [k for k in range(ndb) if k not in axes_b] newaxes_b = axes_b + notin N2 = 1 for axis in axes_b: N2 *= bs[axis] newshape_b = (N2, -1) oldb = [bs[axis] for axis in notin] if builtins.any(dim == 0 for dim in chain(newshape_a, newshape_b)): from sparse import COO dt = np.result_type(a.dtype, b.dtype) res = COO( np.empty((len(olda) + len(oldb), 0), dtype=np.uintp), data=np.empty(0, dtype=dt), shape=tuple(olda + oldb) ) if isinstance(a, np.ndarray) or isinstance(b, np.ndarray): res = res.todense() return res at = a.transpose(newaxes_a).reshape(newshape_a) bt = b.transpose(newaxes_b).reshape(newshape_b) res = _dot(at, bt, return_type) return res.reshape(olda + oldb) def matmul(a, b): """Perform the equivalent of [`numpy.matmul`][] on two arrays. Parameters ---------- a, b : Union[SparseArray, np.ndarray, scipy.sparse.spmatrix] The arrays to perform the `matmul` operation on. Returns ------- Union[SparseArray, numpy.ndarray] The result of the operation. Raises ------ ValueError If all arguments don't have zero fill-values, or the shape of the two arrays is not broadcastable. See Also -------- - [`numpy.matmul`][] : NumPy equivalent function. - `COO.__matmul__`: Equivalent function for COO objects. """ check_zero_fill_value(a, b) if not hasattr(a, "ndim") or not hasattr(b, "ndim"): raise TypeError(f"Cannot perform dot product on types {type(a)}, {type(b)}") if check_class_nan(a) or check_class_nan(b): warnings.warn("Nan will not be propagated in matrix multiplication", RuntimeWarning, stacklevel=1) # When b is 2-d, it is equivalent to dot if b.ndim <= 2: return dot(a, b) # when a is 2-d, we need to transpose result after dot if a.ndim <= 2: res = dot(a, b) axes = list(range(res.ndim)) axes.insert(-1, axes.pop(0)) return res.transpose(axes) # If a can be squeeze to a vector, use dot will be faster if a.ndim <= b.ndim and np.prod(a.shape[:-1]) == 1: res = dot(a.reshape(-1), b) shape = list(res.shape) shape.insert(-1, 1) return res.reshape(shape) # If b can be squeeze to a matrix, use dot will be faster if b.ndim <= a.ndim and np.prod(b.shape[:-2]) == 1: return dot(a, b.reshape(b.shape[-2:])) if a.ndim < b.ndim: a = a[(None,) * (b.ndim - a.ndim)] if a.ndim > b.ndim: b = b[(None,) * (a.ndim - b.ndim)] for i, j in zip(a.shape[:-2], b.shape[:-2], strict=True): if i != 1 and j != 1 and i != j: raise ValueError("shapes of a and b are not broadcastable") def _matmul_recurser(a, b): if a.ndim == 2: return dot(a, b) res = [] for i in range(builtins.max(a.shape[0], b.shape[0])): a_i = a[0] if a.shape[0] == 1 else a[i] b_i = b[0] if b.shape[0] == 1 else b[i] res.append(_matmul_recurser(a_i, b_i)) mask = [isinstance(x, SparseArray) for x in res] if builtins.all(mask): return stack(res) res = [x.todense() if isinstance(x, SparseArray) else x for x in res] return np.stack(res) return _matmul_recurser(a, b) def dot(a, b): """ Perform the equivalent of [`numpy.dot`][] on two arrays. Parameters ---------- a, b : Union[SparseArray, np.ndarray, scipy.sparse.spmatrix] The arrays to perform the `dot` operation on. Returns ------- Union[SparseArray, numpy.ndarray] The result of the operation. Raises ------ ValueError If all arguments don't have zero fill-values. See Also -------- - [`numpy.dot`][] : NumPy equivalent function. - [`sparse.COO.dot`][] : Equivalent function for COO objects. """ check_zero_fill_value(a, b) if not hasattr(a, "ndim") or not hasattr(b, "ndim"): raise TypeError(f"Cannot perform dot product on types {type(a)}, {type(b)}") if a.ndim == 1 and b.ndim == 1: if isinstance(a, SparseArray): a = as_coo(a) if isinstance(b, SparseArray): b = as_coo(b) return (a * b).sum() a_axis = -1 b_axis = -2 if b.ndim == 1: b_axis = -1 return tensordot(a, b, axes=(a_axis, b_axis)) def _dot(a, b, return_type=None): from ._compressed import GCXS from ._coo import COO from ._sparse_array import SparseArray out_shape = (a.shape[0], b.shape[1]) if builtins.all(isinstance(arr, SparseArray) for arr in [a, b]) and builtins.any( isinstance(arr, GCXS) for arr in [a, b] ): a = a.asformat("gcxs") b = b.asformat("gcxs", compressed_axes=a.compressed_axes) if isinstance(a, GCXS) and isinstance(b, GCXS): if a.nbytes > b.nbytes: b = b.change_compressed_axes(a.compressed_axes) else: a = a.change_compressed_axes(b.compressed_axes) if a.compressed_axes == (0,): # csr @ csr compressed_axes = (0,) data, indices, indptr = _dot_csr_csr_type(a.dtype, b.dtype)( out_shape, a.data, b.data, a.indices, b.indices, a.indptr, b.indptr ) elif a.compressed_axes == (1,): # csc @ csc # a @ b = (b.T @ a.T).T compressed_axes = (1,) data, indices, indptr = _dot_csr_csr_type(b.dtype, a.dtype)( out_shape[::-1], b.data, a.data, b.indices, a.indices, b.indptr, a.indptr, ) out = GCXS( (data, indices, indptr), shape=out_shape, compressed_axes=compressed_axes, prune=True, ) if return_type == np.ndarray: return out.todense() if return_type == COO: return out.tocoo() return out if isinstance(a, GCXS) and isinstance(b, np.ndarray): if a.compressed_axes == (0,): # csr @ ndarray if return_type is None or return_type == np.ndarray: return _dot_csr_ndarray_type(a.dtype, b.dtype)(out_shape, a.data, a.indices, a.indptr, b) data, indices, indptr = _dot_csr_ndarray_type_sparse(a.dtype, b.dtype)( out_shape, a.data, a.indices, a.indptr, b ) out = GCXS( (data, indices, indptr), shape=out_shape, compressed_axes=(0,), prune=True, ) if return_type == COO: return out.tocoo() return out if return_type is None or return_type == np.ndarray: # csc @ ndarray return _dot_csc_ndarray_type(a.dtype, b.dtype)(a.shape, b.shape, a.data, a.indices, a.indptr, b) data, indices, indptr = _dot_csc_ndarray_type_sparse(a.dtype, b.dtype)( a.shape, b.shape, a.data, a.indices, a.indptr, b ) compressed_axes = (1,) out = GCXS( (data, indices, indptr), shape=out_shape, compressed_axes=compressed_axes, prune=True, ) if return_type == COO: return out.tocoo() return out if isinstance(a, np.ndarray) and isinstance(b, GCXS): at = a.view(type=np.ndarray).T bt = b.T # constant-time transpose if b.compressed_axes == (0,): if return_type is None or return_type == np.ndarray: out = _dot_csc_ndarray_type(bt.dtype, at.dtype)(bt.shape, at.shape, bt.data, bt.indices, bt.indptr, at) return out.T data, indices, indptr = _dot_csc_ndarray_type_sparse(bt.dtype, at.dtype)( bt.shape, at.shape, bt.data, b.indices, b.indptr, at ) out = GCXS( (data, indices, indptr), shape=out_shape, compressed_axes=(0,), prune=True, ) if return_type == COO: return out.tocoo() return out # compressed_axes == (1,) if return_type is None or return_type == np.ndarray: out = _dot_csr_ndarray_type(bt.dtype, at.dtype)(out_shape[::-1], bt.data, bt.indices, bt.indptr, at) return out.T data, indices, indptr = _dot_csr_ndarray_type_sparse(bt.dtype, at.dtype)( out_shape[::-1], bt.data, bt.indices, bt.indptr, at ) out = GCXS((data, indices, indptr), shape=out_shape, compressed_axes=(1,), prune=True) if return_type == COO: return out.tocoo() return out if isinstance(a, COO) and isinstance(b, COO): # convert to csr a_indptr = np.empty(a.shape[0] + 1, dtype=np.intp) a_indptr[0] = 0 np.cumsum(np.bincount(a.coords[0], minlength=a.shape[0]), out=a_indptr[1:]) b_indptr = np.empty(b.shape[0] + 1, dtype=np.intp) b_indptr[0] = 0 np.cumsum(np.bincount(b.coords[0], minlength=b.shape[0]), out=b_indptr[1:]) coords, data = _dot_coo_coo_type(a.dtype, b.dtype)( out_shape, a.coords, b.coords, a.data, b.data, a_indptr, b_indptr ) out = COO( coords, data, shape=out_shape, has_duplicates=False, sorted=False, prune=True, ) if return_type == np.ndarray: return out.todense() if return_type == GCXS: return out.asformat("gcxs") return out if isinstance(a, COO) and isinstance(b, np.ndarray): b = b.view(type=np.ndarray).T if return_type is None or return_type == np.ndarray: return _dot_coo_ndarray_type(a.dtype, b.dtype)(a.coords, a.data, b, out_shape) coords, data = _dot_coo_ndarray_type_sparse(a.dtype, b.dtype)(a.coords, a.data, b, out_shape) out = COO(coords, data, shape=out_shape, has_duplicates=False, sorted=True) if return_type == GCXS: return out.asformat("gcxs") return out if isinstance(a, np.ndarray) and isinstance(b, COO): a = a.view(type=np.ndarray) if return_type is None or return_type == np.ndarray: return _dot_ndarray_coo_type(a.dtype, b.dtype)(a, b.coords, b.data, out_shape) b = b.T coords, data = _dot_ndarray_coo_type_sparse(a.dtype, b.dtype)(a, b.coords, b.data, out_shape) out = COO(coords, data, shape=out_shape, has_duplicates=False, sorted=True, prune=True) if return_type == GCXS: return out.asformat("gcxs") return out if isinstance(a, np.ndarray) and isinstance(b, np.ndarray): return np.dot(a, b) raise TypeError("Unsupported types.") def _memoize_dtype(f): """ Memoizes a function taking in NumPy dtypes. Parameters ---------- f : Callable Returns ------- wrapped : Callable Examples -------- >>> def func(dt1): ... return object() >>> func = _memoize_dtype(func) >>> func(np.dtype("i8")) is func(np.dtype("int64")) True >>> func(np.dtype("i8")) is func(np.dtype("i4")) False """ cache = {} @wraps(f) def wrapped(*args): key = tuple(arg.name for arg in args) if key in cache: return cache[key] result = f(*args) cache[key] = result return result return wrapped @numba.jit(nopython=True, nogil=True) def _csr_csr_count_nnz(out_shape, a_indices, b_indices, a_indptr, b_indptr): # pragma: no cover """ A function for computing the number of nonzero values in the resulting array from multiplying an array with compressed rows with an array with compressed rows: (a @ b).nnz. Parameters ---------- out_shape : tuple The shape of the output array. a_indices, a_indptr : np.ndarray The indices and index pointer array of ``a``. b_data, b_indices, b_indptr : np.ndarray The indices and index pointer array of ``b``. """ n_row, n_col = out_shape nnz = 0 mask = np.full(n_col, -1) for i in range(n_row): row_nnz = 0 for j in a_indices[a_indptr[i] : a_indptr[i + 1]]: for k in b_indices[b_indptr[j] : b_indptr[j + 1]]: if mask[k] != i: mask[k] = i row_nnz += 1 nnz += row_nnz return nnz @numba.jit(nopython=True, nogil=True) def _csr_ndarray_count_nnz(out_shape, indptr, a_indices, a_indptr, b): # pragma: no cover """ A function for computing the number of nonzero values in the resulting array from multiplying an array with compressed rows with a dense numpy array: (a @ b).nnz. Parameters ---------- out_shape : tuple The shape of the output array. indptr : ndarray The empty index pointer array for the output. a_indices, a_indptr : np.ndarray The indices and index pointer array of ``a``. b : np.ndarray The second input array ``b``. """ nnz = 0 for i in range(out_shape[0]): cur_row = a_indices[a_indptr[i] : a_indptr[i + 1]] for j in range(out_shape[1]): for k in cur_row: if b[k, j] != 0: nnz += 1 break indptr[i + 1] = nnz return nnz @numba.jit(nopython=True, nogil=True) def _csc_ndarray_count_nnz(a_shape, b_shape, indptr, a_indices, a_indptr, b): # pragma: no cover """ A function for computing the number of nonzero values in the resulting array from multiplying an array with compressed columns with a dense numpy array: (a @ b).nnz. Parameters ---------- a_shape, b_shape : tuple The shapes of the input arrays. indptr : ndarray The empty index pointer array for the output. a_indices, a_indptr : np.ndarray The indices and index pointer array of ``a``. b : np.ndarray The second input array ``b``. """ nnz = 0 mask = np.full(a_shape[0], -1) for i in range(b_shape[1]): col_nnz = 0 for j in range(b_shape[0]): for k in a_indices[a_indptr[j] : a_indptr[j + 1]]: if b[j, i] != 0 and mask[k] != i: mask[k] = i col_nnz += 1 nnz += col_nnz indptr[i + 1] = nnz return nnz def _dot_dtype(dt1, dt2): return (np.zeros((), dtype=dt1) * np.zeros((), dtype=dt2)).dtype @_memoize_dtype def _dot_csr_csr_type(dt1, dt2): dtr = _dot_dtype(dt1, dt2) @numba.jit( nopython=True, nogil=True, locals={"data_curr": numba.np.numpy_support.from_dtype(dtr)}, ) def _dot_csr_csr(out_shape, a_data, b_data, a_indices, b_indices, a_indptr, b_indptr): # pragma: no cover """ Utility function taking in two ``GCXS`` objects and calculating their dot product: a @ b for a and b with compressed rows. Parameters ---------- out_shape : tuple The shape of the output array. a_data, a_indices, a_indptr : np.ndarray The data, indices, and index pointer arrays of ``a``. b_data, b_indices, b_indptr : np.ndarray The data, indices, and index pointer arrays of ``b``. """ # much of this is borrowed from: # https://github.com/scipy/scipy/blob/main/scipy/sparse/sparsetools/csr.h # calculate nnz before multiplying so we can use static arrays nnz = _csr_csr_count_nnz(out_shape, a_indices, b_indices, a_indptr, b_indptr) n_row, n_col = out_shape indptr = np.empty(n_row + 1, dtype=np.intp) indptr[0] = 0 indices = np.empty(nnz, dtype=np.intp) data = np.empty(nnz, dtype=dtr) next_ = np.full(n_col, -1) sums = np.zeros(n_col, dtype=dtr) nnz = 0 for i in range(n_row): head = -2 length = 0 next_[:] = -1 for j, av in zip( # noqa: B905 a_indices[a_indptr[i] : a_indptr[i + 1]], a_data[a_indptr[i] : a_indptr[i + 1]], ): for k, bv in zip( # noqa: B905 b_indices[b_indptr[j] : b_indptr[j + 1]], b_data[b_indptr[j] : b_indptr[j + 1]], ): sums[k] += av * bv if next_[k] == -1: next_[k] = head head = k length += 1 for _ in range(length): if next_[head] != -1: indices[nnz] = head data[nnz] = sums[head] nnz += 1 temp = head head = next_[head] next_[temp] = -1 sums[temp] = 0 indptr[i + 1] = nnz if len(indices) == (n_col * n_row): for i in range(len(indices) // n_col): j = n_col * i k = n_col * (1 + i) data[j:k] = data[j:k][::-1] indices[j:k] = indices[j:k][::-1] return data, indices, indptr return _dot_csr_csr @_memoize_dtype def _dot_csr_ndarray_type(dt1, dt2): dtr = _dot_dtype(dt1, dt2) @numba.jit( nopython=True, nogil=True, locals={"data_curr": numba.np.numpy_support.from_dtype(dtr)}, ) def _dot_csr_ndarray(out_shape, a_data, a_indices, a_indptr, b): # pragma: no cover """ Utility function taking in one `GCXS` and one ``ndarray`` and calculating their dot product: a @ b for a with compressed rows. Returns a dense result. Parameters ---------- a_data, a_indices, a_indptr : np.ndarray The data, indices, and index pointers of ``a``. b : np.ndarray The second input array ``b``. out_shape : Tuple[int] The shape of the output array. """ b = np.ascontiguousarray(b) # ensure memory aligned out = np.zeros(out_shape, dtype=dtr) for i in range(out_shape[0]): val = out[i] for k in range(a_indptr[i], a_indptr[i + 1]): ind = a_indices[k] v = a_data[k] for j in range(out_shape[1]): val[j] += v * b[ind, j] return out return _dot_csr_ndarray @_memoize_dtype def _dot_csr_ndarray_type_sparse(dt1, dt2): dtr = _dot_dtype(dt1, dt2) @numba.jit( nopython=True, nogil=True, locals={"data_curr": numba.np.numpy_support.from_dtype(dtr)}, ) def _dot_csr_ndarray_sparse(out_shape, a_data, a_indices, a_indptr, b): # pragma: no cover """ Utility function taking in one `GCXS` and one ``ndarray`` and calculating their dot product: a @ b for a with compressed rows. Returns a sparse result. Parameters ---------- a_data, a_indices, a_indptr : np.ndarray The data, indices, and index pointers of ``a``. b : np.ndarray The second input array ``b``. out_shape : Tuple[int] The shape of the output array. """ indptr = np.empty(out_shape[0] + 1, dtype=np.intp) indptr[0] = 0 nnz = _csr_ndarray_count_nnz(out_shape, indptr, a_indices, a_indptr, b) indices = np.empty(nnz, dtype=np.intp) data = np.empty(nnz, dtype=dtr) current = 0 for i in range(out_shape[0]): for j in range(out_shape[1]): val = 0 nonzero = False for k in range(a_indptr[i], a_indptr[i + 1]): ind = a_indices[k] v = a_data[k] val += v * b[ind, j] if b[ind, j] != 0: nonzero = True if nonzero: data[current] = val indices[current] = j current += 1 return data, indices, indptr return _dot_csr_ndarray_sparse @_memoize_dtype def _dot_csc_ndarray_type_sparse(dt1, dt2): dtr = _dot_dtype(dt1, dt2) @numba.jit( nopython=True, nogil=True, locals={"data_curr": numba.np.numpy_support.from_dtype(dtr)}, ) def _dot_csc_ndarray_sparse(a_shape, b_shape, a_data, a_indices, a_indptr, b): # pragma: no cover """ Utility function taking in one `GCXS` and one ``ndarray`` and calculating their dot product: a @ b for a with compressed columns. Returns a sparse result. Parameters ---------- a_data, a_indices, a_indptr : np.ndarray The data, indices, and index pointers of ``a``. b : np.ndarray The second input array ``b``. a_shape, b_shape : Tuple[int] The shapes of the input arrays. """ indptr = np.empty(b_shape[1] + 1, dtype=np.intp) nnz = _csc_ndarray_count_nnz(a_shape, b_shape, indptr, a_indices, a_indptr, b) indices = np.empty(nnz, dtype=np.intp) data = np.empty(nnz, dtype=dtr) sums = np.zeros(a_shape[0]) mask = np.full(a_shape[0], -1) nnz = 0 indptr[0] = 0 for i in range(b_shape[1]): head = -2 length = 0 for j in range(b_shape[0]): u = b[j, i] if u != 0: for k in range(a_indptr[j], a_indptr[j + 1]): ind = a_indices[k] v = a_data[k] sums[ind] += u * v if mask[ind] == -1: mask[ind] = head head = ind length += 1 for _ in range(length): if sums[head] != 0: indices[nnz] = head data[nnz] = sums[head] nnz += 1 temp = head head = mask[head] mask[temp] = -1 sums[temp] = 0 return data, indices, indptr return _dot_csc_ndarray_sparse @_memoize_dtype def _dot_csc_ndarray_type(dt1, dt2): dtr = _dot_dtype(dt1, dt2) @numba.jit( nopython=True, nogil=True, locals={"data_curr": numba.np.numpy_support.from_dtype(dtr)}, ) def _dot_csc_ndarray(a_shape, b_shape, a_data, a_indices, a_indptr, b): # pragma: no cover """ Utility function taking in one `GCXS` and one ``ndarray`` and calculating their dot product: a @ b for a with compressed columns. Returns a dense result. Parameters ---------- a_data, a_indices, a_indptr : np.ndarray The data, indices, and index pointers of ``a``. b : np.ndarray The second input array ``b``. a_shape, b_shape : Tuple[int] The shapes of the input arrays. """ b = np.ascontiguousarray(b) # ensure memory aligned out = np.zeros((a_shape[0], b_shape[1]), dtype=dtr) for i in range(b_shape[0]): for k in range(a_indptr[i], a_indptr[i + 1]): ind = a_indices[k] v = a_data[k] val = out[ind] for j in range(b_shape[1]): val[j] += v * b[i, j] return out return _dot_csc_ndarray @_memoize_dtype def _dot_coo_coo_type(dt1, dt2): dtr = _dot_dtype(dt1, dt2) @numba.jit( nopython=True, nogil=True, locals={"data_curr": numba.np.numpy_support.from_dtype(dtr)}, ) def _dot_coo_coo(out_shape, a_coords, b_coords, a_data, b_data, a_indptr, b_indptr): # pragma: no cover """ Utility function taking in two ``COO`` objects and calculating their dot product: a @ b. Parameters ---------- a_shape, b_shape : tuple The shapes of the input arrays. a_data, a_coords : np.ndarray The data and coordinates of ``a``. b_data, b_coords : np.ndarray The data and coordinates of ``b``. """ # much of this is borrowed from: # https://github.com/scipy/scipy/blob/main/scipy/sparse/sparsetools/csr.h n_row, n_col = out_shape # calculate nnz before multiplying so we can use static arrays nnz = _csr_csr_count_nnz(out_shape, a_coords[1], b_coords[1], a_indptr, b_indptr) coords = np.empty((2, nnz), dtype=np.intp) data = np.empty(nnz, dtype=dtr) next_ = np.full(n_col, -1) sums = np.zeros(n_col, dtype=dtr) nnz = 0 for i in range(n_row): head = -2 length = 0 next_[:] = -1 for j, av in zip( # noqa: B905 a_coords[1, a_indptr[i] : a_indptr[i + 1]], a_data[a_indptr[i] : a_indptr[i + 1]], ): for k, bv in zip( # noqa: B905 b_coords[1, b_indptr[j] : b_indptr[j + 1]], b_data[b_indptr[j] : b_indptr[j + 1]], ): sums[k] += av * bv if next_[k] == -1: next_[k] = head head = k length += 1 for _ in range(length): if next_[head] != -1: coords[0, nnz] = i coords[1, nnz] = head data[nnz] = sums[head] nnz += 1 temp = head head = next_[head] next_[temp] = -1 sums[temp] = 0 return coords, data return _dot_coo_coo @_memoize_dtype def _dot_coo_ndarray_type(dt1, dt2): dtr = _dot_dtype(dt1, dt2) @numba.jit(nopython=True, nogil=True) def _dot_coo_ndarray(coords1, data1, array2, out_shape): # pragma: no cover """ Utility function taking in one `COO` and one ``ndarray`` and calculating a "sense" of their dot product. Acually computes ``s1 @ x2.T``. Parameters ---------- data1, coords1 : np.ndarray The data and coordinates of ``s1``. array2 : np.ndarray The second input array ``x2``. out_shape : Tuple[int] The output shape. """ out = np.zeros(out_shape, dtype=dtr) didx1 = 0 while didx1 < len(data1): oidx1 = coords1[0, didx1] didx1_curr = didx1 for oidx2 in range(out_shape[1]): didx1 = didx1_curr while didx1 < len(data1) and coords1[0, didx1] == oidx1: out[oidx1, oidx2] += data1[didx1] * array2[oidx2, coords1[1, didx1]] didx1 += 1 return out return _dot_coo_ndarray @_memoize_dtype def _dot_coo_ndarray_type_sparse(dt1, dt2): dtr = _dot_dtype(dt1, dt2) @numba.jit( nopython=True, nogil=True, locals={"data_curr": numba.np.numpy_support.from_dtype(dtr)}, ) def _dot_coo_ndarray(coords1, data1, array2, out_shape): # pragma: no cover """ Utility function taking in one `COO` and one ``ndarray`` and calculating a "sense" of their dot product. Acually computes ``s1 @ x2.T``. Parameters ---------- data1, coords1 : np.ndarray The data and coordinates of ``s1``. array2 : np.ndarray The second input array ``x2``. out_shape : Tuple[int] The output shape. """ out_data = [] out_coords = [] # coords1.shape = (2, len(data1)) # coords1[0, :] = rows, sorted # coords1[1, :] = columns didx1 = 0 while didx1 < len(data1): current_row = coords1[0, didx1] cur_didx1 = didx1 oidx2 = 0 while oidx2 < out_shape[1]: cur_didx1 = didx1 data_curr = 0 while cur_didx1 < len(data1) and coords1[0, cur_didx1] == current_row: data_curr += data1[cur_didx1] * array2[oidx2, coords1[1, cur_didx1]] cur_didx1 += 1 if data_curr != 0: out_data.append(data_curr) out_coords.append((current_row, oidx2)) oidx2 += 1 didx1 = cur_didx1 if len(out_data) == 0: return np.empty((2, 0), dtype=np.intp), np.empty((0,), dtype=dtr) return np.array(out_coords).T, np.array(out_data) return _dot_coo_ndarray @_memoize_dtype def _dot_ndarray_coo_type(dt1, dt2): dtr = _dot_dtype(dt1, dt2) @numba.jit(nopython=True, nogil=True) def _dot_ndarray_coo(array1, coords2, data2, out_shape): # pragma: no cover """ Utility function taking in two one ``ndarray`` and one ``COO`` and calculating a "sense" of their dot product. Acually computes ``x1 @ s2.T``. Parameters ---------- array1 : np.ndarray The input array ``x1``. data2, coords2 : np.ndarray The data and coordinates of ``s2``. out_shape : Tuple[int] The output shape. """ out = np.zeros(out_shape, dtype=dtr) for oidx1 in range(out_shape[0]): for didx2 in range(len(data2)): oidx2 = coords2[1, didx2] out[oidx1, oidx2] += array1[oidx1, coords2[0, didx2]] * data2[didx2] return out return _dot_ndarray_coo @_memoize_dtype def _dot_ndarray_coo_type_sparse(dt1, dt2): dtr = _dot_dtype(dt1, dt2) @numba.jit( nopython=True, nogil=True, locals={"data_curr": numba.np.numpy_support.from_dtype(dtr)}, ) def _dot_ndarray_coo(array1, coords2, data2, out_shape): # pragma: no cover """ Utility function taking in two one ``ndarray`` and one ``COO`` and calculating a "sense" of their dot product. Acually computes ``x1 @ s2.T``. Parameters ---------- array1 : np.ndarray The input array ``x1``. data2, coords2 : np.ndarray The data and coordinates of ``s2``. out_shape : Tuple[int] The output shape. """ out_data = [] out_coords = [] # coords2.shape = (2, len(data2)) # coords2[0, :] = columns, sorted # coords2[1, :] = rows for oidx1 in range(out_shape[0]): data_curr = 0 current_col = 0 for didx2 in range(len(data2)): if coords2[0, didx2] != current_col: if data_curr != 0: out_data.append(data_curr) out_coords.append([oidx1, current_col]) data_curr = 0 current_col = coords2[0, didx2] data_curr += array1[oidx1, coords2[1, didx2]] * data2[didx2] if data_curr != 0: out_data.append(data_curr) out_coords.append([oidx1, current_col]) if len(out_data) == 0: return np.empty((2, 0), dtype=np.intp), np.empty((0,), dtype=dtr) return np.array(out_coords).T, np.array(out_data) return _dot_ndarray_coo # Copied from : https://github.com/numpy/numpy/blob/59fec4619403762a5d785ad83fcbde5a230416fc/numpy/core/einsumfunc.py#L523 # under BSD-3-Clause license : https://github.com/numpy/numpy/blob/v1.24.0/LICENSE.txt def _parse_einsum_input(operands): """ A copy of the numpy parse_einsum_input that does not cast the operands to numpy array. Returns ------- input_strings : str Parsed input strings output_string : str Parsed output string operands : list of array_like The operands to use in the numpy contraction Examples -------- The operand list is simplified to reduce printing: >>> rng = np.random.default_rng(42) >>> a = rng.random((4, 4)) >>> b = rng.random((4, 4, 4)) >>> _parse_einsum_input(("...a,...a->...", a, b)) # doctest: +SKIP ('za,xza', 'xz', [a, b]) >>> _parse_einsum_input((a, [Ellipsis, 0], b, [Ellipsis, 0])) # doctest: +SKIP ('za,xza', 'xz', [a, b]) """ if len(operands) == 0: raise ValueError("No input operands") if isinstance(operands[0], str): subscripts = operands[0].replace(" ", "") operands = operands[1:] # Ensure all characters are valid for s in subscripts: if s in ".,->": continue if not s.isalpha(): raise ValueError(f"Character {s} is not a valid symbol.") else: tmp_operands = list(operands) operand_list = [] subscript_list = [] for _ in range(len(operands) // 2): operand_list.append(tmp_operands.pop(0)) subscript_list.append(tmp_operands.pop(0)) output_list = tmp_operands[-1] if len(tmp_operands) else None operands = operand_list subscripts = "" last = len(subscript_list) - 1 for num, sub in enumerate(subscript_list): for s in sub: if s is Ellipsis: subscripts += "..." else: try: s = index(s) except TypeError as e: raise TypeError("For this input type lists must contain either int or Ellipsis") from e subscripts += _EINSUM_SYMBOLS[s] if num != last: subscripts += "," if output_list is not None: subscripts += "->" for s in output_list: if s is Ellipsis: subscripts += "..." else: try: s = index(s) except TypeError as e: raise TypeError("For this input type lists must contain either int or Ellipsis") from e subscripts += _EINSUM_SYMBOLS[s] # Check for proper "->" if ("-" in subscripts) or (">" in subscripts): invalid = (subscripts.count("-") > 1) or (subscripts.count(">") > 1) if invalid or (subscripts.count("->") != 1): raise ValueError("Subscripts can only contain one '->'.") # Parse ellipses if "." in subscripts: used = subscripts.replace(".", "").replace(",", "").replace("->", "") unused = list(_EINSUM_SYMBOLS_SET - set(used)) ellipse_inds = "".join(unused) longest = 0 if "->" in subscripts: input_tmp, output_sub = subscripts.split("->") split_subscripts = input_tmp.split(",") out_sub = True else: split_subscripts = subscripts.split(",") out_sub = False for num, sub in enumerate(split_subscripts): if "." in sub: if (sub.count(".") != 3) or (sub.count("...") != 1): raise ValueError("Invalid Ellipses.") # Take into account numerical values if operands[num].shape == (): ellipse_count = 0 else: ellipse_count = builtins.max(operands[num].ndim, 1) ellipse_count -= len(sub) - 3 if ellipse_count > longest: longest = ellipse_count if ellipse_count < 0: raise ValueError("Ellipses lengths do not match.") if ellipse_count == 0: split_subscripts[num] = sub.replace("...", "") else: rep_inds = ellipse_inds[-ellipse_count:] split_subscripts[num] = sub.replace("...", rep_inds) subscripts = ",".join(split_subscripts) out_ellipse = "" if longest == 0 else ellipse_inds[-longest:] if out_sub: subscripts += "->" + output_sub.replace("...", out_ellipse) else: # Special care for outputless ellipses output_subscript = "" tmp_subscripts = subscripts.replace(",", "") for s in sorted(set(tmp_subscripts)): if not s.isalpha(): raise ValueError(f"Character {s} is not a valid symbol.") if tmp_subscripts.count(s) == 1: output_subscript += s normal_inds = "".join(sorted(set(output_subscript) - set(out_ellipse))) subscripts += "->" + out_ellipse + normal_inds # Build output string if does not exist if "->" in subscripts: input_subscripts, output_subscript = subscripts.split("->") else: input_subscripts = subscripts # Build output subscripts tmp_subscripts = subscripts.replace(",", "") output_subscript = "" for s in sorted(set(tmp_subscripts)): if not s.isalpha(): raise ValueError(f"Character {s} is not a valid symbol.") if tmp_subscripts.count(s) == 1: output_subscript += s # Make sure output subscripts are in the input for char in output_subscript: if char not in input_subscripts: raise ValueError(f"Output character {char} did not appear in the input") # Make sure number operands is equivalent to the number of terms if len(input_subscripts.split(",")) != len(operands): raise ValueError("Number of einsum subscripts must be equal to the number of operands.") return (input_subscripts, output_subscript, operands) def _einsum_single(lhs, rhs, operand): """Perform a single term einsum, i.e. any combination of transposes, sums and traces of dimensions. Parameters ---------- lhs : str The indices of the input array. rhs : str The indices of the output array. operand : SparseArray The array to perform the einsum on. Returns ------- output : SparseArray """ from ._coo import COO if lhs == rhs: if not rhs: # ensure scalar output return operand.sum() return operand if not isinstance(operand, SparseArray): # just use numpy for dense input return np.einsum(f"{lhs}->{rhs}", operand) # else require COO for operations, but check if should convert back to_output_format = getattr(operand, "from_coo", lambda x: x) operand = as_coo(operand) # check if repeated / 'trace' indices mean we are only taking a subset where = {} for i, ix in enumerate(lhs): where.setdefault(ix, []).append(i) selector = None for locs in where.values(): loc0, *rlocs = locs if rlocs: # repeated index if len({operand.shape[loc] for loc in locs}) > 1: raise ValueError("Repeated indices must have the same dimension.") # only select data where all indices match subselector = (operand.coords[loc0] == operand.coords[rlocs]).all(axis=0) if selector is None: selector = subselector else: selector &= subselector # indices that are removed (i.e. not in the output / `perm`) # are handled by `has_duplicates=True` below perm = [lhs.index(ix) for ix in rhs] new_shape = tuple(operand.shape[i] for i in perm) # select the new COO data if selector is not None: new_coords = operand.coords[:, selector][perm] new_data = operand.data[selector] else: new_coords = operand.coords[perm] new_data = operand.data if not rhs: # scalar output - match numpy behaviour by not wrapping as array return new_data.sum() return to_output_format(COO(new_coords, new_data, shape=new_shape, has_duplicates=True)) def einsum(*operands, **kwargs): """ Perform the equivalent of [`numpy.einsum`][]. Parameters ---------- subscripts : str Specifies the subscripts for summation as comma separated list of subscript labels. An implicit (classical Einstein summation) calculation is performed unless the explicit indicator '->' is included as well as subscript labels of the precise output form. operands : sequence of SparseArray These are the arrays for the operation. dtype : data-type, optional If provided, forces the calculation to use the data type specified. Default is `None`. **kwargs : dict, optional Any additional arguments to pass to the function. Returns ------- output : SparseArray The calculation based on the Einstein summation convention. """ lhs, rhs, operands = _parse_einsum_input(operands) # Parse input check_zero_fill_value(*operands) if "dtype" in kwargs and kwargs["dtype"] is not None: operands = [o.astype(kwargs["dtype"]) for o in operands] if len(operands) == 1: return _einsum_single(lhs, rhs, operands[0]) # if multiple arrays: align, broadcast multiply and then use single einsum # for example: # "aab,cbd->dac" # we first perform single term reductions and align: # aab -> ab.. # cbd -> .bcd # (where dots represent broadcastable size 1 dimensions), then multiply all # to form the 'minimal outer product' and do a final single term einsum: # abcd -> dac # get ordered union of indices from all terms, indicies that only appear # on a single term will be removed in the 'preparation' step below terms = lhs.split(",") total = {} sizes = {} for t, term in enumerate(terms): shape = operands[t].shape for ix, d in zip(term, shape, strict=False): if d != sizes.setdefault(ix, d): raise ValueError(f"Inconsistent shape for index '{ix}'.") total.setdefault(ix, set()).add(t) for ix in rhs: total[ix].add(-1) aligned_term = "".join(ix for ix, apps in total.items() if len(apps) > 1) # NB: if every index appears exactly twice, # we could identify and dispatch to tensordot here? parrays = [] for term, array in zip(terms, operands, strict=True): # calc the target indices for this term pterm = "".join(ix for ix in aligned_term if ix in term) if pterm != term: # perform necessary transpose and reductions array = _einsum_single(term, pterm, array) # calc broadcastable shape shape = tuple(array.shape[pterm.index(ix)] if ix in pterm else 1 for ix in aligned_term) parrays.append(array.reshape(shape) if array.shape != shape else array) aligned_array = reduce(mul, parrays) return _einsum_single(aligned_term, rhs, aligned_array) def stack(arrays, axis=0, compressed_axes=None): """ Stack the input arrays along the given dimension. Parameters ---------- arrays : Iterable[SparseArray] The input arrays to stack. axis : int, optional The axis along which to stack the input arrays. compressed_axes : iterable, optional The axes to compress if returning a GCXS array. Returns ------- SparseArray The output stacked array. Raises ------ ValueError If all elements of `arrays` don't have the same fill-value. See Also -------- [`numpy.stack`][]: NumPy equivalent function """ from ._compressed import GCXS if not builtins.all(isinstance(arr, GCXS) for arr in arrays): from ._coo import stack as coo_stack return coo_stack(arrays, axis) from ._compressed import stack as gcxs_stack return gcxs_stack(arrays, axis, compressed_axes) def concatenate(arrays, axis=0, compressed_axes=None): """ Concatenate the input arrays along the given dimension. Parameters ---------- arrays : Iterable[SparseArray] The input arrays to concatenate. axis : int, optional The axis along which to concatenate the input arrays. The default is zero. compressed_axes : iterable, optional The axes to compress if returning a GCXS array. Returns ------- SparseArray The output concatenated array. Raises ------ ValueError If all elements of `arrays` don't have the same fill-value. See Also -------- [`numpy.concatenate`][] : NumPy equivalent function """ from ._compressed import GCXS if not builtins.all(isinstance(arr, GCXS) for arr in arrays): from ._coo import concatenate as coo_concat return coo_concat(arrays, axis) from ._compressed import concatenate as gcxs_concat return gcxs_concat(arrays, axis, compressed_axes) concat = concatenate @_check_device def eye(N, M=None, k=0, dtype=float, format="coo", *, device=None, **kwargs): """Return a 2-D array in the specified format with ones on the diagonal and zeros elsewhere. Parameters ---------- N : int Number of rows in the output. M : int, optional Number of columns in the output. If None, defaults to `N`. k : int, optional Index of the diagonal: 0 (the default) refers to the main diagonal, a positive value refers to an upper diagonal, and a negative value to a lower diagonal. dtype : data-type, optional Data-type of the returned array. format : str, optional A format string. Returns ------- I : SparseArray of shape (N, M) An array where all elements are equal to zero, except for the `k`-th diagonal, whose values are equal to one. Examples -------- >>> eye(2, dtype=int).todense() # doctest: +NORMALIZE_WHITESPACE array([[1, 0], [0, 1]]) >>> eye(3, k=1).todense() # doctest: +SKIP array([[0., 1., 0.], [0., 0., 1.], [0., 0., 0.]]) """ from ._coo import COO if M is None: M = N N = int(N) M = int(M) k = int(k) data_length = builtins.min(N, M) if k > 0: data_length = builtins.max(builtins.min(data_length, M - k), 0) elif k < 0: data_length = builtins.max(builtins.min(data_length, N + k), 0) if data_length == 0: return zeros((N, M), dtype=dtype, format=format, device=device) if k > 0: n_coords = np.arange(data_length, dtype=np.intp) m_coords = n_coords + k elif k < 0: m_coords = np.arange(data_length, dtype=np.intp) n_coords = m_coords - k else: n_coords = m_coords = np.arange(data_length, dtype=np.intp) coords = np.stack([n_coords, m_coords]) data = np.array(1, dtype=dtype) return COO(coords, data=data, shape=(N, M), has_duplicates=False, sorted=True).asformat(format, **kwargs) @_check_device def full(shape, fill_value, dtype=None, format="coo", order="C", *, device=None, **kwargs): """Return a SparseArray of given shape and type, filled with `fill_value`. Parameters ---------- shape : int or tuple of ints Shape of the new array, e.g., ``(2, 3)`` or ``2``. fill_value : scalar Fill value. dtype : data-type, optional The desired data-type for the array. The default, `None`, means `np.array(fill_value).dtype`. format : str, optional A format string. compressed_axes : iterable, optional The axes to compress if returning a GCXS array. order : {'C', None} Values except these are not currently supported and raise a NotImplementedError. Returns ------- out : SparseArray Array of `fill_value` with the given shape and dtype. Examples -------- >>> full(5, 9).todense() # doctest: +NORMALIZE_WHITESPACE array([9, 9, 9, 9, 9]) >>> full((2, 2), 9, dtype=float).todense() # doctest: +SKIP array([[9., 9.], [9., 9.]]) """ from sparse import COO if dtype is None: dtype = np.array(fill_value).dtype if not isinstance(shape, tuple): shape = (shape,) if order not in {"C", None}: raise NotImplementedError("Currently, only 'C' and None are supported.") data = np.empty(0, dtype=dtype) coords = np.empty((len(shape), 0), dtype=np.intp) return COO( coords, data=data, shape=shape, fill_value=fill_value, has_duplicates=False, sorted=True, ).asformat(format, **kwargs) @_check_device def full_like(a, fill_value, dtype=None, shape=None, format=None, *, device=None, **kwargs): """Return a full array with the same shape and type as a given array. Parameters ---------- a : array_like The shape and data-type of the result will match those of `a`. dtype : data-type, optional Overrides the data type of the result. format : str, optional A format string. compressed_axes : iterable, optional The axes to compress if returning a GCXS array. Returns ------- out : SparseArray Array of `fill_value` with the same shape and type as `a`. Examples -------- >>> x = np.ones((2, 3), dtype="i8") >>> full_like(x, 9.0).todense() # doctest: +NORMALIZE_WHITESPACE array([[9, 9, 9], [9, 9, 9]]) """ if format is None and not isinstance(a, np.ndarray): format = type(a).__name__.lower() elif format is None: format = "coo" compressed_axes = kwargs.pop("compressed_axes", None) if hasattr(a, "compressed_axes") and compressed_axes is None: compressed_axes = a.compressed_axes return full( a.shape if shape is None else shape, fill_value, dtype=(a.dtype if dtype is None else dtype), format=format, **kwargs, ) def zeros(shape, dtype=float, format="coo", *, device=None, **kwargs): """Return a SparseArray of given shape and type, filled with zeros. Parameters ---------- shape : int or tuple of ints Shape of the new array, e.g., ``(2, 3)`` or ``2``. dtype : data-type, optional The desired data-type for the array, e.g., `numpy.int8`. Default is `numpy.float64`. format : str, optional A format string. compressed_axes : iterable, optional The axes to compress if returning a GCXS array. Returns ------- out : SparseArray Array of zeros with the given shape and dtype. Examples -------- >>> zeros(5).todense() # doctest: +SKIP array([0., 0., 0., 0., 0.]) >>> zeros((2, 2), dtype=int).todense() # doctest: +NORMALIZE_WHITESPACE array([[0, 0], [0, 0]]) """ return full(shape, fill_value=0, dtype=np.dtype(dtype), format=format, device=device, **kwargs) def zeros_like(a, dtype=None, shape=None, format=None, *, device=None, **kwargs): """Return a SparseArray of zeros with the same shape and type as ``a``. Parameters ---------- a : array_like The shape and data-type of the result will match those of `a`. dtype : data-type, optional Overrides the data type of the result. format : str, optional A format string. compressed_axes : iterable, optional The axes to compress if returning a GCXS array. Returns ------- out : SparseArray Array of zeros with the same shape and type as `a`. Examples -------- >>> x = np.ones((2, 3), dtype="i8") >>> zeros_like(x).todense() # doctest: +NORMALIZE_WHITESPACE array([[0, 0, 0], [0, 0, 0]]) """ return full_like(a, fill_value=0, dtype=dtype, shape=shape, format=format, device=device, **kwargs) def ones(shape, dtype=float, format="coo", *, device=None, **kwargs): """Return a SparseArray of given shape and type, filled with ones. Parameters ---------- shape : int or tuple of ints Shape of the new array, e.g., ``(2, 3)`` or ``2``. dtype : data-type, optional The desired data-type for the array, e.g., `numpy.int8`. Default is `numpy.float64`. format : str, optional A format string. compressed_axes : iterable, optional The axes to compress if returning a GCXS array. Returns ------- out : SparseArray Array of ones with the given shape and dtype. Examples -------- >>> ones(5).todense() # doctest: +SKIP array([1., 1., 1., 1., 1.]) >>> ones((2, 2), dtype=int).todense() # doctest: +NORMALIZE_WHITESPACE array([[1, 1], [1, 1]]) """ return full(shape, fill_value=1, dtype=np.dtype(dtype), format=format, device=device, **kwargs) def ones_like(a, dtype=None, shape=None, format=None, *, device=None, **kwargs): """Return a SparseArray of ones with the same shape and type as ``a``. Parameters ---------- a : array_like The shape and data-type of the result will match those of `a`. dtype : data-type, optional Overrides the data type of the result. format : str, optional A format string. compressed_axes : iterable, optional The axes to compress if returning a GCXS array. Returns ------- out : SparseArray Array of ones with the same shape and type as `a`. Examples -------- >>> x = np.ones((2, 3), dtype="i8") >>> ones_like(x).todense() # doctest: +NORMALIZE_WHITESPACE array([[1, 1, 1], [1, 1, 1]]) """ return full_like(a, fill_value=1, dtype=dtype, shape=shape, format=format, device=device, **kwargs) def empty(shape, dtype=float, format="coo", *, device=None, **kwargs): return full(shape, fill_value=0, dtype=np.dtype(dtype), format=format, device=device, **kwargs) empty.__doc__ = zeros.__doc__ def empty_like(a, dtype=None, shape=None, format=None, *, device=None, **kwargs): return full_like(a, fill_value=0, dtype=dtype, shape=shape, format=format, device=device, **kwargs) empty_like.__doc__ = zeros_like.__doc__ def can_cast(from_: SparseArray, to: np.dtype, /, *, casting: str = "safe") -> bool: """Determines if one data type can be cast to another data type Parameters ---------- from_ : dtype or SparseArray Source array or dtype. to : dtype Destination dtype. casting: str Casting kind Returns ------- out : bool Whether or not a cast is possible. Examples -------- >>> x = sparse.ones((2, 3), dtype=sparse.int8) >>> sparse.can_cast(x, sparse.float64) True See Also -------- - [`numpy.can_cast`][] : NumPy equivalent function """ from_ = np.dtype(from_) return np.can_cast(from_, to, casting=casting) def outer(a, b, out=None): """ Return outer product of two sparse arrays. Parameters ---------- a, b : sparse.SparseArray The input arrays. out : sparse.SparseArray The output array. Examples -------- >>> import numpy as np >>> import sparse >>> a = sparse.COO(np.arange(4)) >>> o = sparse.outer(a, a) >>> o.todense() array([[0, 0, 0, 0], [0, 1, 2, 3], [0, 2, 4, 6], [0, 3, 6, 9]]) """ from ._coo import COO from ._sparse_array import SparseArray if isinstance(a, SparseArray): a = COO(a) if isinstance(b, SparseArray): b = COO(b) return np.multiply.outer(a.flatten(), b.flatten(), out=out) def asnumpy(a, dtype=None, order=None): """Returns a dense numpy array from an arbitrary source array. Parameters ---------- a: array_like Arbitrary object that can be converted to [`numpy.ndarray`][]. order: ({'C', 'F', 'A'}) The desired memory layout of the output array. When ``order`` is 'A', it uses 'F' if ``a`` is fortran-contiguous and 'C' otherwise. Returns ------- numpy.ndarray: Converted array on the host memory. """ from ._sparse_array import SparseArray if isinstance(a, SparseArray): a = a.todense() return np.asarray(a, dtype=dtype, order=order) # this code was taken from numpy.moveaxis # (cf. numpy/core/numeric.py, lines 1340-1409, v1.18.4) # https://github.com/numpy/numpy/blob/v1.18.4/numpy/core/numeric.py#L1340-L1409 def moveaxis(a, source, destination): """ Move axes of an array to new positions. Other axes remain in their original order. Parameters ---------- a : SparseArray The array whose axes should be reordered. source : int or List[int] Original positions of the axes to move. These must be unique. destination : int or List[int] Destination positions for each of the original axes. These must also be unique. Returns ------- SparseArray Array with moved axes. Examples -------- >>> import numpy as np >>> import sparse >>> x = sparse.COO.from_numpy(np.ones((2, 3, 4, 5))) >>> sparse.moveaxis(x, (0, 1), (2, 3)) """ if not isinstance(source, Iterable): source = (source,) if not isinstance(destination, Iterable): destination = (destination,) source = normalize_axis(source, a.ndim) destination = normalize_axis(destination, a.ndim) if len(source) != len(destination): raise ValueError("`source` and `destination` arguments must have the same number of elements") order = [n for n in range(a.ndim) if n not in source] for dest, src in sorted(zip(destination, source, strict=True)): order.insert(dest, src) return a.transpose(order) def pad(array, pad_width, mode="constant", **kwargs): """ Performs the equivalent of [`sparse.SparseArray`][]. Note that this function returns a new array instead of a view. Parameters ---------- array : SparseArray Sparse array which is to be padded. pad_width : {sequence, array_like, int} Number of values padded to the edges of each axis. ((before_1, after_1), โ€ฆ (before_N, after_N)) unique pad widths for each axis. ((before, after),) yields same before and after pad for each axis. (pad,) or int is a shortcut for before = after = pad width for all axes. mode : str Pads to a constant value which is fill value. Currently only constant mode is implemented constant_values : int The values to set the padded values for each axis. Default is 0. This must be same as fill value. Returns ------- SparseArray The padded sparse array. Raises ------ NotImplementedError If mode != 'constant' or there are unknown arguments. ValueError If constant_values != self.fill_value See Also -------- [`numpy.pad`][] : NumPy equivalent function """ if not isinstance(array, SparseArray): raise NotImplementedError("Input array is not compatible.") if mode.lower() != "constant": raise NotImplementedError(f"Mode '{mode}' is not yet supported.") if not equivalent(kwargs.pop("constant_values", _zero_of_dtype(array.dtype)), array.fill_value): raise ValueError("constant_values can only be equal to fill value.") if kwargs: raise NotImplementedError("Additional Unknown arguments present.") from ._coo import COO array = array.asformat("coo") pad_width = np.broadcast_to(pad_width, (len(array.shape), 2)) new_coords = array.coords + pad_width[:, 0:1] new_shape = tuple([array.shape[i] + pad_width[i, 0] + pad_width[i, 1] for i in range(len(array.shape))]) new_data = array.data return COO(new_coords, new_data, new_shape, fill_value=array.fill_value) def format_to_string(format): if isinstance(format, type): if not issubclass(format, SparseArray): raise ValueError(f"invalid format: {format}") format = format.__name__.lower() if isinstance(format, str): return format raise ValueError(f"invalid format: {format}") @_check_device def asarray(obj, /, *, dtype=None, format="coo", copy=False, device=None): """ Convert the input to a sparse array. Parameters ---------- obj : array_like Object to be converted to an array. dtype : dtype, optional Output array data type. format : str, optional Output array sparse format. device : str, optional Device on which to place the created array. copy : bool, optional Boolean indicating whether or not to copy the input. Returns ------- out : Union[SparseArray, numpy.ndarray] Sparse or 0-D array containing the data from `obj`. Examples -------- >>> x = np.eye(8, dtype="i8") >>> sparse.asarray(x, format="coo") """ if format not in {"coo", "dok", "gcxs", "csc", "csr"}: raise ValueError(f"{format} format not supported.") from ._compressed import CSC, CSR, GCXS from ._coo import COO from ._dok import DOK format_dict = {"coo": COO, "dok": DOK, "gcxs": GCXS, "csc": CSC, "csr": CSR} if isinstance(obj, COO | DOK | GCXS | CSC | CSR): return obj.asformat(format) if _is_scipy_sparse_obj(obj): sparse_obj = format_dict[format].from_scipy_sparse(obj) if dtype is None: dtype = sparse_obj.dtype return sparse_obj.astype(dtype=dtype, copy=copy) if np.isscalar(obj) or isinstance(obj, np.ndarray | Iterable): sparse_obj = format_dict[format].from_numpy(np.asarray(obj)) if dtype is None: dtype = sparse_obj.dtype return sparse_obj.astype(dtype=dtype, copy=copy) raise ValueError(f"{type(obj)} not supported.") def _support_numpy(func): """ In case a NumPy array is passed to `sparse` namespace function we want to flag it and dispatch to NumPy. """ @wraps(func) def wrapper_func(*args, **kwargs): x = args[0] if isinstance(x, np.ndarray | np.number): warnings.warn( f"Sparse {func.__name__} received dense NumPy array instead " "of sparse array. Dispatching to NumPy function.", RuntimeWarning, stacklevel=2, ) return getattr(np, func.__name__)(*args, **kwargs) return func(*args, **kwargs) return wrapper_func def all(x, /, *, axis=None, keepdims=False): """ Tests whether all input array elements evaluate to ``True`` along a specified axis. Parameters ---------- x: array input array. axis: Optional[Union[int, Tuple[int, ...]]] axis or axes along which to perform a logical AND reduction. By default, a logical AND reduction is performed over the entire array. If a tuple of integers, logical AND reductions are performed over multiple axes. A valid ``axis`` is an integer on the interval ``[-N, N)``, where ``N`` is the rank (number of dimensions) of ``x``. If an ``axis`` is specified as a negative integer, the function determines the axis along which to perform a reduction by counting backward from the last dimension (where ``-1`` refers to the last dimension). If provided an invalid ``axis``, the function raiseS an exception. Default: ``None``. keepdims: bool If ``True``, the reduced axes (dimensions) are included in the result as singleton dimensions, and, accordingly, the result is compatible with the input array. Otherwise, if ``False``, the reduced axes (dimensions) are not included in the result. Default: ``False``. Returns ------- out: array if a logical AND reduction was performed over the entire array, the returned array is a zero-dimensional array containing the test result; otherwise, the returned array is a non-zero-dimensional array containing the test results. The returned array has a data type of ``bool``. Special Cases ------------- - Positive infinity, negative infinity, and NaN evaluate to ``True``. - If ``x`` has a complex floating-point data type, elements having a non-zero component (real or imaginary) evaluate to ``True``. - If ``x`` is an empty array or the size of the axis (dimension) along which to evaluate elements is zero, the test result is ``True``. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> o = sparse.all(a, axis=1) >>> o.todense() # doctest: +NORMALIZE_WHITESPACE array([False, False]) """ return x.all(axis=axis, keepdims=keepdims) def any(x, /, *, axis=None, keepdims=False): """ Tests whether any input array element evaluates to ``True`` along a specified axis. Parameters ---------- x: array input array. axis: Optional[Union[int, Tuple[int, ...]]] axis or axes along which to perform a logical OR reduction. By default, a logical OR reduction is performed over the entire array. If a tuple of integers, logical OR reductions are performed over multiple axes. A valid ``axis`` must be an integer on the interval ``[-N, N)``, where ``N`` is the rank (number of dimensions) of ``x``. If an ``axis`` is specified as a negative integer, the function determines the axis along which to perform a reduction by counting backward from the last dimension (where ``-1`` refers to the last dimension). If provided an invalid ``axis``, the function raises an exception. Default: ``None``. keepdims: bool If ``True``, the reduced axes (dimensions) are included in the result as singleton dimensions, and, accordingly, the result must is compatible with the input array. Otherwise, if ``False``, the reduced axes (dimensions) is not included in the result. Default: ``False``. Returns ------- out: array if a logical OR reduction was performed over the entire array, the returned array is a zero-dimensional array containing the test result. Otherwise, the returned array is a non-zero-dimensional array containing the test results. The returned array is of type ``bool``. Special Cases ------------- - Positive infinity, negative infinity, and NaN evaluate to ``True``. - If ``x`` has a complex floating-point data type, elements having a non-zero component (real or imaginary) evaluate to ``True``. - If ``x`` is an empty array or the size of the axis (dimension) along which to evaluate elements is zero, the test result is ``False``. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> o = sparse.any(a, axis=1) >>> o.todense() # doctest: +NORMALIZE_WHITESPACE array([ True, True]) """ return x.any(axis=axis, keepdims=keepdims) def permute_dims(x, /, axes=None): """ Permutes the axes (dimensions) of an array ``x``. Parameters ---------- x: array input array. axes: Tuple[int, ...] tuple containing a permutation of ``(0, 1, ..., N-1)`` where ``N`` is the number of axes (dimensions) of ``x``. Returns ------- out: array an array containing the axes permutation. The returned array must have the same data type as ``x``. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> o = sparse.permute_dims(a, axes=(1, 0)) >>> o.todense() # doctest: +NORMALIZE_WHITESPACE array([[0, 2], [1, 0]]) """ return x.transpose(axes=axes) def max(x, /, *, axis=None, keepdims=False): """ Calculates the maximum value of the input array ``x``. Parameters ---------- x: array input array of a real-valued data type. axis: Optional[Union[int, Tuple[int, ...]]] axis or axes along which maximum values are computed. By default, the maximum value are computed over the entire array. If a tuple of integers, maximum values are computed over multiple axes. Default: ``None``. keepdims: bool If ``True``, the reduced axes (dimensions) are included in the result as singleton dimensions. Accordingly, the result is compatible with the input array. Otherwise, if ``False``, the reduced axes (dimensions) must not be included in the result. Default: ``False``. Returns ------- out: array if the maximum value was computed over the entire array, a zero-dimensional array containing the maximum value. Otherwise, a non-zero-dimensional array containing the maximum values. The returned array has the same data type as ``x``. Special Cases ------------- For floating-point operands, if ``x_i`` is ``NaN``, the maximum value is ``NaN`` (i.e., ``NaN`` values propagate). Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> o = sparse.max(a, axis=1) >>> o.todense() array([1, 2]) """ return x.max(axis=axis, keepdims=keepdims) def mean(x, /, *, axis=None, keepdims=False, dtype=None): """ Calculates the arithmetic mean of the input array ``x``. Parameters ---------- x: array input array of a real-valued floating-point data type. axis: Optional[Union[int, Tuple[int, ...]]] axis or axes along which arithmetic means must be computed. By default, the mean is computed over the entire array. If a tuple of integers, arithmetic means are computed over multiple axes. Default: ``None``. keepdims: bool if ``True``, the reduced axes (dimensions) are included in the result as singleton dimensions. Accordingly, the result is compatible is the input array. Otherwise, if ``False``, the reduced axes (dimensions) are not be included in the result. Default: ``False``. Returns ------- out: array if the arithmetic mean was computed over the entire array, a zero-dimensional array with the arithmetic mean. Otherwise, a non-zero-dimensional array containing the arithmetic means. The returned array has the same data type as ``x``. Special Cases ------------- Let ``N`` equal the number of elements over which to compute the arithmetic mean. If ``N`` is ``0``, the arithmetic mean is ``NaN``. If ``x_i`` is ``NaN``, the arithmetic mean is ``NaN`` (i.e., ``NaN`` values propagate). Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> o = sparse.mean(a, axis=1) >>> o.todense() array([0.5, 1. ]) """ return x.mean(axis=axis, keepdims=keepdims, dtype=dtype) def min(x, /, *, axis=None, keepdims=False): """ Calculates the minimum value of the input array ``x``. Parameters ---------- x: array input array. Should have a real-valued data type. axis: Optional[Union[int, Tuple[int, ...]]] axis or axes along which minimum values are computed. By default, the minimum value must be computed over the entire array. If a tuple of integers, minimum values must be computed over multiple axes. Default: ``None``. keepdims: bool If ``True``, the reduced axes (dimensions) are included in the result as singleton dimensions. Accordingly, the result must be compatible with the input array. Otherwise, if ``False``, the reduced axes (dimensions) are not be included in the result. Default: ``False``. Returns ------- out: array if the minimum value was computed over the entire array, a zero-dimensional array containing the minimum value. Otherwise, a non-zero-dimensional array containing the minimum values. The returned array must have the same data type as ``x``. Special Cases ------------- For floating-point operands, if ``x_i`` is ``NaN``, the minimum value is ``NaN`` (i.e., ``NaN`` values propagate). Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, -1], [-2, 0]])) >>> o = sparse.min(a, axis=1) >>> o.todense() array([-1, -2]) """ return x.min(axis=axis, keepdims=keepdims) def prod(x, /, *, axis=None, dtype=None, keepdims=False): """ Calculates the product of input array ``x`` elements. Parameters ---------- x: array input array of a numeric data type. axis: Optional[Union[int, Tuple[int, ...]]] axis or axes along which products is computed. By default, the product are computed over the entire array. If a tuple of integers, products are computed over multiple axes. Default: ``None``. dtype: Optional[dtype] data type of the returned array. If ``None``, the returned array has the same data type as ``x``, unless ``x`` has an integer data type supporting a smaller range of values than the default integer data type (e.g., ``x`` has an ``int16`` or ``uint32`` data type and the default integer data type is ``int64``). In those latter cases: - if ``x`` has a signed integer data type (e.g., ``int16``), the returned array has the default integer data type. - if ``x`` has an unsigned integer data type (e.g., ``uint16``), the returned array has an unsigned integer data type having the same number of bits as the default integer data type (e.g., if the default integer data type is ``int32``, the returned array must have a ``uint32`` data type). If the data type (either specified or resolved) differs from the data type of ``x``, the input array is cast to the specified data type before computing the sum (rationale: the ``dtype`` keyword argument is intended to help prevent overflows). Default: ``None``. keepdims: bool if ``True``, the reduced axes (dimensions) are included in the result as singleton dimensions. Accordingly, the result are compatible with the input array. Otherwise, if ``False``, the reduced axes (dimensions) are not included in the result. Default: ``False``. Returns ------- out: array if the product was computed over the entire array, a zero-dimensional array containing the product. Otherwise, a non-zero-dimensional array containing the products. The returned array has a data type as described by the ``dtype`` parameter above. Notes ----- Special Cases ------------- Let ``N`` equal the number of elements over which to compute the product. - If ``N`` is ``0``, the product is `1` (i.e., the empty product). Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 2], [-1, 1]])) >>> o = sparse.prod(a, axis=1) >>> o.todense() array([ 0, -1]) """ return x.prod(axis=axis, keepdims=keepdims, dtype=dtype) def std(x, /, *, axis=None, correction=0.0, keepdims=False): """ Calculates the standard deviation of the input array ``x``. Parameters ---------- x: array input array of a real-valued floating-point data type. axis: Optional[Union[int, Tuple[int, ...]]] axis or axes along which standard deviations are computed. By default, the standard deviation is computed over the entire array. If a tuple of integers, standard deviations are computed over multiple axes. Default: ``None``. correction: Union[int, float] degrees of freedom adjustment. Setting this parameter to a value other than ``0`` has the effect of adjusting the divisor during the calculation of the standard deviation according to ``N-c`` where ``N`` corresponds to the total number of elements over which the standard deviation is computed and ``c`` corresponds to the provided degrees of freedom adjustment. When computing the standard deviation of a population, setting this parameter to ``0`` is the standard choice (i.e., the provided array contains data constituting an entire population). When computing the corrected sample standard deviation, setting this parameter to ``1`` is the standard choice (i.e., the provided array contains data sampled from a larger population; this is commonly referred to as Bessel's correction). Default: ``0``. keepdims: bool if ``True``, the reduced axes (dimensions) are included in the result as singleton dimensions, and, accordingly, the result must be compatible with the input array. Otherwise, if ``False``, the reduced axes (dimensions) must not be included in the result. Default: ``False``. Returns ------- out: array if the standard deviation was computed over the entire array, a zero-dimensional array containing the standard deviation; otherwise, a non-zero-dimensional array containing the standard deviations. The returned array has the same data type as ``x``. Special Cases ------------- Let ``N`` equal the number of elements over which to compute the standard deviation. - If ``N - correction`` is less than or equal to ``0``, the standard deviation is ``NaN``. - If ``x_i`` is ``NaN``, the standard deviation is ``NaN`` (i.e., ``NaN`` values propagate). Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 2], [-1, 1]])) >>> o = sparse.std(a, axis=1) >>> o.todense() array([1., 1.]) """ return x.std(axis=axis, ddof=correction, keepdims=keepdims) def sum(x, /, *, axis=None, dtype=None, keepdims=False): """ Calculates the sum of the input array ``x``. Parameters ---------- x: array input array of a numeric data type. axis: Optional[Union[int, Tuple[int, ...]]] axis or axes along which sums are computed. By default, the sum is computed over the entire array. If a tuple of integers, sums must are computed over multiple axes. Default: ``None``. dtype: Optional[dtype] data type of the returned array. If ``None``, the returned array has the same data type as ``x``, unless ``x`` has an integer data type supporting a smaller range of values than the default integer data type (e.g., ``x`` has an ``int16`` or ``uint32`` data type and the default integer data type is ``int64``). In those latter cases: - if ``x`` has a signed integer data type (e.g., ``int16``), the returned array has the default integer data type. - if ``x`` has an unsigned integer data type (e.g., ``uint16``), the returned array has an unsigned integer data type having the same number of bits as the default integer data type (e.g., if the default integer data type is ``int32``, the returned array must have a ``uint32`` data type). If the data type (either specified or resolved) differs from the data type of ``x``, the input array is cast to the specified data type before computing the sum. Rationale: the ``dtype`` keyword argument is intended to help prevent overflows. Default: ``None``. keepdims: bool If ``True``, the reduced axes (dimensions) are included in the result as singleton dimensions. Accordingly, the result is compatible with the input array. Otherwise, if ``False``, the reduced axes (dimensions) are not included in the result. Default: ``False``. Returns ------- out: array if the sum was computed over the entire array, a zero-dimensional array containing the sum. Otherwise, an array containing the sums. The returned array has the data type as described by the ``dtype`` parameter above. Special Cases ------------- Let ``N`` equal the number of elements over which to compute the sum. - If ``N`` is ``0``, the sum is ``0`` (i.e., the empty sum). Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> o = sparse.sum(a, axis=1) >>> o.todense() array([1, 2]) """ return x.sum(axis=axis, keepdims=keepdims, dtype=dtype) def var(x, /, *, axis=None, correction=0.0, keepdims=False): """ Calculates the variance of the input array ``x``. Parameters ---------- x: array input array of a real-valued floating-point data type. axis: Optional[Union[int, Tuple[int, ...]]] axis or axes along which variances are computed. By default, the variance is computed over the entire array. If a tuple of integers, variances are computed over multiple axes. Default: ``None``. correction: Union[int, float] degrees of freedom adjustment. Setting this parameter to a value other than ``0`` has the effect of adjusting the divisor during the calculation of the variance according to ``N-c`` where ``N`` corresponds to the total number of elements over which the variance is computed and ``c`` corresponds to the provided degrees of freedom adjustment. When computing the variance of a population, setting this parameter to ``0`` is the standard choice (i.e., the provided array contains data constituting an entire population). When computing the unbiased sample variance, setting this parameter to ``1`` is the standard choice (i.e., the provided array contains data sampled from a larger population; this is commonly referred to as Bessel's correction). Default: ``0``. keepdims: bool if ``True``, the reduced axes are included in the result as singleton dimensions, and, accordingly, the result is compatible with the input array. Otherwise, if ``False``, the reduced axes (dimensions) are not included in the result. Default: ``False``. Returns ------- out: array if the variance was computed over the entire array, a zero-dimensional array containing the variance; otherwise, a non-zero-dimensional array containing the variances. The returned array must have the same data type as ``x``. Special Cases ------------- Let ``N`` equal the number of elements over which to compute the variance. - If ``N - correction`` is less than or equal to ``0``, the variance is ``NaN``. - If ``x_i`` is ``NaN``, the variance is ``NaN`` (i.e., ``NaN`` values propagate). Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 2], [-1, 1]])) >>> o = sparse.var(a, axis=1) >>> o.todense() array([1., 1.]) """ return x.var(axis=axis, ddof=correction, keepdims=keepdims) def abs(x, /): """ Calculates the absolute value for each element ``x_i`` of the input array ``x``. For real-valued input arrays, the element-wise result has the same magnitude as the respective element in ``x`` but has positive sign. For complex floating-point operands, the complex absolute value is known as the norm, modulus, or magnitude and, for a complex number :math:`z = a + bj` is computed as $$ operatorname{abs}(z) = sqrt{a^2 + b^2} $$ Parameters ---------- x: array input array of a numeric data type. Returns ------- out: array an array containing the absolute value of each element in ``x``. If ``x`` has a real-valued data type, the returned array has the same data type as ``x``. If ``x`` has a complex floating-point data type, the returned array has a real-valued floating-point data type whose precision matches the precision of ``x`` (e.g., if ``x`` is ``complex128``, then the returned array must has a ``float64`` data type). Special Cases ------------- For real-valued floating-point operands, - If ``x_i`` is ``NaN``, the result is ``NaN``. - If ``x_i`` is ``-0``, the result is ``+0``. - If ``x_i`` is ``-infinity``, the result is ``+infinity``. For complex floating-point operands, let ``a = real(x_i)``, ``b = imag(x_i)``, and - If ``a`` is either ``+infinity`` or ``-infinity`` and ``b`` is any value (including ``NaN``), the result is ``+infinity``. - If ``a`` is any value (including ``NaN``) and ``b`` is either ``+infinity`` or ``-infinity``, the result is ``+infinity``. - If ``a`` is either ``+0`` or ``-0``, the result is equal to ``abs(b)``. - If ``b`` is either ``+0`` or ``-0``, the result is equal to ``abs(a)``. - If ``a`` is ``NaN`` and ``b`` is a finite number, the result is ``NaN``. - If ``a`` is a finite number and ``b`` is ``NaN``, the result is ``NaN``. - If ``a`` is ``NaN`` and ``b`` is ``NaN``, the result is ``NaN``. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, -1], [-2, 0]])) >>> o = sparse.abs(a) >>> o.todense() array([[0, 1], [2, 0]]) """ return x.__abs__() def reshape(x, /, shape, *, copy=None): """ Reshapes an array without changing its data. Parameters ---------- x: array input array to reshape. shape: Tuple[int, ...] a new shape compatible with the original shape. One shape dimension is allowed to be ``-1``. When a shape dimension is ``-1``, the corresponding output array shape dimension must be inferred from the length of the array and the remaining dimensions. copy: Optional[bool] whether or not to copy the input array. If ``True``, the function always copies. If ``False``, the function must never copies. If ``None``, the function avoids copying, if possible. Default: ``None``. Returns ------- out: array an output array having the same data type and elements as ``x``. Raises ------ ValueError If ``copy=False`` and a copy would be necessary, a ``ValueError`` will be raised. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> o = sparse.reshape(a, shape=(1, 4)) >>> o.todense() array([[0, 1, 2, 0]]) """ return x.reshape(shape=shape) @_check_device def astype(x, dtype, /, *, copy=True, device=None): """ Copies an array to a specified data type irrespective of type-promotion rules. Parameters ---------- x: array array to cast. dtype: dtype desired data type. copy: bool specifies whether to copy an array when the specified ``dtype`` matches the data type of the input array ``x``. If ``True``, a newly allocated array is always returned. If ``False`` and the specified ``dtype`` matches the data type of the input array, the input array is returned; otherwise, a newly allocated array is returned. Default: ``True``. Notes ----- - When casting a boolean input array to a real-valued data type, a value of ``True`` is cast to a real-valued number equal to ``1``, and a value of ``False`` must cast to a real-valued number equal to ``0``. - When casting a boolean input array to a complex floating-point data type, a value of ``True`` is cast to a complex number equal to ``1 + 0j``, and a value of ``False`` is cast to a complex number equal to ``0 + 0j``. - When casting a real-valued input array to ``bool``, a value of ``0`` is cast to ``False``, and a non-zero value is cast to ``True``. - When casting a complex floating-point array to ``bool``, a value of ``0 + 0j`` is cast to ``False``, and all other values are cast to ``True``. Returns ------- out: array an array having the specified data type. The returned array has the same shape as ``x``. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> o = sparse.astype(a, "float32") >>> o.todense() # doctest: +NORMALIZE_WHITESPACE array([[0., 1.], [2., 0.]], dtype=float32) """ return x.astype(dtype, copy=copy) @_support_numpy def squeeze(x, /, axis=None): """Remove singleton dimensions from array. Parameters ---------- x : SparseArray Input array. axis : int or tuple[int, ...], optional The singleton axes to remove. By default all singleton axes are removed. Returns ------- output : SparseArray Array with singleton dimensions removed. """ return x.squeeze(axis=axis) @_support_numpy def broadcast_to(x, /, shape): """ Broadcasts an array to a specified shape. Parameters ---------- x: array array to broadcast. shape: Tuple[int, ...] array shape. Must be compatible with ``x``. If the array is incompatible with the specified shape, the function raises an exception. Returns ------- out: array an array having a specified shape and having the same data type as ``x``. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> o = sparse.broadcast_to(a, shape=(1, 2, 2)) >>> o.todense() # doctest: +NORMALIZE_WHITESPACE array([[[0, 1], [2, 0]]]) """ return x.broadcast_to(shape) def broadcast_arrays(*arrays): """ Broadcasts one or more arrays against one another. Parameters ---------- arrays: array an arbitrary number of to-be broadcasted arrays. Returns ------- out: List[array] a list of broadcasted arrays. Each array has the same shape. Each array has the same dtype as its corresponding input array. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1]])) >>> b = sparse.COO.from_numpy(np.array([[0], [2]])) >>> oa, ob = sparse.broadcast_arrays(a, b) >>> oa.todense() # doctest: +NORMALIZE_WHITESPACE array([[0, 1], [0, 1]]) >>> ob.todense() # doctest: +NORMALIZE_WHITESPACE array([[0, 0], [2, 2]]) """ shape = np.broadcast_shapes(*[a.shape for a in arrays]) return [a.broadcast_to(shape) for a in arrays] def equal(x1, x2, /): """ Computes the truth value of ``x1_i == x2_i`` for each element ``x1_i`` of the input array ``x1`` with the respective element ``x2_i`` of the input array ``x2``. Parameters ---------- x1: array first input array. May have any data type. x2: array second input array. Must be compatible with ``x1``. May have any data type. Returns ------- out: array an array containing the element-wise results. The returned array is of data type of ``bool``. Special Cases ------------- For real-valued floating-point operands, - If ``x1_i`` is ``NaN`` or ``x2_i`` is ``NaN``, the result is ``False``. - If ``x1_i`` is ``+infinity`` and ``x2_i`` is ``+infinity``, the result is ``True``. - If ``x1_i`` is ``-infinity`` and ``x2_i`` is ``-infinity``, the result is ``True``. - If ``x1_i`` is ``-0`` and ``x2_i`` is either ``+0`` or ``-0``, the result is ``True``. - If ``x1_i`` is ``+0`` and ``x2_i`` is either ``+0`` or ``-0``, the result is ``True``. - If ``x1_i`` is a finite number, ``x2_i`` is a finite number, and ``x1_i`` equals ``x2_i``, the result is ``True``. - In the remaining cases, the result is ``False``. For complex floating-point operands, let ``a = real(x1_i)``, ``b = imag(x1_i)``, ``c = real(x2_i)``, ``d = imag(x2_i)``, and - If ``a``, ``b``, ``c``, or ``d`` is ``NaN``, the result is ``False``. - In the remaining cases, the result is the logical AND of the equality comparison between the real values ``a`` and ``c`` (real components) and between the real values ``b`` and ``d`` (imaginary components), as described above for real-valued floating-point operands (i.e., ``a == c AND b == d``). Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> b = sparse.COO.from_numpy(np.array([[0, 1], [1, 0]])) >>> o = sparse.equal(a, b) # doctest: +SKIP >>> o.todense() # doctest: +SKIP array([[ True, True], [ False, True]]) """ return x1 == x2 @_support_numpy def round(x, /, decimals=0, out=None): return x.round(decimals=decimals, out=out) @_support_numpy def isinf(x, /): """ Tests each element ``x_i`` of the input array ``x`` to determine if equal to positive or negative infinity. Parameters ---------- x: array input array of a numeric data type. Returns ------- out: array an array containing test results. The returned array has a data type of ``bool``. Special Cases ------------- For real-valued floating-point operands, - If ``x_i`` is either ``+infinity`` or ``-infinity``, the result is ``True``. - In the remaining cases, the result is ``False``. For complex floating-point operands, let ``a = real(x_i)``, ``b = imag(x_i)``, and - If ``a`` is either ``+infinity`` or ``-infinity`` and ``b`` is any value (including ``NaN``), the result is ``True``. - If ``a`` is either a finite number or ``NaN`` and ``b`` is either ``+infinity`` or ``-infinity``, the result is ``True``. - In the remaining cases, the result is ``False``. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, np.inf]])) >>> o = sparse.isinf(a) # doctest: +SKIP >>> o.todense() # doctest: +SKIP array([[False, False], [False, True]]) """ return x.isinf() @_support_numpy def isnan(x, /): """ Tests each element ``x_i`` of the input array ``x`` to determine whether the element is ``NaN``. Parameters ---------- x: array input array with a numeric data type. Returns ------- out: array an array containing test results. The returned array has data type ``bool``. Notes ----- For real-valued floating-point operands, - If ``x_i`` is ``NaN``, the result is ``True``. - In the remaining cases, the result is ``False``. For complex floating-point operands, let ``a = real(x_i)``, ``b = imag(x_i)``, and - If ``a`` or ``b`` is ``NaN``, the result is ``True``. - In the remaining cases, the result is ``False``. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, np.nan]])) >>> o = sparse.isnan(a) >>> o.todense() # doctest: +NORMALIZE_WHITESPACE array([[False, False], [False, True]]) """ return x.isnan() def nonzero(x, /): """ Returns the indices of the array elements which are non-zero. If ``x`` has a complex floating-point data type, non-zero elements are those elements having at least one component (real or imaginary) which is non-zero. If ``x`` has a boolean data type, non-zero elements are those elements which are equal to ``True``. Parameters ---------- x: array input array having a positive rank. If ``x`` is zero-dimensional, the function raises an exception. Returns ------- out: Tuple[array, ...] a tuple of ``k`` arrays, one for each dimension of ``x`` and each of size ``n`` (where ``n`` is the total number of non-zero elements), containing the indices of the non-zero elements in that dimension. The indices must are returned in row-major, C-style order. Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0, 1], [2, 0]])) >>> o = sparse.nonzero(a) >>> o (array([0, 1]), array([1, 0])) """ return x.nonzero() def imag(x, /): """ Returns the imaginary component of a complex number for each element ``x_i`` of the input array ``x``. Parameters ---------- x: array input array of a complex floating-point data type. Returns ------- out: array an array containing the element-wise results. The returned array has a floating-point data type with the same floating-point precision as ``x`` (e.g., if ``x`` is ``complex64``, the returned array has the floating-point data type ``float32``). Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0 + 1j, 2 + 0j], [0 + 0j, 3 + 1j]])) >>> o = sparse.imag(a) >>> o.todense() # doctest: +NORMALIZE_WHITESPACE array([[1., 0.], [0., 1.]]) """ return x.imag def real(x, /): """ Returns the real component of a complex number for each element ``x_i`` of the input array ``x``. Parameters ---------- x: array input array of a complex floating-point data type. Returns ------- out: array an array containing the element-wise results. The returned array has a floating-point data type with the same floating-point precision as ``x`` (e.g., if ``x`` is ``complex64``, the returned array has the floating-point data type ``float32``). Examples -------- >>> a = sparse.COO.from_numpy(np.array([[0 + 1j, 2 + 0j], [0 + 0j, 3 + 1j]])) >>> o = sparse.real(a) >>> o.todense() # doctest: +NORMALIZE_WHITESPACE array([[0., 2.], [0., 3.]]) """ return x.real def vecdot(x1, x2, /, *, axis=-1): """ Computes the (vector) dot product of two arrays. Parameters ---------- x1, x2 : array_like Input sparse arrays axis : int The axis to reduce over. Returns ------- out : Union[SparseArray, numpy.ndarray] Sparse or 0-D array containing dot product. """ ndmin = builtins.min((x1.ndim, x2.ndim)) if not (-ndmin <= axis < ndmin) or x1.shape[axis] != x2.shape[axis]: raise ValueError("Shapes must match along `axis`.") if np.issubdtype(x1.dtype, np.complexfloating): x1 = np.conjugate(x1) return np.sum(x1 * x2, axis=axis, dtype=np.result_type(x1, x2)) sparse-0.17.0/sparse/numba_backend/_compressed/000077500000000000000000000000001501262445000214445ustar00rootroot00000000000000sparse-0.17.0/sparse/numba_backend/_compressed/__init__.py000066400000000000000000000002101501262445000235460ustar00rootroot00000000000000from .common import concatenate, stack from .compressed import CSC, CSR, GCXS __all__ = ["GCXS", "CSR", "CSC", "concatenate", "stack"] sparse-0.17.0/sparse/numba_backend/_compressed/common.py000066400000000000000000000073111501262445000233100ustar00rootroot00000000000000import numpy as np from .._utils import can_store, check_consistent_fill_value, normalize_axis def concatenate(arrays, axis=0, compressed_axes=None): from .compressed import GCXS check_consistent_fill_value(arrays) arrays = [arr if isinstance(arr, GCXS) else GCXS(arr, compressed_axes=(axis,)) for arr in arrays] axis = normalize_axis(axis, arrays[0].ndim) dim = sum(x.shape[axis] for x in arrays) shape = list(arrays[0].shape) shape[axis] = dim assert all(x.shape[ax] == arrays[0].shape[ax] for x in arrays for ax in set(range(arrays[0].ndim)) - {axis}) if compressed_axes is None: compressed_axes = (axis,) if arrays[0].ndim == 1: from .._coo.common import concatenate as coo_concat arrays = [arr.tocoo() for arr in arrays] return coo_concat(arrays, axis=axis) # arrays may have different compressed_axes # concatenating becomes easy when compressed_axes are the same arrays = [arr.change_compressed_axes((axis,)) for arr in arrays] ptr_list = [] for i, arr in enumerate(arrays): if i == 0: ptr_list.append(arr.indptr) continue ptr_list.append(arr.indptr[1:]) indptr = np.concatenate(ptr_list) indices = np.concatenate([arr.indices for arr in arrays]) data = np.concatenate([arr.data for arr in arrays]) ptr_len = arrays[0].indptr.shape[0] nnz = arrays[0].nnz total_nnz = sum(int(arr.nnz) for arr in arrays) if not can_store(indptr.dtype, total_nnz): indptr = indptr.astype(np.min_scalar_type(total_nnz)) for i in range(1, len(arrays)): indptr[ptr_len:] += nnz nnz = arrays[i].nnz ptr_len += arrays[i].indptr.shape[0] - 1 return GCXS( (data, indices, indptr), shape=tuple(shape), compressed_axes=arrays[0].compressed_axes, fill_value=arrays[0].fill_value, ).change_compressed_axes(compressed_axes) def stack(arrays, axis=0, compressed_axes=None): from .compressed import GCXS check_consistent_fill_value(arrays) arrays = [arr if isinstance(arr, GCXS) else GCXS(arr, compressed_axes=(axis,)) for arr in arrays] axis = normalize_axis(axis, arrays[0].ndim + 1) assert all(x.shape[ax] == arrays[0].shape[ax] for x in arrays for ax in set(range(arrays[0].ndim)) - {axis}) if compressed_axes is None: compressed_axes = (axis,) if arrays[0].ndim == 1: from .._coo.common import stack as coo_stack arrays = [arr.tocoo() for arr in arrays] return coo_stack(arrays, axis=axis) # arrays may have different compressed_axes # stacking becomes easy when compressed_axes are the same ptr_list = [] for i in range(len(arrays)): shape = list(arrays[i].shape) shape.insert(axis, 1) arrays[i] = arrays[i].reshape(shape).change_compressed_axes((axis,)) if i == 0: ptr_list.append(arrays[i].indptr) continue ptr_list.append(arrays[i].indptr[1:]) shape[axis] = len(arrays) indptr = np.concatenate(ptr_list) indices = np.concatenate([arr.indices for arr in arrays]) data = np.concatenate([arr.data for arr in arrays]) ptr_len = arrays[0].indptr.shape[0] nnz = arrays[0].nnz total_nnz = sum(int(arr.nnz) for arr in arrays) if not can_store(indptr.dtype, total_nnz): indptr = indptr.astype(np.min_scalar_type(total_nnz)) for i in range(1, len(arrays)): indptr[ptr_len:] += nnz nnz = arrays[i].nnz ptr_len += arrays[i].indptr.shape[0] - 1 return GCXS( (data, indices, indptr), shape=tuple(shape), compressed_axes=arrays[0].compressed_axes, fill_value=arrays[0].fill_value, ).change_compressed_axes(compressed_axes) sparse-0.17.0/sparse/numba_backend/_compressed/compressed.py000066400000000000000000000762331501262445000241750ustar00rootroot00000000000000import copy as _copy import operator from collections.abc import Iterable from functools import reduce from typing import Union import numpy as np from numpy.lib.mixins import NDArrayOperatorsMixin from .._coo.common import linear_loc from .._coo.core import COO from .._sparse_array import SparseArray from .._utils import ( _zero_of_dtype, can_store, check_compressed_axes, check_fill_value, equivalent, normalize_axis, ) from .convert import _1d_reshape, _transpose, uncompress_dimension from .indexing import getitem def _from_coo(x, compressed_axes=None, idx_dtype=None): if x.ndim == 0: if compressed_axes is not None: raise ValueError("no axes to compress for 0d array") return ((x.data, x.coords, []), x.shape, None, x.fill_value) if x.ndim == 1: if compressed_axes is not None: raise ValueError("no axes to compress for 1d array") return ((x.data, x.coords[0], ()), x.shape, None, x.fill_value) compressed_axes = normalize_axis(compressed_axes, x.ndim) if compressed_axes is None: # defaults to best compression ratio compressed_axes = (np.argmin(x.shape),) check_compressed_axes(x.shape, compressed_axes) axis_order = list(compressed_axes) # array location where the uncompressed dimensions start axisptr = len(compressed_axes) axis_order.extend(np.setdiff1d(np.arange(len(x.shape)), compressed_axes)) reordered_shape = tuple(x.shape[i] for i in axis_order) row_size = np.prod(reordered_shape[:axisptr]) col_size = np.prod(reordered_shape[axisptr:]) compressed_shape = (row_size, col_size) shape = x.shape if idx_dtype and not can_store(idx_dtype, max(max(compressed_shape), x.nnz)): raise ValueError( f"cannot store array with the compressed shape {compressed_shape} and nnz {x.nnz} with dtype {idx_dtype}." ) if not idx_dtype: idx_dtype = x.coords.dtype if not can_store(idx_dtype, max(max(compressed_shape), x.nnz)): idx_dtype = np.min_scalar_type(max(max(compressed_shape), x.nnz)) # transpose axes, linearize, reshape, and compress linear = linear_loc(x.coords[axis_order], reordered_shape) order = np.argsort(linear) linear = linear[order] coords = np.empty((2, x.nnz), dtype=idx_dtype) strides = 1 for i, d in enumerate(compressed_shape[::-1]): coords[-(i + 1), :] = (linear // strides) % d strides *= d indptr = np.empty(row_size + 1, dtype=idx_dtype) indptr[0] = 0 np.cumsum(np.bincount(coords[0], minlength=row_size), out=indptr[1:]) indices = coords[1] data = x.data[order] return ((data, indices, indptr), shape, compressed_axes, x.fill_value) class GCXS(SparseArray, NDArrayOperatorsMixin): r""" A sparse multidimensional array. This is stored in GCXS format, a generalization of the GCRS/GCCS formats from [Efficient storage scheme for n-dimensional sparse array: GCRS/GCCS]( https://ieeexplore.ieee.org/document/7237032). GCXS generalizes the CRS/CCS sparse matrix formats. For arrays with ndim == 2, GCXS is the same CSR/CSC. For arrays with ndim >2, any combination of axes can be compressed, significantly reducing storage. GCXS consists of 3 arrays. Let the 3 arrays be RO, CO and VL. The first element of array RO is the integer 0 and later elements are the number of cumulative non-zero elements in each row for GCRS, column for GCCS. CO stores column indexes of non-zero elements at each row for GCRS, column for GCCS. VL stores the values of the non-zero array elements. The superiority of the GCRS/GCCS over traditional (CRS/CCS) is shown by both theoretical analysis and experimental results, outlined in the linked research paper. Parameters ---------- arg : tuple (data, indices, indptr) A tuple of arrays holding the data, indices, and index pointers for the nonzero values of the array. shape : tuple[int] (COO.ndim,) The shape of the array. compressed_axes : Iterable[int] The axes to compress. prune : bool, optional A flag indicating whether or not we should prune any fill-values present in the data array. fill_value: scalar, optional The fill value for this array. Attributes ---------- data : numpy.ndarray (nnz,) An array holding the nonzero values corresponding to `indices`. indices : numpy.ndarray (nnz,) An array holding the coordinates of every nonzero element along uncompressed dimensions. indptr : numpy.ndarray An array holding the cumulative sums of the nonzeros along the compressed dimensions. shape : tuple[int] (ndim,) The dimensions of this array. See Also -------- [`sparse.DOK`][] : A mostly write-only sparse array. """ __array_priority__ = 12 def __init__( self, arg, shape=None, compressed_axes=None, prune=False, fill_value=None, idx_dtype=None, ): from .._common import _is_scipy_sparse_obj if _is_scipy_sparse_obj(arg): arg = self.from_scipy_sparse(arg) if isinstance(arg, np.ndarray): (arg, shape, compressed_axes, fill_value) = _from_coo(COO(arg), compressed_axes) elif isinstance(arg, COO): (arg, shape, compressed_axes, fill_value) = _from_coo(arg, compressed_axes, idx_dtype) elif isinstance(arg, GCXS): if compressed_axes is not None and arg.compressed_axes != compressed_axes: arg = arg.change_compressed_axes(compressed_axes) (arg, shape, compressed_axes, fill_value) = ( (arg.data, arg.indices, arg.indptr), arg.shape, arg.compressed_axes, arg.fill_value, ) if shape is None: raise ValueError("missing `shape` argument") check_compressed_axes(len(shape), compressed_axes) if len(shape) == 1: compressed_axes = None self.data, self.indices, self.indptr = arg if self.data.ndim != 1: raise ValueError("data must be a scalar or 1-dimensional.") self.shape = shape if fill_value is None: fill_value = _zero_of_dtype(self.data.dtype) self._compressed_axes = tuple(compressed_axes) if isinstance(compressed_axes, Iterable) else None self.fill_value = self.data.dtype.type(fill_value) if prune: self._prune() def copy(self, deep=True): """Return a copy of the array. Parameters ---------- deep : boolean, optional If True (default), the internal coords and data arrays are also copied. Set to ``False`` to only make a shallow copy. """ return _copy.deepcopy(self) if deep else _copy.copy(self) @classmethod def from_numpy(cls, x, compressed_axes=None, fill_value=None, idx_dtype=None): coo = COO.from_numpy(x, fill_value=fill_value, idx_dtype=idx_dtype) return cls.from_coo(coo, compressed_axes, idx_dtype) @classmethod def from_coo(cls, x, compressed_axes=None, idx_dtype=None): (arg, shape, compressed_axes, fill_value) = _from_coo(x, compressed_axes, idx_dtype) return cls(arg, shape=shape, compressed_axes=compressed_axes, fill_value=fill_value) @classmethod def from_scipy_sparse(cls, x, /, *, fill_value=None): is_csc = x.format == "csc" ca = (1,) if is_csc else (0,) if not is_csc: x = x.asformat("csr") if not x.has_canonical_format: x.eliminate_zeros() x.sum_duplicates() return cls((x.data, x.indices, x.indptr), shape=x.shape, compressed_axes=ca, fill_value=fill_value) @classmethod def from_iter(cls, x, shape=None, compressed_axes=None, fill_value=None, idx_dtype=None): return cls.from_coo( COO.from_iter(x, shape, fill_value), compressed_axes, idx_dtype, ) @property def dtype(self): """ The datatype of this array. Returns ------- numpy.dtype The datatype of this array. See Also -------- - [`numpy.ndarray.dtype`][] : Numpy equivalent property. - [`scipy.sparse.csr_matrix.dtype`][] : Scipy equivalent property. """ return self.data.dtype @property def nnz(self): """ The number of nonzero elements in this array. Returns ------- int The number of nonzero elements in this array. See Also -------- - [`sparse.COO.nnz`][] : Equivalent [`sparse.COO`][] array property. - [`sparse.DOK.nnz`][] : Equivalent [`sparse.DOK`][] array property. - [`numpy.count_nonzero`][] : A similar Numpy function. - [`scipy.sparse.coo_matrix.nnz`][] : The Scipy equivalent property. """ return self.data.shape[0] @property def format(self): """ The storage format of this array. Returns ------- str The storage format of this array. See Also ------- [`scipy.sparse.dok_matrix.format`][] : The Scipy equivalent property. Examples ------- >>> import sparse >>> s = sparse.random((5, 5), density=0.2, format="dok") >>> s.format 'dok' >>> t = sparse.random((5, 5), density=0.2, format="coo") >>> t.format 'coo' """ return "gcxs" @property def nbytes(self): """ The number of bytes taken up by this object. Note that for small arrays, this may undercount the number of bytes due to the large constant overhead. Returns ------- int The approximate bytes of memory taken by this object. See Also -------- [`numpy.ndarray.nbytes`][] : The equivalent Numpy property. """ return self.data.nbytes + self.indices.nbytes + self.indptr.nbytes @property def _axis_order(self): axis_order = list(self.compressed_axes) axis_order.extend(np.setdiff1d(np.arange(len(self.shape)), self.compressed_axes)) return axis_order @property def _axisptr(self): # array location where the uncompressed dimensions start return len(self.compressed_axes) @property def _compressed_shape(self): row_size = np.prod(self._reordered_shape[: self._axisptr]) col_size = np.prod(self._reordered_shape[self._axisptr :]) return (row_size, col_size) @property def _reordered_shape(self): return tuple(self.shape[i] for i in self._axis_order) @property def T(self): return self.transpose() @property def mT(self): if self.ndim < 2: raise ValueError("Cannot compute matrix transpose if `ndim < 2`.") axis = list(range(self.ndim)) axis[-1], axis[-2] = axis[-2], axis[-1] return self.transpose(axis) def __str__(self): summary = ( f"" ) return self._str_impl(summary) __repr__ = __str__ __getitem__ = getitem def _reduce_calc(self, method, axis, keepdims=False, **kwargs): if axis[0] is None or np.array_equal(axis, np.arange(self.ndim, dtype=np.intp)): x = self.flatten().tocoo() out = x.reduce(method, axis=None, keepdims=keepdims, **kwargs) if keepdims: return (out.reshape(np.ones(self.ndim, dtype=np.intp)),) return (out,) r = np.arange(self.ndim, dtype=np.intp) compressed_axes = [a for a in r if a not in set(axis)] x = self.change_compressed_axes(compressed_axes) idx = np.diff(x.indptr) != 0 indptr = x.indptr[:-1][idx] indices = (np.arange(x._compressed_shape[0], dtype=self.indptr.dtype))[idx] data = method.reduceat(x.data, indptr, **kwargs) counts = x.indptr[1:][idx] - x.indptr[:-1][idx] arr_attrs = (x, compressed_axes, indices) n_cols = x._compressed_shape[1] return (data, counts, axis, n_cols, arr_attrs) def _reduce_return(self, data, arr_attrs, result_fill_value): x, compressed_axes, indices = arr_attrs # prune data mask = ~equivalent(data, result_fill_value) data = data[mask] indices = indices[mask] out = GCXS( (data, indices, []), shape=(x._compressed_shape[0],), fill_value=result_fill_value, compressed_axes=None, ) return out.reshape(tuple(self.shape[d] for d in compressed_axes)) def change_compressed_axes(self, new_compressed_axes): """ Returns a new array with specified compressed axes. This operation is similar to converting a scipy.sparse.csc_matrix to a scipy.sparse.csr_matrix. Returns ------- GCXS A new instance of the input array with compression along the specified dimensions. """ if new_compressed_axes == self.compressed_axes: return self if self.ndim == 1: raise NotImplementedError("no axes to compress for 1d array") new_compressed_axes = tuple( normalize_axis(new_compressed_axes[i], self.ndim) for i in range(len(new_compressed_axes)) ) if new_compressed_axes == self.compressed_axes: return self if len(new_compressed_axes) >= len(self.shape): raise ValueError("cannot compress all axes") if len(set(new_compressed_axes)) != len(new_compressed_axes): raise ValueError("repeated axis in compressed_axes") arg = _transpose(self, self.shape, np.arange(self.ndim), new_compressed_axes) return GCXS( arg, shape=self.shape, compressed_axes=new_compressed_axes, fill_value=self.fill_value, ) def tocoo(self): """ Convert this [`sparse.GCXS`][] array to a [`sparse.COO`][]. Returns ------- sparse.COO The converted COO array. """ if self.ndim == 0: return COO( np.array([]), self.data, shape=self.shape, fill_value=self.fill_value, ) if self.ndim == 1: return COO( self.indices[None, :], self.data, shape=self.shape, fill_value=self.fill_value, ) uncompressed = uncompress_dimension(self.indptr) coords = np.vstack((uncompressed, self.indices)) order = np.argsort(self._axis_order) return ( COO( coords, self.data, shape=self._compressed_shape, fill_value=self.fill_value, ) .reshape(self._reordered_shape) .transpose(order) ) def todense(self): """ Convert this [`sparse.GCXS`][] array to a dense [`numpy.ndarray`][]. Note that this may take a large amount of memory if the [`sparse.GCXS`][] object's `shape` is large. Returns ------- numpy.ndarray The converted dense array. See Also -------- - [`sparse.DOK.todense`][] : Equivalent [`sparse.DOK`][] array method. - [`sparse.COO.todense`][] : Equivalent [`sparse.COO`][] array method. - [`scipy.sparse.coo_matrix.todense`][] : Equivalent Scipy method. """ if self.compressed_axes is None: out = np.full(self.shape, self.fill_value, self.dtype) if len(self.indices) != 0: out[self.indices] = self.data else: if len(self.data) != 0: out[()] = self.data[0] return out return self.tocoo().todense() def todok(self): from .. import DOK return DOK.from_coo(self.tocoo()) # probably a temporary solution def to_scipy_sparse(self, accept_fv=None): """ Converts this [`sparse.GCXS`][] object into a [`scipy.sparse.csr_matrix`][] or [`scipy.sparse.csc_matrix`][]. Parameters ---------- accept_fv : scalar or list of scalar, optional The list of accepted fill-values. The default accepts only zero. Returns ------- scipy.sparse.csr_matrix or scipy.sparse.csc_matrix The converted Scipy sparse matrix. Raises ------ ValueError If the array is not two-dimensional. ValueError If all the array doesn't zero fill-values. """ import scipy.sparse check_fill_value(self, accept_fv=accept_fv) if self.ndim != 2: raise ValueError("Can only convert a 2-dimensional array to a Scipy sparse matrix.") if 0 in self.compressed_axes: return scipy.sparse.csr_matrix((self.data, self.indices, self.indptr), shape=self.shape) return scipy.sparse.csc_matrix((self.data, self.indices, self.indptr), shape=self.shape) def asformat(self, format, **kwargs): """ Convert this sparse array to a given format. Parameters ---------- format : str A format string. Returns ------- out : SparseArray The converted array. Raises ------ NotImplementedError If the format isn't supported. """ from .._utils import convert_format format = convert_format(format) ret = None if format == "coo": ret = self.tocoo() elif format == "dok": ret = self.todok() elif format == "csr": ret = CSR(self) elif format == "csc": ret = CSC(self) elif format == "gcxs": compressed_axes = kwargs.pop("compressed_axes", self.compressed_axes) return self.change_compressed_axes(compressed_axes) if len(kwargs) != 0: raise TypeError(f"Invalid keyword arguments provided: {kwargs}") if ret is None: raise NotImplementedError(f"The given format is not supported: {format}") return ret def maybe_densify(self, max_size=1000, min_density=0.25): """ Converts this [`sparse.GCXS`][] array to a [`numpy.ndarray`][] if not too costly. Parameters ---------- max_size : int Maximum number of elements in output min_density : float Minimum density of output Returns ------- numpy.ndarray The dense array. See Also -------- - [sparse.GCXS.todense][]: Converts to Numpy function without checking the cost. - [sparse.COO.maybe_densify][]: The equivalent COO function. Raises ------- ValueError If the returned array would be too large. """ if self.size > max_size and self.density < min_density: raise ValueError("Operation would require converting large sparse array to dense") return self.todense() def flatten(self, order="C"): """ Returns a new [`sparse.GCXS`][] array that is a flattened version of this array. Returns ------- GCXS The flattened output array. Notes ----- The `order` parameter is provided just for compatibility with Numpy and isn't actually supported. """ if order not in {"C", None}: raise NotImplementedError("The `order` parameter is not supported.") return self.reshape(-1) def reshape(self, shape, order="C", compressed_axes=None): """ Returns a new [`sparse.GCXS`][] array that is a reshaped version of this array. Parameters ---------- shape : tuple[int] The desired shape of the output array. compressed_axes : Iterable[int], optional The axes to compress to store the array. Finds the most efficient storage by default. Returns ------- GCXS The reshaped output array. See Also -------- - [`numpy.ndarray.reshape`][] : The equivalent Numpy function. - [sparse.COO.reshape][] : The equivalent COO function. Notes ----- The `order` parameter is provided just for compatibility with Numpy and isn't actually supported. """ shape = tuple(shape) if isinstance(shape, Iterable) else (shape,) if order not in {"C", None}: raise NotImplementedError("The 'order' parameter is not supported") if any(d == -1 for d in shape): extra = int(self.size / np.prod([d for d in shape if d != -1])) shape = tuple([d if d != -1 else extra for d in shape]) if self.shape == shape: return self if self.size != reduce(operator.mul, shape, 1): raise ValueError(f"cannot reshape array of size {self.size} into shape {shape}") if len(shape) == 0: return self.tocoo().reshape(shape).asformat("gcxs") if compressed_axes is None: if len(shape) == self.ndim: compressed_axes = self.compressed_axes elif len(shape) == 1: compressed_axes = None else: compressed_axes = (np.argmin(shape),) if self.ndim == 1: arg = _1d_reshape(self, shape, compressed_axes) else: arg = _transpose(self, shape, np.arange(self.ndim), compressed_axes) return GCXS( arg, shape=tuple(shape), compressed_axes=compressed_axes, fill_value=self.fill_value, ) @property def compressed_axes(self): return self._compressed_axes def transpose(self, axes=None, compressed_axes=None): """ Returns a new array which has the order of the axes switched. Parameters ---------- axes : Iterable[int], optional The new order of the axes compared to the previous one. Reverses the axes by default. compressed_axes : Iterable[int], optional The axes to compress to store the array. Finds the most efficient storage by default. Returns ------- GCXS The new array with the axes in the desired order. See Also -------- - [`sparse.GCXS.T`][] : A quick property to reverse the order of the axes. - [`numpy.ndarray.transpose`][] : Numpy equivalent function. """ if axes is None: axes = list(reversed(range(self.ndim))) # Normalize all axes indices to positive values axes = normalize_axis(axes, self.ndim) if len(np.unique(axes)) < len(axes): raise ValueError("repeated axis in transpose") if not len(axes) == self.ndim: raise ValueError("axes don't match array") axes = tuple(axes) if axes == tuple(range(self.ndim)): return self if self.ndim == 2: return self._2d_transpose() shape = tuple(self.shape[ax] for ax in axes) if compressed_axes is None: compressed_axes = (np.argmin(shape),) arg = _transpose(self, shape, axes, compressed_axes, transpose=True) return GCXS( arg, shape=shape, compressed_axes=compressed_axes, fill_value=self.fill_value, ) def _2d_transpose(self): """ A function for performing constant-time transposes on 2d GCXS arrays. Returns ------- GCXS The new transposed array with the opposite compressed axes as the input. See Also -------- scipy.sparse.csr_matrix.transpose : Scipy equivalent function. scipy.sparse.csc_matrix.transpose : Scipy equivalent function. numpy.ndarray.transpose : Numpy equivalent function. """ if self.ndim != 2: raise ValueError(f"cannot perform 2d transpose on array with dimension {self.ndim}") compressed_axes = [(self.compressed_axes[0] + 1) % 2] shape = self.shape[::-1] return GCXS( (self.data, self.indices, self.indptr), shape=shape, compressed_axes=compressed_axes, fill_value=self.fill_value, ) def dot(self, other): """ Performs the equivalent of `x.dot(y)` for [`sparse.GCXS`][]. Parameters ---------- other : Union[GCXS, COO, numpy.ndarray, scipy.sparse.spmatrix] The second operand of the dot product operation. Returns ------- {GCXS, numpy.ndarray} The result of the dot product. If the result turns out to be dense, then a dense array is returned, otherwise, a sparse array. Raises ------ ValueError If all arguments don't have zero fill-values. See Also -------- - [`sparse.dot`][] : Equivalent function for two arguments. - [`numpy.dot`][] : Numpy equivalent function. - [`scipy.sparse.coo_matrix.dot`][] : Scipy equivalent function. """ from .._common import dot return dot(self, other) def __matmul__(self, other): from .._common import matmul try: return matmul(self, other) except NotImplementedError: return NotImplemented def __rmatmul__(self, other): from .._common import matmul try: return matmul(other, self) except NotImplementedError: return NotImplemented def _prune(self): """ Prunes data so that if any fill-values are present, they are removed from both indices and data. Examples -------- >>> coords = np.array([[0, 1, 2, 3]]) >>> data = np.array([1, 0, 1, 2]) >>> s = COO(coords, data, shape=(4,)).asformat("gcxs") >>> s._prune() >>> s.nnz 3 """ mask = ~equivalent(self.data, self.fill_value) self.data = self.data[mask] if len(self.indptr): coords = np.stack((uncompress_dimension(self.indptr), self.indices)) coords = coords[:, mask] self.indices = coords[1] row_size = self._compressed_shape[0] indptr = np.empty(row_size + 1, dtype=self.indptr.dtype) indptr[0] = 0 np.cumsum(np.bincount(coords[0], minlength=row_size), out=indptr[1:]) self.indptr = indptr else: self.indices = self.indices[mask] def isinf(self): return self.tocoo().isinf().asformat("gcxs", compressed_axes=self.compressed_axes) def isnan(self): return self.tocoo().isnan().asformat("gcxs", compressed_axes=self.compressed_axes) class _Compressed2d(GCXS): class_compressed_axes: tuple[int] def __init__(self, arg, shape=None, compressed_axes=None, prune=False, fill_value=0): if not hasattr(arg, "shape") and shape is None: raise ValueError("missing `shape` argument") if shape is not None and hasattr(arg, "shape"): raise NotImplementedError("Cannot change shape in constructor") nd = len(shape if shape is not None else arg.shape) if nd != 2: raise ValueError(f"{type(self).__name__} must be 2-d, passed {nd}-d shape.") super().__init__( arg, shape=shape, compressed_axes=compressed_axes, prune=prune, fill_value=fill_value, ) def __str__(self): summary = ( f"<{type(self).__name__}: shape={self.shape}, dtype={self.dtype}, nnz={self.nnz}, " f"fill_value={self.fill_value}>" ) return self._str_impl(summary) __repr__ = __str__ @property def ndim(self) -> int: return 2 @classmethod def from_numpy(cls, x, fill_value=0, idx_dtype=None): coo = COO.from_numpy(x, fill_value=fill_value, idx_dtype=idx_dtype) return cls.from_coo(coo, cls.class_compressed_axes, idx_dtype) class CSR(_Compressed2d): """ The CSR or CRS scheme stores a n-dimensional array using n+1 one-dimensional arrays. The 3 arrays are same as GCRS. The remaining n-2 arrays are for storing the indices of the non-zero values of the sparse matrix. CSR is simply the transpose of CSC. Sparse supports 2-D CSR. """ class_compressed_axes: tuple[int] = (0,) def __init__(self, arg, shape=None, compressed_axes=class_compressed_axes, prune=False, fill_value=0): if compressed_axes != self.class_compressed_axes: raise ValueError(f"CSR only accepts rows as compressed axis but got: {compressed_axes}") super().__init__(arg, shape=shape, compressed_axes=compressed_axes, fill_value=fill_value) @classmethod def from_scipy_sparse(cls, x, /, *, fill_value=None): x = x.asformat("csr", copy=False) if not x.has_canonical_format: x.eliminate_zeros() x.sum_duplicates() return cls((x.data, x.indices, x.indptr), shape=x.shape, fill_value=fill_value) def transpose(self, axes: None = None, copy: bool = False) -> Union["CSC", "CSR"]: axes = normalize_axis(axes, self.ndim) if axes not in [(0, 1), (1, 0), None]: raise ValueError(f"Invalid transpose axes: {axes}") if copy: self = self.copy() if axes == (0, 1): return self return CSC((self.data, self.indices, self.indptr), self.shape[::-1], fill_value=self.fill_value) class CSC(_Compressed2d): """ The CSC or CCS scheme stores a n-dimensional array using n+1 one-dimensional arrays. The 3 arrays are same as GCCS. The remaining n-2 arrays are for storing the indices of the non-zero values of the sparse matrix. CSC is simply the transpose of CSR. Sparse supports 2-D CSC. """ class_compressed_axes: tuple[int] = (1,) def __init__(self, arg, shape=None, compressed_axes=class_compressed_axes, prune=False, fill_value=0): if compressed_axes != self.class_compressed_axes: raise ValueError(f"CSC only accepts columns as compressed axis but got: {compressed_axes}") super().__init__(arg, shape=shape, compressed_axes=compressed_axes, fill_value=fill_value) @classmethod def from_scipy_sparse(cls, x, /, *, fill_value=None): x = x.asformat("csc", copy=False) if not x.has_canonical_format: x.eliminate_zeros() x.sum_duplicates() return cls((x.data, x.indices, x.indptr), shape=x.shape, fill_value=fill_value) def transpose(self, axes: None = None, copy: bool = False) -> Union["CSC", "CSR"]: axes = normalize_axis(axes, self.ndim) if axes not in [(0, 1), (1, 0), None]: raise ValueError(f"Invalid transpose axes: {axes}") if copy: self = self.copy() if axes == (0, 1): return self return CSR((self.data, self.indices, self.indptr), self.shape[::-1], fill_value=self.fill_value) sparse-0.17.0/sparse/numba_backend/_compressed/convert.py000066400000000000000000000250761501262445000235100ustar00rootroot00000000000000import operator from functools import reduce import numba from numba.typed import List import numpy as np from .._coo.common import linear_loc from .._utils import check_compressed_axes, get_out_dtype @numba.jit(nopython=True, nogil=True) def convert_to_flat(inds, shape, dtype): """ Converts the indices of either the compressed or uncompressed axes into a linearized form. Prepares the inputs for compute_flat. """ shape_bins = transform_shape(np.asarray(shape)) increments = List() for i in range(len(inds)): increments.append((inds[i] * shape_bins[i]).astype(dtype)) operations = 1 for inc in increments[:-1]: operations *= inc.shape[0] if operations == 0: return np.empty(0, dtype=dtype) cols = increments[-1].repeat(operations).reshape((-1, operations)).T.flatten() if len(increments) == 1: return cols return compute_flat(increments, cols, operations) @numba.jit(nopython=True, nogil=True) def compute_flat(increments, cols, operations): # pragma: no cover """ Iterates through indices and calculates the linearized indices. """ start = 0 end = increments[-1].shape[0] positions = np.zeros(len(increments) - 1, dtype=np.intp) pos = len(increments) - 2 for _ in range(operations): to_add = 0 for j in range(len(increments) - 1): to_add += increments[j][positions[j]] cols[start:end] += to_add start += increments[-1].shape[0] end += increments[-1].shape[0] for j in range(pos, -1, -1): positions[j] += 1 if positions[j] == increments[j].shape[0]: positions[j] = 0 else: break return cols @numba.jit(nopython=True, nogil=True) def transform_shape(shape): # pragma: no cover """ turns a shape into the linearized increments that it represents. For example, given (5,5,5), it returns np.array([25,5,1]). """ shape_bins = np.empty(len(shape), dtype=np.intp) shape_bins[-1] = 1 for i in range(len(shape) - 1): shape_bins[i] = np.prod(shape[i + 1 :]) return shape_bins @numba.jit(nopython=True, nogil=True) def uncompress_dimension(indptr): # pragma: no cover """converts an index pointer array into an array of coordinates""" uncompressed = np.empty(indptr[-1], dtype=indptr.dtype) for i in range(len(indptr) - 1): uncompressed[indptr[i] : indptr[i + 1]] = i return uncompressed @numba.jit(nopython=True, nogil=True) def is_sorted(arr): # pragma: no cover """ function to check if an indexing array is sorted without repeats. If it is, we can use the faster slicing algorithm. """ # numba doesn't recognize the new all(...) format for i in range(len(arr) - 1): # noqa: SIM110 if arr[i + 1] <= arr[i]: return False return True @numba.jit(nopython=True, nogil=True) def _linearize( x_indices, shape, new_axis_order, new_reordered_shape, new_compressed_shape, new_linear, new_coords, ): # pragma: no cover for i, n in enumerate(x_indices): current = unravel_index(n, shape) current_t = current[new_axis_order] new_linear[i] = ravel_multi_index(current_t, new_reordered_shape) new_coords[:, i] = unravel_index(new_linear[i], new_compressed_shape) def _1d_reshape(x, shape, compressed_axes): check_compressed_axes(shape, compressed_axes) new_size = np.prod(shape) end_idx = np.searchsorted(x.indices, new_size, side="left") # for resizeing in one dimension if len(shape) == 1: return (x.data[:end_idx], x.indices[:end_idx], []) new_axis_order = list(compressed_axes) new_axis_order.extend(np.setdiff1d(np.arange(len(shape)), compressed_axes)) new_axis_order = np.asarray(new_axis_order) new_reordered_shape = np.array(shape)[new_axis_order] axisptr = len(compressed_axes) row_size = np.prod(new_reordered_shape[:axisptr]) col_size = np.prod(new_reordered_shape[axisptr:]) new_compressed_shape = np.array((row_size, col_size)) x_indices = x.indices[:end_idx] new_nnz = x_indices.size new_linear = np.empty(new_nnz, dtype=np.intp) coords_dtype = get_out_dtype(x.indices, max(max(new_compressed_shape), x.nnz)) new_coords = np.empty((2, new_nnz), dtype=coords_dtype) _linearize( x_indices, np.array(shape), new_axis_order, new_reordered_shape, new_compressed_shape, new_linear, new_coords, ) order = np.argsort(new_linear) new_coords = new_coords[:, order] indptr = np.empty(row_size + 1, dtype=coords_dtype) indptr[0] = 0 np.cumsum(np.bincount(new_coords[0], minlength=row_size), out=indptr[1:]) indices = new_coords[1] data = x.data[:end_idx][order] return (data, indices, indptr) def _resize(x, shape, compressed_axes): from .compressed import GCXS check_compressed_axes(shape, compressed_axes) size = reduce(operator.mul, shape, 1) if x.ndim == 1: end_idx = np.searchsorted(x.indices, size, side="left") indices = x.indices[:end_idx] data = x.data[:end_idx] out = GCXS((data, indices, []), shape=(size,), fill_value=x.fill_value) return _1d_reshape(out, shape, compressed_axes) uncompressed = uncompress_dimension(x.indptr) coords = np.stack((uncompressed, x.indices)) linear = linear_loc(coords, x._compressed_shape) sorted_axis_order = np.argsort(x._axis_order) linear_dtype = get_out_dtype(x.indices, np.prod(shape)) c_linear = np.empty(x.nnz, dtype=linear_dtype) _c_ordering( linear, c_linear, np.asarray(x._reordered_shape), np.asarray(sorted_axis_order), np.asarray(x.shape), ) order = np.argsort(c_linear, kind="mergesort") data = x.data[order] indices = c_linear[order] end_idx = np.searchsorted(indices, size, side="left") indices = indices[:end_idx] data = data[:end_idx] out = GCXS((data, indices, []), shape=(size,), fill_value=x.fill_value) return _1d_reshape(out, shape, compressed_axes) @numba.jit(nopython=True, nogil=True) def _c_ordering(linear, c_linear, reordered_shape, sorted_axis_order, shape): # pragma: no cover for i, n in enumerate(linear): # c ordering current_coords = unravel_index(n, reordered_shape)[sorted_axis_order] c_linear[i] = ravel_multi_index(current_coords, shape) def _transpose(x, shape, axes, compressed_axes, transpose=False): """ An algorithm for reshaping, resizing, changing compressed axes, and transposing. """ check_compressed_axes(shape, compressed_axes) uncompressed = uncompress_dimension(x.indptr) coords = np.stack((uncompressed, x.indices)) linear = linear_loc(coords, x._compressed_shape) sorted_axis_order = np.argsort(x._axis_order) if len(shape) == 1: dtype = get_out_dtype(x.indices, shape[0]) c_linear = np.empty(x.nnz, dtype=dtype) _c_ordering( linear, c_linear, np.asarray(x._reordered_shape), np.asarray(sorted_axis_order), np.asarray(x.shape), ) order = np.argsort(c_linear, kind="mergesort") data = x.data[order] indices = c_linear[order] return (data, indices, []) new_axis_order = list(compressed_axes) new_axis_order.extend(np.setdiff1d(np.arange(len(shape)), compressed_axes)) new_linear = np.empty(x.nnz, dtype=np.intp) new_reordered_shape = np.array(shape)[new_axis_order] axisptr = len(compressed_axes) row_size = np.prod(new_reordered_shape[:axisptr]) col_size = np.prod(new_reordered_shape[axisptr:]) new_compressed_shape = np.array((row_size, col_size)) coords_dtype = get_out_dtype(x.indices, max(max(new_compressed_shape), x.nnz)) new_coords = np.empty((2, x.nnz), dtype=coords_dtype) _convert_coords( linear, np.asarray(x.shape), np.asarray(x._reordered_shape), sorted_axis_order, np.asarray(axes), np.asarray(shape), np.asarray(new_axis_order), new_reordered_shape, new_linear, new_coords, new_compressed_shape, transpose, ) order = np.argsort(new_linear, kind="mergesort") new_coords = new_coords[:, order] if len(shape) == 1: indptr = [] indices = coords[0, :] else: indptr = np.empty(row_size + 1, dtype=coords_dtype) indptr[0] = 0 np.cumsum(np.bincount(new_coords[0], minlength=row_size), out=indptr[1:]) indices = new_coords[1] data = x.data[order] return (data, indices, indptr) @numba.jit(nopython=True, nogil=True) def unravel_index(n, shape): # pragma: no cover """ implements a subset of the functionality of np.unravel_index. """ out = np.zeros(len(shape), dtype=np.intp) i = 1 while i < len(shape) and n > 0: cur = np.prod(shape[i:]) out[i - 1] = n // cur n -= out[i - 1] * cur i += 1 out[-1] = n return out @numba.jit(nopython=True, nogil=True) def ravel_multi_index(arr, shape): # pragma: no cover """ implements a subset of the functionality of np.ravel_multi_index. """ total = 0 for i, a in enumerate(arr[:-1], 1): total += a * np.prod(shape[i:]) total += arr[-1] return total @numba.jit(nopython=True, nogil=True) def _convert_coords( linear, old_shape, reordered_shape, sorted_axis_order, axes, shape, new_axis_order, new_reordered_shape, new_linear, new_coords, new_compressed_shape, transpose, ): # pragma: no cover if transpose: for i, n in enumerate(linear): # c ordering current_coords = unravel_index(n, reordered_shape)[sorted_axis_order] # transpose current_coords_t = current_coords[axes][new_axis_order] new_linear[i] = ravel_multi_index(current_coords_t, new_reordered_shape) # reshape new_coords[:, i] = unravel_index(new_linear[i], new_compressed_shape) else: for i, n in enumerate(linear): # c ordering current_coords = unravel_index(n, reordered_shape)[sorted_axis_order] # linearize c_current = ravel_multi_index(current_coords, old_shape) # compress c_compressed = unravel_index(c_current, shape) c_compressed = c_compressed[new_axis_order] new_linear[i] = ravel_multi_index(c_compressed, new_reordered_shape) # reshape new_coords[:, i] = unravel_index(new_linear[i], new_compressed_shape) sparse-0.17.0/sparse/numba_backend/_compressed/indexing.py000066400000000000000000000242711501262445000236310ustar00rootroot00000000000000from collections.abc import Iterable from itertools import zip_longest from numbers import Integral import numba from numba.typed import List import numpy as np from .._slicing import normalize_index from .convert import convert_to_flat, is_sorted, uncompress_dimension def getitem(x, key): """ GCXS arrays are stored by transposing and reshaping them into csr matrices. For indexing, we first convert the n-dimensional key to its corresponding 2-dimensional key and then iterate through each of the relevent rows and columns. """ from .compressed import GCXS if x.ndim == 1: result = x.tocoo()[key] if np.isscalar(result): return result return GCXS.from_coo(result) key = list(normalize_index(key, x.shape)) # zip_longest so things like x[..., None] are picked up. if len(key) != 0 and all(isinstance(k, slice) and k == slice(0, dim, 1) for k, dim in zip_longest(key, x.shape)): return x # return a single element if all(isinstance(k, int) for k in key): return get_single_element(x, key) shape = [] compressed_inds = np.zeros(len(x.shape), dtype=np.bool_) uncompressed_inds = np.zeros(len(x.shape), dtype=np.bool_) # which axes will be compressed in the resulting array shape_key = np.zeros(len(x.shape), dtype=np.intp) # remove Nones from key, evaluate them at the end Nones_removed = [k for k in key if k is not None] count = 0 for i, ind in enumerate(Nones_removed): if isinstance(ind, Integral): continue if isinstance(ind, slice): shape_key[i] = count shape.append(len(range(ind.start, ind.stop, ind.step))) if i in x.compressed_axes: compressed_inds[i] = True else: uncompressed_inds[i] = True elif isinstance(ind, Iterable): shape_key[i] = count shape.append(len(ind)) if i in x.compressed_axes: compressed_inds[i] = True else: uncompressed_inds[i] = True count += 1 # reorder the key according to the axis_order of the array reordered_key = [Nones_removed[i] for i in x._axis_order] # if all slices have a positive step and all # iterables are sorted without repeats, we can # use the quicker slicing algorithm pos_slice = True for ind in reordered_key[x._axisptr :]: if isinstance(ind, slice): if ind.step < 0: pos_slice = False elif isinstance(ind, Iterable) and not is_sorted(ind): pos_slice = False # convert all ints and slices to iterables before flattening for i, ind in enumerate(reordered_key): if isinstance(ind, Integral): reordered_key[i] = np.array([ind]) elif isinstance(ind, slice): reordered_key[i] = np.arange(ind.start, ind.stop, ind.step) elif isinstance(ind, np.ndarray) and ind.ndim > 1: raise IndexError("Only one-dimensional iterable indices supported.") reordered_key[i] = reordered_key[i].astype(x.indices.dtype, copy=False) reordered_key = List(reordered_key) shape = np.array(shape) # convert all indices of compressed axes to a single array index # this tells us which 'rows' of the underlying csr matrix to iterate through rows = convert_to_flat( reordered_key[: x._axisptr], x._reordered_shape[: x._axisptr], x.indices.dtype, ) # convert all indices of uncompressed axes to a single array index # this tells us which 'columns' of the underlying csr matrix to iterate through cols = convert_to_flat( reordered_key[x._axisptr :], x._reordered_shape[x._axisptr :], x.indices.dtype, ) starts = x.indptr[:-1][rows] # find the start and end of each of the rows ends = x.indptr[1:][rows] if np.any(compressed_inds): compressed_axes = shape_key[compressed_inds] row_size = shape[compressed_axes] if len(compressed_axes) == 1 else np.prod(shape[compressed_axes]) # if only indexing through uncompressed axes else: compressed_axes = (0,) # defaults to 0 row_size = 1 # this doesn't matter if not np.any(uncompressed_inds): # only indexing compressed axes compressed_axes = (0,) # defaults to 0 row_size = starts.size indptr = np.empty(row_size + 1, dtype=x.indptr.dtype) indptr[0] = 0 if pos_slice: arg = get_slicing_selection(x.data, x.indices, indptr, starts, ends, cols) else: arg = get_array_selection(x.data, x.indices, indptr, starts, ends, cols) data, indices, indptr = arg size = np.prod(shape[1:]) if not np.any(uncompressed_inds): # only indexing compressed axes uncompressed = uncompress_dimension(indptr) if len(shape) == 1: indices = uncompressed indptr = None else: indices = uncompressed % size indptr = np.empty(shape[0] + 1, dtype=x.indptr.dtype) indptr[0] = 0 np.cumsum(np.bincount(uncompressed // size, minlength=shape[0]), out=indptr[1:]) if not np.any(compressed_inds): if len(shape) == 1: indptr = None else: uncompressed = indices // size indptr = np.empty(shape[0] + 1, dtype=x.indptr.dtype) indptr[0] = 0 np.cumsum(np.bincount(uncompressed, minlength=shape[0]), out=indptr[1:]) indices %= size arg = (data, indices, indptr) # if there were Nones in the key, we insert them back here compressed_axes = np.array(compressed_axes) shape = shape.tolist() for i in range(len(key)): if key[i] is None: shape.insert(i, 1) compressed_axes[compressed_axes >= i] += 1 compressed_axes = tuple(compressed_axes) shape = tuple(shape) if len(shape) == 1: compressed_axes = None return GCXS(arg, shape=shape, compressed_axes=compressed_axes, fill_value=x.fill_value) @numba.jit(nopython=True, nogil=True) def get_slicing_selection(arr_data, arr_indices, indptr, starts, ends, col): # pragma: no cover """ When the requested elements come in a strictly ascending order, as is the case with acsending slices, we can iteratively reduce the search space, leading to better performance. We loop through the starts and ends, each time evaluating whether to use a linear filtering procedure or a binary-search-based method. """ indices = [] ind_list = [] for i, (start, end) in enumerate(zip(starts, ends)): # noqa: B905 inds = [] current_row = arr_indices[start:end] if current_row.size < col.size: # linear filtering count = 0 col_count = 0 nnz = 0 while col_count < col.size and count < current_row.size: if current_row[-1] < col[col_count] or current_row[count] > col[-1]: break if current_row[count] == col[col_count]: nnz += 1 ind_list.append(count + start) indices.append(col_count) count += 1 col_count += 1 elif current_row[count] < col[col_count]: count += 1 else: col_count += 1 indptr[i + 1] = indptr[i] + nnz else: # binary searches prev = 0 size = 0 col_count = 0 while col_count < len(col): while ( col_count < len(col) and size < len(current_row) and col[col_count] < current_row[size] ): # skip needless searches col_count += 1 if col_count >= len(col): # check again because of previous loop break if current_row[-1] < col[col_count] or current_row[size] > col[-1]: break s = np.searchsorted(current_row[size:], col[col_count]) size += s s += prev if not (s >= current_row.size or current_row[s] != col[col_count]): s += start inds.append(s) indices.append(col_count) size += 1 prev = size col_count += 1 ind_list.extend(inds) indptr[i + 1] = indptr[i] + len(inds) ind_list = np.array(ind_list, dtype=np.intp) indices = np.array(indices, dtype=indptr.dtype) data = arr_data[ind_list] return (data, indices, indptr) @numba.jit(nopython=True, nogil=True) def get_array_selection(arr_data, arr_indices, indptr, starts, ends, col): # pragma: no cover """ This is a very general algorithm to be used when more optimized methods don't apply. It performs a binary search for each of the requested elements. Consequently it roughly scales by O(n log avg(nnz)). """ indices = [] ind_list = [] for i, (start, end) in enumerate(zip(starts, ends)): # noqa: B905 inds = [] current_row = arr_indices[start:end] if len(current_row) == 0: indptr[i + 1] = indptr[i] continue for c in range(len(col)): s = np.searchsorted(current_row, col[c]) if not (s >= current_row.size or current_row[s] != col[c]): s += start inds.append(s) indices.append(c) ind_list.extend(inds) indptr[i + 1] = indptr[i] + len(inds) ind_list = np.array(ind_list, dtype=np.intp) indices = np.array(indices, dtype=indptr.dtype) data = arr_data[ind_list] return (data, indices, indptr) def get_single_element(x, key): """ A convience function for indexing when returning a single element. """ key = np.array(key)[x._axis_order] # reordering the input ind = np.ravel_multi_index(key, x._reordered_shape) row, col = np.unravel_index(ind, x._compressed_shape) current_row = x.indices[x.indptr[row] : x.indptr[row + 1]] item = np.searchsorted(current_row, col) if not (item >= current_row.size or current_row[item] != col): item += x.indptr[row] return x.data[item] return x.fill_value sparse-0.17.0/sparse/numba_backend/_coo/000077500000000000000000000000001501262445000200605ustar00rootroot00000000000000sparse-0.17.0/sparse/numba_backend/_coo/__init__.py000066400000000000000000000015601501262445000221730ustar00rootroot00000000000000from .common import ( argmax, argmin, argwhere, clip, concatenate, diagonal, diagonalize, expand_dims, flip, isneginf, isposinf, kron, nanmax, nanmean, nanmin, nanprod, nanreduce, nansum, result_type, roll, sort, stack, take, tril, triu, unique_counts, unique_values, where, ) from .core import COO, as_coo __all__ = [ "COO", "as_coo", "argmax", "argmin", "argwhere", "clip", "concatenate", "diagonal", "diagonalize", "expand_dims", "flip", "isneginf", "isposinf", "kron", "nanmax", "nanmean", "nanmin", "nanprod", "nanreduce", "nansum", "result_type", "roll", "sort", "stack", "take", "tril", "triu", "unique_counts", "unique_values", "where", ] sparse-0.17.0/sparse/numba_backend/_coo/common.py000066400000000000000000001267241501262445000217360ustar00rootroot00000000000000import operator import warnings from collections.abc import Iterable from functools import reduce from typing import Any, NamedTuple import numba import numpy as np from .._sparse_array import SparseArray from .._utils import ( can_store, check_consistent_fill_value, check_zero_fill_value, is_unsigned_dtype, isscalar, normalize_axis, ) def asCOO(x, name="asCOO", check=True): """ Convert the input to [`sparse.COO`][]. Passes through [`sparse.COO`][] objects as-is. Parameters ---------- x : Union[SparseArray, scipy.sparse.spmatrix, numpy.ndarray] The input array to convert. name : str, optional The name of the operation to use in the exception. check : bool, optional Whether to check for a dense input. Returns ------- COO The converted [`sparse.COO`][] array. Raises ------ ValueError If `check` is true and a dense input is supplied. """ from .._common import _is_sparse from .core import COO if check and not _is_sparse(x): raise ValueError(f"Performing this operation would produce a dense result: {name}") if not isinstance(x, COO): x = COO(x) return x def linear_loc(coords, shape): if shape == () and len(coords) == 0: # `np.ravel_multi_index` is not aware of arrays, so cannot produce a # sensible result here (https://github.com/numpy/numpy/issues/15690). # Since `coords` is an array and not a sequence, we know the correct # dimensions. return np.zeros(coords.shape[1:], dtype=np.intp) return np.ravel_multi_index(coords, shape) def kron(a, b): """Kronecker product of 2 sparse arrays. Parameters ---------- a, b : SparseArray, scipy.sparse.spmatrix, or np.ndarray The arrays over which to compute the Kronecker product. Returns ------- res : COO The kronecker product Raises ------ ValueError If all arguments are dense or arguments have nonzero fill-values. Examples -------- >>> from sparse import eye >>> a = eye(3, dtype="i8") >>> b = np.array([1, 2, 3], dtype="i8") >>> res = kron(a, b) >>> res.todense() # doctest: +SKIP array([[1, 2, 3, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 2, 3, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 2, 3]], dtype=int64) """ from .._common import _is_sparse from .._umath import _cartesian_product from .core import COO check_zero_fill_value(a, b) a_sparse = _is_sparse(a) b_sparse = _is_sparse(b) a_ndim = np.ndim(a) b_ndim = np.ndim(b) if not (a_sparse or b_sparse): raise ValueError("Performing this operation would produce a dense result: kron") if a_ndim == 0 or b_ndim == 0: return a * b a = asCOO(a, check=False) b = asCOO(b, check=False) # Match dimensions max_dim = max(a.ndim, b.ndim) a = a.reshape((1,) * (max_dim - a.ndim) + a.shape) b = b.reshape((1,) * (max_dim - b.ndim) + b.shape) a_idx, b_idx = _cartesian_product(np.arange(a.nnz), np.arange(b.nnz)) a_expanded_coords = a.coords[:, a_idx] b_expanded_coords = b.coords[:, b_idx] o_coords = a_expanded_coords * np.asarray(b.shape)[:, None] + b_expanded_coords o_data = a.data[a_idx] * b.data[b_idx] o_shape = tuple(i * j for i, j in zip(a.shape, b.shape, strict=True)) return COO(o_coords, o_data, shape=o_shape, has_duplicates=False) def concatenate(arrays, axis=0): """ Concatenate the input arrays along the given dimension. Parameters ---------- arrays : Iterable[SparseArray] The input arrays to concatenate. axis : int, optional The axis along which to concatenate the input arrays. The default is zero. Returns ------- COO The output concatenated array. Raises ------ ValueError If all elements of `arrays` don't have the same fill-value. See Also -------- [`numpy.concatenate`][] : NumPy equivalent function """ from .core import COO check_consistent_fill_value(arrays) if axis is None: axis = 0 arrays = [x.flatten() for x in arrays] arrays = [x if isinstance(x, COO) else COO(x) for x in arrays] axis = normalize_axis(axis, arrays[0].ndim) assert all(x.shape[ax] == arrays[0].shape[ax] for x in arrays for ax in set(range(arrays[0].ndim)) - {axis}) nnz = 0 dim = sum(x.shape[axis] for x in arrays) shape = list(arrays[0].shape) shape[axis] = dim data = np.concatenate([x.data for x in arrays]) coords = np.concatenate([x.coords for x in arrays], axis=1) if not can_store(coords.dtype, max(shape)): coords = coords.astype(np.min_scalar_type(max(shape))) dim = 0 for x in arrays: if dim: coords[axis, nnz : x.nnz + nnz] += dim dim += x.shape[axis] nnz += x.nnz return COO( coords, data, shape=shape, has_duplicates=False, sorted=(axis == 0), fill_value=arrays[0].fill_value, ) def stack(arrays, axis=0): """ Stack the input arrays along the given dimension. Parameters ---------- arrays : Iterable[SparseArray] The input arrays to stack. axis : int, optional The axis along which to stack the input arrays. Returns ------- COO The output stacked array. Raises ------ ValueError If all elements of `arrays` don't have the same fill-value. See Also -------- [`numpy.stack`][] : NumPy equivalent function """ from .core import COO check_consistent_fill_value(arrays) assert len({x.shape for x in arrays}) == 1 arrays = [x if isinstance(x, COO) else COO(x) for x in arrays] axis = normalize_axis(axis, arrays[0].ndim + 1) data = np.concatenate([x.data for x in arrays]) coords = np.concatenate([x.coords for x in arrays], axis=1) shape = list(arrays[0].shape) shape.insert(axis, len(arrays)) nnz = 0 new = np.empty(shape=(coords.shape[1],), dtype=np.intp) for dim, x in enumerate(arrays): new[nnz : x.nnz + nnz] = dim nnz += x.nnz coords = [coords[i] for i in range(coords.shape[0])] coords.insert(axis, new) coords = np.stack(coords, axis=0) return COO( coords, data, shape=shape, has_duplicates=False, sorted=(axis == 0), fill_value=arrays[0].fill_value, ) def triu(x, k=0): """ Returns an array with all elements below the k-th diagonal set to zero. Parameters ---------- x : COO The input array. k : int, optional The diagonal below which elements are set to zero. The default is zero, which corresponds to the main diagonal. Returns ------- COO The output upper-triangular matrix. Raises ------ ValueError If `x` doesn't have zero fill-values. See Also -------- - [`numpy.triu`][] : NumPy equivalent function """ from .core import COO check_zero_fill_value(x) if not x.ndim >= 2: raise NotImplementedError("sparse.triu is not implemented for scalars or 1-D arrays.") mask = x.coords[-2] + k <= x.coords[-1] coords = x.coords[:, mask] data = x.data[mask] return COO(coords, data, shape=x.shape, has_duplicates=False, sorted=True) def tril(x, k=0): """ Returns an array with all elements above the k-th diagonal set to zero. Parameters ---------- x : COO The input array. k : int, optional The diagonal above which elements are set to zero. The default is zero, which corresponds to the main diagonal. Returns ------- COO The output lower-triangular matrix. Raises ------ ValueError If `x` doesn't have zero fill-values. See Also -------- - [`numpy.tril`][] : NumPy equivalent function """ from .core import COO check_zero_fill_value(x) if not x.ndim >= 2: raise NotImplementedError("sparse.tril is not implemented for scalars or 1-D arrays.") mask = x.coords[-2] + k >= x.coords[-1] coords = x.coords[:, mask] data = x.data[mask] return COO(coords, data, shape=x.shape, has_duplicates=False, sorted=True) def nansum(x, axis=None, keepdims=False, dtype=None, out=None): """ Performs a ``NaN`` skipping sum operation along the given axes. Uses all axes by default. Parameters ---------- x : SparseArray The array to perform the reduction on. axis : Union[int, Iterable[int]], optional The axes along which to sum. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. dtype : numpy.dtype The data type of the output array. Returns ------- COO The reduced output sparse array. See Also -------- - [`sparse.COO.sum`][] : Function without ``NaN`` skipping. - [`numpy.nansum`][] : Equivalent Numpy function. """ assert out is None x = asCOO(x, name="nansum") return nanreduce(x, np.add, axis=axis, keepdims=keepdims, dtype=dtype) def nanmean(x, axis=None, keepdims=False, dtype=None, out=None): """ Performs a `NaN` skipping mean operation along the given axes. Uses all axes by default. Parameters ---------- x : SparseArray The array to perform the reduction on. axis : Union[int, Iterable[int]], optional The axes along which to compute the mean. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. dtype : numpy.dtype The data type of the output array. Returns ------- COO The reduced output sparse array. See Also -------- - [`sparse.COO.mean`][] : Function without `NaN` skipping. - [`numpy.nanmean`][] : Equivalent Numpy function. """ assert out is None x = asCOO(x, name="nanmean") if not (np.issubdtype(x.dtype, np.floating) or np.issubdtype(x.dtype, np.complexfloating)): return x.mean(axis=axis, keepdims=keepdims, dtype=dtype) mask = np.isnan(x) x2 = where(mask, 0, x) # Count the number non-nan elements along axis nancount = mask.sum(axis=axis, dtype="i8", keepdims=keepdims) if axis is None: axis = tuple(range(x.ndim)) elif not isinstance(axis, tuple): axis = (axis,) den = reduce(operator.mul, (x.shape[i] for i in axis), 1) den -= nancount if (den == 0).any(): warnings.warn("Mean of empty slice", RuntimeWarning, stacklevel=1) num = np.sum(x2, axis=axis, dtype=dtype, keepdims=keepdims) with np.errstate(invalid="ignore", divide="ignore"): if num.ndim: return np.true_divide(num, den, casting="unsafe") return (num / den).astype(dtype if dtype is not None else x.dtype) def nanmax(x, axis=None, keepdims=False, dtype=None, out=None): """ Maximize along the given axes, skipping `NaN` values. Uses all axes by default. Parameters ---------- x : SparseArray The array to perform the reduction on. axis : Union[int, Iterable[int]], optional The axes along which to maximize. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. dtype : numpy.dtype The data type of the output array. Returns ------- COO The reduced output sparse array. See Also -------- - [`sparse.COO.max`][] : Function without `NaN` skipping. - [`numpy.nanmax`][] : Equivalent Numpy function. """ assert out is None x = asCOO(x, name="nanmax") ar = x.reduce(np.fmax, axis=axis, keepdims=keepdims, dtype=dtype) if (isscalar(ar) and np.isnan(ar)) or np.isnan(ar.data).any(): warnings.warn("All-NaN slice encountered", RuntimeWarning, stacklevel=1) return ar def nanmin(x, axis=None, keepdims=False, dtype=None, out=None): """ Minimize along the given axes, skipping ``NaN`` values. Uses all axes by default. Parameters ---------- x : SparseArray The array to perform the reduction on. axis : Union[int, Iterable[int]], optional The axes along which to minimize. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. dtype : numpy.dtype The data type of the output array. Returns ------- COO The reduced output sparse array. See Also -------- - [`sparse.COO.min`][] : Function without `NaN` skipping. - [`numpy.nanmin`][] : Equivalent Numpy function. """ assert out is None x = asCOO(x, name="nanmin") ar = x.reduce(np.fmin, axis=axis, keepdims=keepdims, dtype=dtype) if (isscalar(ar) and np.isnan(ar)) or np.isnan(ar.data).any(): warnings.warn("All-NaN slice encountered", RuntimeWarning, stacklevel=1) return ar def nanprod(x, axis=None, keepdims=False, dtype=None, out=None): """ Performs a product operation along the given axes, skipping `NaN` values. Uses all axes by default. Parameters ---------- x : SparseArray The array to perform the reduction on. axis : Union[int, Iterable[int]], optional The axes along which to multiply. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. dtype : numpy.dtype The data type of the output array. Returns ------- COO The reduced output sparse array. See Also -------- - [`sparse.COO.prod`][] : Function without `NaN` skipping. - [`numpy.nanprod`][] : Equivalent Numpy function. """ assert out is None x = asCOO(x) return nanreduce(x, np.multiply, axis=axis, keepdims=keepdims, dtype=dtype) def where(condition, x=None, y=None): """ Select values from either ``x`` or ``y`` depending on ``condition``. If ``x`` and ``y`` are not given, returns indices where ``condition`` is nonzero. Performs the equivalent of [`numpy.where`][]. Parameters ---------- condition : SparseArray The condition based on which to select values from either ``x`` or ``y``. x : SparseArray, optional The array to select values from if ``condition`` is nonzero. y : SparseArray, optional The array to select values from if ``condition`` is zero. Returns ------- COO The output array with selected values if `x` and `y` are given; else where the array is nonzero. Raises ------ ValueError If the operation would produce a dense result; or exactly one of `x` and `y` are given. See Also -------- [`numpy.where`][] : Equivalent Numpy function. """ from .._umath import elemwise x_given = x is not None y_given = y is not None if not (x_given or y_given): check_zero_fill_value(condition) condition = asCOO(condition, name=str(np.where)) return tuple(condition.coords) if x_given != y_given: raise ValueError("either both or neither of x and y should be given") return elemwise(np.where, condition, x, y) def argwhere(a): """ Find the indices of array elements that are non-zero, grouped by element. Parameters ---------- a : array_like Input data. Returns ------- index_array : numpy.ndarray See Also -------- [`sparse.where`][], [`sparse.COO.nonzero`][] Examples -------- >>> import sparse >>> x = sparse.COO(np.arange(6).reshape((2, 3))) >>> sparse.argwhere(x > 1) array([[0, 2], [1, 0], [1, 1], [1, 2]]) """ return np.transpose(a.nonzero()) def argmax(x, /, *, axis=None, keepdims=False): """ Returns the indices of the maximum values along a specified axis. When the maximum value occurs multiple times, only the indices corresponding to the first occurrence are returned. Parameters ---------- x : SparseArray Input array. The fill value must be ``0.0`` and all non-zero values must be greater than ``0.0``. axis : int, optional Axis along which to search. If ``None``, the function must return the index of the maximum value of the flattened array. Default: ``None``. keepdims : bool, optional If ``True``, the reduced axes (dimensions) must be included in the result as singleton dimensions, and, accordingly, the result must be compatible with the input array. Otherwise, if ``False``, the reduced axes (dimensions) must not be included in the result. Default: ``False``. Returns ------- out : numpy.ndarray If ``axis`` is ``None``, a zero-dimensional array containing the index of the first occurrence of the maximum value. Otherwise, a non-zero-dimensional array containing the indices of the maximum values. """ return _arg_minmax_common(x, axis=axis, keepdims=keepdims, mode="max") def argmin(x, /, *, axis=None, keepdims=False): """ Returns the indices of the minimum values along a specified axis. When the minimum value occurs multiple times, only the indices corresponding to the first occurrence are returned. Parameters ---------- x : SparseArray Input array. The fill value must be ``0.0`` and all non-zero values must be less than ``0.0``. axis : int, optional Axis along which to search. If ``None``, the function must return the index of the minimum value of the flattened array. Default: ``None``. keepdims : bool, optional If ``True``, the reduced axes (dimensions) must be included in the result as singleton dimensions, and, accordingly, the result must be compatible with the input array. Otherwise, if ``False``, the reduced axes (dimensions) must not be included in the result. Default: ``False``. Returns ------- out : numpy.ndarray If ``axis`` is ``None``, a zero-dimensional array containing the index of the first occurrence of the minimum value. Otherwise, a non-zero-dimensional array containing the indices of the minimum values. """ return _arg_minmax_common(x, axis=axis, keepdims=keepdims, mode="min") def _replace_nan(array, value): """ Replaces ``NaN``s in ``array`` with ``value``. Parameters ---------- array : COO The input array. value : numpy.number The values to replace ``NaN`` with. Returns ------- COO A copy of ``array`` with the ``NaN``s replaced. """ if not np.issubdtype(array.dtype, np.floating): return array return where(np.isnan(array), value, array) def nanreduce(x, method, identity=None, axis=None, keepdims=False, **kwargs): """ Performs an `NaN` skipping reduction on this array. See the documentation on [`sparse.COO.reduce`][] for examples. Parameters ---------- x : COO The array to reduce. method : numpy.ufunc The method to use for performing the reduction. identity : numpy.number The identity value for this reduction. Inferred from ``method`` if not given. Note that some ``ufunc`` objects don't have this, so it may be necessary to give it. axis : Union[int, Iterable[int]], optional The axes along which to perform the reduction. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. **kwargs : dict Any extra arguments to pass to the reduction operation. Returns ------- COO The result of the reduction operation. Raises ------ ValueError If reducing an all-zero axis would produce a nonzero result. See Also -------- [`sparse.COO.reduce`][] : Similar method without `NaN` skipping functionality. """ arr = _replace_nan(x, method.identity if identity is None else identity) return arr.reduce(method, axis, keepdims, **kwargs) def roll(a, shift, axis=None): """ Shifts elements of an array along specified axis. Elements that roll beyond the last position are circulated and re-introduced at the first. Parameters ---------- a : COO Input array shift : int or tuple of ints Number of index positions that elements are shifted. If a tuple is provided, then axis must be a tuple of the same size, and each of the given axes is shifted by the corresponding number. If an int while axis is a tuple of ints, then broadcasting is used so the same shift is applied to all axes. axis : int or tuple of ints, optional Axis or tuple specifying multiple axes. By default, the array is flattened before shifting, after which the original shape is restored. Returns ------- res : ndarray Output array, with the same shape as a. """ from .core import COO, as_coo a = as_coo(a) # roll flattened array if axis is None: return roll(a.reshape((-1,)), shift, 0).reshape(a.shape) # roll across specified axis # parse axis input, wrap in tuple axis = normalize_axis(axis, a.ndim) if not isinstance(axis, tuple): axis = (axis,) # make shift iterable if not isinstance(shift, Iterable): shift = (shift,) elif np.ndim(shift) > 1: raise ValueError("'shift' and 'axis' must be integers or 1D sequences.") # handle broadcasting if len(shift) == 1: shift = np.full(len(axis), shift) # check if dimensions are consistent if len(axis) != len(shift): raise ValueError("If 'shift' is a 1D sequence, 'axis' must have equal length.") if not can_store(a.coords.dtype, max(a.shape + shift)): raise ValueError( f"cannot roll with coords.dtype {a.coords.dtype} and shift {shift}. Try casting coords to a larger dtype." ) # shift elements coords, data = np.copy(a.coords), np.copy(a.data) try: for sh, ax in zip(shift, axis, strict=True): coords[ax] += sh coords[ax] %= a.shape[ax] except TypeError as e: if is_unsigned_dtype(coords.dtype): raise ValueError( f"rolling with coords.dtype as {coords.dtype} is not safe. Try using a signed dtype." ) from e return COO( coords, data=data, shape=a.shape, has_duplicates=False, fill_value=a.fill_value, ) def diagonal(a, offset=0, axis1=0, axis2=1): """ Extract diagonal from a COO array. The equivalent of [`numpy.diagonal`][]. Parameters ---------- a : COO The array to perform the operation on. offset : int, optional Offset of the diagonal from the main diagonal. Defaults to main diagonal (0). axis1 : int, optional First axis from which the diagonals should be taken. Defaults to first axis (0). axis2 : int, optional Second axis from which the diagonals should be taken. Defaults to second axis (1). Examples -------- >>> import sparse >>> x = sparse.as_coo(np.arange(9).reshape(3, 3)) >>> sparse.diagonal(x).todense() array([0, 4, 8]) >>> sparse.diagonal(x, offset=1).todense() array([1, 5]) >>> x = sparse.as_coo(np.arange(12).reshape((2, 3, 2))) >>> x_diag = sparse.diagonal(x, axis1=0, axis2=2) >>> x_diag.shape (3, 2) >>> x_diag.todense() array([[ 0, 7], [ 2, 9], [ 4, 11]]) Returns ------- out: COO The result of the operation. Raises ------ ValueError If a.shape[axis1] != a.shape[axis2] See Also -------- [`numpy.diagonal`][] : NumPy equivalent function """ from .core import COO if a.shape[axis1] != a.shape[axis2]: raise ValueError("a.shape[axis1] != a.shape[axis2]") diag_axes = [axis for axis in range(len(a.shape)) if axis != axis1 and axis != axis2] + [axis1] diag_shape = [a.shape[axis] for axis in diag_axes] diag_shape[-1] -= abs(offset) diag_idx = _diagonal_idx(a.coords, axis1, axis2, offset) diag_coords = [a.coords[axis][diag_idx] for axis in diag_axes] diag_data = a.data[diag_idx] return COO(diag_coords, diag_data, diag_shape) def diagonalize(a, axis=0): """ Diagonalize a COO array. The new dimension is appended at the end. !!! warning [`sparse.diagonalize`][] is not [numpy][] compatible as there is no direct [numpy][] equivalent. The API may change in the future. Parameters ---------- a : Union[COO, np.ndarray, scipy.sparse.spmatrix] The array to diagonalize. axis : int, optional The axis to diagonalize. Defaults to first axis (0). Examples -------- >>> import sparse >>> x = sparse.as_coo(np.arange(1, 4)) >>> sparse.diagonalize(x).todense() array([[1, 0, 0], [0, 2, 0], [0, 0, 3]]) >>> x = sparse.as_coo(np.arange(24).reshape((2, 3, 4))) >>> x_diag = sparse.diagonalize(x, axis=1) >>> x_diag.shape (2, 3, 4, 3) [`sparse.diagonalize`][] is the inverse of [`sparse.diagonal`][] >>> a = sparse.random((3, 3, 3, 3, 3), density=0.3) >>> a_diag = sparse.diagonalize(a, axis=2) >>> (sparse.diagonal(a_diag, axis1=2, axis2=5) == a.transpose([0, 1, 3, 4, 2])).all() np.True_ Returns ------- out: COO The result of the operation. See Also -------- [`numpy.diag`][] : NumPy equivalent for 1D array """ from .core import COO, as_coo a = as_coo(a) diag_shape = a.shape + (a.shape[axis],) diag_coords = np.vstack([a.coords, a.coords[axis]]) return COO(diag_coords, a.data, diag_shape) def isposinf(x, out=None): """ Test element-wise for positive infinity, return result as sparse `bool` array. Parameters ---------- x Input out, optional Output array Examples -------- >>> import sparse >>> x = sparse.as_coo(np.array([np.inf])) >>> sparse.isposinf(x).todense() array([ True]) See Also -------- [`numpy.isposinf`][] : The NumPy equivalent """ from sparse import elemwise return elemwise(lambda x, out=None, dtype=None: np.isposinf(x, out=out), x, out=out) def isneginf(x, out=None): """ Test element-wise for negative infinity, return result as sparse `bool` array. Parameters ---------- x Input out, optional Output array Examples -------- >>> import sparse >>> x = sparse.as_coo(np.array([-np.inf])) >>> sparse.isneginf(x).todense() array([ True]) See Also -------- [`numpy.isneginf`][] : The NumPy equivalent """ from sparse import elemwise return elemwise(lambda x, out=None, dtype=None: np.isneginf(x, out=out), x, out=out) def result_type(*arrays_and_dtypes): """Returns the type that results from applying the NumPy type promotion rules to the arguments. See Also -------- [`numpy.result_type`][] : The NumPy equivalent """ return np.result_type(*(_as_result_type_arg(x) for x in arrays_and_dtypes)) def _as_result_type_arg(x): if not isinstance(x, SparseArray): return x if x.ndim > 0: return x.dtype # 0-dimensional arrays give different result_type outputs than their dtypes return x.todense() @numba.jit(nopython=True, nogil=True) def _diagonal_idx(coordlist, axis1, axis2, offset): """ Utility function that returns all indices that correspond to a diagonal element. Parameters ---------- coordlist : list of lists Coordinate indices. axis1, axis2 : int The axes of the diagonal. offset : int Offset of the diagonal from the main diagonal. Defaults to main diagonal (0). """ return np.array([i for i in range(len(coordlist[axis1])) if coordlist[axis1][i] + offset == coordlist[axis2][i]]) def clip(a, min=None, max=None, out=None): """ Clip (limit) the values in the array. Return an array whose values are limited to ``[min, max]``. One of min or max must be given. Parameters ---------- a min : scalar or `SparseArray` or `None` Minimum value. If `None`, clipping is not performed on lower interval edge. max : scalar or `SparseArray` or `None` Maximum value. If `None`, clipping is not performed on upper interval edge. out : SparseArray, optional If provided, the results will be placed in this array. It may be the input array for in-place clipping. `out` must be of the right shape to hold the output. Its type is preserved. Returns ------- clipped_array : SparseArray An array with the elements of `self`, but where values < `min` are replaced with `min`, and those > `max` with `max`. Examples -------- >>> import sparse >>> x = sparse.COO.from_numpy([0, 0, 0, 1, 2, 3]) >>> sparse.clip(x, min=1).todense() # doctest: +NORMALIZE_WHITESPACE array([1, 1, 1, 1, 2, 3]) >>> sparse.clip(x, max=1).todense() # doctest: +NORMALIZE_WHITESPACE array([0, 0, 0, 1, 1, 1]) >>> sparse.clip(x, min=1, max=2).todense() # doctest: +NORMALIZE_WHITESPACE array([1, 1, 1, 1, 2, 2]) See Also -------- numpy.clip : Equivalent NumPy function """ a = asCOO(a, name="clip") return a.clip(min, max) def expand_dims(x, /, *, axis=0): """ Expands the shape of an array by inserting a new axis (dimension) of size one at the position specified by ``axis``. Parameters ---------- a : COO Input COO array. axis : int Position in the expanded axes where the new axis is placed. Returns ------- result : COO An expanded output COO array having the same data type as ``x``. Examples -------- >>> import sparse >>> x = sparse.COO.from_numpy([[1, 0, 0, 0, 2, -3]]) >>> x.shape (1, 6) >>> y1 = sparse.expand_dims(x, axis=1) >>> y1.shape (1, 1, 6) >>> y2 = sparse.expand_dims(x, axis=2) >>> y2.shape (1, 6, 1) """ x = _validate_coo_input(x) if not isinstance(axis, int): raise IndexError(f"Invalid axis position: {axis}") axis = normalize_axis(axis, x.ndim + 1) new_coords = np.insert(x.coords, obj=axis, values=np.zeros(x.nnz, dtype=np.intp), axis=0) new_shape = list(x.shape) new_shape.insert(axis, 1) new_shape = tuple(new_shape) from .core import COO return COO( new_coords, x.data, shape=new_shape, fill_value=x.fill_value, ) def flip(x, /, *, axis=None): """ Reverses the order of elements in an array along the given axis. The shape of the array is preserved. Parameters ---------- a : COO Input COO array. axis : int or tuple of ints, optional Axis (or axes) along which to flip. If ``axis`` is ``None``, the function must flip all input array axes. If ``axis`` is negative, the function must count from the last dimension. If provided more than one axis, the function must flip only the specified axes. Default: ``None``. Returns ------- result : COO An output array having the same data type and shape as ``x`` and whose elements, relative to ``x``, are reordered. """ x = _validate_coo_input(x) if axis is None: axis = range(x.ndim) if not isinstance(axis, Iterable): axis = (axis,) new_coords = x.coords.copy() for ax in axis: new_coords[ax, :] = x.shape[ax] - 1 - x.coords[ax, :] from .core import COO return COO( new_coords, x.data, shape=x.shape, fill_value=x.fill_value, ) # Array API set functions class UniqueCountsResult(NamedTuple): values: np.ndarray counts: np.ndarray def unique_counts(x, /): """ Returns the unique elements of an input array `x`, and the corresponding counts for each unique element in `x`. Parameters ---------- x : COO Input COO array. It will be flattened if it is not already 1-D. Returns ------- out : namedtuple The result containing: * values - The unique elements of an input array. * counts - The corresponding counts for each unique element. Raises ------ ValueError If the input array is in a different format than COO. Examples -------- >>> import sparse >>> x = sparse.COO.from_numpy([1, 0, 2, 1, 2, -3]) >>> sparse.unique_counts(x) UniqueCountsResult(values=array([-3, 0, 1, 2]), counts=array([1, 1, 2, 2])) """ x = _validate_coo_input(x) x = x.flatten() values, counts = np.unique(x.data, return_counts=True) if x.nnz < x.size: values = np.concatenate([[x.fill_value], values]) counts = np.concatenate([[x.size - x.nnz], counts]) sorted_indices = np.argsort(values) values[sorted_indices] = values.copy() counts[sorted_indices] = counts.copy() return UniqueCountsResult(values, counts) def unique_values(x, /): """ Returns the unique elements of an input array `x`. Parameters ---------- x : COO Input COO array. It will be flattened if it is not already 1-D. Returns ------- out : ndarray The unique elements of an input array. Raises ------ ValueError If the input array is in a different format than COO. Examples -------- >>> import sparse >>> x = sparse.COO.from_numpy([1, 0, 2, 1, 2, -3]) >>> sparse.unique_values(x) array([-3, 0, 1, 2]) """ x = _validate_coo_input(x) x = x.flatten() values = np.unique(x.data) if x.nnz < x.size: values = np.sort(np.concatenate([[x.fill_value], values])) return values def sort(x, /, *, axis=-1, descending=False, stable=False): """ Returns a sorted copy of an input array ``x``. Parameters ---------- x : SparseArray Input array. Should have a real-valued data type. axis : int Axis along which to sort. If set to ``-1``, the function must sort along the last axis. Default: ``-1``. descending : bool Sort order. If ``True``, the array must be sorted in descending order (by value). If ``False``, the array must be sorted in ascending order (by value). Default: ``False``. stable : bool Whether the sort is stable. Only ``False`` is supported currently. Returns ------- out : COO A sorted array. Raises ------ ValueError If the input array isn't and can't be converted to COO format. Examples -------- >>> import sparse >>> x = sparse.COO.from_numpy([1, 0, 2, 0, 2, -3]) >>> sparse.sort(x).todense() array([-3, 0, 0, 1, 2, 2]) >>> sparse.sort(x, descending=True).todense() array([ 2, 2, 1, 0, 0, -3]) """ from .._common import moveaxis from .core import COO x = _validate_coo_input(x) if stable: raise ValueError("`stable=True` isn't currently supported.") original_ndim = x.ndim if x.ndim == 1: x = x[None, :] axis = -1 x = moveaxis(x, source=axis, destination=-1) x_shape = x.shape x = x.reshape((-1, x_shape[-1])) new_coords, new_data = _sort_coo(x.coords, x.data, x.fill_value, sort_axis_len=x_shape[-1], descending=descending) x = COO(new_coords, new_data, x.shape, has_duplicates=False, sorted=True, fill_value=x.fill_value) x = x.reshape(x_shape[:-1] + (x_shape[-1],)) x = moveaxis(x, source=-1, destination=axis) if original_ndim == x.ndim: return x x = x.squeeze() if x.shape == (): return x[None] return x def take(x, indices, /, *, axis=None): """ Returns elements of an array along an axis. Parameters ---------- x : SparseArray Input array. indices : ndarray Array indices. The array must be one-dimensional and have an integer data type. axis : int Axis over which to select values. If ``axis`` is negative, the function must determine the axis along which to select values by counting from the last dimension. For ``None``, the flattened input array is used. Default: ``None``. Returns ------- out : COO A COO array with requested indices. Raises ------ ValueError If the input array isn't and can't be converted to COO format. """ x = _validate_coo_input(x) if axis is None: x = x.flatten() return x[indices] axis = normalize_axis(axis, x.ndim) full_index = (slice(None),) * axis + (indices, ...) return x[full_index] def _validate_coo_input(x: Any): from .._common import _is_scipy_sparse_obj from .core import COO if _is_scipy_sparse_obj(x): x = COO.from_scipy_sparse(x) elif not isinstance(x, SparseArray): raise ValueError(f"Input must be an instance of SparseArray, but it's {type(x)}.") elif not isinstance(x, COO): x = x.asformat(COO) return x @numba.jit(nopython=True, nogil=True) def _sort_coo( coords: np.ndarray, data: np.ndarray, fill_value: float, sort_axis_len: int, descending: bool ) -> tuple[np.ndarray, np.ndarray]: assert coords.shape[0] == 2 group_coords = coords[0, :] sort_coords = coords[1, :] data = data.copy() result_indices = np.empty_like(sort_coords) # We iterate through all groups and sort each one of them. # first and last index of a group is tracked. prev_group = -1 group_first_idx = -1 group_last_idx = -1 # We add `-1` sentinel to know when the last group ends for idx, group in enumerate(np.append(group_coords, -1)): if group == prev_group: continue if prev_group != -1: group_last_idx = idx group_slice = slice(group_first_idx, group_last_idx) group_size = group_last_idx - group_first_idx # SORT VALUES if group_size > 1: # np.sort in numba doesn't support `np.sort`'s arguments so `stable` # keyword can't be supported. # https://numba.pydata.org/numba-doc/latest/reference/numpysupported.html#other-methods data[group_slice] = np.sort(data[group_slice]) if descending: data[group_slice] = data[group_slice][::-1] # SORT INDICES fill_value_count = sort_axis_len - group_size indices = np.arange(group_size) # find a place where fill_value would be for pos in range(group_size): if (not descending and fill_value < data[group_slice][pos]) or ( descending and fill_value > data[group_slice][pos] ): indices[pos:] += fill_value_count break result_indices[group_first_idx:group_last_idx] = indices prev_group = group group_first_idx = idx return np.vstack((group_coords, result_indices)), data @numba.jit(nopython=True, nogil=True) def _compute_minmax_args( coords: np.ndarray, data: np.ndarray, reduce_size: int, fill_value: float, max_mode_flag: bool, ) -> tuple[np.ndarray, np.ndarray]: assert coords.shape[0] == 2 reduce_coords = coords[0, :] index_coords = coords[1, :] result_indices = np.unique(index_coords) result_data = [] # we iterate through each trace for result_index in np.nditer(result_indices): mask = index_coords == result_index masked_reduce_coords = reduce_coords[mask] masked_data = data[mask] compared_data = operator.gt(masked_data, fill_value) if max_mode_flag else operator.lt(masked_data, fill_value) if np.any(compared_data) or len(masked_data) == reduce_size: # best value is a non-fill value best_arg = np.argmax(masked_data) if max_mode_flag else np.argmin(masked_data) result_data.append(masked_reduce_coords[best_arg]) else: # best value is a fill value, find the first occurrence of it current_coord = np.array(-1, dtype=coords.dtype) found = False for idx, new_coord in enumerate(np.nditer(np.sort(masked_reduce_coords))): # there is at least one fill value between consecutive non-fill values if new_coord - current_coord > 1: result_data.append(idx) found = True break current_coord = new_coord # get the first fill value after all non-fill values if not found: result_data.append(current_coord + 1) return (result_indices, np.array(result_data, dtype=np.intp)) def _arg_minmax_common( x: SparseArray, axis: int | None, keepdims: bool, mode: str, ): """ Internal implementation for argmax and argmin functions. """ assert mode in ("max", "min") max_mode_flag = mode == "max" x = _validate_coo_input(x) if not isinstance(axis, int | type(None)): raise ValueError(f"`axis` must be `int` or `None`, but it's: {type(axis)}.") if isinstance(axis, int) and axis >= x.ndim: raise ValueError(f"`axis={axis}` is out of bounds for array of dimension {x.ndim}.") if x.ndim == 0: raise ValueError("Input array must be at least 1-D, but it's 0-D.") # If `axis` is None then we need to flatten the input array and memorize # the original dimensionality for the final reshape operation. axis_none_original_ndim: int | None = None if axis is None: axis_none_original_ndim = x.ndim x = x.reshape(-1)[:, None] axis = 0 # A 1-D array must have one more singleton dimension. if axis == 0 and x.ndim == 1: x = x[:, None] # We need to move `axis` to the front. new_transpose = list(range(x.ndim)) new_transpose.insert(0, new_transpose.pop(axis)) new_transpose = tuple(new_transpose) # And reshape it to 2-D (reduce axis, the rest of axes flattened) new_shape = list(x.shape) new_shape.insert(0, new_shape.pop(axis)) new_shape = tuple(new_shape) x = x.transpose(new_transpose) x = x.reshape((new_shape[0], np.prod(new_shape[1:]))) # Compute max/min arguments result_indices, result_data = _compute_minmax_args( x.coords.copy(), x.data.copy(), reduce_size=x.shape[0], fill_value=x.fill_value, max_mode_flag=max_mode_flag, ) from .core import COO result = COO(result_indices, result_data, shape=(x.shape[1],), fill_value=0, prune=True) # Let's reshape the result to the original shape. result = result.reshape((1, *new_shape[1:])) new_transpose = list(range(result.ndim)) new_transpose.insert(axis, new_transpose.pop(0)) result = result.transpose(new_transpose) # If `axis=None` we need to reshape flattened array into original dimensionality. if axis_none_original_ndim is not None: result = result.reshape([1 for _ in range(axis_none_original_ndim)]) return result if keepdims else result.squeeze() def matrix_transpose(x, /): """ Transposes a matrix or a stack of matrices. Parameters ---------- x : SparseArray Input array. Returns ------- out : COO Transposed COO array. Raises ------ ValueError If the input array isn't and can't be converted to COO format, or if ``x.ndim < 2``. """ if hasattr(x, "ndim") and x.ndim < 2: raise ValueError("`x.ndim >= 2` must hold.") x = _validate_coo_input(x) transpose_axes = list(range(x.ndim)) transpose_axes[-2:] = transpose_axes[-2:][::-1] return x.transpose(transpose_axes) sparse-0.17.0/sparse/numba_backend/_coo/core.py000066400000000000000000001436551501262445000214000ustar00rootroot00000000000000import copy as _copy import operator import warnings from collections import defaultdict, deque from collections.abc import Iterable, Iterator, Sized from functools import reduce import numba import numpy as np from numpy.lib.mixins import NDArrayOperatorsMixin from .._sparse_array import SparseArray from .._umath import broadcast_to from .._utils import ( _zero_of_dtype, can_store, check_fill_value, check_zero_fill_value, equivalent, normalize_axis, ) from .indexing import getitem class COO(SparseArray, NDArrayOperatorsMixin): # lgtm [py/missing-equals] """ A sparse multidimensional array. This is stored in COO format. It depends on NumPy and Scipy.sparse for computation, but supports arrays of arbitrary dimension. Parameters ---------- coords : numpy.ndarray (COO.ndim, COO.nnz) An array holding the index locations of every value Should have shape (number of dimensions, number of non-zeros). data : numpy.ndarray (COO.nnz,) An array of Values. A scalar can also be supplied if the data is the same across all coordinates. If not given, defers to [`sparse.as_coo`][]. shape : tuple[int] (COO.ndim,) The shape of the array. has_duplicates : bool, optional A value indicating whether the supplied value for [`sparse.COO.coords`][] has duplicates. Note that setting this to `False` when `coords` does have duplicates may result in undefined behaviour. sorted : bool, optional A value indicating whether the values in `coords` are sorted. Note that setting this to `True` when [`sparse.COO.coords`][] isn't sorted may result in undefined behaviour. prune : bool, optional A flag indicating whether or not we should prune any fill-values present in `data`. cache : bool, optional Whether to enable cacheing for various operations. See [`sparse.COO.enable_caching`][]. fill_value: scalar, optional The fill value for this array. Attributes ---------- coords : numpy.ndarray (ndim, nnz) An array holding the coordinates of every nonzero element. data : numpy.ndarray (nnz,) An array holding the values corresponding to [`sparse.COO.coords`][]. shape : tuple[int] (ndim,) The dimensions of this array. See Also -------- - [`sparse.DOK`][]: A mostly write-only sparse array. - [`sparse.as_coo`][]: Convert any given format to [`sparse.COO`][]. Examples -------- You can create [`sparse.COO`][] objects from Numpy arrays. >>> x = np.eye(4, dtype=np.uint8) >>> x[2, 3] = 5 >>> s = COO.from_numpy(x) >>> s >>> s.data # doctest: +NORMALIZE_WHITESPACE array([1, 1, 1, 5, 1], dtype=uint8) >>> s.coords # doctest: +NORMALIZE_WHITESPACE array([[0, 1, 2, 2, 3], [0, 1, 2, 3, 3]]) [`sparse.COO`][] objects support basic arithmetic and binary operations. >>> x2 = np.eye(4, dtype=np.uint8) >>> x2[3, 2] = 5 >>> s2 = COO.from_numpy(x2) >>> (s + s2).todense() # doctest: +NORMALIZE_WHITESPACE array([[2, 0, 0, 0], [0, 2, 0, 0], [0, 0, 2, 5], [0, 0, 5, 2]], dtype=uint8) >>> (s * s2).todense() # doctest: +NORMALIZE_WHITESPACE array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], dtype=uint8) Binary operations support broadcasting. >>> x3 = np.zeros((4, 1), dtype=np.uint8) >>> x3[2, 0] = 1 >>> s3 = COO.from_numpy(x3) >>> (s * s3).todense() # doctest: +NORMALIZE_WHITESPACE array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 5], [0, 0, 0, 0]], dtype=uint8) [`sparse.COO`][] objects also support dot products and reductions. >>> s.dot(s.T).sum(axis=0).todense() # doctest: +NORMALIZE_WHITESPACE array([ 1, 1, 31, 6], dtype=uint64) You can use Numpy `ufunc` operations on [`sparse.COO`][] arrays as well. >>> np.sum(s, axis=1).todense() # doctest: +NORMALIZE_WHITESPACE array([1, 1, 6, 1], dtype=uint64) >>> np.round(np.sqrt(s, dtype=np.float64), decimals=1).todense() # doctest: +SKIP array([[ 1. , 0. , 0. , 0. ], [ 0. , 1. , 0. , 0. ], [ 0. , 0. , 1. , 2.2], [ 0. , 0. , 0. , 1. ]]) Operations that will result in a dense array will usually result in a different fill value, such as the following. >>> np.exp(s) You can also create [`sparse.COO`][] arrays from coordinates and data. >>> coords = [[0, 0, 0, 1, 1], [0, 1, 2, 0, 3], [0, 3, 2, 0, 1]] >>> data = [1, 2, 3, 4, 5] >>> s4 = COO(coords, data, shape=(3, 4, 5)) >>> s4 If the data is same across all coordinates, you can also specify a scalar. >>> coords = [[0, 0, 0, 1, 1], [0, 1, 2, 0, 3], [0, 3, 2, 0, 1]] >>> data = 1 >>> s5 = COO(coords, data, shape=(3, 4, 5)) >>> s5 Following scipy.sparse conventions you can also pass these as a tuple with rows and columns >>> rows = [0, 1, 2, 3, 4] >>> cols = [0, 0, 0, 1, 1] >>> data = [10, 20, 30, 40, 50] >>> z = COO((data, (rows, cols)), shape=(5, 2)) >>> z.todense() # doctest: +NORMALIZE_WHITESPACE array([[10, 0], [20, 0], [30, 0], [ 0, 40], [ 0, 50]]) You can also pass a dictionary or iterable of index/value pairs. Repeated indices imply summation: >>> d = {(0, 0, 0): 1, (1, 2, 3): 2, (1, 1, 0): 3} >>> COO(d, shape=(2, 3, 4)) >>> L = [((0, 0), 1), ((1, 1), 2), ((0, 0), 3)] >>> COO(L, shape=(2, 2)).todense() # doctest: +NORMALIZE_WHITESPACE array([[4, 0], [0, 2]]) You can convert [`sparse.DOK`][] arrays to [`sparse.COO`][] arrays. >>> from sparse import DOK >>> s6 = DOK((5, 5), dtype=np.int64) >>> s6[1:3, 1:3] = [[4, 5], [6, 7]] >>> s6 >>> s7 = s6.asformat("coo") >>> s7 >>> s7.todense() # doctest: +NORMALIZE_WHITESPACE array([[0, 0, 0, 0, 0], [0, 4, 5, 0, 0], [0, 6, 7, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) """ __array_priority__ = 12 def __init__( self, coords, data=None, shape=None, has_duplicates=True, sorted=False, prune=False, cache=False, fill_value=None, idx_dtype=None, ): if isinstance(coords, COO): self._make_shallow_copy_of(coords) if data is not None or shape is not None: raise ValueError("If `coords` is `COO`, then no other arguments should be provided.") if fill_value is not None: self.fill_value = self.data.dtype.type(fill_value) return self._cache = None if cache: self.enable_caching() if data is None: arr = as_coo(coords, shape=shape, fill_value=fill_value, idx_dtype=idx_dtype) self._make_shallow_copy_of(arr) if cache: self.enable_caching() return self.data = np.asarray(data) self.coords = np.asarray(coords) if self.coords.ndim == 1: if self.coords.size == 0 and shape is not None: self.coords = self.coords.reshape((len(shape), len(data))) else: self.coords = self.coords[None, :] if self.data.ndim == 0: self.data = np.broadcast_to(self.data, self.coords.shape[1]) if self.data.ndim != 1: raise ValueError("`data` must be a scalar or 1-dimensional.") if shape is None: raise ValueError("`shape` was not provided.") if not isinstance(shape, Iterable): shape = (shape,) if isinstance(shape, np.ndarray): shape = tuple(shape) if shape and not self.coords.size: self.coords = np.zeros((len(shape) if isinstance(shape, Iterable) else 1, 0), dtype=np.intp) super().__init__(shape, fill_value=fill_value) if idx_dtype: if not can_store(idx_dtype, max(shape)): raise ValueError(f"cannot cast array with shape {shape} to dtype {idx_dtype}.") self.coords = self.coords.astype(idx_dtype) if self.shape: if len(self.data) != self.coords.shape[1]: msg = "The data length does not match the coordinates given.\nlen(data) = {}, but {} coords specified." raise ValueError(msg.format(len(data), self.coords.shape[1])) if len(self.shape) != self.coords.shape[0]: msg = ( "Shape specified by `shape` doesn't match the " "shape of `coords`; len(shape)={} != coords.shape[0]={}" "(and coords.shape={})" ) raise ValueError(msg.format(len(shape), self.coords.shape[0], self.coords.shape)) from .._settings import WARN_ON_TOO_DENSE if WARN_ON_TOO_DENSE and self.nbytes >= self.size * self.data.itemsize: warnings.warn( "Attempting to create a sparse array that takes no less " "memory than than an equivalent dense array. You may want to " "use a dense array here instead.", RuntimeWarning, stacklevel=1, ) if not sorted: self._sort_indices() if has_duplicates: self._sum_duplicates() if prune: self._prune() def __getstate__(self): return (self.coords, self.data, self.shape, self.fill_value) def __setstate__(self, state): self.coords, self.data, self.shape, self.fill_value = state self._cache = None def __dask_tokenize__(self): "Produce a deterministic, content-based hash for dask." from dask.base import normalize_token return normalize_token((type(self), self.coords, self.data, self.shape, self.fill_value)) def copy(self, deep=True): """Return a copy of the array. Parameters ---------- deep : boolean, optional If True (default), the internal coords and data arrays are also copied. Set to ``False`` to only make a shallow copy. """ return _copy.deepcopy(self) if deep else _copy.copy(self) def enable_caching(self): """Enable caching of reshape, transpose, and tocsr/csc operations This enables efficient iterative workflows that make heavy use of csr/csc operations, such as tensordot. This maintains a cache of recent results of reshape and transpose so that operations like tensordot (which uses both internally) store efficiently stored representations for repeated use. This can significantly cut down on computational costs in common numeric algorithms. However, this also assumes that neither this object, nor the downstream objects will have their data mutated. Examples -------- >>> s.enable_caching() # doctest: +SKIP >>> csr1 = s.transpose((2, 0, 1)).reshape((100, 120)).tocsr() # doctest: +SKIP >>> csr2 = s.transpose((2, 0, 1)).reshape((100, 120)).tocsr() # doctest: +SKIP >>> csr1 is csr2 # doctest: +SKIP True """ self._cache = defaultdict(lambda: deque(maxlen=3)) @classmethod def from_numpy(cls, x, fill_value=None, idx_dtype=None): """ Convert the given [`sparse.COO`][] object. Parameters ---------- x : np.ndarray The dense array to convert. fill_value : scalar The fill value of the constructed [`sparse.COO`][] array. Zero if unspecified. Returns ------- COO The converted COO array. Examples -------- >>> x = np.eye(5) >>> s = COO.from_numpy(x) >>> s >>> x[x == 0] = np.nan >>> COO.from_numpy(x, fill_value=np.nan) """ x = np.asanyarray(x).view(type=np.ndarray) if fill_value is None: fill_value = _zero_of_dtype(x.dtype) if x.shape else x coords = np.atleast_2d(np.flatnonzero(~equivalent(x, fill_value))) data = x.ravel()[tuple(coords)] return cls( coords, data, shape=x.size, has_duplicates=False, sorted=True, fill_value=fill_value, idx_dtype=idx_dtype, ).reshape(x.shape) def todense(self): """ Convert this [`sparse.COO`][] array to a dense [`numpy.ndarray`][]. Note that this may take a large amount of memory if the `COO` object's `shape` is large. Returns ------- numpy.ndarray The converted dense array. See Also -------- - [`sparse.DOK.todense`][] : Equivalent `DOK` array method. - [`scipy.sparse.coo_matrix.todense`][] : Equivalent Scipy method. Examples -------- >>> x = np.random.randint(100, size=(7, 3)) >>> s = COO.from_numpy(x) >>> x2 = s.todense() >>> np.array_equal(x, x2) True """ x = np.full(self.shape, self.fill_value, self.dtype) coords = tuple([self.coords[i, :] for i in range(self.ndim)]) data = self.data if len(coords) != 0: x[coords] = data else: if len(data) != 0: assert data.shape == (1,) x[...] = data[0] return x @classmethod def from_scipy_sparse(cls, x, /, *, fill_value=None): """ Construct a [`sparse.COO`][] array from a [`scipy.sparse.spmatrix`][] Parameters ---------- x : scipy.sparse.spmatrix The sparse matrix to construct the array from. fill_value : scalar The fill-value to use when converting. Returns ------- COO The converted [`sparse.COO`][] object. Examples -------- >>> import scipy.sparse >>> x = scipy.sparse.rand(6, 3, density=0.2) >>> s = COO.from_scipy_sparse(x) >>> np.array_equal(x.todense(), s.todense()) True """ x = x.asformat("coo") if not x.has_canonical_format: x.eliminate_zeros() x.sum_duplicates() coords = np.empty((2, x.nnz), dtype=x.row.dtype) coords[0, :] = x.row coords[1, :] = x.col return COO( coords, x.data, shape=x.shape, has_duplicates=not x.has_canonical_format, sorted=x.has_canonical_format, fill_value=fill_value, ) @classmethod def from_iter(cls, x, shape, fill_value=None, dtype=None): """ Converts an iterable in certain formats to a [`sparse.COO`][] array. See examples for details. Parameters ---------- x : Iterable or Iterator The iterable to convert to [`sparse.COO`][]. shape : tuple[int] The shape of the array. fill_value : scalar The fill value for this array. dtype : numpy.dtype The dtype of the input array. Inferred from the input if not given. Returns ------- out : COO The output [`sparse.COO`][] array. Examples -------- You can convert items of the format [`sparse.COO`][]. Here, the first part represents the coordinate and the second part represents the value. >>> x = [((0, 0), 1), ((1, 1), 1)] >>> s = COO.from_iter(x, shape=(2, 2)) >>> s.todense() array([[1, 0], [0, 1]]) You can also have a similar format with a dictionary. >>> x = {(0, 0): 1, (1, 1): 1} >>> s = COO.from_iter(x, shape=(2, 2)) >>> s.todense() array([[1, 0], [0, 1]]) The third supported format is ``(data, (..., row, col))``. >>> x = ([1, 1], ([0, 1], [0, 1])) >>> s = COO.from_iter(x, shape=(2, 2)) >>> s.todense() array([[1, 0], [0, 1]]) You can also pass in a [`collections.abc.Iterator`][] object. >>> x = [((0, 0), 1), ((1, 1), 1)].__iter__() >>> s = COO.from_iter(x, shape=(2, 2)) >>> s.todense() array([[1, 0], [0, 1]]) """ if isinstance(x, dict): x = list(x.items()) if not isinstance(x, Sized): x = list(x) if len(x) != 2 and not all(len(item) == 2 for item in x): raise ValueError("Invalid iterable to convert to COO.") if not x: ndim = 0 if shape is None else len(shape) coords = np.empty((ndim, 0), dtype=np.uint8) data = np.empty((0,), dtype=dtype) shape = () if shape is None else shape elif not isinstance(x[0][0], Iterable): coords = np.stack(x[1], axis=0) data = np.asarray(x[0], dtype=dtype) else: coords = np.array([item[0] for item in x]).T data = np.array([item[1] for item in x], dtype=dtype) if not ( coords.ndim == 2 and data.ndim == 1 and np.issubdtype(coords.dtype, np.integer) and np.all(coords >= 0) ): raise ValueError("Invalid iterable to convert to COO.") return COO(coords, data, shape=shape, fill_value=fill_value) @property def dtype(self): """ The datatype of this array. Returns ------- numpy.dtype The datatype of this array. See Also -------- - [`numpy.ndarray.dtype`][] : Numpy equivalent property. - [`scipy.sparse.coo_matrix.dtype`][] : Scipy equivalent property. Examples -------- >>> x = (200 * np.random.rand(5, 4)).astype(np.int32) >>> s = COO.from_numpy(x) >>> s.dtype dtype('int32') >>> x.dtype == s.dtype True """ return self.data.dtype @property def nnz(self): """ The number of nonzero elements in this array. Note that any duplicates in `coords` are counted multiple times. Returns ------- int The number of nonzero elements in this array. See Also -------- - [`sparse.DOK.nnz`][] : Equivalent [`sparse.DOK`][] array property. - [`numpy.count_nonzero`][] : A similar Numpy function. - [`scipy.sparse.coo_matrix.nnz`][] : The Scipy equivalent property. Examples -------- >>> x = np.array([0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 0]) >>> np.count_nonzero(x) 6 >>> s = COO.from_numpy(x) >>> s.nnz 6 >>> np.count_nonzero(x) == s.nnz True """ return self.coords.shape[1] @property def format(self): """ The storage format of this array. Returns ------- str The storage format of this array. See Also -------- [`scipy.sparse.dok_matrix.format`][] : The Scipy equivalent property. Examples ------- >>> import sparse >>> s = sparse.random((5, 5), density=0.2, format="dok") >>> s.format 'dok' >>> t = sparse.random((5, 5), density=0.2, format="coo") >>> t.format 'coo' """ return "coo" @property def nbytes(self): """ The number of bytes taken up by this object. Note that for small arrays, this may undercount the number of bytes due to the large constant overhead. Returns ------- int The approximate bytes of memory taken by this object. See Also -------- [`numpy.ndarray.nbytes`][] : The equivalent Numpy property. Examples -------- >>> data = np.arange(6, dtype=np.uint8) >>> coords = np.random.randint(1000, size=(3, 6), dtype=np.uint16) >>> s = COO(coords, data, shape=(1000, 1000, 1000)) >>> s.nbytes 42 """ return self.data.nbytes + self.coords.nbytes def __len__(self): """ Get "length" of array, which is by definition the size of the first dimension. Returns ------- int The size of the first dimension. See Also -------- numpy.ndarray.__len__ : Numpy equivalent property. Examples -------- >>> x = np.zeros((10, 10)) >>> s = COO.from_numpy(x) >>> len(s) 10 """ return self.shape[0] def __sizeof__(self): return self.nbytes __getitem__ = getitem def __str__(self): summary = f"" return self._str_impl(summary) __repr__ = __str__ def _reduce_calc(self, method, axis, keepdims=False, **kwargs): if axis == (None,): axis = tuple(range(self.ndim)) axis = tuple(a if a >= 0 else a + self.ndim for a in axis) neg_axis = tuple(ax for ax in range(self.ndim) if ax not in set(axis)) a = self.transpose(neg_axis + axis) a = a.reshape( ( np.prod([self.shape[d] for d in neg_axis], dtype=np.intp), np.prod([self.shape[d] for d in axis], dtype=np.intp), ) ) data, inv_idx, counts = _grouped_reduce(a.data, a.coords[0], method, **kwargs) n_cols = a.shape[1] arr_attrs = (a, neg_axis, inv_idx) return (data, counts, axis, n_cols, arr_attrs) def _reduce_return(self, data, arr_attrs, result_fill_value): a, neg_axis, inv_idx = arr_attrs coords = a.coords[0:1, inv_idx] out = COO( coords, data, shape=(a.shape[0],), has_duplicates=False, sorted=True, prune=True, fill_value=result_fill_value, ) return out.reshape(tuple(self.shape[d] for d in neg_axis)) def transpose(self, axes=None): """ Returns a new array which has the order of the axes switched. Parameters ---------- axes : Iterable[int], optional The new order of the axes compared to the previous one. Reverses the axes by default. Returns ------- COO The new array with the axes in the desired order. See Also -------- - [`sparse.COO.T`][] : A quick property to reverse the order of the axes. - [`numpy.ndarray.transpose`][] : Numpy equivalent function. Examples -------- We can change the order of the dimensions of any [`sparse.COO`][] array with this function. >>> x = np.add.outer(np.arange(5), np.arange(5)[::-1]) >>> x # doctest: +NORMALIZE_WHITESPACE array([[4, 3, 2, 1, 0], [5, 4, 3, 2, 1], [6, 5, 4, 3, 2], [7, 6, 5, 4, 3], [8, 7, 6, 5, 4]]) >>> s = COO.from_numpy(x) >>> s.transpose((1, 0)).todense() # doctest: +NORMALIZE_WHITESPACE array([[4, 5, 6, 7, 8], [3, 4, 5, 6, 7], [2, 3, 4, 5, 6], [1, 2, 3, 4, 5], [0, 1, 2, 3, 4]]) Note that by default, this reverses the order of the axes rather than switching the last and second-to-last axes as required by some linear algebra operations. >>> x = np.random.rand(2, 3, 4) >>> s = COO.from_numpy(x) >>> s.transpose().shape (4, 3, 2) """ if axes is None: axes = list(reversed(range(self.ndim))) # Normalize all axes indices to positive values axes = normalize_axis(axes, self.ndim) if len(np.unique(axes)) < len(axes): raise ValueError("repeated axis in transpose") if not len(axes) == self.ndim: raise ValueError("axes don't match array") axes = tuple(axes) if axes == tuple(range(self.ndim)): return self if self._cache is not None: for ax, value in self._cache["transpose"]: if ax == axes: return value shape = tuple(self.shape[ax] for ax in axes) result = COO( self.coords[axes, :], self.data, shape, has_duplicates=False, cache=self._cache is not None, fill_value=self.fill_value, ) if self._cache is not None: self._cache["transpose"].append((axes, result)) return result @property def T(self): """ Returns a new array which has the order of the axes reversed. Returns ------- COO The new array with the axes in the desired order. See Also -------- - [`sparse.COO.transpose`][] : A method where you can specify the order of the axes. - [`numpy.ndarray.T`][] : Numpy equivalent property. Examples -------- We can change the order of the dimensions of any [`sparse.COO`][] array with this function. >>> x = np.add.outer(np.arange(5), np.arange(5)[::-1]) >>> x # doctest: +NORMALIZE_WHITESPACE array([[4, 3, 2, 1, 0], [5, 4, 3, 2, 1], [6, 5, 4, 3, 2], [7, 6, 5, 4, 3], [8, 7, 6, 5, 4]]) >>> s = COO.from_numpy(x) >>> s.T.todense() # doctest: +NORMALIZE_WHITESPACE array([[4, 5, 6, 7, 8], [3, 4, 5, 6, 7], [2, 3, 4, 5, 6], [1, 2, 3, 4, 5], [0, 1, 2, 3, 4]]) Note that by default, this reverses the order of the axes rather than switching the last and second-to-last axes as required by some linear algebra operations. >>> x = np.random.rand(2, 3, 4) >>> s = COO.from_numpy(x) >>> s.T.shape (4, 3, 2) """ return self.transpose(tuple(range(self.ndim))[::-1]) @property def mT(self): """ Transpose of a matrix (or a stack of matrices). If an array instance has fewer than two dimensions, an error should be raised. Returns ------- COO array whose last two dimensions (axes) are permuted in reverse order relative to original array (i.e., for an array instance having shape (..., M, N), the returned array must have shape (..., N, M)). The returned array must have the same data type as the original array. See Also -------- - [`sparse.COO.transpose`][] : A method where you can specify the order of the axes. - [`numpy.ndarray.mT`][] : Numpy equivalent property. Examples -------- >>> x = np.arange(8).reshape((2, 2, 2)) >>> x # doctest: +NORMALIZE_WHITESPACE array([[[0, 1], [2, 3]], [[4, 5], [6, 7]]]) >>> s = COO.from_numpy(x) >>> s.mT.todense() # doctest: +NORMALIZE_WHITESPACE array([[[0, 2], [1, 3]], [[4, 6], [5, 7]]]) """ if self.ndim < 2: raise ValueError("Cannot compute matrix transpose if `ndim < 2`.") axis = list(range(self.ndim)) axis[-1], axis[-2] = axis[-2], axis[-1] return self.transpose(axis) def swapaxes(self, axis1, axis2): """Returns array that has axes axis1 and axis2 swapped. Parameters ---------- axis1 : int first axis to swap axis2 : int second axis to swap Returns ------- COO The new array with the axes axis1 and axis2 swapped. Examples -------- >>> x = COO.from_numpy(np.ones((2, 3, 4))) >>> x.swapaxes(0, 2) """ # Normalize all axis1, axis2 to positive values axis1, axis2 = normalize_axis((axis1, axis2), self.ndim) # checks if axis1,2 are in range + raises ValueError axes = list(range(self.ndim)) axes[axis1], axes[axis2] = axes[axis2], axes[axis1] return self.transpose(axes) def dot(self, other): """ Performs the equivalent of `x.dot(y)` for [`sparse.COO`][]. Parameters ---------- other : Union[COO, numpy.ndarray, scipy.sparse.spmatrix] The second operand of the dot product operation. Returns ------- {COO, numpy.ndarray} The result of the dot product. If the result turns out to be dense, then a dense array is returned, otherwise, a sparse array. Raises ------ ValueError If all arguments don't have zero fill-values. See Also -------- - [`sparse.dot`][] : Equivalent function for two arguments. - [`numpy.dot`][] : Numpy equivalent function. - [`scipy.sparse.coo_matrix.dot`][] : Scipy equivalent function. Examples -------- >>> x = np.arange(4).reshape((2, 2)) >>> s = COO.from_numpy(x) >>> s.dot(s) # doctest: +SKIP array([[ 2, 3], [ 6, 11]], dtype=int64) """ from .._common import dot return dot(self, other) def __matmul__(self, other): from .._common import matmul try: return matmul(self, other) except NotImplementedError: return NotImplemented def __rmatmul__(self, other): from .._common import matmul try: return matmul(other, self) except NotImplementedError: return NotImplemented def linear_loc(self): """ The nonzero coordinates of a flattened version of this array. Note that the coordinates may be out of order. Returns ------- numpy.ndarray The flattened coordinates. See Also -------- [`numpy.flatnonzero`][] : Equivalent Numpy function. Examples -------- >>> x = np.eye(5) >>> s = COO.from_numpy(x) >>> s.linear_loc() # doctest: +NORMALIZE_WHITESPACE array([ 0, 6, 12, 18, 24]) >>> np.array_equal(np.flatnonzero(x), s.linear_loc()) True """ from .common import linear_loc return linear_loc(self.coords, self.shape) def flatten(self, order="C"): """ Returns a new [`sparse.COO`][] array that is a flattened version of this array. Returns ------- COO The flattened output array. Notes ----- The `order` parameter is provided just for compatibility with Numpy and isn't actually supported. Examples -------- >>> s = COO.from_numpy(np.arange(10)) >>> s2 = s.reshape((2, 5)).flatten() >>> s2.todense() array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) """ if order not in {"C", None}: raise NotImplementedError("The `order` parameter is notsupported.") return self.reshape(-1) def reshape(self, shape, order="C"): """ Returns a new [`sparse.COO`][] array that is a reshaped version of this array. Parameters ---------- shape : tuple[int] The desired shape of the output array. Returns ------- COO The reshaped output array. See Also -------- [`numpy.ndarray.reshape`][] : The equivalent Numpy function. Notes ----- The `order` parameter is provided just for compatibility with Numpy and isn't actually supported. Examples -------- >>> s = COO.from_numpy(np.arange(25)) >>> s2 = s.reshape((5, 5)) >>> s2.todense() # doctest: +NORMALIZE_WHITESPACE array([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19], [20, 21, 22, 23, 24]]) """ shape = tuple(shape) if isinstance(shape, Iterable) else (shape,) if order not in {"C", None}: raise NotImplementedError("The `order` parameter is not supported") if self.shape == shape: return self if any(d == -1 for d in shape): extra = int(self.size / np.prod([d for d in shape if d != -1])) shape = tuple([d if d != -1 else extra for d in shape]) if self.size != reduce(operator.mul, shape, 1): raise ValueError(f"cannot reshape array of size {self.size} into shape {shape}") if self._cache is not None: for sh, value in self._cache["reshape"]: if sh == shape: return value # TODO: this self.size enforces a 2**64 limit to array size linear_loc = self.linear_loc() idx_dtype = self.coords.dtype if shape != () and not can_store(idx_dtype, max(shape)): idx_dtype = np.min_scalar_type(max(shape)) coords = np.empty((len(shape), self.nnz), dtype=idx_dtype) strides = 1 for i, d in enumerate(shape[::-1]): coords[-(i + 1), :] = (linear_loc // strides) % d strides *= d result = COO( coords, self.data, shape, has_duplicates=False, sorted=True, cache=self._cache is not None, fill_value=self.fill_value, ) if self._cache is not None: self._cache["reshape"].append((shape, result)) return result def squeeze(self, axis=None): """ Removes singleton dimensions (axes) from ``x``. Parameters ---------- axis : Union[None, int, Tuple[int, ...]] The axis (or axes) to squeeze. If a specified axis has a size greater than one, a `ValueError` is raised. ``axis=None`` removes all singleton dimensions. Default: ``None``. Returns ------- COO The output array without ``axis`` dimensions. Examples -------- >>> s = COO.from_numpy(np.eye(2)).reshape((2, 1, 2, 1)) >>> s.squeeze().shape (2, 2) >>> s.squeeze(axis=1).shape (2, 2, 1) """ squeezable_dims = tuple([d for d in range(self.ndim) if self.shape[d] == 1]) if axis is None: axis = squeezable_dims if isinstance(axis, int): axis = (axis,) elif isinstance(axis, Iterable): axis = tuple(axis) else: raise ValueError(f"Invalid axis parameter: `{axis}`.") for d in axis: if d not in squeezable_dims: raise ValueError(f"Specified axis `{d}` has a size greater than one: {self.shape[d]}") retained_dims = [d for d in range(self.ndim) if d not in axis] coords = self.coords[retained_dims, :] shape = tuple([s for idx, s in enumerate(self.shape) if idx in retained_dims]) return COO( coords, self.data, shape, has_duplicates=False, sorted=True, cache=self._cache is not None, fill_value=self.fill_value, ) def to_scipy_sparse(self, /, *, accept_fv=None): """ Converts this [`sparse.COO`][] object into a [`scipy.sparse.coo_matrix`][]. Parameters ---------- accept_fv : scalar or list of scalar, optional The list of accepted fill-values. The default accepts only zero. Returns ------- scipy.sparse.coo_matrix The converted Scipy sparse matrix. Raises ------ ValueError If the array is not two-dimensional. ValueError If all the array doesn't zero fill-values. See Also -------- - [`sparse.COO.tocsr`][] : Convert to a [`scipy.sparse.csr_matrix`][]. - [`sparse.COO.tocsc`][] : Convert to a [`scipy.sparse.csc_matrix`][]. """ import scipy.sparse check_fill_value(self, accept_fv=accept_fv) if self.ndim != 2: raise ValueError("Can only convert a 2-dimensional array to a Scipy sparse matrix.") result = scipy.sparse.coo_matrix((self.data, (self.coords[0], self.coords[1])), shape=self.shape) result.has_canonical_format = True return result def _tocsr(self): import scipy.sparse if self.ndim != 2: raise ValueError("This array must be two-dimensional for this conversion to work.") row, col = self.coords # Pass 3: count nonzeros in each row indptr = np.zeros(self.shape[0] + 1, dtype=np.int64) np.cumsum(np.bincount(row, minlength=self.shape[0]), out=indptr[1:]) return scipy.sparse.csr_matrix((self.data, col, indptr), shape=self.shape) def tocsr(self): """ Converts this array to a [`scipy.sparse.csr_matrix`][]. Returns ------- scipy.sparse.csr_matrix The result of the conversion. Raises ------ ValueError If the array is not two-dimensional. ValueError If all the array doesn't have zero fill-values. See Also -------- - [`sparse.COO.tocsc`][] : Convert to a [`scipy.sparse.csc_matrix`][]. - [`sparse.COO.to_scipy_sparse`][] : Convert to a [`scipy.sparse.coo_matrix`][]. - [`scipy.sparse.coo_matrix.tocsr`][] : Equivalent Scipy function. """ check_zero_fill_value(self) if self._cache is not None: try: return self._csr except AttributeError: pass try: self._csr = self._csc.tocsr() return self._csr except AttributeError: pass self._csr = csr = self._tocsr() else: csr = self._tocsr() return csr def tocsc(self): """ Converts this array to a [`scipy.sparse.csc_matrix`][]. Returns ------- scipy.sparse.csc_matrix The result of the conversion. Raises ------ ValueError If the array is not two-dimensional. ValueError If the array doesn't have zero fill-values. See Also -------- - [`sparse.COO.tocsr`][] : Convert to a [`scipy.sparse.csr_matrix`][]. - [`sparse.COO.to_scipy_sparse`][] : Convert to a [`scipy.sparse.coo_matrix`][]. - [`scipy.sparse.coo_matrix.tocsc`][] : Equivalent Scipy function. """ check_zero_fill_value(self) if self._cache is not None: try: return self._csc except AttributeError: pass try: self._csc = self._csr.tocsc() return self._csc except AttributeError: pass self._csc = csc = self.tocsr().tocsc() else: csc = self.tocsr().tocsc() return csc def _sort_indices(self): """ Sorts the :obj:`COO.coords` attribute. Also sorts the data in :obj:`COO.data` to match. Examples -------- >>> coords = np.array([[1, 2, 0]], dtype=np.uint8) >>> data = np.array([4, 1, 3], dtype=np.uint8) >>> s = COO(coords, data, shape=(3,)) >>> s._sort_indices() >>> s.coords # doctest: +NORMALIZE_WHITESPACE array([[0, 1, 2]], dtype=uint8) >>> s.data # doctest: +NORMALIZE_WHITESPACE array([3, 4, 1], dtype=uint8) """ linear = self.linear_loc() if (np.diff(linear) >= 0).all(): # already sorted return order = np.argsort(linear, kind="mergesort") self.coords = self.coords[:, order] self.data = self.data[order] def _sum_duplicates(self): """ Sums data corresponding to duplicates in :obj:`COO.coords`. See Also -------- scipy.sparse.coo_matrix.sum_duplicates : Equivalent Scipy function. Examples -------- >>> coords = np.array([[0, 1, 1, 2]], dtype=np.uint8) >>> data = np.array([6, 5, 2, 2], dtype=np.uint8) >>> s = COO(coords, data, shape=(3,)) >>> s._sum_duplicates() >>> s.coords # doctest: +NORMALIZE_WHITESPACE array([[0, 1, 2]], dtype=uint8) >>> s.data # doctest: +NORMALIZE_WHITESPACE array([6, 7, 2], dtype=uint8) """ # Inspired by scipy/sparse/coo.py::sum_duplicates # See https://github.com/scipy/scipy/blob/main/LICENSE.txt linear = self.linear_loc() unique_mask = np.diff(linear) != 0 if unique_mask.sum() == len(unique_mask): # already unique return unique_mask = np.append(True, unique_mask) coords = self.coords[:, unique_mask] (unique_inds,) = np.nonzero(unique_mask) data = np.add.reduceat(self.data, unique_inds, dtype=self.data.dtype) self.data = data self.coords = coords def _prune(self): """ Prunes data so that if any fill-values are present, they are removed from both coordinates and data. Examples -------- >>> coords = np.array([[0, 1, 2, 3]]) >>> data = np.array([1, 0, 1, 2]) >>> s = COO(coords, data, shape=(4,)) >>> s._prune() >>> s.nnz 3 """ mask = ~equivalent(self.data, self.fill_value) self.coords = self.coords[:, mask] self.data = self.data[mask] def broadcast_to(self, shape): """ Performs the equivalent of [`sparse.COO`][]. Note that this function returns a new array instead of a view. Parameters ---------- shape : tuple[int] The shape to broadcast the data to. Returns ------- COO The broadcasted sparse array. Raises ------ ValueError If the operand cannot be broadcast to the given shape. See Also -------- [`numpy.broadcast_to`][] : NumPy equivalent function """ return broadcast_to(self, shape) def maybe_densify(self, max_size=1000, min_density=0.25): """ Converts this [`sparse.COO`][] array to a [`numpy.ndarray`][] if not too costly. Parameters ---------- max_size : int Maximum number of elements in output min_density : float Minimum density of output Returns ------- numpy.ndarray The dense array. Raises ------ ValueError If the returned array would be too large. Examples -------- Convert a small sparse array to a dense array. >>> s = COO.from_numpy(np.random.rand(2, 3, 4)) >>> x = s.maybe_densify() >>> np.allclose(x, s.todense()) True You can also specify the minimum allowed density or the maximum number of output elements. If both conditions are unmet, this method will throw an error. >>> x = np.zeros((5, 5), dtype=np.uint8) >>> x[2, 2] = 1 >>> s = COO.from_numpy(x) >>> s.maybe_densify(max_size=5, min_density=0.25) Traceback (most recent call last): ... ValueError: Operation would require converting large sparse array to dense """ if self.size > max_size and self.density < min_density: raise ValueError("Operation would require converting large sparse array to dense") return self.todense() def nonzero(self): """ Get the indices where this array is nonzero. Returns ------- idx : tuple[`numpy.ndarray`] The indices where this array is nonzero. See Also -------- [`numpy.ndarray.nonzero`][] : NumPy equivalent function Raises ------ ValueError If the array doesn't have zero fill-values. Examples -------- >>> s = COO.from_numpy(np.eye(5)) >>> s.nonzero() (array([0, 1, 2, 3, 4]), array([0, 1, 2, 3, 4])) """ check_zero_fill_value(self) if self.ndim == 0: raise ValueError("`nonzero` is undefined for `self.ndim == 0`.") return tuple(self.coords) def asformat(self, format, **kwargs): """ Convert this sparse array to a given format. Parameters ---------- format : str A format string. Returns ------- out : SparseArray The converted array. Raises ------ NotImplementedError If the format isn't supported. """ from .._utils import convert_format format = convert_format(format) if format == "gcxs": from .._compressed import GCXS return GCXS.from_coo(self, **kwargs) if len(kwargs) != 0: raise TypeError(f"Invalid keyword arguments provided: {kwargs}") if format == "coo": return self if format == "dok": from .._dok import DOK return DOK.from_coo(self, **kwargs) return self.asformat("gcxs", **kwargs).asformat(format, **kwargs) def isinf(self): """ Tests each element ``x_i`` of the array to determine if equal to positive or negative infinity. """ new_fill_value = bool(np.isinf(self.fill_value)) new_data = np.isinf(self.data) return COO( self.coords, new_data, shape=self.shape, fill_value=new_fill_value, prune=True, ) def isnan(self): """ Tests each element ``x_i`` of the array to determine whether the element is ``NaN``. """ new_fill_value = bool(np.isnan(self.fill_value)) new_data = np.isnan(self.data) return COO( self.coords, new_data, shape=self.shape, fill_value=new_fill_value, prune=True, ) def as_coo(x, shape=None, fill_value=None, idx_dtype=None): """ Converts any given format to [`sparse.COO`][]. See the "See Also" section for details. Parameters ---------- x : SparseArray or numpy.ndarray or scipy.sparse.spmatrix or Iterable. The item to convert. shape : tuple[int], optional The shape of the output array. Can only be used in case of Iterable. Returns ------- out : COO The converted [`sparse.COO`][] array. See Also -------- - [`sparse.SparseArray.asformat`][] : A utility function to convert between formats in this library. - [`sparse.COO.from_numpy`][] : Convert a Numpy array to [`sparse.COO`][]. - [`sparse.COO.from_scipy_sparse`][] : Convert a SciPy sparse matrix to [`sparse.COO`][]. - [`sparse.COO.from_iter`][] : Convert an iterable to [`sparse.COO`][]. """ from .._common import _is_scipy_sparse_obj if hasattr(x, "shape") and shape is not None: raise ValueError("Cannot provide a shape in combination with something that already has a shape.") if hasattr(x, "fill_value") and fill_value is not None: raise ValueError("Cannot provide a fill-value in combination with something that already has a fill-value.") if isinstance(x, SparseArray): return x.asformat("coo") if isinstance(x, np.ndarray) or np.isscalar(x): return COO.from_numpy(x, fill_value=fill_value, idx_dtype=idx_dtype) if _is_scipy_sparse_obj(x): return COO.from_scipy_sparse(x) if isinstance(x, Iterable | Iterator): return COO.from_iter(x, shape=shape, fill_value=fill_value) raise NotImplementedError( f"Format not supported for conversion. Supplied type is " f"{type(x)}, see help(sparse.as_coo) for supported formats." ) @numba.jit(nopython=True, nogil=True) # pragma: no cover def _calc_counts_invidx(groups): inv_idx = [] counts = [] if len(groups) == 0: return ( np.array(inv_idx, dtype=groups.dtype), np.array(counts, dtype=groups.dtype), ) inv_idx.append(0) last_group = groups[0] for i in range(1, len(groups)): if groups[i] != last_group: counts.append(i - inv_idx[-1]) inv_idx.append(i) last_group = groups[i] counts.append(len(groups) - inv_idx[-1]) return (np.array(inv_idx, dtype=groups.dtype), np.array(counts, dtype=groups.dtype)) def _grouped_reduce(x, groups, method, **kwargs): """ Performs a :code:`ufunc` grouped reduce. Parameters ---------- x : np.ndarray The data to reduce. groups : np.ndarray The groups the data belongs to. The groups must be contiguous. method : np.ufunc The :code:`ufunc` to use to perform the reduction. **kwargs : dict The kwargs to pass to the :code:`ufunc`'s :code:`reduceat` function. Returns ------- result : np.ndarray The result of the grouped reduce operation. inv_idx : np.ndarray The index of the first element where each group is found. counts : np.ndarray The number of elements in each group. """ # Partial credit to @shoyer # Ref: https://gist.github.com/shoyer/f538ac78ae904c936844 inv_idx, counts = _calc_counts_invidx(groups) result = method.reduceat(x, inv_idx, **kwargs) return result, inv_idx, counts sparse-0.17.0/sparse/numba_backend/_coo/indexing.py000066400000000000000000000502771501262445000222520ustar00rootroot00000000000000from itertools import zip_longest from numbers import Integral import numba import numpy as np from .._slicing import normalize_index from .._utils import _zero_of_dtype, equivalent def getitem(x, index): """ This function implements the indexing functionality for COO. The overall algorithm has three steps: 1. Normalize the index to canonical form. Function: normalize_index 2. Get the mask, which is a list of integers corresponding to the indices in coords/data for the output data. Function: _mask 3. Transform the coordinates to what they will be in the output. Parameters ---------- x : COO The array to apply the indexing operation on. index : {tuple, str} The index into the array. """ from .core import COO # If string, this is an index into an np.void # Custom dtype. if isinstance(index, str): data = x.data[index] idx = np.where(data) data = data[idx].flatten() coords = list(x.coords[:, idx[0]]) coords.extend(idx[1:]) fill_value_idx = np.asarray(x.fill_value[index]).flatten() fill_value = fill_value_idx[0] if fill_value_idx.size else _zero_of_dtype(data.dtype)[()] if not equivalent(fill_value, fill_value_idx).all(): raise ValueError("Fill-values in the array are inconsistent.") return COO( coords, data, shape=x.shape + x.data.dtype[index].shape, has_duplicates=False, sorted=True, fill_value=fill_value, ) # Otherwise, convert into a tuple. if not isinstance(index, tuple): index = (index,) # Check if the last index is an ellipsis. last_ellipsis = len(index) > 0 and index[-1] is Ellipsis # Normalize the index into canonical form. index = normalize_index(index, x.shape) # zip_longest so things like x[..., None] are picked up. if len(index) != 0 and all( isinstance(ind, slice) and ind == slice(0, dim, 1) for ind, dim in zip_longest(index, x.shape) ): return x # Get the mask mask, adv_idx = _mask(x.coords, index, x.shape) # Get the length of the mask n = len(range(mask.start, mask.stop, mask.step)) if isinstance(mask, slice) else len(mask) coords = [] shape = [] i = 0 sorted = adv_idx is None or adv_idx.pos == 0 adv_idx_added = False for ind in index: # Nothing is added to shape or coords if the index is an integer. if isinstance(ind, Integral): i += 1 continue # Add to the shape and transform the coords in the case of a slice. if isinstance(ind, slice): shape.append(len(range(ind.start, ind.stop, ind.step))) coords.append((x.coords[i, mask] - ind.start) // ind.step) i += 1 if ind.step < 0: sorted = False # Add the index and shape for the advanced index. if isinstance(ind, np.ndarray): if not adv_idx_added: shape.append(adv_idx.length) coords.append(adv_idx.idx) adv_idx_added = True i += 1 # Add a dimension for None. if ind is None: coords.append(np.zeros(n, dtype=np.intp)) shape.append(1) # Join all the transformed coords. if coords: coords = np.stack(coords, axis=0) else: # If index result is a scalar, return a 0-d COO or # a scalar depending on whether the last index is an ellipsis. if last_ellipsis: coords = np.empty((0, n), dtype=np.uint8) else: if n != 0: return x.data[mask][0] return x.fill_value shape = tuple(shape) data = x.data[mask] return COO( coords, data, shape=shape, has_duplicates=False, sorted=sorted, fill_value=x.fill_value, ) def _mask(coords, indices, shape): indices = _prune_indices(indices, shape) indices, adv_idx, adv_idx_pos = _separate_adv_indices(indices) if len(adv_idx) != 0: if len(adv_idx) != 1: # Ensure if multiple advanced indices are passed, all are of the same length # Also check each advanced index to ensure each is only a one-dimensional iterable adv_ix_len = len(adv_idx[0]) for ai in adv_idx: if len(ai) != adv_ix_len: raise IndexError( "shape mismatch: indexing arrays could not be broadcast together. Ensure all indexing arrays " "are of the same length." ) if ai.ndim != 1: raise IndexError("Only one-dimensional iterable indices supported.") mask, aidxs = _compute_multi_axis_multi_mask( coords, _ind_ar_from_indices(indices), np.array(adv_idx, dtype=np.intp), np.array(adv_idx_pos, dtype=np.intp), ) return mask, _AdvIdxInfo(aidxs, adv_idx_pos, adv_ix_len) adv_idx = adv_idx[0] adv_idx_pos = adv_idx_pos[0] if adv_idx.ndim != 1: raise IndexError("Only one-dimensional iterable indices supported.") mask, aidxs = _compute_multi_mask(coords, _ind_ar_from_indices(indices), adv_idx, adv_idx_pos) return mask, _AdvIdxInfo(aidxs, adv_idx_pos, len(adv_idx)) mask, is_slice = _compute_mask(coords, _ind_ar_from_indices(indices)) if is_slice: return slice(mask[0], mask[1], 1), None return mask, None def _ind_ar_from_indices(indices): """ Computes an index "array" from indices, such that ``indices[i]`` is transformed to ``ind_ar[i]`` and ``ind_ar[i].shape == (3,)``. It has the format ``[start, stop, step]``. Integers are converted into steps as well. Parameters ---------- indices : Iterable Input indices (slices and integers) Returns ------- ind_ar : np.ndarray The output array. Examples -------- >>> _ind_ar_from_indices([1]) array([[1, 2, 1]]) >>> _ind_ar_from_indices([slice(5, 7, 2)]) array([[5, 7, 2]]) """ ind_ar = np.empty((len(indices), 3), dtype=np.intp) for i, idx in enumerate(indices): if isinstance(idx, slice): ind_ar[i] = [idx.start, idx.stop, idx.step] elif isinstance(idx, Integral): ind_ar[i] = [idx, idx + 1, 1] return ind_ar def _prune_indices(indices, shape, prune_none=True): """ Gets rid of the indices that do not contribute to the overall mask, e.g. None and full slices. Parameters ---------- indices : tuple The indices to the array. shape : tuple[int] The shape of the array. Returns ------- indices : tuple The filtered indices. Examples -------- >>> _prune_indices((None, 5), (10,)) # None won't affect the mask [5] >>> _prune_indices((slice(0, 10, 1),), (10,)) # Full slices don't affect the mask [] """ if prune_none: indices = [idx for idx in indices if idx is not None] i = 0 for idx, sh in zip(indices[::-1], shape[::-1], strict=True): if not isinstance(idx, slice): break if idx.start == 0 and idx.stop == sh and idx.step == 1: i += 1 continue if idx.start == sh - 1 and idx.stop == -1 and idx.step == -1: i += 1 continue break if i != 0: indices = indices[:-i] return indices def _separate_adv_indices(indices): """ Separates advanced from normal indices. Parameters ---------- indices : list The input indices Returns ------- new_idx : list The normal indices. adv_idx : list The advanced indices. adv_idx_pos : list The positions of the advanced indices. """ adv_idx_pos = [] new_idx = [] adv_idx = [] for i, idx in enumerate(indices): if isinstance(idx, np.ndarray): adv_idx.append(idx) adv_idx_pos.append(i) else: new_idx.append(idx) return new_idx, adv_idx, adv_idx_pos @numba.jit(nopython=True, nogil=True) def _compute_multi_axis_multi_mask(coords, indices, adv_idx, adv_idx_pos): # pragma: no cover """ Computes a mask with the advanced index, and also returns the advanced index dimension. Parameters ---------- coords : np.ndarray Coordinates of the input array. indices : np.ndarray The indices in slice format. adv_idx : np.ndarray List of advanced indices. adv_idx_pos : np.ndarray The position of the advanced indices. Returns ------- mask : np.ndarray The mask. aidxs : np.ndarray The advanced array index. """ n_adv_idx = len(adv_idx_pos) mask = numba.typed.List.empty_list(numba.types.intp) a_indices = numba.typed.List.empty_list(numba.types.intp) full_idx = np.empty((len(indices) + len(adv_idx_pos), 3), dtype=np.intp) # Get location of non-advanced indices if len(indices) != 0: ixx = 0 for ix in range(coords.shape[0]): isin = False for ax in adv_idx_pos: if ix == ax: isin = True break if not isin: full_idx[ix] = indices[ixx] ixx += 1 for i in range(len(adv_idx[0])): for ii in range(n_adv_idx): full_idx[adv_idx_pos[ii]] = [adv_idx[ii][i], adv_idx[ii][i] + 1, 1] partial_mask, is_slice = _compute_mask(coords, full_idx) if is_slice: slice_mask = numba.typed.List.empty_list(numba.types.intp) for j in range(partial_mask[0], partial_mask[1]): slice_mask.append(j) partial_mask = array_from_list_intp(slice_mask) for j in range(len(partial_mask)): mask.append(partial_mask[j]) a_indices.append(i) return array_from_list_intp(mask), array_from_list_intp(a_indices) @numba.jit(nopython=True, nogil=True) def _compute_multi_mask(coords, indices, adv_idx, adv_idx_pos): # pragma: no cover """ Computes a mask with the advanced index, and also returns the advanced index dimension. Parameters ---------- coords : np.ndarray Coordinates of the input array. indices : np.ndarray The indices in slice format. adv_idx : list(int) The advanced index. adv_idx_pos : list(int) The position of the advanced index. Returns ------- mask : np.ndarray The mask. aidxs : np.ndarray The advanced array index. """ mask = numba.typed.List.empty_list(numba.types.intp) a_indices = numba.typed.List.empty_list(numba.types.intp) full_idx = np.empty((len(indices) + 1, 3), dtype=np.intp) full_idx[:adv_idx_pos] = indices[:adv_idx_pos] full_idx[adv_idx_pos + 1 :] = indices[adv_idx_pos:] for i, aidx in enumerate(adv_idx): full_idx[adv_idx_pos] = [aidx, aidx + 1, 1] partial_mask, is_slice = _compute_mask(coords, full_idx) if is_slice: slice_mask = numba.typed.List.empty_list(numba.types.intp) for j in range(partial_mask[0], partial_mask[1]): slice_mask.append(j) partial_mask = array_from_list_intp(slice_mask) for j in range(len(partial_mask)): mask.append(partial_mask[j]) a_indices.append(i) return array_from_list_intp(mask), array_from_list_intp(a_indices) @numba.jit(nopython=True, nogil=True) def _compute_mask(coords, indices): # pragma: no cover """ Gets the mask for the coords given the indices in slice format. Works with either start-stop ranges of matching indices into coords called "pairs" (start-stop pairs) or filters the mask directly, based on which is faster. Exploits the structure in sorted coords, which is that for a constant value of coords[i - 1], coords[i - 2] and so on, coords[i] is sorted. Concretely, ``coords[i, coords[i - 1] == v1 & coords[i - 2] = v2, ...]`` is always sorted. It uses this sortedness to find sub-pairs for each dimension given the previous, and so on. This is efficient for small slices or ints, but not for large ones. After it detects that working with pairs is rather inefficient (or after going through each possible index), it constructs a filtered mask from the start-stop pairs. Parameters ---------- coords : np.ndarray The coordinates of the array. indices : np.ndarray The indices in the form of slices such that indices[:, 0] are starts, indices[:, 1] are stops and indices[:, 2] are steps. Returns ------- mask : np.ndarray The starts and stops in the mask. is_slice : bool Whether or not the array represents a continuous slice. Examples -------- Let's create some mock coords and indices >>> import numpy as np >>> coords = np.array([[0, 0, 1, 1, 2, 2]]) >>> indices = np.array([[0, 3, 2]]) # Equivalent to slice(0, 3, 2) Now let's get the mask. Notice that the indices of ``0`` and ``2`` are matched. >>> _compute_mask(coords, indices) (array([0, 1, 4, 5]), False) Now, let's try with a more "continuous" slice. Matches ``0`` and ``1``. >>> indices = np.array([[0, 2, 1]]) >>> _compute_mask(coords, indices) (array([0, 4]), True) This is equivalent to mask being ``slice(0, 4, 1)``. """ # Set the initial mask to be the entire range of coordinates. starts = numba.typed.List.empty_list(numba.types.intp) starts.append(0) stops = numba.typed.List.empty_list(numba.types.intp) stops.append(coords.shape[1]) n_matches = np.intp(coords.shape[1]) i = 0 while i < len(indices): # Guesstimate whether working with pairs is more efficient or # working with the mask directly. # One side is the estimate of time taken for binary searches # (n_searches * log(avg_length)) # The other is an estimated time of a linear filter for the mask. n_pairs = len(starts) n_current_slices = len(range(indices[i, 0], indices[i, 1], indices[i, 2])) * n_pairs + 2 if n_current_slices * np.log(n_current_slices / max(n_pairs, 1)) > n_matches + n_pairs: break # For each of the pairs, search inside the coordinates for other # matching sub-pairs. # This gets the start-end coordinates in coords for each 'sub-array' # Which would come out of indexing a single integer. starts, stops, n_matches = _get_mask_pairs(starts, stops, coords[i], indices[i]) i += 1 # Combine adjacent pairs starts, stops = _join_adjacent_pairs(starts, stops) # If just one pair is left over, treat it as a slice. if i == len(indices) and len(starts) == 1: return np.array([starts[0], stops[0]]), True # Convert start-stop pairs into mask, filtering by remaining # coordinates. mask = _filter_pairs(starts, stops, coords[i:], indices[i:]) return array_from_list_intp(mask), False @numba.jit(nopython=True, nogil=True) def _get_mask_pairs(starts_old, stops_old, c, idx): # pragma: no cover """ Gets the pairs for a following dimension given the pairs for a dimension. For each pair, it searches in the following dimension for matching coords and returns those. The total combined length of all pairs is returned to help with the performance guesstimate. Parameters ---------- starts_old, stops_old : list[int] The starts and stops from the previous index. c : np.ndarray The coords for this index's dimension. idx : np.ndarray The index in the form of a slice. idx[0], idx[1], idx[2] = start, stop, step Returns ------- starts, stops: list The starts and stops after applying the current index. n_matches : int The sum of elements in all ranges. Examples -------- >>> c = np.array([1, 2, 1, 2, 1, 1, 2, 2]) >>> starts_old = numba.typed.List() >>> starts_old.append(4) >>> stops_old = numba.typed.List() >>> stops_old.append(8) >>> idx = np.array([1, 2, 1]) >>> _get_mask_pairs(starts_old, stops_old, c, idx) (ListType[int64]([4]), ListType[int64]([6]), 2) """ starts = numba.typed.List.empty_list(numba.types.intp) stops = numba.typed.List.empty_list(numba.types.intp) n_matches = np.intp(0) for j in range(len(starts_old)): # For each matching "integer" in the slice, search within the "sub-coords" # Using binary search. for p_match in range(idx[0], idx[1], idx[2]): start = np.searchsorted(c[starts_old[j] : stops_old[j]], p_match, side="left") + starts_old[j] stop = np.searchsorted(c[starts_old[j] : stops_old[j]], p_match, side="right") + starts_old[j] if start != stop: starts.append(start) stops.append(stop) n_matches += stop - start return starts, stops, n_matches @numba.jit(nopython=True, nogil=True) def _filter_pairs(starts, stops, coords, indices): # pragma: no cover """ Converts all the pairs into a single integer mask, additionally filtering by the indices. Parameters ---------- starts, stops : list[int] The starts and stops to convert into an array. coords : np.ndarray The coordinates to filter by. indices : np.ndarray The indices in the form of slices such that indices[:, 0] are starts, indices[:, 1] are stops and indices[:, 2] are steps. Returns ------- mask : list The output integer mask. Examples -------- >>> import numpy as np >>> starts = numba.typed.List() >>> starts.append(2) >>> stops = numba.typed.List() >>> stops.append(7) >>> coords = np.array([[0, 1, 2, 3, 4, 5, 6, 7]]) >>> indices = np.array([[2, 8, 2]]) # Start, stop, step pairs >>> _filter_pairs(starts, stops, coords, indices) ListType[int64]([2, 4, 6]) """ mask = numba.typed.List.empty_list(numba.types.intp) # For each pair, for i in range(len(starts)): # For each element match within the pair range for j in range(starts[i], stops[i]): match = True # Check if it matches all indices for k in range(len(indices)): idx = indices[k] elem = coords[k, j] match &= (elem - idx[0]) % idx[2] == 0 and ( (idx[2] > 0 and idx[0] <= elem < idx[1]) or (idx[2] < 0 and idx[0] >= elem > idx[1]) ) # and append to the mask if so. if match: mask.append(j) return mask @numba.jit(nopython=True, nogil=True) def _join_adjacent_pairs(starts_old, stops_old): # pragma: no cover """ Joins adjacent pairs into one. For example, 2-5 and 5-7 will reduce to 2-7 (a single pair). This may help in returning a slice in the end which could be faster. Parameters ---------- starts_old, stops_old : list[int] The input starts and stops Returns ------- starts, stops : list[int] The reduced starts and stops. Examples -------- >>> starts = numba.typed.List() >>> starts.append(2) >>> starts.append(5) >>> stops = numba.typed.List() >>> stops.append(5) >>> stops.append(7) >>> _join_adjacent_pairs(starts, stops) (ListType[int64]([2]), ListType[int64]([7])) """ if len(starts_old) <= 1: return starts_old, stops_old starts = numba.typed.List.empty_list(numba.types.intp) starts.append(starts_old[0]) stops = numba.typed.List.empty_list(numba.types.intp) for i in range(1, len(starts_old)): if starts_old[i] != stops_old[i - 1]: starts.append(starts_old[i]) stops.append(stops_old[i - 1]) stops.append(stops_old[-1]) return starts, stops @numba.jit(nopython=True, nogil=True) def array_from_list_intp(x): # pragma: no cover n = len(x) a = np.empty(n, dtype=np.intp) for i in range(n): a[i] = x[i] return a class _AdvIdxInfo: def __init__(self, idx, pos, length): self.idx = idx self.pos = pos self.length = length sparse-0.17.0/sparse/numba_backend/_coo/numba_extension.py000066400000000000000000000226351501262445000236400ustar00rootroot00000000000000""" Numba support for COO objects. For now, this just supports attribute access """ import contextlib import numba from numba.core import cgutils, types from numba.core.imputils import impl_ret_borrowed, lower_builtin, lower_constant from numba.core.typing.typeof import typeof_impl from numba.extending import ( NativeValue, box, make_attribute_wrapper, models, register_model, type_callable, unbox, ) import numpy as np from .._utils import _zero_of_dtype from . import COO __all__ = ["COOType"] class COOType(types.Type): def __init__(self, data_dtype: np.dtype, coords_dtype: np.dtype, ndim: int): assert isinstance(data_dtype, np.dtype) assert isinstance(coords_dtype, np.dtype) self.data_dtype = data_dtype self.coords_dtype = coords_dtype self.ndim = ndim super().__init__( name=f"COOType[{numba.from_dtype(data_dtype)!r}, {numba.from_dtype(coords_dtype)!r}, {ndim!r}]" ) @property def key(self): return self.data_dtype, self.coords_dtype, self.ndim @property def data_type(self): return numba.from_dtype(self.data_dtype)[:] @property def coords_type(self): return numba.from_dtype(self.coords_dtype)[:, :] @property def shape_type(self): dt = numba.np.numpy_support.from_dtype(self.coords_dtype) return types.UniTuple(dt, self.ndim) @property def fill_value_type(self): return numba.from_dtype(self.data_dtype) @typeof_impl.register(COO) def _typeof_COO(val: COO, c) -> COOType: return COOType(data_dtype=val.data.dtype, coords_dtype=val.coords.dtype, ndim=val.ndim) @register_model(COOType) class COOModel(models.StructModel): def __init__(self, dmm, fe_type): members = [ ("data", fe_type.data_type), ("coords", fe_type.coords_type), ("shape", fe_type.shape_type), ("fill_value", fe_type.fill_value_type), ] models.StructModel.__init__(self, dmm, fe_type, members) @type_callable(COO) def type_COO(context): # TODO: accept a fill_value kwarg def typer(coords, data, shape): return COOType( coords_dtype=numba.np.numpy_support.as_dtype(coords.dtype), data_dtype=numba.np.numpy_support.as_dtype(data.dtype), ndim=len(shape), ) return typer @lower_builtin(COO, types.Any, types.Any, types.Any) def impl_COO(context, builder, sig, args): typ = sig.return_type coords, data, shape = args coo = cgutils.create_struct_proxy(typ)(context, builder) coo.coords = coords coo.data = data coo.shape = shape coo.fill_value = context.get_constant_generic(builder, typ.fill_value_type, _zero_of_dtype(typ.data_dtype)) return impl_ret_borrowed(context, builder, sig.return_type, coo._getvalue()) @lower_constant(COOType) def lower_constant_COO(context, builder, typ, pyval): coords = context.get_constant_generic(builder, typ.coords_type, pyval.coords) data = context.get_constant_generic(builder, typ.data_type, pyval.data) shape = context.get_constant_generic(builder, typ.shape_type, pyval.shape) fill_value = context.get_constant_generic(builder, typ.fill_value_type, pyval.fill_value) return impl_ret_borrowed( context, builder, typ, cgutils.pack_struct(builder, (data, coords, shape, fill_value)), ) @contextlib.contextmanager def local_return(builder): """ Create a scope which can be broken from locally. Used as:: with local_return(c.builder) as ret: with c.builder.if(abort_cond): ret() do_some_other_stuff # no ret needed at the end, it's implied stuff_that_runs_unconditionally """ end_blk = builder.append_basic_block("end") def return_(): builder.branch(end_blk) yield return_ builder.branch(end_blk) # make sure all remaining code goes to the next block builder.position_at_end(end_blk) def _unbox_native_field(typ, obj, field_name: str, c): ret_ptr = cgutils.alloca_once(c.builder, c.context.get_value_type(typ)) is_error_ptr = cgutils.alloca_once_value(c.builder, cgutils.false_bit) fail_obj = c.context.get_constant_null(typ) with local_return(c.builder) as ret: fail_blk = c.builder.append_basic_block("fail") with c.builder.goto_block(fail_blk): c.builder.store(cgutils.true_bit, is_error_ptr) c.builder.store(fail_obj, ret_ptr) ret() field_obj = c.pyapi.object_getattr_string(obj, field_name) with cgutils.if_unlikely(c.builder, cgutils.is_null(c.builder, field_obj)): c.builder.branch(fail_blk) field_native = c.unbox(typ, field_obj) c.pyapi.decref(field_obj) with cgutils.if_unlikely(c.builder, field_native.is_error): c.builder.branch(fail_blk) c.builder.store(cgutils.false_bit, is_error_ptr) c.builder.store(field_native.value, ret_ptr) return NativeValue(c.builder.load(ret_ptr), is_error=c.builder.load(is_error_ptr)) @unbox(COOType) def unbox_COO(typ: COOType, obj: COO, c) -> NativeValue: ret_ptr = cgutils.alloca_once(c.builder, c.context.get_value_type(typ)) is_error_ptr = cgutils.alloca_once_value(c.builder, cgutils.false_bit) fail_obj = c.context.get_constant_null(typ) with local_return(c.builder) as ret: fail_blk = c.builder.append_basic_block("fail") with c.builder.goto_block(fail_blk): c.builder.store(cgutils.true_bit, is_error_ptr) c.builder.store(fail_obj, ret_ptr) ret() data = _unbox_native_field(typ.data_type, obj, "data", c) with cgutils.if_unlikely(c.builder, data.is_error): c.builder.branch(fail_blk) coords = _unbox_native_field(typ.coords_type, obj, "coords", c) with cgutils.if_unlikely(c.builder, coords.is_error): c.builder.branch(fail_blk) shape = _unbox_native_field(typ.shape_type, obj, "shape", c) with cgutils.if_unlikely(c.builder, shape.is_error): c.builder.branch(fail_blk) fill_value = _unbox_native_field(typ.fill_value_type, obj, "fill_value", c) with cgutils.if_unlikely(c.builder, fill_value.is_error): c.builder.branch(fail_blk) coo = cgutils.create_struct_proxy(typ)(c.context, c.builder) coo.coords = coords.value coo.data = data.value coo.shape = shape.value coo.fill_value = fill_value.value c.builder.store(cgutils.false_bit, is_error_ptr) c.builder.store(coo._getvalue(), ret_ptr) return NativeValue(c.builder.load(ret_ptr), is_error=c.builder.load(is_error_ptr)) @box(COOType) def box_COO(typ: COOType, val, c) -> COO: ret_ptr = cgutils.alloca_once(c.builder, c.pyapi.pyobj) fail_obj = c.pyapi.get_null_object() coo = cgutils.create_struct_proxy(typ)(c.context, c.builder, value=val) with local_return(c.builder) as ret: data_obj = c.box(typ.data_type, coo.data) with cgutils.if_unlikely(c.builder, cgutils.is_null(c.builder, data_obj)): c.builder.store(fail_obj, ret_ptr) ret() coords_obj = c.box(typ.coords_type, coo.coords) with cgutils.if_unlikely(c.builder, cgutils.is_null(c.builder, coords_obj)): c.pyapi.decref(data_obj) c.builder.store(fail_obj, ret_ptr) ret() shape_obj = c.box(typ.shape_type, coo.shape) with cgutils.if_unlikely(c.builder, cgutils.is_null(c.builder, shape_obj)): c.pyapi.decref(coords_obj) c.pyapi.decref(data_obj) c.builder.store(fail_obj, ret_ptr) ret() fill_value_obj = c.box(typ.fill_value_type, coo.fill_value) with cgutils.if_unlikely(c.builder, cgutils.is_null(c.builder, fill_value_obj)): c.pyapi.decref(shape_obj) c.pyapi.decref(coords_obj) c.pyapi.decref(data_obj) c.builder.store(fail_obj, ret_ptr) ret() class_obj = c.pyapi.unserialize(c.pyapi.serialize_object(COO)) with cgutils.if_unlikely(c.builder, cgutils.is_null(c.builder, class_obj)): c.pyapi.decref(shape_obj) c.pyapi.decref(coords_obj) c.pyapi.decref(data_obj) c.pyapi.decref(fill_value_obj) c.builder.store(fail_obj, ret_ptr) ret() args = c.pyapi.tuple_pack([coords_obj, data_obj, shape_obj]) c.pyapi.decref(shape_obj) c.pyapi.decref(coords_obj) c.pyapi.decref(data_obj) with cgutils.if_unlikely(c.builder, cgutils.is_null(c.builder, args)): c.pyapi.decref(fill_value_obj) c.pyapi.decref(class_obj) c.builder.store(fail_obj, ret_ptr) ret() kwargs = c.pyapi.dict_pack([("fill_value", fill_value_obj)]) c.pyapi.decref(fill_value_obj) with cgutils.if_unlikely(c.builder, cgutils.is_null(c.builder, kwargs)): c.pyapi.decref(class_obj) c.builder.store(fail_obj, ret_ptr) ret() c.builder.store(c.pyapi.call(class_obj, args, kwargs), ret_ptr) c.pyapi.decref(class_obj) c.pyapi.decref(args) c.pyapi.decref(kwargs) return c.builder.load(ret_ptr) make_attribute_wrapper(COOType, "data", "data") make_attribute_wrapper(COOType, "coords", "coords") make_attribute_wrapper(COOType, "shape", "shape") make_attribute_wrapper(COOType, "fill_value", "fill_value") sparse-0.17.0/sparse/numba_backend/_dok.py000066400000000000000000000400421501262445000204270ustar00rootroot00000000000000from collections.abc import Iterable from numbers import Integral import numpy as np from numpy.lib.mixins import NDArrayOperatorsMixin from ._slicing import normalize_index from ._sparse_array import SparseArray from ._utils import equivalent class DOK(SparseArray, NDArrayOperatorsMixin): """ A class for building sparse multidimensional arrays. Parameters ---------- shape : tuple[int] (DOK.ndim,) The shape of the array. data : dict, optional The key-value pairs for the data in this array. dtype : np.dtype, optional The data type of this array. If left empty, it is inferred from the first element. fill_value : scalar, optional The fill value of this array. Attributes ---------- dtype : numpy.dtype The datatype of this array. Can be `None` if no elements have been set yet. shape : tuple[int] The shape of this array. data : dict The keys of this dictionary contain all the indices and the values contain the nonzero entries. See Also -------- [`sparse.COO`][] : A read-only sparse array. Examples -------- You can create [`sparse.DOK`][] objects from Numpy arrays. >>> x = np.eye(5, dtype=np.uint8) >>> x[2, 3] = 5 >>> s = DOK.from_numpy(x) >>> s You can also create them from just shapes, and use slicing assignment. >>> s2 = DOK((5, 5), dtype=np.int64) >>> s2[1:3, 1:3] = [[4, 5], [6, 7]] >>> s2 You can convert [`sparse.DOK`][] arrays to [`sparse.COO`][] arrays, or [`numpy.ndarray`][] objects. >>> from sparse import COO >>> s3 = COO(s2) >>> s3 >>> s2.todense() # doctest: +NORMALIZE_WHITESPACE array([[0, 0, 0, 0, 0], [0, 4, 5, 0, 0], [0, 6, 7, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) >>> s4 = COO.from_numpy(np.eye(4, dtype=np.uint8)) >>> s4 >>> s5 = DOK.from_coo(s4) >>> s5 You can also create [`sparse.DOK`][] arrays from a shape and a dict of values. Zeros are automatically ignored. >>> values = { ... (1, 2, 3): 4, ... (3, 2, 1): 0, ... } >>> s6 = DOK((5, 5, 5), values) >>> s6 """ def __init__(self, shape, data=None, dtype=None, fill_value=None): from ._common import _is_scipy_sparse_obj from ._coo import COO self.data = {} if isinstance(shape, COO): ar = DOK.from_coo(shape) self._make_shallow_copy_of(ar) return if isinstance(shape, np.ndarray): ar = DOK.from_numpy(shape) self._make_shallow_copy_of(ar) return if _is_scipy_sparse_obj(shape): ar = DOK.from_scipy_sparse(shape) self._make_shallow_copy_of(ar) return self.dtype = np.dtype(dtype) if not data: data = {} super().__init__(shape, fill_value=fill_value) if isinstance(data, dict): if not dtype: if not len(data): self.dtype = np.dtype("float64") else: self.dtype = np.result_type(*(np.asarray(x).dtype for x in data.values())) for c, d in data.items(): self[c] = d else: raise ValueError("data must be a dict.") @classmethod def from_scipy_sparse(cls, x, /, *, fill_value=None): """ Create a [`sparse.DOK`][] array from a [`scipy.sparse.spmatrix`][]. Parameters ---------- x : scipy.sparse.spmatrix The matrix to convert. fill_value : scalar The fill-value to use when converting. Returns ------- DOK The equivalent [`sparse.DOK`][] array. Examples -------- >>> import scipy.sparse >>> x = scipy.sparse.rand(6, 3, density=0.2) >>> s = DOK.from_scipy_sparse(x) >>> np.array_equal(x.todense(), s.todense()) True """ from sparse import COO return COO.from_scipy_sparse(x, fill_value=fill_value).asformat(cls) @classmethod def from_coo(cls, x): """ Get a [`sparse.DOK`][] array from a [`sparse.COO`][] array. Parameters ---------- x : COO The array to convert. Returns ------- DOK The equivalent [`sparse.DOK`][] array. Examples -------- >>> from sparse import COO >>> s = COO.from_numpy(np.eye(4)) >>> s2 = DOK.from_coo(s) >>> s2 """ ar = cls(x.shape, dtype=x.dtype, fill_value=x.fill_value) for c, d in zip(x.coords.T, x.data, strict=True): ar.data[tuple(c)] = d return ar def to_coo(self): """ Convert this [`sparse.DOK`][] array to a [`sparse.COO`][] array. Returns ------- COO The equivalent [`sparse.COO`][] array. Examples -------- >>> s = DOK((5, 5)) >>> s[1:3, 1:3] = [[4, 5], [6, 7]] >>> s >>> s2 = s.to_coo() >>> s2 """ from ._coo import COO return COO(self) @classmethod def from_numpy(cls, x): """ Get a [`sparse.DOK`][] array from a Numpy array. Parameters ---------- x : np.ndarray The array to convert. Returns ------- DOK The equivalent [`sparse.DOK`][] array. Examples -------- >>> s = DOK.from_numpy(np.eye(4)) >>> s """ ar = cls(x.shape, dtype=x.dtype) coords = np.nonzero(x) data = x[coords] for c in zip(data, *coords, strict=True): d, c = c[0], c[1:] ar.data[c] = d return ar @property def nnz(self): """ The number of nonzero elements in this array. Returns ------- int The number of nonzero elements. See Also -------- - [`sparse.COO.nnz`][] : Equivalent [`sparse.COO`][] array property. - [`numpy.count_nonzero`][] : A similar Numpy function. - [`scipy.sparse.coo_matrix.nnz`][] : The Scipy equivalent property. Examples -------- >>> values = { ... (1, 2, 3): 4, ... (3, 2, 1): 0, ... } >>> s = DOK((5, 5, 5), values) >>> s.nnz 1 """ return len(self.data) @property def format(self): """ The storage format of this array. Returns ------- str The storage format of this array. See Also ------- [`scipy.sparse.dok_matrix.format`][] : The Scipy equivalent property. Examples ------- >>> import sparse >>> s = sparse.random((5, 5), density=0.2, format="dok") >>> s.format 'dok' >>> t = sparse.random((5, 5), density=0.2, format="coo") >>> t.format 'coo' """ return "dok" @property def nbytes(self): """ The number of bytes taken up by this object. Note that for small arrays, this may undercount the number of bytes due to the large constant overhead. Returns ------- int The approximate bytes of memory taken by this object. See Also -------- [`numpy.ndarray.nbytes`][] : The equivalent Numpy property. Examples -------- >>> import sparse >>> x = sparse.random((100, 100), density=0.1, format="dok") >>> x.nbytes 8000 """ return self.nnz * self.dtype.itemsize def __getitem__(self, key): if not isinstance(key, tuple): key = (key,) if all(isinstance(k, Iterable) for k in key): if len(key) != self.ndim: raise NotImplementedError(f"Index sequences for all {self.ndim} array dimensions needed!") if not all(len(key[0]) == len(k) for k in key): raise IndexError("Unequal length of index sequences!") return self._fancy_getitem(key) key = normalize_index(key, self.shape) ret = self.asformat("coo")[key] if isinstance(ret, SparseArray): ret = ret.asformat("dok") return ret def _fancy_getitem(self, key): """Subset of fancy indexing, when all dimensions are accessed""" new_data = {} for i, k in enumerate(zip(*key, strict=True)): if k in self.data: new_data[i] = self.data[k] return DOK( shape=(len(key[0])), data=new_data, dtype=self.dtype, fill_value=self.fill_value, ) def __setitem__(self, key, value): value = np.asarray(value, dtype=self.dtype) # 1D fancy indexing if self.ndim == 1 and isinstance(key, Iterable) and all(isinstance(i, int | np.integer) for i in key): key = (key,) if isinstance(key, tuple) and all(isinstance(k, Iterable) for k in key): if len(key) != self.ndim: raise NotImplementedError(f"Index sequences for all {self.ndim} array dimensions needed!") if not all(len(key[0]) == len(k) for k in key): raise IndexError("Unequal length of index sequences!") self._fancy_setitem(key, value) return key = normalize_index(key, self.shape) key_list = [int(k) if isinstance(k, Integral) else k for k in key] self._setitem(key_list, value) def _fancy_setitem(self, idxs, values): idxs = tuple(np.asanyarray(idxs) for idxs in idxs) if not all(np.issubdtype(k.dtype, np.integer) for k in idxs): raise IndexError("Indices must be sequences of integer types!") if idxs[0].ndim != 1: raise IndexError("Indices are not 1d sequences!") if values.ndim == 0: values = np.full(idxs[0].size, values, self.dtype) elif values.ndim > 1: raise ValueError(f"Dimension of values ({values.ndim}) must be 0 or 1!") if not idxs[0].shape == values.shape: raise ValueError(f"Shape mismatch of indices ({idxs[0].shape}) and values ({values.shape})!") fill_value = self.fill_value data = self.data for idx, value in zip(zip(*idxs, strict=True), values, strict=True): if value != fill_value: data[idx] = value elif idx in data: del data[idx] def _setitem(self, key_list, value): value_missing_dims = len([ind for ind in key_list if isinstance(ind, slice)]) - value.ndim if value_missing_dims < 0: raise ValueError("setting an array element with a sequence.") for i, ind in enumerate(key_list): if isinstance(ind, slice): step = ind.step if ind.step is not None else 1 if step > 0: start = ind.start if ind.start is not None else 0 start = max(start, 0) stop = ind.stop if ind.stop is not None else self.shape[i] stop = min(stop, self.shape[i]) if start > stop: start = stop else: start = ind.start or self.shape[i] - 1 stop = ind.stop if ind.stop is not None else -1 start = min(start, self.shape[i] - 1) stop = max(stop, -1) if start < stop: start = stop key_list_temp = key_list[:] for v_idx, ki in enumerate(range(start, stop, step)): key_list_temp[i] = ki vi = value if value_missing_dims > 0 else (value[0] if value.shape[0] == 1 else value[v_idx]) self._setitem(key_list_temp, vi) return if not isinstance(ind, Integral): raise IndexError("All indices must be slices or integers when setting an item.") key = tuple(key_list) if not equivalent(value, self.fill_value): self.data[key] = value[()] elif key in self.data: del self.data[key] def __str__(self): summary = f"" return self._str_impl(summary) __repr__ = __str__ def todense(self): """ Convert this [`sparse.DOK`][] array into a Numpy array. Returns ------- numpy.ndarray The equivalent dense array. See Also -------- - [`sparse.COO.todense`][] : Equivalent `COO` array method. - [`scipy.sparse.coo_matrix.todense`][] : Equivalent Scipy method. Examples -------- >>> s = DOK((5, 5)) >>> s[1:3, 1:3] = [[4, 5], [6, 7]] >>> s.todense() # doctest: +SKIP array([[0., 0., 0., 0., 0.], [0., 4., 5., 0., 0.], [0., 6., 7., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]]) """ result = np.full(self.shape, self.fill_value, self.dtype) for c, d in self.data.items(): result[c] = d return result def asformat(self, format, **kwargs): """ Convert this sparse array to a given format. Parameters ---------- format : str A format string. Returns ------- out : SparseArray The converted array. Raises ------ NotImplementedError If the format isn't supported. """ from ._utils import convert_format format = convert_format(format) if format == "dok": return self if format == "coo": from ._coo import COO if len(kwargs) != 0: raise ValueError(f"Extra kwargs found: {kwargs}") return COO.from_iter( self.data, shape=self.shape, fill_value=self.fill_value, dtype=self.dtype, ) return self.asformat("coo").asformat(format, **kwargs) def reshape(self, shape, order="C"): """ Returns a new [`sparse.DOK`][] array that is a reshaped version of this array. Parameters ---------- shape : tuple[int] The desired shape of the output array. Returns ------- DOK The reshaped output array. See Also -------- [`numpy.ndarray.reshape`][] : The equivalent Numpy function. Notes ----- The `order` parameter is provided just for compatibility with Numpy and isn't actually supported. Examples -------- >>> s = DOK.from_numpy(np.arange(25)) >>> s2 = s.reshape((5, 5)) >>> s2.todense() # doctest: +NORMALIZE_WHITESPACE array([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19], [20, 21, 22, 23, 24]]) """ if order not in {"C", None}: raise NotImplementedError("The 'order' parameter is not supported") return DOK.from_coo(self.to_coo().reshape(shape)) def to_slice(k): """Convert integer indices to one-element slices for consistency""" if isinstance(k, Integral): return slice(k, k + 1, 1) return k sparse-0.17.0/sparse/numba_backend/_io.py000066400000000000000000000074321501262445000202670ustar00rootroot00000000000000import numpy as np from ._compressed import GCXS from ._coo.core import COO def save_npz(filename, matrix, compressed=True): """Save a sparse matrix to disk in numpy's `.npz` format. Note: This is not binary compatible with scipy's `save_npz()`. This binary format is not currently stable. Will save a file that can only be opend with this package's `load_npz()`. Parameters ---------- filename : string or file Either the file name (string) or an open file (file-like object) where the data will be saved. If file is a string or a Path, the `.npz` extension will be appended to the file name if it is not already there matrix : SparseArray The matrix to save to disk compressed : bool Whether to save in compressed or uncompressed mode Examples -------- Store sparse matrix to disk, and load it again: >>> import os >>> import sparse >>> import numpy as np >>> dense_mat = np.array([[[0.0, 0.0], [0.0, 0.70677779]], [[0.0, 0.0], [0.0, 0.86522495]]]) >>> mat = sparse.COO(dense_mat) >>> mat >>> sparse.save_npz("mat.npz", mat) >>> loaded_mat = sparse.load_npz("mat.npz") >>> loaded_mat >>> os.remove("mat.npz") See Also -------- - [`sparse.load_npz`][] - [`scipy.sparse.save_npz`][] - [`scipy.sparse.load_npz`][] - [`numpy.savez`][] - [`numpy.load`][] """ nodes = { "data": matrix.data, "shape": matrix.shape, "fill_value": matrix.fill_value, } if type(matrix) is COO: nodes["coords"] = matrix.coords elif type(matrix) is GCXS: nodes["indices"] = matrix.indices nodes["indptr"] = matrix.indptr nodes["compressed_axes"] = matrix.compressed_axes if compressed: np.savez_compressed(filename, **nodes) else: np.savez(filename, **nodes) def load_npz(filename): """Load a sparse matrix in numpy's `.npz` format from disk. Note: This is not binary compatible with scipy's `save_npz()` output. This binary format is not currently stable. Will only load files saved by this package. Parameters ---------- filename : file-like object, string, or pathlib.Path The file to read. File-like objects must support the `seek()` and `read()` methods. Returns ------- SparseArray The sparse matrix at path `filename`. Examples -------- See [`sparse.save_npz`][] for usage examples. See Also -------- - [`sparse.save_npz`][] - [`scipy.sparse.save_npz`][] - [`scipy.sparse.load_npz`][] - [`numpy.savez`][] - [`numpy.load`][] """ with np.load(filename) as fp: try: coords = fp["coords"] data = fp["data"] shape = tuple(fp["shape"]) fill_value = fp["fill_value"][()] return COO( coords=coords, data=data, shape=shape, sorted=True, has_duplicates=False, fill_value=fill_value, ) except KeyError: pass try: data = fp["data"] indices = fp["indices"] indptr = fp["indptr"] comp_axes = fp["compressed_axes"] shape = tuple(fp["shape"]) fill_value = fp["fill_value"][()] return GCXS( (data, indices, indptr), shape=shape, fill_value=fill_value, compressed_axes=comp_axes, ) except KeyError as e: raise RuntimeError(f"The file {filename!s} does not contain a valid sparse matrix") from e sparse-0.17.0/sparse/numba_backend/_numba_extension.py000066400000000000000000000002701501262445000230470ustar00rootroot00000000000000def _init_extension(): """ Load extensions when numba is loaded. This name must match the one in pyproject.toml """ from ._coo import numba_extension # noqa: F401 sparse-0.17.0/sparse/numba_backend/_settings.py000066400000000000000000000024271501262445000215170ustar00rootroot00000000000000import os import numpy as np AUTO_DENSIFY = bool(int(os.environ.get("SPARSE_AUTO_DENSIFY", "0"))) WARN_ON_TOO_DENSE = bool(int(os.environ.get("SPARSE_WARN_ON_TOO_DENSE", "0"))) IS_NUMPY2 = np.lib.NumpyVersion(np.__version__) >= "2.0.0a1" def _is_nep18_enabled(): class A: def __array_function__(self, *args, **kwargs): return True try: return np.concatenate([A()]) except ValueError: return False NEP18_ENABLED = _is_nep18_enabled() class ArrayNamespaceInfo: def __init__(self): self.np_info = np.__array_namespace_info__() def capabilities(self): np_capabilities = self.np_info.capabilities() return { "boolean indexing": True, "data-dependent shapes": True, "max dimensions": np_capabilities.get("max dimensions", 64) - 1, } def default_device(self): return self.np_info.default_device() def default_dtypes(self, *, device=None): return self.np_info.default_dtypes(device=device) def devices(self): return self.np_info.devices() def dtypes(self, *, device=None, kind=None): return self.np_info.dtypes(device=device, kind=kind) def __array_namespace_info__() -> ArrayNamespaceInfo: return ArrayNamespaceInfo() sparse-0.17.0/sparse/numba_backend/_slicing.py000066400000000000000000000210431501262445000213020ustar00rootroot00000000000000# Most of this file is taken from https://github.com/dask/dask/blob/main/dask/array/slicing.py # See license at https://github.com/dask/dask/blob/main/LICENSE.txt import math from collections.abc import Iterable from numbers import Integral, Number import numpy as np def normalize_index(idx, shape): """Normalize slicing indexes 1. Replaces ellipses with many full slices 2. Adds full slices to end of index 3. Checks bounding conditions 4. Replaces numpy arrays with lists 5. Posify's slices integers and lists 6. Normalizes slices to canonical form Examples -------- >>> normalize_index(1, (10,)) (1,) >>> normalize_index(-1, (10,)) (9,) >>> normalize_index([-1], (10,)) (array([9]),) >>> normalize_index(slice(-3, 10, 1), (10,)) (slice(7, 10, 1),) >>> normalize_index((Ellipsis, None), (10,)) (slice(0, 10, 1), None) """ if not isinstance(idx, tuple): idx = (idx,) idx = replace_ellipsis(len(shape), idx) n_sliced_dims = 0 for i in idx: if hasattr(i, "ndim") and i.ndim >= 1: n_sliced_dims += i.ndim elif i is None: continue else: n_sliced_dims += 1 idx += (slice(None),) * (len(shape) - n_sliced_dims) if len([i for i in idx if i is not None]) > len(shape): raise IndexError("Too many indices for array") none_shape = [] i = 0 for ind in idx: if ind is not None: none_shape.append(shape[i]) i += 1 else: none_shape.append(None) for i, d in zip(idx, none_shape, strict=True): if d is not None: check_index(i, d) idx = tuple(map(sanitize_index, idx)) idx = tuple(map(replace_none, idx, none_shape)) idx = posify_index(none_shape, idx) return tuple(map(clip_slice, idx, none_shape)) def replace_ellipsis(n, index): """Replace ... with slices, :, : ,: >>> replace_ellipsis(4, (3, Ellipsis, 2)) (3, slice(None, None, None), slice(None, None, None), 2) >>> replace_ellipsis(2, (Ellipsis, None)) (slice(None, None, None), slice(None, None, None), None) """ # Careful about using in or index because index may contain arrays isellipsis = [i for i, ind in enumerate(index) if ind is Ellipsis] if not isellipsis: return index if len(isellipsis) > 1: raise IndexError("an index can only have a single ellipsis ('...')") loc = isellipsis[0] extra_dimensions = n - (len(index) - sum(i is None for i in index) - 1) return index[:loc] + (slice(None, None, None),) * extra_dimensions + index[loc + 1 :] def check_index(ind, dimension): """Check validity of index for a given dimension Examples -------- >>> check_index(3, 5) >>> check_index(5, 5) Traceback (most recent call last): ... IndexError: Index is not smaller than dimension 5 >= 5 >>> check_index(6, 5) Traceback (most recent call last): ... IndexError: Index is not smaller than dimension 6 >= 5 >>> check_index(-1, 5) >>> check_index(-6, 5) Traceback (most recent call last): ... IndexError: Negative index is not greater than negative dimension -6 <= -5 >>> check_index([1, 2], 5) >>> check_index([6, 3], 5) Traceback (most recent call last): ... IndexError: Index out of bounds for dimension 5 >>> check_index(slice(0, 3), 5) """ # unknown dimension, assumed to be in bounds if isinstance(ind, Iterable): x = np.asanyarray(ind) if np.issubdtype(x.dtype, np.integer) and ((x >= dimension) | (x < -dimension)).any(): raise IndexError(f"Index out of bounds for dimension {dimension:d}") if x.dtype == np.bool_ and len(x) != dimension: raise IndexError( f"boolean index did not match indexed array; dimension is {dimension:d} " f"but corresponding boolean dimension is {len(x):d}" ) elif isinstance(ind, slice): return elif not isinstance(ind, Integral): raise IndexError( "only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and " "integer or boolean arrays are valid indices" ) elif ind >= dimension: raise IndexError(f"Index is not smaller than dimension {ind:d} >= {dimension:d}") elif ind < -dimension: msg = "Negative index is not greater than negative dimension {:d} <= -{:d}" raise IndexError(msg.format(ind, dimension)) def sanitize_index(ind): """Sanitize the elements for indexing along one axis >>> sanitize_index([2, 3, 5]) array([2, 3, 5]) >>> sanitize_index([True, False, True, False]) array([0, 2]) >>> sanitize_index(np.array([1, 2, 3])) array([1, 2, 3]) >>> sanitize_index(np.array([False, True, True])) array([1, 2]) >>> type(sanitize_index(np.int32(0))) # doctest: +SKIP >>> sanitize_index(0.5) # doctest: +SKIP Traceback (most recent call last): ... IndexError: only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices """ if ind is None: return None if isinstance(ind, slice): return slice( _sanitize_index_element(ind.start), _sanitize_index_element(ind.stop), _sanitize_index_element(ind.step), ) if isinstance(ind, Number): return _sanitize_index_element(ind) if not hasattr(ind, "dtype") and len(ind) == 0: ind = np.array([], dtype=np.intp) ind = np.asarray(ind) if ind.dtype == np.bool_: nonzero = np.nonzero(ind) if len(nonzero) == 1: # If a 1-element tuple, unwrap the element nonzero = nonzero[0] return np.asanyarray(nonzero) if np.issubdtype(ind.dtype, np.integer): return ind raise IndexError( "only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and " "integer or boolean arrays are valid indices" ) def _sanitize_index_element(ind): """Sanitize a one-element index.""" if ind is None: return None return int(ind) def posify_index(shape, ind): """Flip negative indices around to positive ones >>> posify_index(10, 3) 3 >>> posify_index(10, -3) 7 >>> posify_index(10, [3, -3]) array([3, 7]) >>> posify_index((10, 20), (3, -3)) (3, 17) >>> posify_index((10, 20), (3, [3, 4, -3])) # doctest: +NORMALIZE_WHITESPACE (3, array([ 3, 4, 17])) """ if isinstance(ind, tuple): return tuple(map(posify_index, shape, ind)) if isinstance(ind, Integral): if ind < 0 and not math.isnan(shape): return ind + shape return ind if isinstance(ind, np.ndarray | list) and not math.isnan(shape): ind = np.asanyarray(ind) return np.where(ind < 0, ind + shape, ind) if isinstance(ind, slice): start, stop, step = ind.start, ind.stop, ind.step if start < 0: start += shape if not (0 > stop >= step) and stop < 0: stop += shape return slice(start, stop, ind.step) return ind def clip_slice(idx, dim): """ Clip slice to its effective size given the shape. Parameters ---------- idx : The index. dim : The size along the corresponding dimension. Returns ------- idx : slice Examples -------- >>> clip_slice(slice(0, 20, 1), 10) slice(0, 10, 1) """ if not isinstance(idx, slice): return idx start, stop, step = idx.start, idx.stop, idx.step if step > 0: start = max(start, 0) stop = min(stop, dim) if start > stop: start = stop else: start = min(start, dim - 1) stop = max(stop, -1) if start < stop: start = stop return slice(start, stop, step) def replace_none(idx, dim): """ Normalize slices to canonical form, i.e. replace ``None`` with the appropriate integers. Parameters ---------- idx : slice or other index dim : dimension length Examples -------- >>> replace_none(slice(None, None, None), 10) slice(0, 10, 1) """ if not isinstance(idx, slice): return idx start, stop, step = idx.start, idx.stop, idx.step if step is None: step = 1 if step > 0: if start is None: start = 0 if stop is None: stop = dim else: if start is None: start = dim - 1 if stop is None: stop = -1 return slice(start, stop, step) sparse-0.17.0/sparse/numba_backend/_sparse_array.py000066400000000000000000000737771501262445000223720ustar00rootroot00000000000000import contextlib import operator import warnings from abc import ABCMeta, abstractmethod from collections.abc import Callable, Iterable from functools import reduce from numbers import Integral import numpy as np from ._umath import elemwise from ._utils import _zero_of_dtype, equivalent, html_table, normalize_axis _reduce_super_ufunc = {np.add: np.multiply, np.multiply: np.power} class SparseArray: """ An abstract base class for all the sparse array classes. Attributes ---------- dtype : numpy.dtype The data type of this array. fill_value : scalar The fill value of this array. """ __metaclass__ = ABCMeta def __init__(self, shape, fill_value=None): if not isinstance(shape, Iterable): shape = (shape,) if not all(isinstance(sh, Integral) and int(sh) >= 0 for sh in shape): raise ValueError("shape must be an non-negative integer or a tuple of non-negative integers.") self.shape = tuple(int(sh) for sh in shape) if fill_value is not None: if not hasattr(fill_value, "dtype") or fill_value.dtype != self.dtype: self.fill_value = self.dtype.type(fill_value) else: self.fill_value = fill_value else: self.fill_value = _zero_of_dtype(self.dtype) dtype = None @property def device(self): data = getattr(self, "data", None) return getattr(data, "device", "cpu") def to_device(self, device, /, *, stream=None): if device != "cpu": raise ValueError("Only `device='cpu'` is supported.") return self @property @abstractmethod def nnz(self): """ The number of nonzero elements in this array. Note that any duplicates in `coords` are counted multiple times. Returns ------- int The number of nonzero elements in this array. See Also -------- - [`sparse.DOK.nnz`][] : Equivalent [`sparse.DOK`][] array property. - [`numpy.count_nonzero`][] : A similar Numpy function. - [`scipy.sparse.coo_matrix.nnz`][] : The Scipy equivalent property. Examples -------- >>> import numpy as np >>> from sparse import COO >>> x = np.array([0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 0]) >>> np.count_nonzero(x) 6 >>> s = COO.from_numpy(x) >>> s.nnz 6 >>> np.count_nonzero(x) == s.nnz True """ @property def ndim(self): """ The number of dimensions of this array. Returns ------- int The number of dimensions of this array. See Also -------- - [`sparse.DOK.ndim`][] : Equivalent property for [`sparse.DOK`][] arrays. - [`numpy.ndarray.ndim`][] : Numpy equivalent property. Examples -------- >>> from sparse import COO >>> import numpy as np >>> x = np.random.rand(1, 2, 3, 1, 2) >>> s = COO.from_numpy(x) >>> s.ndim 5 >>> s.ndim == x.ndim True """ return len(self.shape) @property def size(self): """ The number of all elements (including zeros) in this array. Returns ------- int The number of elements. See Also -------- [`numpy.ndarray.size`][] : Numpy equivalent property. Examples -------- >>> from sparse import COO >>> import numpy as np >>> x = np.zeros((10, 10)) >>> s = COO.from_numpy(x) >>> s.size 100 """ # We use this instead of np.prod because np.prod # returns a float64 for an empty shape. return reduce(operator.mul, self.shape, 1) @property def density(self): """ The ratio of nonzero to all elements in this array. Returns ------- float The ratio of nonzero to all elements. See Also -------- - [`sparse.COO.size`][] : Number of elements. - [`sparse.COO.nnz`][] : Number of nonzero elements. Examples -------- >>> import numpy as np >>> from sparse import COO >>> x = np.zeros((8, 8)) >>> x[0, :] = 1 >>> s = COO.from_numpy(x) >>> s.density 0.125 """ with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=RuntimeWarning) return float(np.float64(self.nnz) / np.float64(self.size)) def _repr_html_(self): """ Diagnostic report about this array. Renders in Jupyter. """ try: from matrepr import to_html from matrepr.adapters.sparse_driver import PyDataSparseDriver return to_html(PyDataSparseDriver.adapt(self), notebook=True) except (ImportError, ValueError): return html_table(self) def _str_impl(self, summary): """ A human-readable representation of this array, including a metadata summary and a tabular view of the array values. Values view only included if `matrepr` is available. Parameters ---------- summary A type-specific summary of this array, used as the first line of return value. Returns ------- str A human-readable representation of this array. """ try: from matrepr import to_str from matrepr.adapters.sparse_driver import PyDataSparseDriver values = to_str( PyDataSparseDriver.adapt(self), title=False, # disable matrepr description width_str=0, # autodetect terminal width max_cols=9999, ) return f"{summary}\n{values}" except (ImportError, ValueError): return summary @abstractmethod def asformat(self, format): """ Convert this sparse array to a given format. Parameters ---------- format : str A format string. Returns ------- out : SparseArray The converted array. Raises ------ NotImplementedError If the format isn't supported. """ @abstractmethod def todense(self): """ Convert this [`sparse.SparseArray`][] array to a dense [`numpy.ndarray`][]. Note that this may take a large amount of memory and time. Returns ------- numpy.ndarray The converted dense array. See Also -------- - [`sparse.DOK.todense`][] : Equivalent `DOK` array method. - [`sparse.COO.todense`][] : Equivalent `COO` array method. - [`scipy.sparse.coo_matrix.todense`][] : Equivalent Scipy method. Examples -------- >>> import sparse >>> x = np.random.randint(100, size=(7, 3)) >>> s = sparse.COO.from_numpy(x) >>> x2 = s.todense() >>> np.array_equal(x, x2) True """ def _make_shallow_copy_of(self, other): self.__dict__ = other.__dict__.copy() def __array__(self, *args, **kwargs): from ._settings import AUTO_DENSIFY if not AUTO_DENSIFY: raise RuntimeError( "Cannot convert a sparse array to dense automatically. To manually densify, use the todense method." ) return np.asarray(self.todense(), *args, **kwargs) def __array_function__(self, func, types, args, kwargs): import sparse as module sparse_func = None try: submodules = getattr(func, "__module__", "numpy").split(".")[1:] for submodule in submodules: module = getattr(module, submodule) sparse_func = getattr(module, func.__name__) except AttributeError: pass else: return sparse_func(*args, **kwargs) with contextlib.suppress(AttributeError): sparse_func = getattr(type(self), func.__name__) if not isinstance(sparse_func, Callable) and len(args) == 1 and len(kwargs) == 0: try: return getattr(self, func.__name__) except AttributeError: pass if sparse_func is None: return NotImplemented return sparse_func(*args, **kwargs) @staticmethod def _reduce(method, *args, **kwargs): from ._common import _is_scipy_sparse_obj assert len(args) == 1 self = args[0] if _is_scipy_sparse_obj(self): self = type(self).from_scipy_sparse(self) return self.reduce(method, **kwargs) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): out = kwargs.pop("out", None) if out is not None and not all(isinstance(x, type(self)) for x in out): return NotImplemented if getattr(ufunc, "signature", None) is not None: return self.__array_function__(ufunc, (np.ndarray, type(self)), inputs, kwargs) if out is not None: test_args = [np.empty((1,), dtype=a.dtype) if hasattr(a, "dtype") else a for a in inputs] test_kwargs = kwargs.copy() if method == "reduce": test_kwargs["axis"] = None test_out = tuple(np.empty((1,), dtype=a.dtype) for a in out) if len(test_out) == 1: test_out = test_out[0] getattr(ufunc, method)(*test_args, out=test_out, **test_kwargs) kwargs["dtype"] = out[0].dtype if method == "outer": method = "__call__" cum_ndim = 0 inputs_transformed = [] for inp in reversed(inputs): inputs_transformed.append(inp[(Ellipsis,) + (None,) * cum_ndim]) cum_ndim += inp.ndim inputs = tuple(reversed(inputs_transformed)) if method == "__call__": result = elemwise(ufunc, *inputs, **kwargs) elif method == "reduce": result = SparseArray._reduce(ufunc, *inputs, **kwargs) else: return NotImplemented if out is not None: (out,) = out if out.shape != result.shape: raise ValueError( f"non-broadcastable output operand with shape {out.shape} " f"doesn't match the broadcast shape {result.shape}" ) out._make_shallow_copy_of(result) return out return result def reduce(self, method, axis=(0,), keepdims=False, **kwargs): """ Performs a reduction operation on this array. Parameters ---------- method : numpy.ufunc The method to use for performing the reduction. axis : Union[int, Iterable[int]], optional The axes along which to perform the reduction. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. **kwargs : dict Any extra arguments to pass to the reduction operation. See Also -------- - [`numpy.ufunc.reduce`][] : A similar Numpy method. - [`sparse.COO.reduce`][] : This method implemented on COO arrays. - [`sparse.GCXS.reduce`][] : This method implemented on GCXS arrays. """ axis = normalize_axis(axis, self.ndim) zero_reduce_result = method.reduce([self.fill_value, self.fill_value], **kwargs) reduce_super_ufunc = _reduce_super_ufunc.get(method) if not equivalent(zero_reduce_result, self.fill_value) and reduce_super_ufunc is None: raise ValueError(f"Performing this reduction operation would produce a dense result: {method!s}") if not isinstance(axis, tuple): axis = (axis,) out = self._reduce_calc(method, axis, keepdims, **kwargs) if len(out) == 1: return out[0] data, counts, axis, n_cols, arr_attrs = out result_fill_value = self.fill_value if reduce_super_ufunc is None: missing_counts = counts != n_cols data[missing_counts] = method(data[missing_counts], self.fill_value, **kwargs) else: data = method( data, reduce_super_ufunc(self.fill_value, n_cols - counts), ).astype(data.dtype) result_fill_value = reduce_super_ufunc(self.fill_value, n_cols) out = self._reduce_return(data, arr_attrs, result_fill_value) if keepdims: shape = list(self.shape) for ax in axis: shape[ax] = 1 out = out.reshape(shape) if out.ndim == 0: return out[()] return out def _reduce_calc(self, method, axis, keepdims, **kwargs): raise NotImplementedError def _reduce_return(self, data, arr_attrs, result_fill_value): raise NotImplementedError def sum(self, axis=None, keepdims=False, dtype=None, out=None): """ Performs a sum operation along the given axes. Uses all axes by default. Parameters ---------- axis : Union[int, Iterable[int]], optional The axes along which to sum. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. dtype : numpy.dtype The data type of the output array. Returns ------- SparseArray The reduced output sparse array. See Also -------- - [`numpy.sum`][] : Equivalent numpy function. - [`scipy.sparse.coo_matrix.sum`][] : Equivalent Scipy function. """ return np.add.reduce(self, out=out, axis=axis, keepdims=keepdims, dtype=dtype) def max(self, axis=None, keepdims=False, out=None): """ Maximize along the given axes. Uses all axes by default. Parameters ---------- axis : Union[int, Iterable[int]], optional The axes along which to maximize. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. out : numpy.dtype The data type of the output array. Returns ------- SparseArray The reduced output sparse array. See Also -------- - [`numpy.max`][] : Equivalent numpy function. - [`scipy.sparse.coo_matrix.max`][] : Equivalent Scipy function. """ return np.maximum.reduce(self, out=out, axis=axis, keepdims=keepdims) amax = max def any(self, axis=None, keepdims=False, out=None): """ See if any values along array are ``True``. Uses all axes by default. Parameters ---------- axis : Union[int, Iterable[int]], optional The axes along which to minimize. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. Returns ------- SparseArray The reduced output sparse array. See Also -------- [`numpy.any`][] : Equivalent numpy function. """ return np.logical_or.reduce(self, out=out, axis=axis, keepdims=keepdims) def all(self, axis=None, keepdims=False, out=None): """ See if all values in an array are ``True``. Uses all axes by default. Parameters ---------- axis : Union[int, Iterable[int]], optional The axes along which to minimize. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. Returns ------- SparseArray The reduced output sparse array. See Also -------- [`numpy.all`][] : Equivalent numpy function. """ return np.logical_and.reduce(self, out=out, axis=axis, keepdims=keepdims) def min(self, axis=None, keepdims=False, out=None): """ Minimize along the given axes. Uses all axes by default. Parameters ---------- axis : Union[int, Iterable[int]], optional The axes along which to minimize. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. out : numpy.dtype The data type of the output array. Returns ------- SparseArray The reduced output sparse array. See Also -------- - [`numpy.min`][] : Equivalent numpy function. - [`scipy.sparse.coo_matrix.min`][] : Equivalent Scipy function. """ return np.minimum.reduce(self, out=out, axis=axis, keepdims=keepdims) amin = min def prod(self, axis=None, keepdims=False, dtype=None, out=None): """ Performs a product operation along the given axes. Uses all axes by default. Parameters ---------- axis : Union[int, Iterable[int]], optional The axes along which to multiply. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. dtype : numpy.dtype The data type of the output array. Returns ------- SparseArray The reduced output sparse array. See Also -------- [`numpy.prod`][] : Equivalent numpy function. """ return np.multiply.reduce(self, out=out, axis=axis, keepdims=keepdims, dtype=dtype) def round(self, decimals=0, out=None): """ Evenly round to the given number of decimals. See Also -------- - [`numpy.round`][] : NumPy equivalent ufunc. - [`sparse.elemwise`][] : Apply an arbitrary element-wise function to one or two arguments. """ if out is not None and not isinstance(out, tuple): out = (out,) return self.__array_ufunc__(np.round, "__call__", self, decimals=decimals, out=out) round_ = round def clip(self, min=None, max=None, out=None): """ Clip (limit) the values in the array. Return an array whose values are limited to ``[min, max]``. One of min or max must be given. See Also -------- - [sparse.clip][] : For full documentation and more details. - [`numpy.clip`][] : Equivalent NumPy function. """ if out is not None and not isinstance(out, tuple): out = (out,) return self.__array_ufunc__(np.clip, "__call__", self, a_min=min, a_max=max, out=out) def astype(self, dtype, casting="unsafe", copy=True): """ Copy of the array, cast to a specified type. See Also -------- - [`scipy.sparse.coo_matrix.astype`][] : SciPy sparse equivalent function - [`numpy.ndarray.astype`][] : NumPy equivalent ufunc. - [`sparse.elemwise`][] : Apply an arbitrary element-wise function to one or two arguments. """ # this matches numpy's behavior if self.dtype == dtype and not copy: return self return self.__array_ufunc__(np.ndarray.astype, "__call__", self, dtype=dtype, copy=copy, casting=casting) def mean(self, axis=None, keepdims=False, dtype=None, out=None): """ Compute the mean along the given axes. Uses all axes by default. Parameters ---------- axis : Union[int, Iterable[int]], optional The axes along which to compute the mean. Uses all axes by default. keepdims : bool, optional Whether or not to keep the dimensions of the original array. dtype : numpy.dtype The data type of the output array. Returns ------- SparseArray The reduced output sparse array. See Also -------- - [`numpy.ndarray.mean`][] : Equivalent numpy method. - [`scipy.sparse.coo_matrix.mean`][] : Equivalent Scipy method. Notes ----- * The `out` parameter is provided just for compatibility with Numpy and isn't actually supported. Examples -------- You can use [`sparse.COO.mean`][] to compute the mean of an array across any dimension. >>> from sparse import COO >>> x = np.array([[1, 2, 0, 0], [0, 1, 0, 0]], dtype="i8") >>> s = COO.from_numpy(x) >>> s2 = s.mean(axis=1) >>> s2.todense() # doctest: +SKIP array([0.5, 1.5, 0., 0.]) You can also use the `keepdims` argument to keep the dimensions after the mean. >>> s3 = s.mean(axis=0, keepdims=True) >>> s3.shape (1, 4) You can pass in an output datatype, if needed. >>> s4 = s.mean(axis=0, dtype=np.float16) >>> s4.dtype dtype('float16') By default, this reduces the array down to one number, computing the mean along all axes. >>> s.mean() np.float64(0.5) """ if axis is None: axis = tuple(range(self.ndim)) elif not isinstance(axis, tuple): axis = (axis,) den = reduce(operator.mul, (self.shape[i] for i in axis), 1) if dtype is None: if issubclass(self.dtype.type, np.integer | np.bool_): dtype = inter_dtype = np.dtype("f8") else: dtype = self.dtype inter_dtype = np.dtype("f4") if issubclass(dtype.type, np.float16) else dtype else: inter_dtype = dtype num = self.sum(axis=axis, keepdims=keepdims, dtype=inter_dtype) if num.ndim: out = np.true_divide(num, den, casting="unsafe") return out.astype(dtype) if out.dtype != dtype else out return np.divide(num, den, dtype=dtype, out=out) def var(self, axis=None, dtype=None, out=None, ddof=0, keepdims=False): """ Compute the variance along the given axes. Uses all axes by default. Parameters ---------- axis : Union[int, Iterable[int]], optional The axes along which to compute the variance. Uses all axes by default. dtype : numpy.dtype, optional The output datatype. out : SparseArray, optional The array to write the output to. ddof : int The degrees of freedom. keepdims : bool, optional Whether or not to keep the dimensions of the original array. Returns ------- SparseArray The reduced output sparse array. See Also -------- [`numpy.ndarray.var`][] : Equivalent numpy method. Examples -------- You can use [`sparse.COO.var`][] to compute the variance of an array across any dimension. >>> from sparse import COO >>> x = np.array([[1, 2, 0, 0], [0, 1, 0, 0]], dtype="i8") >>> s = COO.from_numpy(x) >>> s2 = s.var(axis=1) >>> s2.todense() # doctest: +SKIP array([0.6875, 0.1875]) You can also use the `keepdims` argument to keep the dimensions after the variance. >>> s3 = s.var(axis=0, keepdims=True) >>> s3.shape (1, 4) You can pass in an output datatype, if needed. >>> s4 = s.var(axis=0, dtype=np.float16) >>> s4.dtype dtype('float16') By default, this reduces the array down to one number, computing the variance along all axes. >>> s.var() np.float64(0.5) """ axis = normalize_axis(axis, self.ndim) if axis is None: axis = tuple(range(self.ndim)) if not isinstance(axis, tuple): axis = (axis,) rcount = reduce(operator.mul, (self.shape[a] for a in axis), 1) # Make this warning show up on top. if ddof >= rcount: warnings.warn("Degrees of freedom <= 0 for slice", RuntimeWarning, stacklevel=1) # Cast bool, unsigned int, and int to float64 by default if dtype is None and issubclass(self.dtype.type, np.integer | np.bool_): dtype = np.dtype("f8") arrmean = self.sum(axis, dtype=dtype, keepdims=True)[...] np.divide(arrmean, rcount, out=arrmean) x = self - arrmean if issubclass(self.dtype.type, np.complexfloating): x = x.real * x.real + x.imag * x.imag else: x = np.multiply(x, x, out=x) ret = x.sum(axis=axis, dtype=dtype, out=out, keepdims=keepdims) # Compute degrees of freedom and make sure it is not negative. rcount = max([rcount - ddof, 0]) ret = ret[...] np.divide(ret, rcount, out=ret, casting="unsafe") return ret[()] def std(self, axis=None, dtype=None, out=None, ddof=0, keepdims=False): """ Compute the standard deviation along the given axes. Uses all axes by default. Parameters ---------- axis : Union[int, Iterable[int]], optional The axes along which to compute the standard deviation. Uses all axes by default. dtype : numpy.dtype, optional The output datatype. out : SparseArray, optional The array to write the output to. ddof : int The degrees of freedom. keepdims : bool, optional Whether or not to keep the dimensions of the original array. Returns ------- SparseArray The reduced output sparse array. See Also -------- [`numpy.ndarray.std`][] : Equivalent numpy method. Examples -------- You can use [`sparse.COO.std`][] to compute the standard deviation of an array across any dimension. >>> from sparse import COO >>> x = np.array([[1, 2, 0, 0], [0, 1, 0, 0]], dtype="i8") >>> s = COO.from_numpy(x) >>> s2 = s.std(axis=1) >>> s2.todense() # doctest: +SKIP array([0.8291562, 0.4330127]) You can also use the `keepdims` argument to keep the dimensions after the standard deviation. >>> s3 = s.std(axis=0, keepdims=True) >>> s3.shape (1, 4) You can pass in an output datatype, if needed. >>> s4 = s.std(axis=0, dtype=np.float16) >>> s4.dtype dtype('float16') By default, this reduces the array down to one number, computing the standard deviation along all axes. >>> s.std() # doctest: +SKIP 0.7071067811865476 """ ret = self.var(axis=axis, dtype=dtype, out=out, ddof=ddof, keepdims=keepdims) return np.sqrt(ret) @property def real(self): """The real part of the array. Examples -------- >>> from sparse import COO >>> x = COO.from_numpy([1 + 0j, 0 + 1j]) >>> x.real.todense() # doctest: +SKIP array([1., 0.]) >>> x.real.dtype dtype('float64') Returns ------- out : SparseArray The real component of the array elements. If the array dtype is real, the dtype of the array is used for the output. If the array is complex, the output dtype is float. See Also -------- - [`numpy.ndarray.real`][] : NumPy equivalent attribute. - [`numpy.real`][] : NumPy equivalent function. """ return self.__array_ufunc__(np.real, "__call__", self) @property def imag(self): """The imaginary part of the array. Examples -------- >>> from sparse import COO >>> x = COO.from_numpy([1 + 0j, 0 + 1j]) >>> x.imag.todense() # doctest: +SKIP array([0., 1.]) >>> x.imag.dtype dtype('float64') Returns ------- out : SparseArray The imaginary component of the array elements. If the array dtype is real, the dtype of the array is used for the output. If the array is complex, the output dtype is float. See Also -------- - [`numpy.ndarray.imag`][] : NumPy equivalent attribute. - [`numpy.imag`][] : NumPy equivalent function. """ return self.__array_ufunc__(np.imag, "__call__", self) def conj(self): """Return the complex conjugate, element-wise. The complex conjugate of a complex number is obtained by changing the sign of its imaginary part. Examples -------- >>> from sparse import COO >>> x = COO.from_numpy([1 + 2j, 2 - 1j]) >>> res = x.conj() >>> res.todense() # doctest: +SKIP array([1.-2.j, 2.+1.j]) >>> res.dtype dtype('complex128') Returns ------- out : SparseArray The complex conjugate, with same dtype as the input. See Also -------- - [`numpy.ndarray.conj`][] : NumPy equivalent method. - [`numpy.conj`][] : NumPy equivalent function. """ return np.conj(self) def __array_namespace__(self, *, api_version=None): if api_version is None: api_version = "2024.12" if api_version not in {"2021.12", "2022.12", "2023.12", "2024.12"}: raise ValueError(f'"{api_version}" Array API version not supported.') import sparse return sparse def __bool__(self): """ """ return self._to_scalar(bool) def __float__(self): """ """ return self._to_scalar(float) def __int__(self): """ """ return self._to_scalar(int) def __index__(self): """ """ return self._to_scalar(int) def __complex__(self): """ """ return self._to_scalar(complex) def _to_scalar(self, builtin): if self.size != 1 or self.shape != (): raise ValueError(f"{builtin} can be computed for one-element arrays only.") return builtin(self.todense().flatten()[0]) @abstractmethod def isinf(self): """ """ @abstractmethod def isnan(self): """ """ sparse-0.17.0/sparse/numba_backend/_umath.py000066400000000000000000000570521501262445000210010ustar00rootroot00000000000000import itertools import operator from functools import reduce from itertools import zip_longest import numba import numpy as np from ._utils import _zero_of_dtype, equivalent, isscalar def elemwise(func, *args, **kwargs): """ Apply a function to any number of arguments. Parameters ---------- func : Callable The function to apply. Must support broadcasting. *args : tuple, optional The arguments to the function. Can be [`sparse.SparseArray`][] objects or [`scipy.sparse.spmatrix`][] objects. **kwargs : dict, optional Any additional arguments to pass to the function. Returns ------- SparseArray The result of applying the function. Raises ------ ValueError If the operation would result in a dense matrix, or if the operands don't have broadcastable shapes. See Also -------- [`numpy.ufunc`][] : A similar Numpy construct. Note that any `ufunc` can be used as the `func` input to this function. Notes ----- Previously, operations with Numpy arrays were sometimes supported. Now, it is necessary to convert Numpy arrays to [`sparse.COO`][] objects. """ return _Elemwise(func, *args, **kwargs).get_result() @numba.jit(nopython=True, nogil=True) def _match_arrays(a, b): # pragma: no cover """ Finds all indexes into a and b such that a[i] = b[j]. The outputs are sorted in lexographical order. Parameters ---------- a, b : np.ndarray The input 1-D arrays to match. If matching of multiple fields is needed, use np.recarrays. These two arrays must be sorted. Returns ------- a_idx, b_idx : np.ndarray The output indices of every possible pair of matching elements. """ if len(a) == 0 or len(b) == 0: return np.empty(0, dtype=np.uintp), np.empty(0, dtype=np.uintp) a_ind, b_ind = [], [] nb = len(b) ib = 0 match = 0 for ia, j in enumerate(a): if j == b[match]: ib = match while ib < nb and j >= b[ib]: if j == b[ib]: a_ind.append(ia) b_ind.append(ib) if b[match] < b[ib]: match = ib ib += 1 return np.array(a_ind, dtype=np.uintp), np.array(b_ind, dtype=np.uintp) def _get_nary_broadcast_shape(*shapes): """ Broadcast any number of shapes to a result shape. Parameters ---------- *shapes : tuple[tuple[int]] The shapes to broadcast. Returns ------- tuple[int] The output shape. Raises ------ ValueError If the input shapes cannot be broadcast to a single shape. """ result_shape = () for shape in shapes: try: result_shape = _get_broadcast_shape(shape, result_shape) except ValueError as e: # noqa: PERF203 shapes_str = ", ".join(str(shape) for shape in shapes) raise ValueError(f"operands could not be broadcast together with shapes {shapes_str}") from e return result_shape def _get_broadcast_shape(shape1, shape2, is_result=False): """ Get the overall broadcasted shape. Parameters ---------- shape1, shape2 : tuple[int] The input shapes to broadcast together. is_result : bool Whether or not shape2 is also the result shape. Returns ------- result_shape : tuple[int] The overall shape of the result. Raises ------ ValueError If the two shapes cannot be broadcast together. """ # https://stackoverflow.com/a/47244284/774273 if not all( (l1 == l2) or (l1 == 1) or ((l2 == 1) and not is_result) for l1, l2 in zip(shape1[::-1], shape2[::-1], strict=False) ): raise ValueError(f"operands could not be broadcast together with shapes {shape1}, {shape2}") return tuple(l1 if l1 != 1 else l2 for l1, l2 in zip_longest(shape1[::-1], shape2[::-1], fillvalue=1))[::-1] def _get_broadcast_parameters(shape, broadcast_shape): """ Get the broadcast parameters. Parameters ---------- shape : tuple[int] The input shape. broadcast_shape The shape to broadcast to. Returns ------- params : list A list containing None if the dimension isn't in the original array, False if it needs to be broadcast, and True if it doesn't. """ return [ None if l1 is None else l1 == l2 for l1, l2 in zip_longest(shape[::-1], broadcast_shape[::-1], fillvalue=None) ][::-1] def _get_reduced_coords(coords, params): """ Gets only those dimensions of the coordinates that don't need to be broadcast. Parameters ---------- coords : np.ndarray The coordinates to reduce. params : list The params from which to check which dimensions to get. Returns ------- reduced_coords : np.ndarray The reduced coordinates. """ reduced_params = [bool(param) for param in params] return coords[reduced_params] def _get_reduced_shape(shape, params): """ Gets only those dimensions of the coordinates that don't need to be broadcast. Parameters ---------- shape : np.ndarray The coordinates to reduce. params : list The params from which to check which dimensions to get. Returns ------- reduced_coords : np.ndarray The reduced coordinates. """ return tuple(sh for sh, p in zip(shape, params, strict=True) if p) def _get_expanded_coords_data(coords, data, params, broadcast_shape): """ Expand coordinates/data to broadcast_shape. Does most of the heavy lifting for broadcast_to. Produces sorted output for sorted inputs. Parameters ---------- coords : np.ndarray The coordinates to expand. data : np.ndarray The data corresponding to the coordinates. params : list The broadcast parameters. broadcast_shape : tuple[int] The shape to broadcast to. Returns ------- expanded_coords : np.ndarray List of 1-D arrays. Each item in the list has one dimension of coordinates. expanded_data : np.ndarray The data corresponding to expanded_coords. """ first_dim = -1 expand_shapes = [] for d, p, sh in zip(range(len(broadcast_shape)), params, broadcast_shape, strict=True): if p and first_dim == -1: expand_shapes.append(coords.shape[1]) first_dim = d if not p: expand_shapes.append(sh) all_idx = _cartesian_product(*(np.arange(d, dtype=np.intp) for d in expand_shapes)) false_dim = 0 dim = 0 expanded_coords = np.empty((len(broadcast_shape), all_idx.shape[1]), dtype=np.intp) if first_dim != -1: expanded_data = data[all_idx[first_dim]] else: expanded_coords = all_idx if len(data) else np.empty((0, all_idx.shape[1]), dtype=np.intp) expanded_data = np.repeat(data, reduce(operator.mul, broadcast_shape, 1)) return np.asarray(expanded_coords), np.asarray(expanded_data) for d, p in zip(range(len(broadcast_shape)), params, strict=True): if p: expanded_coords[d] = coords[dim, all_idx[first_dim]] else: expanded_coords[d] = all_idx[false_dim + (d > first_dim)] false_dim += 1 if p is not None: dim += 1 return np.asarray(expanded_coords), np.asarray(expanded_data) # (c) senderle # Taken from https://stackoverflow.com/a/11146645/774273 # License: https://creativecommons.org/licenses/by-sa/3.0/ def _cartesian_product(*arrays): """ Get the cartesian product of a number of arrays. Parameters ---------- *arrays : Tuple[np.ndarray] The arrays to get a cartesian product of. Always sorted with respect to the original array. Returns ------- out : np.ndarray The overall cartesian product of all the input arrays. """ broadcastable = np.ix_(*arrays) broadcasted = np.broadcast_arrays(*broadcastable) rows, cols = np.prod(broadcasted[0].shape), len(broadcasted) dtype = np.result_type(*arrays) out = np.empty(rows * cols, dtype=dtype) start, end = 0, rows for a in broadcasted: out[start:end] = a.reshape(-1) start, end = end, end + rows return out.reshape(cols, rows) def _get_matching_coords(coords, params): """ Get the matching coords across a number of broadcast operands. Parameters ---------- coords : list[`numpy.ndarray`] The input coordinates. params : list[Union[bool, none]] The broadcast parameters. Returns ------- numpy.ndarray The broacasted coordinates """ matching_coords = [] dims = np.zeros(len(coords), dtype=np.uint8) for p_all in zip(*params, strict=True): for i, p in enumerate(p_all): if p: matching_coords.append(coords[i][dims[i]]) break else: matching_coords.append(coords[dims[0]]) for i, p in enumerate(p_all): if p is not None: dims[i] += 1 return np.asarray(matching_coords, dtype=np.intp) def broadcast_to(x, shape): """ Performs the equivalent of `numpy.broadcast_to` for `COO`. Note that this function returns a new array instead of a view. Parameters ---------- shape : tuple[int] The shape to broadcast the data to. Returns ------- COO The broadcasted sparse array. Raises ------ ValueError If the operand cannot be broadcast to the given shape. See Also -------- :obj:`numpy.broadcast_to` : NumPy equivalent function """ from ._coo import COO if shape == x.shape: return x result_shape = _get_broadcast_shape(x.shape, shape, is_result=True) params = _get_broadcast_parameters(x.shape, result_shape) coords, data = _get_expanded_coords_data(x.coords, x.data, params, result_shape) # Check if all the non-broadcast axes are next to each other nonbroadcast_idx = [idx for idx, p in enumerate(params) if p] diff_nonbroadcast_idx = [a - b for a, b in zip(nonbroadcast_idx[1:], nonbroadcast_idx[:-1], strict=True)] sorted = all(d == 1 for d in diff_nonbroadcast_idx) return COO( coords, data, shape=result_shape, has_duplicates=False, sorted=sorted, fill_value=x.fill_value, ) class _Elemwise: def __init__(self, func, *args, **kwargs): """ Initialize the element-wise function calculator. Parameters ---------- func : types.Callable The function to compute *args : tuple[Union[SparseArray, ndarray, scipy.sparse.spmatrix]] The arguments to compute the function on. **kwargs : dict Extra arguments to pass to the function. """ from ._common import _is_scipy_sparse_obj from ._compressed import GCXS from ._coo import COO from ._dok import DOK from ._sparse_array import SparseArray processed_args = [] out_type = GCXS out_kwargs = {} sparse_args = [arg for arg in args if isinstance(arg, SparseArray)] if len(sparse_args) == 0: raise ValueError(f"None of the args is sparse: {args}") if all(isinstance(arg, DOK) for arg in sparse_args): out_type = DOK elif all(isinstance(arg, GCXS) for arg in sparse_args): out_type = GCXS if len({arg.compressed_axes for arg in sparse_args}) == 1: out_kwargs["compressed_axes"] = sparse_args[0].compressed_axes else: out_type = COO for arg in args: if _is_scipy_sparse_obj(arg): processed_args.append(COO.from_scipy_sparse(arg)) elif isscalar(arg) or isinstance(arg, np.ndarray): # Faster and more reliable to pass ()-shaped ndarrays as scalars. processed_args.append(arg) elif isinstance(arg, SparseArray): if not isinstance(arg, COO): arg = arg.asformat(COO) if arg.ndim == 0: arg = arg.todense() processed_args.append(arg) else: self.args = None return self.out_type = out_type self.out_kwargs = out_kwargs self.args = tuple(processed_args) self.func = func self.dtype = kwargs.pop("dtype", None) self.kwargs = kwargs self.cache = {} self._dense_result = False self._check_broadcast() self._get_fill_value() def get_result(self): from ._coo import COO if self.args is None: return NotImplemented if self._dense_result: args = [a.todense() if isinstance(a, COO) else a for a in self.args] return self.func(*args, **self.kwargs) if any(s == 0 for s in self.shape): data = np.empty((0,), dtype=self.fill_value.dtype) coords = np.empty((0, len(self.shape)), dtype=np.intp) return COO( coords, data, shape=self.shape, has_duplicates=False, fill_value=self.fill_value, ) data_list = [] coords_list = [] for mask in itertools.product(*[[True, False] if isinstance(arg, COO) else [None] for arg in self.args]): if not any(mask): continue r = self._get_func_coords_data(mask) if r is not None: coords_list.append(r[0]) data_list.append(r[1]) # Concatenate matches and mismatches data = np.concatenate(data_list) if len(data_list) else np.empty((0,), dtype=self.fill_value.dtype) coords = ( np.concatenate(coords_list, axis=1) if len(coords_list) else np.empty((0, len(self.shape)), dtype=np.intp) ) return COO( coords, data, shape=self.shape, has_duplicates=False, fill_value=self.fill_value, ).asformat(self.out_type, **self.out_kwargs) def _get_fill_value(self): """ A function that finds and returns the fill-value. Raises ------ ValueError If the fill-value is inconsistent. """ from ._coo import COO def get_zero_arg(x): if isinstance(x, COO): return np.atleast_1d(x.fill_value) if isinstance(x, np.generic | np.ndarray): return np.atleast_1d(x) return x zero_args = tuple(get_zero_arg(a) for a in self.args) # Some elemwise functions require a dtype argument, some abhorr it. try: fill_value_array = self.func(*zero_args, dtype=self.dtype, **self.kwargs) except TypeError: fill_value_array = self.func(*zero_args, **self.kwargs) try: fill_value = fill_value_array[(0,) * fill_value_array.ndim] except IndexError: zero_args = tuple( arg.fill_value if isinstance(arg, COO) else _zero_of_dtype(arg.dtype) for arg in self.args ) fill_value = self.func(*zero_args, **self.kwargs)[()] equivalent_fv = equivalent(fill_value, fill_value_array, loose=True).all() if not equivalent_fv and self.shape != self.ndarray_shape: raise ValueError( "Performing a mixed sparse-dense operation that would result in a dense array. " "Please make sure that func(sparse_fill_values, ndarrays) is a constant array." ) if not equivalent_fv: self._dense_result = True # Store dtype separately if needed. if self.dtype is not None: fill_value = fill_value.astype(self.dtype) self.fill_value = fill_value self.dtype = self.fill_value.dtype def _check_broadcast(self): """ Checks if adding the ndarrays changes the broadcast shape. Raises ------ ValueError If the check fails. """ from ._coo import COO full_shape = _get_nary_broadcast_shape(*tuple(np.shape(arg) for arg in self.args)) non_ndarray_shape = _get_nary_broadcast_shape(*tuple(arg.shape for arg in self.args if isinstance(arg, COO))) ndarray_shape = _get_nary_broadcast_shape(*tuple(arg.shape for arg in self.args if isinstance(arg, np.ndarray))) self.shape = full_shape self.ndarray_shape = ndarray_shape self.non_ndarray_shape = non_ndarray_shape def _get_func_coords_data(self, mask): """ Gets the coords/data for a certain mask Parameters ---------- mask : tuple[Union[bool, NoneType]] The mask determining whether to match or unmatch. Returns ------- None or tuple The coords/data tuple for the given mask. """ from ._coo import COO matched_args = [arg for arg, m in zip(self.args, mask, strict=True) if m is not None and m] unmatched_args = [arg for arg, m in zip(self.args, mask, strict=True) if m is not None and not m] ndarray_args = [arg for arg, m in zip(self.args, mask, strict=True) if m is None] matched_broadcast_shape = _get_nary_broadcast_shape( *tuple(np.shape(arg) for arg in itertools.chain(matched_args, ndarray_args)) ) matched_arrays = self._match_coo(*matched_args, cache=self.cache, broadcast_shape=matched_broadcast_shape) func_args = [] m_arg = 0 for arg, m in zip(self.args, mask, strict=True): if m is None: func_args.append(np.broadcast_to(arg, matched_broadcast_shape)[tuple(matched_arrays[0].coords)]) continue if m: func_args.append(matched_arrays[m_arg].data) m_arg += 1 else: func_args.append(arg.fill_value) # Try our best to preserve the output dtype. try: func_data = self.func(*func_args, dtype=self.dtype, **self.kwargs) except TypeError: try: func_args = np.broadcast_arrays(*func_args) out = np.empty(func_args[0].shape, dtype=self.dtype) func_data = self.func(*func_args, out=out, **self.kwargs) except TypeError: func_data = self.func(*func_args, **self.kwargs).astype(self.dtype) unmatched_mask = ~equivalent(func_data, self.fill_value) if not unmatched_mask.any(): return None func_coords = matched_arrays[0].coords[:, unmatched_mask] func_data = func_data[unmatched_mask] if matched_arrays[0].shape != self.shape: params = _get_broadcast_parameters(matched_arrays[0].shape, self.shape) func_coords, func_data = _get_expanded_coords_data(func_coords, func_data, params, self.shape) if all(m is None or m for m in mask): return func_coords, func_data # Not really sorted but we need the sortedness. func_array = COO(func_coords, func_data, self.shape, has_duplicates=False, sorted=True) unmatched_mask = np.ones(func_array.nnz, dtype=np.bool_) for arg in unmatched_args: matched_idx = self._match_coo(func_array, arg, return_midx=True)[0] unmatched_mask[matched_idx] = False coords = np.asarray(func_array.coords[:, unmatched_mask], order="C") data = np.asarray(func_array.data[unmatched_mask], order="C") return coords, data @staticmethod def _match_coo(*args, **kwargs): """ Matches the coordinates for any number of input :obj:`COO` arrays. Equivalent to "sparse" broadcasting for all arrays. Parameters ---------- *args : Tuple[COO] The input :obj:`COO` arrays. return_midx : bool Whether to return matched indices or matched arrays. Matching only supported for two arrays. ``False`` by default. cache : dict Cache of things already matched. No cache by default. Returns ------- matched_idx : List[ndarray] The indices of matched elements in the original arrays. Only returned if ``return_midx`` is ``True``. matched_arrays : List[COO] The expanded, matched :obj:`COO` objects. Only returned if ``return_midx`` is ``False``. """ from ._coo import COO from ._coo.common import linear_loc cache = kwargs.pop("cache", None) return_midx = kwargs.pop("return_midx", False) broadcast_shape = kwargs.pop("broadcast_shape", None) if kwargs: raise ValueError(f"Unknown kwargs: {kwargs.keys()}") if return_midx and (len(args) != 2 or cache is not None): raise NotImplementedError("Matching indices only supported for two args, and no cache.") matched_arrays = [args[0]] cache_key = [id(args[0])] for arg2 in args[1:]: cache_key.append(id(arg2)) key = tuple(cache_key) if cache is not None and key in cache: matched_arrays = cache[key] continue cargs = [matched_arrays[0], arg2] current_shape = _get_broadcast_shape(matched_arrays[0].shape, arg2.shape) params = [_get_broadcast_parameters(arg.shape, current_shape) for arg in cargs] reduced_params = [all(p) for p in zip(*params, strict=True)] reduced_shape = _get_reduced_shape(arg2.shape, _rev_idx(reduced_params, arg2.ndim)) reduced_coords = [_get_reduced_coords(arg.coords, _rev_idx(reduced_params, arg.ndim)) for arg in cargs] linear = [linear_loc(rc, reduced_shape) for rc in reduced_coords] sorted_idx = [np.argsort(idx) for idx in linear] linear = [idx[s] for idx, s in zip(linear, sorted_idx, strict=True)] matched_idx = _match_arrays(*linear) if return_midx: return [sidx[midx] for sidx, midx in zip(sorted_idx, matched_idx, strict=True)] coords = [arg.coords[:, s] for arg, s in zip(cargs, sorted_idx, strict=True)] mcoords = [c[:, idx] for c, idx in zip(coords, matched_idx, strict=True)] mcoords = _get_matching_coords(mcoords, params) mdata = [arg.data[sorted_idx[0]][matched_idx[0]] for arg in matched_arrays] mdata.append(arg2.data[sorted_idx[1]][matched_idx[1]]) # The coords aren't truly sorted, but we don't need them, so it's # best to avoid the extra cost. matched_arrays = [COO(mcoords, md, shape=current_shape, sorted=True, has_duplicates=False) for md in mdata] if cache is not None: cache[key] = matched_arrays if broadcast_shape is not None and matched_arrays[0].shape != broadcast_shape: params = _get_broadcast_parameters(matched_arrays[0].shape, broadcast_shape) coords, idx = _get_expanded_coords_data( matched_arrays[0].coords, np.arange(matched_arrays[0].nnz), params, broadcast_shape, ) matched_arrays = [ COO( coords, arr.data[idx], shape=broadcast_shape, sorted=True, has_duplicates=False, ) for arr in matched_arrays ] return matched_arrays def _rev_idx(arg, idx): if idx == 0: return arg[len(arg) :] return arg[-idx:] sparse-0.17.0/sparse/numba_backend/_utils.py000066400000000000000000000462321501262445000210210ustar00rootroot00000000000000import functools import warnings from collections.abc import Iterable from numbers import Integral import numba import numpy as np def assert_eq(x, y, check_nnz=True, compare_dtype=True, **kwargs): from ._coo import COO assert x.shape == y.shape if compare_dtype: assert x.dtype == y.dtype check_equal = ( np.array_equal if (np.issubdtype(x.dtype, np.integer) and np.issubdtype(y.dtype, np.integer)) or (np.issubdtype(x.dtype, np.flexible) and np.issubdtype(y.dtype, np.flexible)) else functools.partial(np.allclose, equal_nan=True) ) if isinstance(x, COO): assert is_canonical(x) if isinstance(y, COO): assert is_canonical(y) if isinstance(x, COO) and isinstance(y, COO) and check_nnz: assert np.array_equal(x.coords, y.coords) assert check_equal(x.data, y.data, **kwargs) assert x.fill_value == y.fill_value or (np.isnan(x.fill_value) and np.isnan(y.fill_value)) return if hasattr(x, "todense"): xx = x.todense() if check_nnz: assert_nnz(x, xx) else: xx = x if hasattr(y, "todense"): yy = y.todense() if check_nnz: assert_nnz(y, yy) else: yy = y assert check_equal(xx, yy, **kwargs) def assert_gcxs_slicing(s, x): """ Util function to test slicing of GCXS matrices after product multiplication. For simplicity, it tests only tensors with number of dimension = 3. Parameters ---------- s: sparse product matrix x: dense product matrix """ rng = np.random.default_rng() row = rng.integers(s.shape[s.ndim - 2]) assert np.allclose(s[0][row].data, [num for num in x[0][row] if num != 0]) # regression test col = s.shape[s.ndim - 1] for i in range(len(s.indices) // col): j = col * i k = col * (1 + i) s.data[j:k] = s.data[j:k][::-1] s.indices[j:k] = s.indices[j:k][::-1] assert np.array_equal(s[0][row].data, np.array([])) def assert_nnz(s, x): fill_value = s.fill_value if hasattr(s, "fill_value") else _zero_of_dtype(s.dtype) assert np.sum(~equivalent(x, fill_value)) == s.nnz def is_canonical(x): return not x.shape or ((np.diff(x.linear_loc()) > 0).all() and not equivalent(x.data, x.fill_value).any()) def _zero_of_dtype(dtype): """ Creates a ()-shaped 0-dimensional zero array of a given dtype. Parameters ---------- dtype : numpy.dtype The dtype for the array. Returns ------- np.ndarray The zero array. """ return np.zeros((), dtype=dtype)[()] @numba.jit(nopython=True, nogil=True) def algD(n, N, random_state): """ Random Sampling without Replacement Alg D proposed by J.S. Vitter in Faster Methods for Random Sampling Parameters: n = sample size (nnz) N = size of system (elements) random_state = seed for random number generation """ n = np.intp(n + 1) N = np.intp(N) qu1 = N - n + 1 Vprime = np.exp(np.log(random_state.random()) / n) i = 0 arr = np.zeros(n - 1, dtype=np.intp) arr[-1] = -1 while n > 1: nmin1inv = 1 / (n - 1) while True: while True: X = N * (1 - Vprime) S = np.intp(X) if qu1 > S: break Vprime = np.exp(np.log(random_state.random()) / n) y1 = np.exp(np.log(random_state.random() * N / qu1) * nmin1inv) Vprime = y1 * (1 - X / N) * (qu1 / (qu1 - S)) if Vprime <= 1: break y2 = 1 top = N - 1 if n - 1 > S: bottom = N - n limit = N - S else: bottom = N - S - 1 limit = qu1 t = N - 1 while t >= limit: y2 *= top / bottom top -= 1 bottom -= 1 t -= 1 if y1 * np.exp(np.log(y2) / nmin1inv) <= N / (N - X): Vprime = np.exp(np.log(random_state.random()) * nmin1inv) break Vprime = np.exp(np.log(random_state.random()) / n) arr[i] = arr[i - 1] + S + 1 i += 1 N = N - S - 1 n -= 1 qu1 = qu1 - S return arr @numba.jit(nopython=True, nogil=True) def algA(n, N, random_state): """ Random Sampling without Replacement Alg A proposed by J.S. Vitter in Faster Methods for Random Sampling Parameters: n = sample size (nnz) N = size of system (elements) random_state = seed for random number generation """ n = np.intp(n) N = np.intp(N) arr = np.zeros(n, dtype=np.intp) arr[-1] = -1 i = 0 top = N - n while n >= 2: V = random_state.random() S = 0 quot = top / N while quot > V: S += 1 top -= 1 N -= 1 quot *= top / N arr[i] = arr[i - 1] + S + 1 i += 1 N -= 1 n -= 1 S = np.intp(N * random_state.random()) arr[i] = arr[i - 1] + S + 1 i += 1 return arr @numba.jit(nopython=True, nogil=True) def reverse(inv, N): """ If density of random matrix is greater than .5, it is faster to sample states not included Parameters: arr = np.array(np.intp) of indices to be excluded from sample N = size of the system (elements) """ N = np.intp(N) a = np.zeros(np.intp(N - len(inv)), dtype=np.intp) j = 0 k = 0 for i in range(N): if j == len(inv): a[k:] = np.arange(i, N) break if i == inv[j]: j += 1 else: a[k] = i k += 1 return a default_rng = np.random.default_rng() def random( shape, density=None, nnz=None, random_state=None, data_rvs=None, format="coo", fill_value=None, idx_dtype=None, **kwargs, ): """Generate a random sparse multidimensional array Parameters ---------- shape : Tuple[int] Shape of the array density : float, optional Density of the generated array; default is 0.01. Mutually exclusive with `nnz`. nnz : int, optional Number of nonzero elements in the generated array. Mutually exclusive with `density`. random_state : Union[`numpy.random.Generator, int`], optional Random number generator or random seed. If not given, the singleton numpy.random will be used. This random state will be used for sampling the sparsity structure, but not necessarily for sampling the values of the structurally nonzero entries of the matrix. data_rvs : Callable Data generation callback. Must accept one single parameter: number of `nnz` elements, and return one single NumPy array of exactly that length. format : str The format to return the output array in. fill_value : scalar The fill value of the output array. Returns ------- SparseArray The generated random matrix. See Also -------- - [`scipy.sparse.rand`][] : Equivalent Scipy function. - [`numpy.random.rand`][] : Similar Numpy function. Examples -------- >>> from scipy import stats >>> rng = np.random.default_rng(42) >>> rvs = lambda x: stats.poisson(25, loc=10).rvs(x, random_state=rng) >>> s = sparse.random((2, 3, 4), density=0.25, random_state=rng, data_rvs=rvs) >>> s.todense() array([[[39, 0, 0, 0], [28, 33, 0, 37], [ 0, 0, 0, 0]], [[ 0, 0, 0, 0], [ 0, 0, 34, 0], [ 0, 0, 0, 36]]]) """ # Copied, in large part, from scipy.sparse.random # See https://github.com/scipy/scipy/blob/main/LICENSE.txt from ._coo import COO if density is not None and nnz is not None: raise ValueError("'density' and 'nnz' are mutually exclusive") if density is None: density = 0.01 if not (0 <= density <= 1): raise ValueError(f"density {density} is not in the unit interval") elements = np.prod(shape, dtype=np.intp) if nnz is None: nnz = int(elements * density) if not (0 <= nnz <= elements): raise ValueError(f"cannot generate {nnz} nonzero elements for an array with {elements} total elements") if random_state is None: random_state = default_rng elif isinstance(random_state, Integral): random_state = np.random.default_rng(random_state) if data_rvs is None: data_rvs = random_state.random if nnz == elements or density >= 1: ind = np.arange(elements) elif nnz < 2: ind = random_state.choice(elements, nnz) # Faster to find non-sampled indices and remove them for dens > .5 elif elements - nnz < 2: ind = reverse(random_state.choice(elements, elements - nnz), elements) elif nnz > elements / 2: nnztemp = elements - nnz # Using algorithm A for dens > .1 if elements > 10 * nnztemp: ind = reverse( algD(nnztemp, elements, random_state), elements, ) else: ind = reverse( algA(nnztemp, elements, random_state), elements, ) else: ind = algD(nnz, elements, random_state) if elements > 10 * nnz else algA(nnz, elements, random_state) data = data_rvs(nnz) ar = COO( ind[None, :], data, shape=elements, fill_value=fill_value, ).reshape(shape) if idx_dtype: if can_store(idx_dtype, max(shape)): ar.coords = ar.coords.astype(idx_dtype) else: raise ValueError(f"cannot cast array with shape {shape} to dtype {idx_dtype}.") return ar.asformat(format, **kwargs) def isscalar(x): from ._sparse_array import SparseArray return not isinstance(x, SparseArray) and np.isscalar(x) def random_value_array(value, fraction): def replace_values(n): i = int(n * fraction) ar = np.empty((n,), dtype=np.float64) ar[:i] = value ar[i:] = default_rng.random(n - i) return ar return replace_values def normalize_axis(axis, ndim): """ Normalize negative axis indices to their positive counterpart for a given number of dimensions. Parameters ---------- axis : Union[int, Iterable[int], None] The axis indices. ndim : int Number of dimensions to normalize axis indices against. Returns ------- axis The normalized axis indices. """ if axis is None: return None if isinstance(axis, Integral): axis = int(axis) if axis < 0: axis += ndim if axis >= ndim or axis < 0: raise ValueError(f"Invalid axis index {axis} for ndim={ndim}") return axis if isinstance(axis, Iterable): if not all(isinstance(a, Integral) for a in axis): raise ValueError(f"axis {axis} not understood") return tuple(normalize_axis(a, ndim) for a in axis) raise ValueError(f"axis {axis} not understood") def equivalent(x, y, /, loose=False): """ Checks the equivalence of two scalars or arrays with broadcasting. Assumes a consistent dtype. Parameters ---------- x : scalar or numpy.ndarray y : scalar or numpy.ndarray Returns ------- equivalent : scalar or numpy.ndarray The element-wise comparison of where two arrays are equivalent. Examples -------- >>> equivalent(1, 1) np.True_ >>> equivalent(np.nan, np.nan + 1) np.True_ >>> equivalent(1, 2) np.False_ >>> equivalent(np.inf, np.inf) np.True_ >>> equivalent(np.float64(0.0), np.float64(-0.0)) np.False_ """ x = np.asarray(x) y = np.asarray(y) # Can't contain NaNs dt = np.result_type(x.dtype, y.dtype) if not any(np.issubdtype(dt, t) for t in [np.floating, np.complexfloating]): return x == y if loose: if np.issubdtype(dt, np.complexfloating): return equivalent(x.real, y.real, loose=True) & equivalent(x.imag, y.imag, loose=True) # TODO: Rec array handling return (x == y) | ((x != x) & (y != y)) if x.size == 0 or y.size == 0: shape = np.broadcast_shapes(x.shape, y.shape) return np.empty(shape, dtype=np.bool_) x, y = np.broadcast_arrays(x[..., None], y[..., None]) return (x.astype(dt).view(np.uint8) == y.astype(dt).view(np.uint8)).all(axis=-1) # copied from zarr # See https://github.com/zarr-developers/zarr-python/blob/main/zarr/util.py def human_readable_size(size): if size < 2**10: return str(size) if size < 2**20: return f"{size / 2**10:.1f}K" if size < 2**30: return f"{size / 2**20:.1f}M" if size < 2**40: return f"{size / 2**30:.1f}G" if size < 2**50: return f"{size / 2**40:.1f}T" return f"{size / 2**50:.1f}P" def html_table(arr): table = [""] headings = ["Format", "Data Type", "Shape", "nnz", "Density", "Read-only"] info = [ type(arr).__name__.lower(), str(arr.dtype), str(arr.shape), str(arr.nnz), str(arr.density), ] # read-only info.append(str(not hasattr(arr, "__setitem__"))) if hasattr(arr, "nbytes"): headings.append("Size") info.append(human_readable_size(arr.nbytes)) headings.append("Storage ratio") with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RuntimeWarning) ratio = float(np.float64(arr.nbytes) / np.float64(arr.size * arr.dtype.itemsize)) info.append(f"{ratio:.2f}") # compressed_axes if type(arr).__name__ == "GCXS": headings.append("Compressed Axes") info.append(str(arr.compressed_axes)) for h, i in zip(headings, info, strict=True): table.append(f'') table.append("
{h}{i}
") return "".join(table) def check_compressed_axes(ndim, compressed_axes): """ Checks if the given compressed_axes are compatible with the shape of the array. Parameters ---------- ndim : int compressed_axes : Iterable Raises ------ ValueError If the compressed_axes are incompatible with the number of dimensions """ if compressed_axes is None: return if isinstance(ndim, Iterable): ndim = len(ndim) if not isinstance(compressed_axes, Iterable): raise ValueError("compressed_axes must be an iterable") if len(compressed_axes) == ndim: raise ValueError("cannot compress all axes") if not np.array_equal(list(set(compressed_axes)), compressed_axes): raise ValueError("axes must be sorted without repeats") if not all(isinstance(a, Integral) for a in compressed_axes): raise ValueError("axes must be represented with integers") if min(compressed_axes) < 0 or max(compressed_axes) >= ndim: raise ValueError("axis out of range") def check_fill_value(x, /, *, accept_fv=None) -> None: """Raises on incorrect fill-values. Parameters ---------- x : SparseArray The array to check accept_fv : scalar or list of scalar, optional The list of accepted fill-values. The default accepts only zero. Raises ------ ValueError If the fill-value doesn't match. """ if accept_fv is None: accept_fv = [0] if not isinstance(accept_fv, Iterable): accept_fv = [accept_fv] if not any(equivalent(fv, x.fill_value, loose=True) for fv in accept_fv): raise ValueError(f"{x.fill_value=} but should be in {accept_fv}.") def check_zero_fill_value(*args, loose=True): """ Checks if all the arguments have zero fill-values. Parameters ---------- *args : Iterable[SparseArray] Raises ------ ValueError If all arguments don't have zero fill-values. Examples -------- >>> import sparse >>> s1 = sparse.random((10,), density=0.5) >>> s2 = sparse.random((10,), density=0.5, fill_value=0.5) >>> check_zero_fill_value(s1) >>> check_zero_fill_value(s2) Traceback (most recent call last): ... ValueError: This operation requires zero fill values, but argument 0 had a fill value of 0.5. >>> check_zero_fill_value(s1, s2) Traceback (most recent call last): ... ValueError: This operation requires zero fill values, but argument 1 had a fill value of 0.5. """ for i, arg in enumerate(args): if hasattr(arg, "fill_value") and not equivalent(arg.fill_value, _zero_of_dtype(arg.dtype), loose=loose): raise ValueError( f"This operation requires zero fill values, but argument {i:d} had a fill value of {arg.fill_value!s}." ) def check_consistent_fill_value(arrays): """ Checks if all the arguments have consistent fill-values. Parameters ---------- args : Iterable[SparseArray] Raises ------ ValueError If all elements of :code:`arrays` don't have the same fill-value. Examples -------- >>> import sparse >>> s1 = sparse.random((10,), density=0.5, fill_value=0.1) >>> s2 = sparse.random((10,), density=0.5, fill_value=0.5) >>> check_consistent_fill_value([s1, s1]) >>> check_consistent_fill_value([s1, s2]) # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... ValueError: This operation requires consistent fill-values, but argument 1 had a fill value of 0.5,\ which is different from a fill_value of 0.1 in the first argument. """ arrays = list(arrays) from ._sparse_array import SparseArray if not all(isinstance(s, SparseArray) for s in arrays): raise ValueError("All arrays must be instances of SparseArray.") if len(arrays) == 0: raise ValueError("At least one array required.") fv = arrays[0].fill_value for i, arg in enumerate(arrays): if not equivalent(fv, arg.fill_value): raise ValueError( "This operation requires consistent fill-values, " f"but argument {i:d} had a fill value of {arg.fill_value!s}, which " f"is different from a fill_value of {fv!s} in the first " "argument." ) def get_out_dtype(arr, scalar): out_type = arr.dtype if not can_store(out_type, scalar): out_type = np.min_scalar_type(scalar) return out_type def can_store(dtype, scalar): try: with warnings.catch_warnings(): warnings.simplefilter("ignore") warnings.filterwarnings("error", "out-of-bound", DeprecationWarning) return np.array(scalar, dtype=dtype) == np.array(scalar) except (ValueError, OverflowError): return False def is_unsigned_dtype(dtype): return np.issubdtype(dtype, np.integer) and np.iinfo(dtype).min == 0 def convert_format(format): from ._sparse_array import SparseArray if isinstance(format, type): if not issubclass(format, SparseArray): raise ValueError(f"Invalid format: {format}") return format.__name__.lower() if isinstance(format, str): return format raise ValueError(f"Invalid format: {format}") sparse-0.17.0/sparse/numba_backend/tests/000077500000000000000000000000001501262445000203035ustar00rootroot00000000000000sparse-0.17.0/sparse/numba_backend/tests/__init__.py000066400000000000000000000000001501262445000224020ustar00rootroot00000000000000sparse-0.17.0/sparse/numba_backend/tests/conftest.py000066400000000000000000000002141501262445000224770ustar00rootroot00000000000000import pytest @pytest.fixture(scope="session") def rng(): from sparse.numba_backend._utils import default_rng return default_rng sparse-0.17.0/sparse/numba_backend/tests/test_array_function.py000066400000000000000000000071451501262445000247460ustar00rootroot00000000000000import sparse from sparse.numba_backend._settings import NEP18_ENABLED from sparse.numba_backend._utils import assert_eq import pytest import numpy as np import scipy if not NEP18_ENABLED: pytest.skip("NEP18 is not enabled", allow_module_level=True) @pytest.mark.parametrize( "func", [ np.mean, np.std, np.var, np.sum, lambda x: np.sum(x, axis=0), lambda x: np.transpose(x), ], ) def test_unary(func): y = sparse.random((50, 50), density=0.25) x = y.todense() xx = func(x) yy = func(y) assert_eq(xx, yy) @pytest.mark.parametrize("arg_order", [(0, 1), (1, 0), (1, 1)]) @pytest.mark.parametrize("func", [np.dot, np.result_type, np.tensordot, np.matmul]) def test_binary(func, arg_order): y = sparse.random((50, 50), density=0.25) x = y.todense() xx = func(x, x) args = [(x, y)[i] for i in arg_order] yy = func(*args) if isinstance(xx, np.ndarray): assert_eq(xx, yy) else: # result_type returns a dtype assert xx == yy def test_stack(): """stack(), by design, does not allow for mixed type inputs""" y = sparse.random((50, 50), density=0.25) x = y.todense() xx = np.stack([x, x]) yy = np.stack([y, y]) assert_eq(xx, yy) @pytest.mark.parametrize( "arg_order", [(0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)], ) @pytest.mark.parametrize("func", [lambda a, b, c: np.where(a.astype(bool), b, c)]) def test_ternary(func, arg_order): y = sparse.random((50, 50), density=0.25) x = y.todense() xx = func(x, x, x) args = [(x, y)[i] for i in arg_order] yy = func(*args) assert_eq(xx, yy) @pytest.mark.parametrize("func", [np.shape, np.size, np.ndim]) def test_property(func): y = sparse.random((50, 50), density=0.25) x = y.todense() xx = func(x) yy = func(y) assert xx == yy def test_broadcast_to_scalar(): s = sparse.COO.from_numpy([0, 0, 1, 2]) actual = np.broadcast_to(np.zeros_like(s, shape=()), (3,)) expected = np.broadcast_to(np.zeros_like(s.todense(), shape=()), (3,)) assert isinstance(actual, sparse.COO) assert_eq(actual, expected) def test_zeros_like_order(): s = sparse.COO.from_numpy([0, 0, 1, 2]) actual = np.zeros_like(s, order="C") expected = np.zeros_like(s.todense(), order="C") assert isinstance(actual, sparse.COO) assert_eq(actual, expected) @pytest.mark.parametrize("format", ["dok", "gcxs", "coo"]) def test_format(format): s = sparse.random((5, 5), density=0.2, format=format) assert s.format == format class TestAsarray: np_eye = np.eye(5) @pytest.mark.parametrize( "input", [ np_eye, scipy.sparse.csr_matrix(np_eye), scipy.sparse.csc_matrix(np_eye), 4, np.array(5), np.arange(12).reshape((2, 3, 2)), sparse.COO.from_numpy(np_eye), sparse.GCXS.from_numpy(np_eye), sparse.DOK.from_numpy(np_eye), ], ) @pytest.mark.parametrize("dtype", [np.int64, np.float64, np.complex128]) @pytest.mark.parametrize("format", ["dok", "gcxs", "coo"]) def test_asarray(self, input, dtype, format): if format == "dok" and (np.isscalar(input) or input.ndim == 0): # scalars and 0-D arrays aren't supported in DOK format return s = sparse.asarray(input, dtype=dtype, format=format) actual = s.todense() if hasattr(s, "todense") else s expected = input.todense() if hasattr(input, "todense") else np.asarray(input) np.testing.assert_equal(actual, expected) sparse-0.17.0/sparse/numba_backend/tests/test_compressed.py000066400000000000000000000324661501262445000240730ustar00rootroot00000000000000import sparse from sparse.numba_backend._compressed import GCXS from sparse.numba_backend._utils import assert_eq, equivalent import pytest import numpy as np @pytest.fixture(scope="module", params=["f8", "f4", "i8", "i4"]) def random_sparse(request, rng): dtype = request.param if np.issubdtype(dtype, np.integer): def data_rvs(n): return rng.integers(-1000, 1000, n) else: data_rvs = None return sparse.random((20, 30, 40), density=0.25, format="gcxs", data_rvs=data_rvs, random_state=rng).astype(dtype) @pytest.fixture(scope="module", params=["f8", "f4", "i8", "i4"]) def random_sparse_small(request, rng): dtype = request.param if np.issubdtype(dtype, np.integer): def data_rvs(n): return rng.integers(-10, 10, n) else: data_rvs = None return sparse.random((20, 30, 40), density=0.25, format="gcxs", data_rvs=data_rvs, random_state=rng).astype(dtype) @pytest.mark.parametrize( "reduction, kwargs", [ ("sum", {}), ("sum", {"dtype": np.float32}), ("mean", {}), ("mean", {"dtype": np.float32}), ("prod", {}), ("max", {}), ("min", {}), ("std", {}), ("var", {}), ], ) @pytest.mark.parametrize("axis", [None, 0, 1, 2, (0, 2), -3, (1, -1)]) @pytest.mark.parametrize("keepdims", [True, False]) def test_reductions(reduction, random_sparse, axis, keepdims, kwargs): x = random_sparse y = x.todense() xx = getattr(x, reduction)(axis=axis, keepdims=keepdims, **kwargs) yy = getattr(y, reduction)(axis=axis, keepdims=keepdims, **kwargs) assert_eq(xx, yy) @pytest.mark.xfail(reason=("Setting output dtype=float16 produces results inconsistent with numpy")) @pytest.mark.filterwarnings("ignore:overflow") @pytest.mark.parametrize( "reduction, kwargs", [("sum", {"dtype": np.float16}), ("mean", {"dtype": np.float16})], ) @pytest.mark.parametrize("axis", [None, 0, 1, 2, (0, 2)]) def test_reductions_float16(random_sparse, reduction, kwargs, axis): x = random_sparse y = x.todense() xx = getattr(x, reduction)(axis=axis, **kwargs) yy = getattr(y, reduction)(axis=axis, **kwargs) assert_eq(xx, yy, atol=1e-2) @pytest.mark.parametrize("reduction,kwargs", [("any", {}), ("all", {})]) @pytest.mark.parametrize("axis", [None, 0, 1, 2, (0, 2), -3, (1, -1)]) @pytest.mark.parametrize("keepdims", [True, False]) def test_reductions_bool(random_sparse, reduction, kwargs, axis, keepdims): y = np.zeros((2, 3, 4), dtype=bool) y[0] = True y[1, 1, 1] = True x = sparse.COO.from_numpy(y) xx = getattr(x, reduction)(axis=axis, keepdims=keepdims, **kwargs) yy = getattr(y, reduction)(axis=axis, keepdims=keepdims, **kwargs) assert_eq(xx, yy) @pytest.mark.parametrize( "reduction,kwargs", [ (np.max, {}), (np.sum, {}), (np.sum, {"dtype": np.float32}), (np.mean, {}), (np.mean, {"dtype": np.float32}), (np.prod, {}), (np.min, {}), ], ) @pytest.mark.parametrize("axis", [None, 0, 1, 2, (0, 2), -1, (0, -1)]) @pytest.mark.parametrize("keepdims", [True, False]) def test_ufunc_reductions(random_sparse, reduction, kwargs, axis, keepdims): x = random_sparse y = x.todense() xx = reduction(x, axis=axis, keepdims=keepdims, **kwargs) yy = reduction(y, axis=axis, keepdims=keepdims, **kwargs) assert_eq(xx, yy) # If not a scalar/1 element array, must be a sparse array if xx.size > 1: assert isinstance(xx, GCXS) @pytest.mark.parametrize( "reduction,kwargs", [ (np.max, {}), (np.sum, {"axis": 0}), (np.prod, {"keepdims": True}), (np.minimum.reduce, {"axis": 0}), ], ) @pytest.mark.parametrize("fill_value", [0, 1.0, -1, -2.2, 5.0]) def test_ufunc_reductions_kwargs(reduction, kwargs, fill_value): x = sparse.random((2, 3, 4), density=0.5, format="gcxs", fill_value=fill_value) y = x.todense() xx = reduction(x, **kwargs) yy = reduction(y, **kwargs) assert_eq(xx, yy) # If not a scalar/1 element array, must be a sparse array if xx.size > 1: assert isinstance(xx, GCXS) @pytest.mark.parametrize( "a,b", [ [(3, 4), (3, 4)], [(12,), (3, 4)], [(12,), (3, -1)], [(3, 4), (12,)], [(3, 4), (-1, 4)], [(3, 4), (3, -1)], [(2, 3, 4, 5), (8, 15)], [(2, 3, 4, 5), (24, 5)], [(2, 3, 4, 5), (20, 6)], [(), ()], ], ) def test_reshape(a, b): s = sparse.random(a, density=0.5, format="gcxs") x = s.todense() assert_eq(x.reshape(b), s.reshape(b)) def test_reshape_same(): s = sparse.random((3, 5), density=0.5, format="gcxs") assert s.reshape(s.shape) is s @pytest.mark.parametrize( "a,b", [ [(3, 4, 5), (2, 1, 0)], [(12,), None], [(9, 10), (1, 0)], [(4, 3, 5), (1, 0, 2)], [(5, 4, 3), (0, 2, 1)], [(3, 4, 5, 6), (0, 2, 1, 3)], ], ) def test_tranpose(a, b): s = sparse.random(a, density=0.5, format="gcxs") x = s.todense() assert_eq(x.transpose(b), s.transpose(b)) @pytest.mark.parametrize("fill_value_in", [0, np.inf, np.nan, 5, None]) @pytest.mark.parametrize("fill_value_out", [0, np.inf, np.nan, 5, None]) @pytest.mark.parametrize("format", [sparse.COO, sparse._compressed.CSR]) def test_to_scipy_sparse(fill_value_in, fill_value_out, format): s = sparse.random((3, 5), density=0.5, format=format, fill_value=fill_value_in) if not ((fill_value_in in {0, None} and fill_value_out in {0, None}) or equivalent(fill_value_in, fill_value_out)): with pytest.raises(ValueError, match=r"fill_value=.* but should be in .*\."): s.to_scipy_sparse(accept_fv=fill_value_out) return sps_matrix = s.to_scipy_sparse(accept_fv=fill_value_in) s2 = format.from_scipy_sparse(sps_matrix, fill_value=fill_value_out) assert_eq(s, s2) def test_tocoo(): coo = sparse.random((5, 6), density=0.5) b = GCXS.from_coo(coo) assert_eq(b.tocoo(), coo) @pytest.mark.parametrize("complex", [True, False]) def test_complex_methods(complex): x = np.array([1 + 2j, 2 - 1j, 0, 1, 0]) if complex else np.array([1, 2, 0, 0, 0]) s = GCXS.from_numpy(x) assert_eq(s.imag, x.imag) assert_eq(s.real, x.real) assert_eq(s.conj(), x.conj()) @pytest.mark.parametrize( "index", [ # Integer 0, 1, -1, (1, 1, 1), # Pure slices (slice(0, 2),), (slice(None, 2), slice(None, 2)), (slice(1, None), slice(1, None)), (slice(None, None),), (slice(None, None, -1),), (slice(None, 2, -1), slice(None, 2, -1)), (slice(1, None, 2), slice(1, None, 2)), (slice(None, None, 2),), (slice(None, 2, -1), slice(None, 2, -2)), (slice(1, None, 2), slice(1, None, 1)), (slice(None, None, -2),), # Combinations (0, slice(0, 2)), (slice(0, 1), 0), (None, slice(1, 3), 0), (slice(0, 3), None, 0), (slice(1, 2), slice(2, 4)), (slice(1, 2), slice(None, None)), (slice(1, 2), slice(None, None), 2), (slice(1, 2, 2), slice(None, None), 2), (slice(1, 2, None), slice(None, None, 2), 2), (slice(1, 2, -2), slice(None, None), -2), (slice(1, 2, None), slice(None, None, -2), 2), (slice(1, 2, -1), slice(None, None), -1), (slice(1, 2, None), slice(None, None, -1), 2), (slice(2, 0, -1), slice(None, None), -1), (slice(-2, None, None),), (slice(-1, None, None), slice(-2, None, None)), # With ellipsis (Ellipsis, slice(1, 3)), (1, Ellipsis, slice(1, 3)), (slice(0, 1), Ellipsis), (Ellipsis, None), (None, Ellipsis), (1, Ellipsis), (1, Ellipsis, None), (1, 1, 1, Ellipsis), (Ellipsis, 1, None), # Pathological - Slices larger than array (slice(None, 1000)), (slice(None), slice(None, 1000)), (slice(None), slice(1000, -1000, -1)), (slice(None), slice(1000, -1000, -50)), # Pathological - Wrong ordering of start/stop (slice(5, 0),), (slice(0, 5, -1),), ], ) @pytest.mark.parametrize("compressed_axes", [(0,), (1,), (2,), (0, 1), (0, 2), (1, 2)]) def test_slicing(index, compressed_axes): s = sparse.random((2, 3, 4), density=0.5, format="gcxs", compressed_axes=compressed_axes) x = s.todense() assert_eq(x[index], s[index]) @pytest.mark.parametrize( "index", [ ([1, 0], 0), (1, [0, 2]), (0, [1, 0], 0), (1, [2, 0], 0), ([True, False], slice(1, None), slice(-2, None)), (slice(1, None), slice(-2, None), [True, False, True, False]), ([1, 0],), (Ellipsis, [2, 1, 3]), (slice(None), [2, 1, 2]), (1, [2, 0, 1]), ], ) @pytest.mark.parametrize("compressed_axes", [(0,), (1,), (2,), (0, 1), (0, 2), (1, 2)]) def test_advanced_indexing(index, compressed_axes): s = sparse.random((2, 3, 4), density=0.5, format="gcxs", compressed_axes=compressed_axes) x = s.todense() assert_eq(x[index], s[index]) @pytest.mark.parametrize( "index", [ (Ellipsis, Ellipsis), (1, 1, 1, 1), (slice(None),) * 4, 5, -5, "foo", [True, False, False], 0.5, [0.5], {"potato": "kartoffel"}, ([[0, 1]],), ], ) def test_slicing_errors(index): s = sparse.random((2, 3, 4), density=0.5, format="gcxs") with pytest.raises(IndexError): s[index] def test_change_compressed_axes(): coo = sparse.random((3, 4, 5), density=0.5) s = GCXS.from_coo(coo, compressed_axes=(0, 1)) b = GCXS.from_coo(coo, compressed_axes=(1, 2)) assert_eq(s, b) s.change_compressed_axes((1, 2)) assert_eq(s, b) def test_concatenate(): xx = sparse.random((2, 3, 4), density=0.5, format="gcxs") x = xx.todense() yy = sparse.random((5, 3, 4), density=0.5, format="gcxs") y = yy.todense() zz = sparse.random((4, 3, 4), density=0.5, format="gcxs") z = zz.todense() assert_eq(np.concatenate([x, y, z], axis=0), sparse.concatenate([xx, yy, zz], axis=0)) xx = sparse.random((5, 3, 1), density=0.5, format="gcxs") x = xx.todense() yy = sparse.random((5, 3, 3), density=0.5, format="gcxs") y = yy.todense() zz = sparse.random((5, 3, 2), density=0.5, format="gcxs") z = zz.todense() assert_eq(np.concatenate([x, y, z], axis=2), sparse.concatenate([xx, yy, zz], axis=2)) assert_eq(np.concatenate([x, y, z], axis=-1), sparse.concatenate([xx, yy, zz], axis=-1)) @pytest.mark.parametrize("axis", [0, 1]) @pytest.mark.parametrize("func", [sparse.stack, sparse.concatenate]) def test_concatenate_mixed(func, axis): s = sparse.random((10, 10), density=0.5, format="gcxs") d = s.todense() with pytest.raises(ValueError): func([d, s, s], axis=axis) def test_concatenate_noarrays(): with pytest.raises(ValueError): sparse.concatenate([]) @pytest.mark.parametrize("shape", [(5,), (2, 3, 4), (5, 2)]) @pytest.mark.parametrize("axis", [0, 1, -1]) def test_stack(shape, axis): xx = sparse.random(shape, density=0.5, format="gcxs") x = xx.todense() yy = sparse.random(shape, density=0.5, format="gcxs") y = yy.todense() zz = sparse.random(shape, density=0.5, format="gcxs") z = zz.todense() assert_eq(np.stack([x, y, z], axis=axis), sparse.stack([xx, yy, zz], axis=axis)) @pytest.mark.parametrize("in_shape", [(5, 5), 62, (3, 3, 3)]) def test_flatten(in_shape): s = sparse.random(in_shape, format="gcxs", density=0.5) x = s.todense() a = s.flatten() e = x.flatten() assert_eq(e, a) def test_gcxs_valerr(): a = np.arange(300) with pytest.raises(ValueError): GCXS.from_numpy(a, idx_dtype=np.int8) def test_upcast(): a = sparse.random((50, 50, 50), density=0.1, format="coo", idx_dtype=np.uint8) b = a.asformat("gcxs") assert b.indices.dtype == np.uint16 a = sparse.random((8, 7, 6), density=0.5, format="gcxs", idx_dtype=np.uint8) b = sparse.random((6, 6, 6), density=0.8, format="gcxs", idx_dtype=np.uint8) assert sparse.concatenate((a, a)).indptr.dtype == np.uint16 assert sparse.stack((b, b)).indptr.dtype == np.uint16 def test_from_coo(): a = sparse.random((5, 5, 5), density=0.1, format="coo") b = GCXS(a) assert_eq(a, b) def test_from_coo_valerr(): a = sparse.random((25, 25, 25), density=0.01, format="coo") with pytest.raises(ValueError): GCXS.from_coo(a, idx_dtype=np.int8) @pytest.mark.parametrize( "pad_width", [ 2, (2, 1), ((2), (1)), ((1, 2), (4, 5), (7, 8)), ], ) @pytest.mark.parametrize("constant_values", [0, 1, 150, np.nan]) def test_pad_valid(pad_width, constant_values): y = sparse.random((50, 50, 3), density=0.15, fill_value=constant_values, format="gcxs") x = y.todense() xx = np.pad(x, pad_width=pad_width, constant_values=constant_values) yy = np.pad(y, pad_width=pad_width, constant_values=constant_values) assert_eq(xx, yy) @pytest.mark.parametrize( "pad_width", [ ((2, 1), (5, 7)), ], ) @pytest.mark.parametrize("constant_values", [150, 2, (1, 2)]) def test_pad_invalid(pad_width, constant_values, fill_value=0): y = sparse.random((50, 50, 3), density=0.15, format="gcxs") with pytest.raises(ValueError): np.pad(y, pad_width, constant_values=constant_values) sparse-0.17.0/sparse/numba_backend/tests/test_compressed_2d.py000066400000000000000000000071301501262445000244460ustar00rootroot00000000000000import sparse from sparse import COO from sparse.numba_backend._compressed.compressed import CSC, CSR, GCXS from sparse.numba_backend._utils import assert_eq import pytest import numpy as np import scipy.sparse import scipy.stats @pytest.fixture(scope="module", params=[CSR, CSC]) def cls(request): return request.param @pytest.fixture(scope="module", params=["f8", "f4", "i8", "i4"]) def dtype(request): return request.param @pytest.fixture(scope="module") def random_sparse(cls, dtype, rng): if np.issubdtype(dtype, np.integer): def data_rvs(n): return rng.integers(-1000, 1000, n) else: data_rvs = None return cls(sparse.random((20, 30), density=0.25, data_rvs=data_rvs).astype(dtype)) @pytest.fixture(scope="module") def random_sparse_small(cls, dtype, rng): if np.issubdtype(dtype, np.integer): def data_rvs(n): return rng.integers(-10, 10, n) else: data_rvs = None return cls(sparse.random((20, 20), density=0.25, data_rvs=data_rvs).astype(dtype)) def test_repr(random_sparse): cls = type(random_sparse).__name__ str_repr = repr(random_sparse) assert cls in str_repr def test_bad_constructor_input(cls): with pytest.raises(ValueError, match=r".*shape.*"): cls(arg="hello world") @pytest.mark.parametrize("n", [0, 1, 3]) def test_bad_nd_input(cls, n): a = np.ones(shape=tuple(5 for _ in range(n))) with pytest.raises(ValueError, match=f"{n}-d"): cls(a) @pytest.mark.parametrize("source_type", ["gcxs", "coo"]) def test_from_sparse(cls, source_type): gcxs = sparse.random((20, 30), density=0.25, format=source_type) result = cls(gcxs) assert_eq(result, gcxs) @pytest.mark.parametrize("scipy_type", ["coo", "csr", "csc", "lil"]) @pytest.mark.parametrize("CLS", [CSR, CSC, GCXS]) def test_from_scipy_sparse(scipy_type, CLS, dtype): orig = scipy.sparse.random(20, 30, density=0.2, format=scipy_type, dtype=dtype) ref = COO.from_scipy_sparse(orig) result = CLS.from_scipy_sparse(orig) assert_eq(ref, result) result_via_init = CLS(orig) assert_eq(ref, result_via_init) @pytest.mark.parametrize("cls_str", ["coo", "dok", "csr", "csc", "gcxs"]) def test_to_sparse(cls_str, random_sparse): result = random_sparse.asformat(cls_str) assert_eq(random_sparse, result) @pytest.mark.parametrize("copy", [True, False]) def test_transpose(random_sparse, copy): from operator import is_, is_not t = random_sparse.transpose(copy=copy) tt = t.transpose(copy=copy) # Check if a copy was made check = is_not if copy else is_ assert check(random_sparse.data, t.data) assert check(random_sparse.indices, t.indices) assert check(random_sparse.indptr, t.indptr) assert random_sparse.shape == t.shape[::-1] assert_eq(random_sparse, tt) assert type(random_sparse) is type(tt) assert_eq(random_sparse.transpose(axes=(0, 1)), random_sparse) assert_eq(random_sparse.transpose(axes=(1, 0)), t) with pytest.raises(ValueError, match="Invalid transpose axes"): random_sparse.transpose(axes=0) @pytest.mark.parametrize("format", ["csr", "csc"]) def test_mT_fill_value(format): fv = 1.0 arr = sparse.full((10, 20), fill_value=fv, format=format) assert_eq(arr.mT, sparse.full((20, 10), fill_value=fv)) def test_transpose_error(random_sparse): with pytest.raises(ValueError): random_sparse.transpose(axes=1) def test_matmul(random_sparse_small): arr = random_sparse_small.todense() actual = random_sparse_small @ random_sparse_small expected = arr @ arr assert_eq(actual, expected) sparse-0.17.0/sparse/numba_backend/tests/test_compressed_convert.py000066400000000000000000000051671501262445000256310ustar00rootroot00000000000000from sparse.numba_backend._compressed import convert from sparse.numba_backend._utils import assert_eq import pytest from numba.typed import List import numpy as np def make_inds(shape): return [np.arange(1, a - 1) for a in shape] def make_increments(shape): inds = make_inds(shape) shape_bins = convert.transform_shape(np.asarray(shape)) return List([inds[i] * shape_bins[i] for i in range(len(shape))]) @pytest.mark.parametrize( "shape, expected_subsample, subsample", [ [(5, 6, 7, 8, 9), np.array([3610, 6892, 10338]), 1000], [(13, 12, 12, 9, 7), np.array([9899, 34441, 60635, 86703]), 10000], [ (12, 15, 7, 14, 9), np.array([14248, 36806, 61382, 85956, 110532, 135106]), 10000, ], [(9, 9, 12, 7, 12), np.array([10177, 34369, 60577]), 10000], ], ) def test_convert_to_flat(shape, expected_subsample, subsample): inds = make_inds(shape) dtype = inds[0].dtype assert_eq( convert.convert_to_flat(inds, shape, dtype)[::subsample], expected_subsample.astype(dtype), ) @pytest.mark.parametrize( "shape, expected_subsample, subsample", [ [(5, 6, 7, 8, 9), np.array([3610, 6892, 10338]), 1000], [(13, 12, 12, 9, 7), np.array([9899, 34441, 60635, 86703]), 10000], [ (12, 15, 7, 14, 9), np.array([14248, 36806, 61382, 85956, 110532, 135106]), 10000, ], [(9, 9, 12, 7, 12), np.array([10177, 34369, 60577]), 10000], ], ) def test_compute_flat(shape, expected_subsample, subsample): increments = make_increments(shape) dtype = increments[0].dtype operations = np.prod([inc.shape[0] for inc in increments[:-1]], dtype=dtype) cols = np.tile(increments[-1], operations) assert_eq( convert.compute_flat(increments, cols, operations)[::subsample], expected_subsample.astype(dtype), ) @pytest.mark.parametrize( "shape, expected_shape", [ [(5, 6, 7, 8, 9), np.array([3024, 504, 72, 9, 1])], [(13, 12, 12, 9, 7), np.array([9072, 756, 63, 7, 1])], [(12, 15, 7, 14, 9), np.array([13230, 882, 126, 9, 1])], [ (18, 5, 12, 14, 9, 11, 8, 14), np.array([9313920, 1862784, 155232, 11088, 1232, 112, 14, 1]), ], [ (11, 6, 13, 11, 17, 7, 15), np.array([1531530, 255255, 19635, 1785, 105, 15, 1]), ], [(9, 9, 12, 7, 12), np.array([9072, 1008, 84, 12, 1])], ], ) def test_transform_shape(shape, expected_shape): assert_eq(convert.transform_shape(np.asarray(shape)), expected_shape, compare_dtype=False) sparse-0.17.0/sparse/numba_backend/tests/test_conversion.py000066400000000000000000000031051501262445000241000ustar00rootroot00000000000000import sparse from sparse.numba_backend._utils import assert_eq import pytest import numpy as np import scipy.sparse as sps FORMATS_ND = [ sparse.COO, sparse.DOK, sparse.GCXS, ] FORMATS_2D = [ sparse.numba_backend._compressed.CSC, sparse.numba_backend._compressed.CSR, ] FORMATS = FORMATS_2D + FORMATS_ND @pytest.mark.parametrize("format1", FORMATS) @pytest.mark.parametrize("format2", FORMATS) def test_conversion(format1, format2): x = sparse.random((10, 10), density=0.5, format=format1, fill_value=0.5) y = x.asformat(format2) assert_eq(x, y) def test_extra_kwargs(): x = sparse.full((2, 2), 1, format="gcxs", compressed_axes=[1]) y = sparse.full_like(x, 1) assert_eq(x, y) @pytest.mark.parametrize("format1", FORMATS_ND) @pytest.mark.parametrize("format2", FORMATS_ND) def test_conversion_scalar(format1, format2): x = sparse.random((), format=format1, fill_value=0.5) y = x.asformat(format2) assert_eq(x, y) def test_non_canonical_conversion(): """ Regression test for gh-602. Adapted from https://github.com/LiberTEM/sparseconverter/blob/4cfc0ee2ad4c37b07742db8f3643bcbd858a4e85/src/sparseconverter/__init__.py#L154-L183 """ data = np.array((2.0, 1.0, 3.0, 3.0, 1.0)) indices = np.array((1, 0, 0, 1, 1), dtype=int) indptr = np.array((0, 2, 5), dtype=int) x = sps.csr_matrix((data, indices, indptr), shape=(2, 2)) ref = np.array(((1.0, 2.0), (3.0, 4.0))) gcxs_check = sparse.GCXS(x) assert np.all(gcxs_check[:1].todense() == ref[:1]) and np.all(gcxs_check[1:].todense() == ref[1:]) sparse-0.17.0/sparse/numba_backend/tests/test_coo.py000066400000000000000000001551631501262445000225070ustar00rootroot00000000000000import contextlib import operator import pickle import sys import sparse from sparse import COO, DOK from sparse.numba_backend._settings import NEP18_ENABLED from sparse.numba_backend._utils import assert_eq, html_table, random_value_array import pytest import numpy as np import scipy.sparse import scipy.stats @pytest.fixture(scope="module", params=["f8", "f4", "i8", "i4"]) def random_sparse(request, rng): dtype = request.param if np.issubdtype(dtype, np.integer): def data_rvs(n): return rng.integers(-1000, 1000, n) else: data_rvs = None return sparse.random((20, 30, 40), density=0.25, data_rvs=data_rvs).astype(dtype) @pytest.fixture(scope="module", params=["f8", "f4", "i8", "i4"]) def random_sparse_small(request, rng): dtype = request.param if np.issubdtype(dtype, np.integer): def data_rvs(n): return rng.integers(-10, 10, n) else: data_rvs = None return sparse.random((20, 30, 40), density=0.25, data_rvs=data_rvs).astype(dtype) @pytest.mark.parametrize("reduction, kwargs", [("sum", {}), ("sum", {"dtype": np.float32}), ("prod", {})]) @pytest.mark.parametrize("axis", [None, 0, 1, 2, (0, 2), -3, (1, -1)]) @pytest.mark.parametrize("keepdims", [True, False]) def test_reductions_fv(reduction, random_sparse_small, axis, keepdims, kwargs, rng): x = random_sparse_small + rng.integers(-1, 1, dtype="i4") y = x.todense() xx = getattr(x, reduction)(axis=axis, keepdims=keepdims, **kwargs) yy = getattr(y, reduction)(axis=axis, keepdims=keepdims, **kwargs) assert_eq(xx, yy) @pytest.mark.parametrize( "reduction, kwargs", [ ("sum", {}), ("sum", {"dtype": np.float32}), ("mean", {}), ("mean", {"dtype": np.float32}), ("prod", {}), ("max", {}), ("min", {}), ("std", {}), ("var", {}), ], ) @pytest.mark.parametrize("axis", [None, 0, 1, 2, (0, 2), -3, (1, -1)]) @pytest.mark.parametrize("keepdims", [True, False]) def test_reductions(reduction, random_sparse, axis, keepdims, kwargs): x = random_sparse y = x.todense() xx = getattr(x, reduction)(axis=axis, keepdims=keepdims, **kwargs) yy = getattr(y, reduction)(axis=axis, keepdims=keepdims, **kwargs) assert_eq(xx, yy) @pytest.mark.xfail(reason=("Setting output dtype=float16 produces results inconsistent with numpy")) @pytest.mark.filterwarnings("ignore:overflow") @pytest.mark.parametrize( "reduction, kwargs", [("sum", {"dtype": np.float16}), ("mean", {"dtype": np.float16})], ) @pytest.mark.parametrize("axis", [None, 0, 1, 2, (0, 2)]) def test_reductions_float16(random_sparse, reduction, kwargs, axis): x = random_sparse y = x.todense() xx = getattr(x, reduction)(axis=axis, **kwargs) yy = getattr(y, reduction)(axis=axis, **kwargs) assert_eq(xx, yy, atol=1e-2) @pytest.mark.parametrize("reduction,kwargs", [("any", {}), ("all", {})]) @pytest.mark.parametrize("axis", [None, 0, 1, 2, (0, 2), -3, (1, -1)]) @pytest.mark.parametrize("keepdims", [True, False]) def test_reductions_bool(random_sparse, reduction, kwargs, axis, keepdims): y = np.zeros((2, 3, 4), dtype=bool) y[0] = True y[1, 1, 1] = True x = sparse.COO.from_numpy(y) xx = getattr(x, reduction)(axis=axis, keepdims=keepdims, **kwargs) yy = getattr(y, reduction)(axis=axis, keepdims=keepdims, **kwargs) assert_eq(xx, yy) @pytest.mark.parametrize( "reduction,kwargs", [ (np.max, {}), (np.sum, {}), (np.sum, {"dtype": np.float32}), (np.mean, {}), (np.mean, {"dtype": np.float32}), (np.prod, {}), (np.min, {}), ], ) @pytest.mark.parametrize("axis", [None, 0, 1, 2, (0, 2), -1, (0, -1)]) @pytest.mark.parametrize("keepdims", [True, False]) def test_ufunc_reductions(random_sparse, reduction, kwargs, axis, keepdims): x = random_sparse y = x.todense() xx = reduction(x, axis=axis, keepdims=keepdims, **kwargs) yy = reduction(y, axis=axis, keepdims=keepdims, **kwargs) assert_eq(xx, yy) # If not a scalar/1 element array, must be a sparse array if xx.size > 1: assert isinstance(xx, COO) @pytest.mark.parametrize( "reduction,kwargs", [ (np.max, {}), (np.sum, {"axis": 0}), (np.prod, {"keepdims": True}), (np.add.reduce, {}), (np.add.reduce, {"keepdims": True}), (np.minimum.reduce, {"axis": 0}), ], ) def test_ufunc_reductions_kwargs(reduction, kwargs): x = sparse.random((2, 3, 4), density=0.5) y = x.todense() xx = reduction(x, **kwargs) yy = reduction(y, **kwargs) assert_eq(xx, yy) # If not a scalar/1 element array, must be a sparse array if xx.size > 1: assert isinstance(xx, COO) @pytest.mark.parametrize("reduction", ["nansum", "nanmean", "nanprod", "nanmax", "nanmin"]) @pytest.mark.parametrize("axis", [None, 0, 1]) @pytest.mark.parametrize("keepdims", [False]) @pytest.mark.parametrize("fraction", [0.25, 0.5, 0.75, 1.0]) @pytest.mark.filterwarnings("ignore:All-NaN") @pytest.mark.filterwarnings("ignore:Mean of empty slice") def test_nan_reductions(reduction, axis, keepdims, fraction): s = sparse.random((2, 3, 4), data_rvs=random_value_array(np.nan, fraction), density=0.25) x = s.todense() expected = getattr(np, reduction)(x, axis=axis, keepdims=keepdims) actual = getattr(sparse, reduction)(s, axis=axis, keepdims=keepdims) assert_eq(expected, actual) @pytest.mark.parametrize("reduction", ["nanmax", "nanmin", "nanmean"]) @pytest.mark.parametrize("axis", [None, 0, 1]) def test_all_nan_reduction_warning(reduction, axis): x = random_value_array(np.nan, 1.0)(2 * 3 * 4).reshape(2, 3, 4) s = COO.from_numpy(x) with pytest.warns(RuntimeWarning): getattr(sparse, reduction)(s, axis=axis) @pytest.mark.parametrize( "axis", [None, (1, 2, 0), (2, 1, 0), (0, 1, 2), (0, 1, -1), (0, -2, -1), (-3, -2, -1)], ) def test_transpose(axis): x = sparse.random((2, 3, 4), density=0.25) y = x.todense() xx = x.transpose(axis) yy = y.transpose(axis) assert_eq(xx, yy) @pytest.mark.parametrize( "axis", [ (0, 1), # too few (0, 1, 2, 3), # too many (3, 1, 0), # axis 3 illegal (0, -1, -4), # axis -4 illegal (0, 0, 1), # duplicate axis 0 (0, -1, 2), # duplicate axis -1 == 2 0.3, # Invalid type in axis ((0, 1, 2),), # Iterable inside iterable ], ) def test_transpose_error(axis): x = sparse.random((2, 3, 4), density=0.25) with pytest.raises(ValueError): x.transpose(axis) @pytest.mark.parametrize("axis1", [-3, -2, -1, 0, 1, 2]) @pytest.mark.parametrize("axis2", [-3, -2, -1, 0, 1, 2]) def test_swapaxes(axis1, axis2): x = sparse.random((2, 3, 4), density=0.25) y = x.todense() xx = x.swapaxes(axis1, axis2) yy = y.swapaxes(axis1, axis2) assert_eq(xx, yy) @pytest.mark.parametrize("axis1", [-4, 3]) @pytest.mark.parametrize("axis2", [-4, 3, 0]) def test_swapaxes_error(axis1, axis2): x = sparse.random((2, 3, 4), density=0.25) with pytest.raises(ValueError): x.swapaxes(axis1, axis2) @pytest.mark.parametrize( "source, destination", [ [0, 1], [2, 1], [-2, 1], [-2, -3], [(0, 1), (2, 3)], [(-1, 0), (0, 1)], [(0, 1, 2), (2, 1, 0)], [(0, 1, 2), (-2, -3, -1)], ], ) def test_moveaxis(source, destination): x = sparse.random((2, 3, 4, 5), density=0.25) y = x.todense() xx = sparse.moveaxis(x, source, destination) yy = np.moveaxis(y, source, destination) assert_eq(xx, yy) @pytest.mark.parametrize("source, destination", [[0, -4], [(0, 5), (1, 2)], [(0, 1, 2), (2, 1)]]) def test_moveaxis_error(source, destination): x = sparse.random((2, 3, 4), density=0.25) with pytest.raises(ValueError): sparse.moveaxis(x, source, destination) @pytest.mark.parametrize( "a,b", [ [(3, 4), (3, 4)], [(12,), (3, 4)], [(12,), (3, -1)], [(3, 4), (12,)], [(3, 4), (-1, 4)], [(3, 4), (3, -1)], [(2, 3, 4, 5), (8, 15)], [(2, 3, 4, 5), (24, 5)], [(2, 3, 4, 5), (20, 6)], [(), ()], ], ) @pytest.mark.parametrize("format", ["coo", "dok"]) def test_reshape(a, b, format): s = sparse.random(a, density=0.5, format=format) x = s.todense() assert_eq(x.reshape(b), s.reshape(b)) def test_large_reshape(): n = 100 m = 10 row = np.arange(n, dtype=np.uint16) col = row % m data = np.ones(n, dtype=np.uint8) x = COO((data, (row, col)), shape=(100, 10), sorted=True, has_duplicates=False) assert_eq(x, x.reshape(x.shape)) def test_reshape_same(): s = sparse.random((3, 5), density=0.5) assert s.reshape(s.shape) is s @pytest.mark.parametrize("format", [COO, DOK]) def test_reshape_function(format): s = sparse.random((5, 3), density=0.5, format=format) x = s.todense() shape = (3, 5) s2 = np.reshape(s, shape) assert isinstance(s2, format) assert_eq(s2, x.reshape(shape)) def test_reshape_upcast(): a = sparse.random((10, 10, 10), density=0.5, format="coo", idx_dtype=np.uint8) assert a.reshape(1000).coords.dtype == np.uint16 @pytest.mark.parametrize("format", [COO, DOK]) def test_reshape_errors(format): s = sparse.random((5, 3), density=0.5, format=format) with pytest.raises(NotImplementedError): s.reshape((3, 5, 1), order="F") @pytest.mark.parametrize("a_ndim", [1, 2, 3]) @pytest.mark.parametrize("b_ndim", [1, 2, 3]) def test_kron(a_ndim, b_ndim): a_shape = (2, 3, 4)[:a_ndim] b_shape = (5, 6, 7)[:b_ndim] sa = sparse.random(a_shape, density=0.5) a = sa.todense() sb = sparse.random(b_shape, density=0.5) b = sb.todense() sol = np.kron(a, b) assert_eq(sparse.kron(sa, sb), sol) assert_eq(sparse.kron(sa, b), sol) assert_eq(sparse.kron(a, sb), sol) with pytest.raises(ValueError): assert_eq(sparse.kron(a, b), sol) @pytest.mark.parametrize("a_spmatrix, b_spmatrix", [(True, True), (True, False), (False, True)]) def test_kron_spmatrix(a_spmatrix, b_spmatrix): sa = sparse.random((3, 4), density=0.5) a = sa.todense() sb = sparse.random((5, 6), density=0.5) b = sb.todense() if a_spmatrix: sa = sa.tocsr() if b_spmatrix: sb = sb.tocsr() sol = np.kron(a, b) assert_eq(sparse.kron(sa, sb), sol) assert_eq(sparse.kron(sa, b), sol) assert_eq(sparse.kron(a, sb), sol) with pytest.raises(ValueError): assert_eq(sparse.kron(a, b), sol) @pytest.mark.parametrize("ndim", [1, 2, 3]) def test_kron_scalar(ndim): if ndim: a_shape = (3, 4, 5)[:ndim] sa = sparse.random(a_shape, density=0.5) a = sa.todense() else: sa = a = np.array(6) scalar = np.array(5) sol = np.kron(a, scalar) assert_eq(sparse.kron(sa, scalar), sol) assert_eq(sparse.kron(scalar, sa), sol) def test_gt(): s = sparse.random((2, 3, 4), density=0.5) x = s.todense() m = x.mean() assert_eq(x > m, s > m) m = s.data[2] assert_eq(x > m, s > m) assert_eq(x >= m, s >= m) @pytest.mark.parametrize( "index", [ # Integer 0, 1, -1, (1, 1, 1), # Pure slices (slice(0, 2),), (slice(None, 2), slice(None, 2)), (slice(1, None), slice(1, None)), (slice(None, None),), (slice(None, None, -1),), (slice(None, 2, -1), slice(None, 2, -1)), (slice(1, None, 2), slice(1, None, 2)), (slice(None, None, 2),), (slice(None, 2, -1), slice(None, 2, -2)), (slice(1, None, 2), slice(1, None, 1)), (slice(None, None, -2),), # Combinations (0, slice(0, 2)), (slice(0, 1), 0), (None, slice(1, 3), 0), (slice(0, 3), None, 0), (slice(1, 2), slice(2, 4)), (slice(1, 2), slice(None, None)), (slice(1, 2), slice(None, None), 2), (slice(1, 2, 2), slice(None, None), 2), (slice(1, 2, None), slice(None, None, 2), 2), (slice(1, 2, -2), slice(None, None), -2), (slice(1, 2, None), slice(None, None, -2), 2), (slice(1, 2, -1), slice(None, None), -1), (slice(1, 2, None), slice(None, None, -1), 2), (slice(2, 0, -1), slice(None, None), -1), (slice(-2, None, None),), (slice(-1, None, None), slice(-2, None, None)), # With ellipsis (Ellipsis, slice(1, 3)), (1, Ellipsis, slice(1, 3)), (slice(0, 1), Ellipsis), (Ellipsis, None), (None, Ellipsis), (1, Ellipsis), (1, Ellipsis, None), (1, 1, 1, Ellipsis), (Ellipsis, 1, None), # With multi-axis advanced indexing ([0, 1],) * 2, ([0, 1], [0, 2]), ([0, 0, 0], [0, 1, 2], [1, 2, 1]), # Pathological - Slices larger than array (slice(None, 1000)), (slice(None), slice(None, 1000)), (slice(None), slice(1000, -1000, -1)), (slice(None), slice(1000, -1000, -50)), # Pathological - Wrong ordering of start/stop (slice(5, 0),), (slice(0, 5, -1),), (slice(0, 0, None),), ], ) def test_slicing(index): s = sparse.random((2, 3, 4), density=0.5) x = s.todense() assert_eq(x[index], s[index]) @pytest.mark.parametrize( "index", [ ([1, 0], 0), (1, [0, 2]), (0, [1, 0], 0), (1, [2, 0], 0), (1, [], 0), ([True, False], slice(1, None), slice(-2, None)), (slice(1, None), slice(-2, None), [True, False, True, False]), ([1, 0],), (Ellipsis, [2, 1, 3]), (slice(None), [2, 1, 2]), (1, [2, 0, 1]), ], ) def test_advanced_indexing(index): s = sparse.random((2, 3, 4), density=0.5) x = s.todense() assert_eq(x[index], s[index]) def test_custom_dtype_slicing(): dt = np.dtype([("part1", np.float64), ("part2", np.int64, (2,)), ("part3", np.int64, (2, 2))]) x = np.zeros((2, 3, 4), dtype=dt) x[1, 1, 1] = (0.64, [4, 2], [[1, 2], [3, 0]]) s = COO.from_numpy(x) assert x[1, 1, 1] == s[1, 1, 1] assert x[0, 1, 2] == s[0, 1, 2] assert_eq(x["part1"], s["part1"]) assert_eq(x["part2"], s["part2"]) assert_eq(x["part3"], s["part3"]) @pytest.mark.parametrize( "index", [ (Ellipsis, Ellipsis), (1, 1, 1, 1), (slice(None),) * 4, 5, -5, "foo", [True, False, False], 0.5, [0.5], {"potato": "kartoffel"}, ([[0, 1]],), ], ) def test_slicing_errors(index): s = sparse.random((2, 3, 4), density=0.5) with pytest.raises(IndexError): s[index] def test_concatenate(): xx = sparse.random((2, 3, 4), density=0.5) x = xx.todense() yy = sparse.random((5, 3, 4), density=0.5) y = yy.todense() zz = sparse.random((4, 3, 4), density=0.5) z = zz.todense() assert_eq(np.concatenate([x, y, z], axis=0), sparse.concatenate([xx, yy, zz], axis=0)) xx = sparse.random((5, 3, 1), density=0.5) x = xx.todense() yy = sparse.random((5, 3, 3), density=0.5) y = yy.todense() zz = sparse.random((5, 3, 2), density=0.5) z = zz.todense() assert_eq(np.concatenate([x, y, z], axis=2), sparse.concatenate([xx, yy, zz], axis=2)) assert_eq(np.concatenate([x, y, z], axis=-1), sparse.concatenate([xx, yy, zz], axis=-1)) @pytest.mark.parametrize("axis", [0, 1]) @pytest.mark.parametrize("func", [sparse.stack, sparse.concatenate]) def test_concatenate_mixed(func, axis): s = sparse.random((10, 10), density=0.5) d = s.todense() with pytest.raises(ValueError): func([d, s, s], axis=axis) def test_concatenate_noarrays(): with pytest.raises(ValueError): sparse.concatenate([]) @pytest.mark.parametrize("shape", [(5,), (2, 3, 4), (5, 2)]) @pytest.mark.parametrize("axis", [0, 1, -1]) def test_stack(shape, axis): xx = sparse.random(shape, density=0.5) x = xx.todense() yy = sparse.random(shape, density=0.5) y = yy.todense() zz = sparse.random(shape, density=0.5) z = zz.todense() assert_eq(np.stack([x, y, z], axis=axis), sparse.stack([xx, yy, zz], axis=axis)) def test_large_concat_stack(): data = np.array([1], dtype=np.uint8) coords = np.array([[255]], dtype=np.uint8) xs = COO(coords, data, shape=(256,), has_duplicates=False, sorted=True) x = xs.todense() assert_eq(np.stack([x, x]), sparse.stack([xs, xs])) assert_eq(np.concatenate((x, x)), sparse.concatenate((xs, xs))) def test_addition(): a = sparse.random((2, 3, 4), density=0.5) x = a.todense() b = sparse.random((2, 3, 4), density=0.5) y = b.todense() assert_eq(x + y, a + b) assert_eq(x - y, a - b) @pytest.mark.parametrize("scalar", [2, 2.5, np.float32(2.0), np.int8(3)]) def test_scalar_multiplication(scalar): a = sparse.random((2, 3, 4), density=0.5) x = a.todense() assert_eq(x * scalar, a * scalar) assert (a * scalar).nnz == a.nnz assert_eq(scalar * x, scalar * a) assert (scalar * a).nnz == a.nnz assert_eq(x / scalar, a / scalar) assert (a / scalar).nnz == a.nnz assert_eq(x // scalar, a // scalar) # division may reduce nnz. @pytest.mark.filterwarnings("ignore:divide by zero") def test_scalar_exponentiation(): a = sparse.random((2, 3, 4), density=0.5) x = a.todense() assert_eq(x**2, a**2) assert_eq(x**0.5, a**0.5) assert_eq(x**-1, a**-1) def test_create_with_lists_of_tuples(): L = [((0, 0, 0), 1), ((1, 2, 1), 1), ((1, 1, 1), 2), ((1, 3, 2), 3)] s = COO(L, shape=(2, 4, 3)) x = np.zeros((2, 4, 3), dtype=np.asarray([1, 2, 3]).dtype) for ind, value in L: x[ind] = value assert_eq(s, x) def test_sizeof(): x = np.eye(100) y = COO.from_numpy(x) nb = sys.getsizeof(y) assert 400 < nb < x.nbytes / 10 def test_scipy_sparse_interface(rng): n = 100 m = 10 row = rng.integers(0, n, size=n, dtype=np.uint16) col = rng.integers(0, m, size=n, dtype=np.uint16) data = np.ones(n, dtype=np.uint8) inp = (data, (row, col)) x = scipy.sparse.coo_matrix(inp, shape=(n, m)) xx = sparse.COO(inp, shape=(n, m)) assert_eq(x, xx, check_nnz=False) assert_eq(x.T, xx.T, check_nnz=False) assert_eq(xx.to_scipy_sparse(), x, check_nnz=False) assert_eq(COO.from_scipy_sparse(xx.to_scipy_sparse()), xx, check_nnz=False) assert_eq(x, xx, check_nnz=False) assert_eq(x.T.dot(x), xx.T.dot(xx), check_nnz=False) assert isinstance(x + xx, COO) assert isinstance(xx + x, COO) @pytest.mark.parametrize("scipy_format", ["coo", "csr", "dok", "csc"]) def test_scipy_sparse_interaction(scipy_format): x = sparse.random((10, 20), density=0.2).todense() sp = getattr(scipy.sparse, scipy_format + "_matrix")(x) coo = COO(x) assert isinstance(sp + coo, COO) assert isinstance(coo + sp, COO) assert_eq(sp, coo) @pytest.mark.parametrize( "func", [operator.mul, operator.add, operator.sub, operator.gt, operator.lt, operator.ne], ) def test_op_scipy_sparse(func): xs = sparse.random((3, 4), density=0.5) y = sparse.random((3, 4), density=0.5).todense() ys = scipy.sparse.csr_matrix(y) x = xs.todense() assert_eq(func(x, y), func(xs, ys)) @pytest.mark.parametrize( "func", [ operator.add, operator.sub, pytest.param( operator.mul, marks=pytest.mark.xfail(reason="Scipy sparse auto-densifies in this case."), ), pytest.param( operator.gt, marks=pytest.mark.xfail(reason="Scipy sparse doesn't support this yet."), ), pytest.param( operator.lt, marks=pytest.mark.xfail(reason="Scipy sparse doesn't support this yet."), ), pytest.param( operator.ne, marks=pytest.mark.xfail(reason="Scipy sparse doesn't support this yet."), ), ], ) def test_op_scipy_sparse_left(func): ys = sparse.random((3, 4), density=0.5) x = sparse.random((3, 4), density=0.5).todense() xs = scipy.sparse.csr_matrix(x) y = ys.todense() assert_eq(func(x, y), func(xs, ys)) def test_cache_csr(): x = sparse.random((10, 5), density=0.5).todense() s = COO(x, cache=True) assert isinstance(s.tocsr(), scipy.sparse.csr_matrix) assert isinstance(s.tocsc(), scipy.sparse.csc_matrix) assert s.tocsr() is s.tocsr() assert s.tocsc() is s.tocsc() def test_single_dimension(): x = COO([1, 3], [1.0, 3.0], shape=(4,)) assert_eq(x, np.array([0, 1.0, 0, 3.0])) def test_large_sum(rng): n = 500000 x = rng.integers(0, 10000, size=(n,)) y = rng.integers(0, 1000, size=(n,)) z = rng.integers(0, 3, size=(n,)) data = rng.random(n) a = COO((x, y, z), data, shape=(10000, 1000, 3)) b = a.sum(axis=2) assert b.nnz > 100000 def test_add_many_sparse_arrays(): x = COO({(1, 1): 1}, shape=(2, 2)) y = sum([x] * 100) assert y.nnz < np.prod(y.shape) def test_caching(): x = COO({(9, 9, 9): 1}, shape=(10, 10, 10)) assert x[:].reshape((100, 10)).transpose().tocsr() is not x[:].reshape((100, 10)).transpose().tocsr() x = COO({(9, 9, 9): 1}, shape=(10, 10, 10), cache=True) assert x[:].reshape((100, 10)).transpose().tocsr() is x[:].reshape((100, 10)).transpose().tocsr() x = COO({(1, 1, 1, 1, 1, 1, 1, 2): 1}, shape=(2, 2, 2, 2, 2, 2, 2, 3), cache=True) for _ in range(x.ndim): x.reshape(x.size) assert len(x._cache["reshape"]) < 5 def test_scalar_slicing(): x = np.array([0, 1]) s = COO(x) assert np.isscalar(s[0]) assert_eq(x[0], s[0]) assert isinstance(s[0, ...], COO) assert s[0, ...].shape == () assert_eq(x[0, ...], s[0, ...]) assert np.isscalar(s[1]) assert_eq(x[1], s[1]) assert isinstance(s[1, ...], COO) assert s[1, ...].shape == () assert_eq(x[1, ...], s[1, ...]) @pytest.mark.parametrize( "shape, k", [((3, 4), 0), ((3, 4, 5), 1), ((4, 2), -1), ((2, 4), -2), ((4, 4), 1000)], ) def test_triul(shape, k): s = sparse.random(shape, density=0.5) x = s.todense() assert_eq(np.triu(x, k), sparse.triu(s, k)) assert_eq(np.tril(x, k), sparse.tril(s, k)) def test_empty_reduction(): x = np.zeros((2, 3, 4), dtype=np.float64) xs = COO.from_numpy(x) assert_eq(x.sum(axis=(0, 2)), xs.sum(axis=(0, 2))) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4)]) @pytest.mark.parametrize("density", [0.1, 0.3, 0.5, 0.7]) def test_random_shape(shape, density): s = sparse.random(shape, density) assert isinstance(s, COO) assert s.shape == shape expected_nnz = density * np.prod(shape) assert np.floor(expected_nnz) <= s.nnz <= np.ceil(expected_nnz) @pytest.mark.parametrize("shape, nnz", [((1,), 1), ((2,), 0), ((3, 4), 5)]) def test_random_nnz(shape, nnz): s = sparse.random(shape, nnz=nnz) assert isinstance(s, COO) assert s.nnz == nnz @pytest.mark.parametrize("density, nnz", [(1, 1), (1.01, None), (-0.01, None), (None, 2)]) def test_random_invalid_density_and_nnz(density, nnz): with pytest.raises(ValueError): sparse.random((1,), density, nnz=nnz) def test_two_random_unequal(): s1 = sparse.random((2, 3, 4), 0.3) s2 = sparse.random((2, 3, 4), 0.3) assert not np.allclose(s1.todense(), s2.todense()) def test_two_random_same_seed(rng): state = rng.integers(100) s1 = sparse.random((2, 3, 4), 0.3, random_state=state) s2 = sparse.random((2, 3, 4), 0.3, random_state=state) assert_eq(s1, s2) @pytest.mark.parametrize( "rvs, dtype", [ (None, np.float64), (scipy.stats.poisson(25, loc=10).rvs, np.int64), (lambda x: np.random.default_rng().choice([True, False], size=x), np.bool_), ], ) @pytest.mark.parametrize("shape", [(2, 4, 5), (20, 40, 50)]) @pytest.mark.parametrize("density", [0.0, 0.01, 0.1, 0.2]) def test_random_rvs(rvs, dtype, shape, density): x = sparse.random(shape, density, data_rvs=rvs) assert x.shape == shape assert x.dtype == dtype @pytest.mark.parametrize("format", ["coo", "dok"]) def test_random_fv(format, rng): fv = rng.random() s = sparse.random((2, 3, 4), density=0.5, format=format, fill_value=fv) assert s.fill_value == fv def test_scalar_shape_construction(rng): x = rng.random(5) coords = np.arange(5)[None] s = COO(coords, x, shape=5) assert_eq(x, s) def test_len(): s = sparse.random((20, 30, 40)) assert len(s) == 20 def test_density(): s = sparse.random((20, 30, 40), density=0.1) assert np.isclose(s.density, 0.1) def test_size(): s = sparse.random((20, 30, 40)) assert s.size == 20 * 30 * 40 def test_np_array(): s = sparse.random((20, 30, 40)) with pytest.raises(RuntimeError): np.array(s) @pytest.mark.parametrize( "shapes", [ [(2,), (3, 2), (4, 3, 2)], [(3,), (2, 3), (2, 2, 3)], [(2,), (2, 2), (2, 2, 2)], [(4,), (4, 4), (4, 4, 4)], [(4,), (4, 4), (4, 4, 4)], [(4,), (4, 4), (4, 4, 4)], [(1, 1, 2), (1, 3, 1), (4, 1, 1)], [(2,), (2, 1), (2, 1, 1)], [(3,), (), (2, 3)], [(4, 4), (), ()], ], ) def test_three_arg_where(shapes): cs = sparse.random(shapes[0], density=0.5).astype(np.bool_) xs = sparse.random(shapes[1], density=0.5) ys = sparse.random(shapes[2], density=0.5) c = cs.todense() x = xs.todense() y = ys.todense() expected = np.where(c, x, y) actual = sparse.where(cs, xs, ys) assert isinstance(actual, COO) assert_eq(expected, actual) def test_one_arg_where(): s = sparse.random((2, 3, 4), density=0.5) x = s.todense() expected = np.where(x) actual = sparse.where(s) assert len(expected) == len(actual) for e, a in zip(expected, actual, strict=True): assert_eq(e, a, compare_dtype=False) def test_one_arg_where_dense(rng): x = rng.random((2, 3, 4)) with pytest.raises(ValueError): sparse.where(x) def test_two_arg_where(): cs = sparse.random((2, 3, 4), density=0.5).astype(np.bool_) xs = sparse.random((2, 3, 4), density=0.5) with pytest.raises(ValueError): sparse.where(cs, xs) @pytest.mark.parametrize("func", [operator.imul, operator.iadd, operator.isub]) def test_inplace_invalid_shape(func): xs = sparse.random((3, 4), density=0.5) ys = sparse.random((2, 3, 4), density=0.5) with pytest.raises(ValueError): func(xs, ys) def test_nonzero(): s = sparse.random((2, 3, 4), density=0.5) x = s.todense() expected = x.nonzero() actual = s.nonzero() assert isinstance(actual, tuple) assert len(expected) == len(actual) for e, a in zip(expected, actual, strict=True): assert_eq(e, a, compare_dtype=False) def test_argwhere(): s = sparse.random((2, 3, 4), density=0.5) x = s.todense() assert_eq(np.argwhere(s), np.argwhere(x), compare_dtype=False) @pytest.mark.parametrize("format", ["coo", "dok"]) def test_asformat(format): s = sparse.random((2, 3, 4), density=0.5, format="coo") s2 = s.asformat(format) assert_eq(s, s2) @pytest.mark.parametrize("format", [sparse.COO, sparse.DOK, scipy.sparse.csr_matrix, np.asarray]) def test_as_coo(format): x = format(sparse.random((3, 4), density=0.5, format="coo").todense()) s1 = sparse.as_coo(x) s2 = COO(x) assert_eq(x, s1) assert_eq(x, s2) def test_invalid_attrs_error(): s = sparse.random((3, 4), density=0.5, format="coo") with pytest.raises(ValueError): sparse.as_coo(s, shape=(2, 3)) with pytest.raises(ValueError): COO(s, shape=(2, 3)) with pytest.raises(ValueError): sparse.as_coo(s, fill_value=0.0) def test_invalid_iterable_error(): with pytest.raises(ValueError): x = [(3, 4, 5)] COO.from_iter(x, shape=(6,)) with pytest.raises(ValueError): x = [((2.3, 4.5), 3.2)] COO.from_iter(x, shape=(5,)) with pytest.raises(TypeError): COO.from_iter({(1, 1): 1}) def test_prod_along_axis(): s1 = sparse.random((10, 10), density=0.1) s2 = 1 - s1 x1 = s1.todense() x2 = s2.todense() assert_eq(s1.prod(axis=0), x1.prod(axis=0)) assert_eq(s2.prod(axis=0), x2.prod(axis=0)) class TestRoll: # test on 1d array # @pytest.mark.parametrize("shift", [0, 2, -2, 20, -20]) def test_1d(self, shift): xs = sparse.random((100,), density=0.5) x = xs.todense() assert_eq(np.roll(x, shift), sparse.roll(xs, shift)) assert_eq(np.roll(x, shift), sparse.roll(x, shift)) # test on 2d array # @pytest.mark.parametrize("shift", [0, 2, -2, 20, -20]) @pytest.mark.parametrize("ax", [None, 0, 1, (0, 1)]) def test_2d(self, shift, ax): xs = sparse.random((10, 10), density=0.5) x = xs.todense() assert_eq(np.roll(x, shift, axis=ax), sparse.roll(xs, shift, axis=ax)) assert_eq(np.roll(x, shift, axis=ax), sparse.roll(x, shift, axis=ax)) # test on rolling multiple axes at once # @pytest.mark.parametrize("shift", [(0, 0), (1, -1), (-1, 1), (10, -10)]) @pytest.mark.parametrize("ax", [(0, 1), (0, 2), (1, 2), (-1, 1)]) def test_multiaxis(self, shift, ax): xs = sparse.random((9, 9, 9), density=0.5) x = xs.todense() assert_eq(np.roll(x, shift, axis=ax), sparse.roll(xs, shift, axis=ax)) assert_eq(np.roll(x, shift, axis=ax), sparse.roll(x, shift, axis=ax)) # test original is unchanged # @pytest.mark.parametrize("shift", [0, 2, -2, 20, -20]) @pytest.mark.parametrize("ax", [None, 0, 1, (0, 1)]) def test_original_is_copied(self, shift, ax): xs = sparse.random((10, 10), density=0.5) xc = COO(np.copy(xs.coords), np.copy(xs.data), shape=xs.shape) sparse.roll(xs, shift, axis=ax) assert_eq(xs, xc) # test on empty array # def test_empty(self): x = np.array([]) assert_eq(np.roll(x, 1), sparse.roll(sparse.as_coo(x), 1)) # test error handling # @pytest.mark.parametrize( "args", [ # iterable shift, but axis not iterable ((1, 1), 0), # ndim(axis) != 1 (1, [[0, 1]]), # ndim(shift) != 1 ([[0, 1]], [0, 1]), ([[0, 1], [0, 1]], [0, 1]), ], ) def test_valerr(self, args): x = sparse.random((2, 2, 2), density=1) with pytest.raises(ValueError): sparse.roll(x, *args) @pytest.mark.parametrize("dtype", [np.uint8, np.int8]) @pytest.mark.parametrize("shift", [300, -300]) def test_dtype_errors(self, dtype, shift): x = sparse.random((5, 5, 5), density=0.2, idx_dtype=dtype) with pytest.raises(ValueError): sparse.roll(x, shift) def test_unsigned_type_error(self): x = sparse.random((5, 5, 5), density=0.3, idx_dtype=np.uint8) with pytest.raises(ValueError): sparse.roll(x, -1) def test_clip(): x = np.array([[0, 0, 1, 0, 2], [5, 0, 0, 3, 0]]) s = sparse.COO.from_numpy(x) assert_eq(s.clip(min=1), x.clip(min=1)) assert_eq(s.clip(max=3), x.clip(max=3)) assert_eq(s.clip(min=1, max=3), x.clip(min=1, max=3)) assert_eq(s.clip(min=1, max=3.0), x.clip(min=1, max=3.0)) assert_eq(np.clip(s, 1, 3), np.clip(x, 1, 3)) out = sparse.COO.from_numpy(np.zeros_like(x)) out2 = s.clip(min=1, max=3, out=out) assert out is out2 assert_eq(out, x.clip(min=1, max=3)) class TestFailFillValue: # Check failed fill_value op def test_nonzero_fv(self): xs = sparse.random((2, 3), density=0.5, fill_value=1) ys = sparse.random((3, 4), density=0.5) with pytest.raises(ValueError): sparse.dot(xs, ys) def test_inconsistent_fv(self): xs = sparse.random((3, 4), density=0.5, fill_value=1) ys = sparse.random((3, 4), density=0.5, fill_value=2) with pytest.raises(ValueError): sparse.concatenate([xs, ys]) def test_pickle(): x = sparse.COO.from_numpy([1, 0, 0, 0, 0]).reshape((5, 1)) # Enable caching and add some data to it x.enable_caching() x.T # noqa: B018 assert x._cache is not None # Pickle sends data but not cache x2 = pickle.loads(pickle.dumps(x)) assert_eq(x, x2) assert x2._cache is None @pytest.mark.parametrize("deep", [True, False]) def test_copy(deep): x = sparse.COO.from_numpy([1, 0, 0, 0, 0]).reshape((5, 1)) # Enable caching and add some data to it x.enable_caching() x.T # noqa: B018 assert x._cache is not None x2 = x.copy(deep) assert_eq(x, x2) assert (x2.data is x.data) is not deep assert (x2.coords is x.coords) is not deep assert x2._cache is None @pytest.mark.parametrize("ndim", [2, 3, 4, 5]) def test_initialization(ndim, rng): shape = [10] * ndim shape[1] *= 2 shape = tuple(shape) coords = rng.integers(10, size=(ndim, 20)) data = rng.random(20) COO(coords, data=data, shape=shape) with pytest.raises(ValueError, match="data length"): COO(coords, data=data[:5], shape=shape) with pytest.raises(ValueError, match="shape of `coords`"): coords = rng.integers(10, size=(1, 20)) COO(coords, data=data, shape=shape) @pytest.mark.parametrize("N, M", [(4, None), (4, 10), (10, 4), (0, 10)]) def test_eye(N, M): m = M or N for k in [0, N - 2, N + 2, m - 2, m + 2, np.iinfo(np.intp).min]: assert_eq(sparse.eye(N, M=M, k=k), np.eye(N, M=M, k=k)) assert_eq(sparse.eye(N, M=M, k=k, dtype="i4"), np.eye(N, M=M, k=k, dtype="i4")) @pytest.mark.parametrize("from_", [np.int8, np.int64, np.float32, np.float64, np.complex64, np.complex128]) @pytest.mark.parametrize("to", [np.int8, np.int64, np.float32, np.float64, np.complex64, np.complex128]) @pytest.mark.parametrize("casting", ["no", "safe", "same_kind"]) def test_can_cast(from_, to, casting): assert sparse.can_cast(sparse.zeros((2, 2), dtype=from_), to, casting=casting) == np.can_cast( np.zeros((2, 2), dtype=from_), to, casting=casting ) assert sparse.can_cast(from_, to, casting=casting) == np.can_cast(from_, to, casting=casting) @pytest.mark.parametrize("funcname", ["ones", "zeros"]) def test_ones_zeros(funcname): sp_func = getattr(sparse, funcname) np_func = getattr(np, funcname) assert_eq(sp_func(5), np_func(5)) assert_eq(sp_func((5, 4)), np_func((5, 4))) assert_eq(sp_func((5, 4), dtype="i4"), np_func((5, 4), dtype="i4")) assert_eq(sp_func((5, 4), dtype=None), np_func((5, 4), dtype=None)) @pytest.mark.parametrize("funcname", ["ones_like", "zeros_like"]) def test_ones_zeros_like(funcname): sp_func = getattr(sparse, funcname) np_func = getattr(np, funcname) x = np.ones((5, 5), dtype="i8") assert_eq(sp_func(x), np_func(x)) assert_eq(sp_func(x, dtype="f8"), np_func(x, dtype="f8")) assert_eq(sp_func(x, dtype=None), np_func(x, dtype=None)) assert_eq(sp_func(x, shape=(2, 2)), np_func(x, shape=(2, 2))) def test_full(): assert_eq(sparse.full(5, 9), np.full(5, 9)) assert_eq(sparse.full(5, 9, dtype="f8"), np.full(5, 9, dtype="f8")) assert_eq(sparse.full((5, 4), 9.5), np.full((5, 4), 9.5)) assert_eq(sparse.full((5, 4), 9.5, dtype="i4"), np.full((5, 4), 9.5, dtype="i4")) def test_full_like(): x = np.zeros((5, 5), dtype="i8") assert_eq(sparse.full_like(x, 9.5), np.full_like(x, 9.5)) assert_eq(sparse.full_like(x, 9.5, dtype="f8"), np.full_like(x, 9.5, dtype="f8")) assert_eq(sparse.full_like(x, 9.5, shape=(2, 2)), np.full_like(x, 9.5, shape=(2, 2))) @pytest.mark.parametrize( "x", [ np.array([1, 2, 0, 0, 0]), np.array([1 + 2j, 2 - 1j, 0, 1, 0]), np.array(["a", "b", "c"]), ], ) def test_complex_methods(x): s = sparse.COO.from_numpy(x) assert_eq(s.imag, x.imag) assert_eq(s.real, x.real) if np.issubdtype(s.dtype, np.number): assert_eq(s.conj(), x.conj()) def test_np_matrix(rng): x = rng.random((10, 1)).view(type=np.matrix) s = sparse.COO.from_numpy(x) assert_eq(x, s) def test_out_dtype(): a = sparse.eye(5, dtype="float32") b = sparse.eye(5, dtype="float64") assert np.positive(a, out=b).dtype == np.positive(a.todense(), out=b.todense()).dtype assert ( np.positive(a, out=b, dtype="float64").dtype == np.positive(a.todense(), out=b.todense(), dtype="float64").dtype ) @contextlib.contextmanager def auto_densify(): "For use in tests only! Not threadsafe." import os from importlib import reload os.environ["SPARSE_AUTO_DENSIFY"] = "1" reload(sparse.numba_backend._settings) yield del os.environ["SPARSE_AUTO_DENSIFY"] reload(sparse.numba_backend._settings) def test_setting_into_numpy_slice(): actual = np.zeros((5, 5)) s = sparse.COO(data=[1, 1], coords=(2, 4), shape=(5,)) # This calls s.__array__(dtype('float64')) which means that __array__ # must accept a positional argument. If not this will raise, of course, # TypeError: __array__() takes 1 positional argument but 2 were given with auto_densify(): actual[:, 0] = s # Might as well check the content of the result as well. expected = np.zeros((5, 5)) expected[:, 0] = s.todense() assert_eq(actual, expected) # Without densification, setting is unsupported. with pytest.raises(RuntimeError): actual[:, 0] = s def test_successful_densification(): s = sparse.random((3, 4, 5), density=0.5) with auto_densify(): x = np.array(s) assert isinstance(x, np.ndarray) assert_eq(s, x) def test_failed_densification(): s = sparse.random((3, 4, 5), density=0.5) with pytest.raises(RuntimeError): np.array(s) def test_warn_on_too_dense(): import os from importlib import reload os.environ["SPARSE_WARN_ON_TOO_DENSE"] = "1" reload(sparse.numba_backend._settings) with pytest.warns(RuntimeWarning): sparse.random((3, 4, 5), density=1.0) del os.environ["SPARSE_WARN_ON_TOO_DENSE"] reload(sparse.numba_backend._settings) def test_prune_coo(): coords = np.array([[0, 1, 2, 3]]) data = np.array([1, 0, 1, 2]) s1 = COO(coords, data, shape=(4,)) s2 = COO(coords, data, shape=(4,), prune=True) assert s2.nnz == 3 # Densify s1 because it isn't canonical assert_eq(s1.todense(), s2, check_nnz=False) def test_diagonal(): a = sparse.random((4, 4), density=0.5) assert_eq(sparse.diagonal(a, offset=0), np.diagonal(a.todense(), offset=0)) assert_eq(sparse.diagonal(a, offset=1), np.diagonal(a.todense(), offset=1)) assert_eq(sparse.diagonal(a, offset=2), np.diagonal(a.todense(), offset=2)) a = sparse.random((4, 5, 4, 6), density=0.5) assert_eq( sparse.diagonal(a, offset=0, axis1=0, axis2=2), np.diagonal(a.todense(), offset=0, axis1=0, axis2=2), ) assert_eq( sparse.diagonal(a, offset=1, axis1=0, axis2=2), np.diagonal(a.todense(), offset=1, axis1=0, axis2=2), ) assert_eq( sparse.diagonal(a, offset=2, axis1=0, axis2=2), np.diagonal(a.todense(), offset=2, axis1=0, axis2=2), ) def test_diagonalize(): assert_eq(sparse.diagonalize(np.ones(3)), sparse.eye(3)) assert_eq( sparse.diagonalize(scipy.sparse.coo_matrix(np.eye(3))), sparse.diagonalize(sparse.eye(3)), ) # inverse of diagonal b = sparse.random((4, 3, 2), density=0.5) b_diag = sparse.diagonalize(b, axis=1) assert_eq(b, sparse.diagonal(b_diag, axis1=1, axis2=3).transpose([0, 2, 1])) RESULT_TYPE_DTYPES = [ "i1", "i2", "i4", "i8", "u1", "u2", "u4", "u8", "f4", "f8", "c8", "c16", object, ] @pytest.mark.parametrize("t1", RESULT_TYPE_DTYPES) @pytest.mark.parametrize("t2", RESULT_TYPE_DTYPES) @pytest.mark.parametrize( "func", [ sparse.result_type, pytest.param( np.result_type, marks=pytest.mark.skipif(not NEP18_ENABLED, reason="NEP18 is not enabled"), ), ], ) @pytest.mark.parametrize("data", [1, [1]]) # Not the same outputs! def test_result_type(t1, t2, func, data): a = np.array(data, dtype=t1) b = np.array(data, dtype=t2) expect = np.result_type(a, b) assert func(a, sparse.COO(b)) == expect assert func(sparse.COO(a), b) == expect assert func(sparse.COO(a), sparse.COO(b)) == expect assert func(a.dtype, sparse.COO(b)) == np.result_type(a.dtype, b) assert func(sparse.COO(a), b.dtype) == np.result_type(a, b.dtype) @pytest.mark.parametrize("in_shape", [(5, 5), 62, (3, 3, 3)]) def test_flatten(in_shape): s = sparse.random(in_shape, density=0.5) x = s.todense() a = s.flatten() e = x.flatten() assert_eq(e, a) def test_asnumpy(): s = sparse.COO(data=[1], coords=[2], shape=(5,)) assert_eq(sparse.asnumpy(s), s.todense()) assert_eq(sparse.asnumpy(s, dtype=np.float64), np.asarray(s.todense(), dtype=np.float64)) a = np.array([1, 2, 3]) # Array passes through with no copying. assert sparse.asnumpy(a) is a @pytest.mark.parametrize("shape1", [(2,), (2, 3), (2, 3, 4)]) @pytest.mark.parametrize("shape2", [(2,), (2, 3), (2, 3, 4)]) def test_outer(shape1, shape2): s1 = sparse.random(shape1, density=0.5) s2 = sparse.random(shape2, density=0.5) x1 = s1.todense() x2 = s2.todense() assert_eq(sparse.outer(s1, s2), np.outer(x1, x2)) assert_eq(np.multiply.outer(s1, s2), np.multiply.outer(x1, x2)) def test_scalar_list_init(): a = sparse.COO([], [], ()) b = sparse.COO([], [1], ()) assert a.todense() == 0 assert b.todense() == 1 def test_raise_on_nd_data(): s1 = sparse.random((2, 3, 4), density=0.5) with pytest.raises(ValueError): sparse.COO(s1.coords, s1.data[:, None], shape=(2, 3, 4)) def test_astype_casting(): s1 = sparse.random((2, 3, 4), density=0.5) with pytest.raises(TypeError): s1.astype(dtype=np.int64, casting="safe") def test_astype_no_copy(): s1 = sparse.random((2, 3, 4), density=0.5) s2 = s1.astype(s1.dtype, copy=False) assert s1 is s2 def test_coo_valerr(): a = np.arange(300) with pytest.raises(ValueError): COO.from_numpy(a, idx_dtype=np.int8) def test_random_idx_dtype(): with pytest.raises(ValueError): sparse.random((300,), density=0.1, format="coo", idx_dtype=np.int8) def test_html_for_size_zero(): arr = sparse.COO.from_numpy(np.array(())) ground_truth = "" ground_truth += '' ground_truth += '' ground_truth += '' ground_truth += '' ground_truth += '' ground_truth += '' ground_truth += '' ground_truth += '' ground_truth += "
Formatcoo
Data Typefloat64
Shape(0,)
nnz0
Densitynan
Read-onlyTrue
Size0
Storage rationan
" table = html_table(arr) assert table == ground_truth @pytest.mark.parametrize( "pad_width", [ 2, (2, 1), ((2), (1)), ((1, 2), (4, 5), (7, 8)), ], ) @pytest.mark.parametrize("constant_values", [0, 1, 150, np.nan]) def test_pad_valid(pad_width, constant_values): y = sparse.random((50, 50, 3), density=0.15, fill_value=constant_values) x = y.todense() xx = np.pad(x, pad_width=pad_width, constant_values=constant_values) yy = np.pad(y, pad_width=pad_width, constant_values=constant_values) assert_eq(xx, yy) @pytest.mark.parametrize( "pad_width", [ ((2, 1), (5, 7)), ], ) @pytest.mark.parametrize("constant_values", [150, 2, (1, 2)]) def test_pad_invalid(pad_width, constant_values, fill_value=0): y = sparse.random((50, 50, 3), density=0.15) with pytest.raises(ValueError): np.pad(y, pad_width, constant_values=constant_values) @pytest.mark.parametrize("val", [0, 5]) def test_scalar_from_numpy(val): x = np.int64(val) s = sparse.COO.from_numpy(x) assert s.nnz == 0 assert_eq(x, s) def test_scalar_elemwise(rng): s1 = sparse.random((), density=0.5) x2 = rng.random(2) x1 = s1.todense() assert_eq(s1 * x2, x1 * x2) def test_array_as_shape(): coords = [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]] data = [10, 20, 30, 40, 50] sparse.COO(coords, data, shape=np.array((5, 5))) @pytest.mark.parametrize( "arr", [np.array([[0, 3, 0], [1, 2, 0]]), np.array([[[0, 0], [1, 0]], [[5, 0], [0, -3]]])], ) @pytest.mark.parametrize("axis", [None, 0, 1]) @pytest.mark.parametrize("keepdims", [True, False]) @pytest.mark.parametrize("mode", [(sparse.argmax, np.argmax), (sparse.argmin, np.argmin)]) def test_argmax_argmin(arr, axis, keepdims, mode): sparse_func, np_func = mode s_arr = sparse.COO.from_numpy(arr) result = sparse_func(s_arr, axis=axis, keepdims=keepdims).todense() expected = np_func(arr, axis=axis, keepdims=keepdims) np.testing.assert_equal(result, expected) @pytest.mark.parametrize("axis", [None, 0, 1, 2]) @pytest.mark.parametrize("mode", [(sparse.argmax, np.argmax), (sparse.argmin, np.argmin)]) def test_argmax_argmin_3D(axis, mode): sparse_func, np_func = mode s_arr = sparse.zeros(shape=(1000, 550, 3), format="dok") s_arr[100, 100, 0] = 3 s_arr[100, 100, 1] = 3 s_arr[100, 99, 0] = -2 s_arr = s_arr.to_coo() result = sparse_func(s_arr, axis=axis).todense() expected = np_func(s_arr.todense(), axis=axis) np.testing.assert_equal(result, expected) @pytest.mark.parametrize("func", [sparse.argmax, sparse.argmin]) def test_argmax_argmin_constraint(func): s = sparse.COO.from_numpy(np.full((2, 2), 2), fill_value=2) with pytest.raises(ValueError, match="`axis=2` is out of bounds for array of dimension 2."): func(s, axis=2) @pytest.mark.parametrize("config", [(np.inf, "isinf"), (np.nan, "isnan")]) def test_isinf_isnan(config): obj, func_name = config arr = np.array([[1, 1, obj], [-obj, 1, 1]]) s = sparse.COO.from_numpy(arr) result = getattr(s, func_name)().todense() expected = getattr(np, func_name)(arr) np.testing.assert_equal(result, expected) class TestSqueeze: eye_arr = np.eye(2).reshape(1, 2, 1, 2) @pytest.mark.parametrize( "arr_and_axis", [ (eye_arr, None), (eye_arr, 0), (eye_arr, 2), (eye_arr, (0, 2)), (np.zeros((5,)), None), ], ) def test_squeeze(self, arr_and_axis): arr, axis = arr_and_axis s_arr = sparse.COO.from_numpy(arr) result_1 = sparse.squeeze(s_arr, axis=axis).todense() result_2 = s_arr.squeeze(axis=axis).todense() expected = np.squeeze(arr, axis=axis) np.testing.assert_equal(result_1, result_2) np.testing.assert_equal(result_1, expected) def test_squeeze_validation(self): s_arr = sparse.COO.from_numpy(np.eye(3)) with pytest.raises(IndexError, match="tuple index out of range"): s_arr.squeeze(3) with pytest.raises(ValueError, match="Invalid axis parameter: `1.1`."): s_arr.squeeze(1.1) with pytest.raises(ValueError, match="Specified axis `0` has a size greater than one: 3"): s_arr.squeeze(0) class TestUnique: arr = np.array([[0, 0, 1, 5, 3, 0], [1, 0, 4, 0, 3, 0], [0, 1, 0, 1, 1, 0]], dtype=np.int64) arr_empty = np.zeros((5, 5)) arr_full = np.arange(1, 10) @pytest.mark.parametrize("arr", [arr, arr_empty, arr_full]) @pytest.mark.parametrize("fill_value", [-1, 0, 1]) def test_unique_counts(self, arr, fill_value): s_arr = sparse.COO.from_numpy(arr, fill_value) result_values, result_counts = sparse.unique_counts(s_arr) expected_values, expected_counts = np.unique(arr, return_counts=True) np.testing.assert_equal(result_values, expected_values) np.testing.assert_equal(result_counts, expected_counts) @pytest.mark.parametrize("arr", [arr, arr_empty, arr_full]) @pytest.mark.parametrize("fill_value", [-1, 0, 1]) def test_unique_values(self, arr, fill_value): s_arr = sparse.COO.from_numpy(arr, fill_value) result = sparse.unique_values(s_arr) expected = np.unique(arr) np.testing.assert_equal(result, expected) @pytest.mark.parametrize("func", [sparse.unique_counts, sparse.unique_values]) def test_input_validation(self, func): with pytest.raises(ValueError, match="Input must be an instance of SparseArray"): func(self.arr) @pytest.mark.parametrize("axis", [-1, 0, 1, 2, 3]) def test_expand_dims(axis): arr = np.arange(24).reshape((2, 3, 4)) s_arr = sparse.COO.from_numpy(arr) result = sparse.expand_dims(s_arr, axis=axis) expected = np.expand_dims(arr, axis=axis) np.testing.assert_equal(result.todense(), expected) @pytest.mark.parametrize( "arr", [ np.array([[0, 0, 1, 5, 3, 0], [1, 0, 4, 0, 3, 0], [0, 1, 0, 1, 1, 0]], dtype=np.int64), np.array([[[2, 0], [0, 5]], [[1, 0], [4, 0]], [[0, 1], [0, -1]]], dtype=np.float64), np.arange(3, 10), ], ) @pytest.mark.parametrize("fill_value", [-1, 0, 1, 3]) @pytest.mark.parametrize("axis", [0, 1, -1]) @pytest.mark.parametrize("descending", [False, True]) @pytest.mark.parametrize( "stable", [False, pytest.param(True, marks=pytest.mark.xfail(reason="Numba doesn't support `stable=True`."))] ) def test_sort(arr, fill_value, axis, descending, stable): if axis >= arr.ndim: return s_arr = sparse.COO.from_numpy(arr, fill_value) kind = "mergesort" if stable else "quicksort" result = sparse.sort(s_arr, axis=axis, descending=descending, stable=stable) expected = -np.sort(-arr, axis=axis, kind=kind) if descending else np.sort(arr, axis=axis, kind=kind) np.testing.assert_equal(result.todense(), expected) # make sure no inplace changes happened np.testing.assert_equal(s_arr.todense(), arr) @pytest.mark.parametrize("fill_value", [-1, 0, 1]) @pytest.mark.parametrize("descending", [False, True]) def test_sort_only_fill_value(fill_value, descending): arr = np.full((3, 3), fill_value=fill_value) s_arr = sparse.COO.from_numpy(arr, fill_value) result = sparse.sort(s_arr, axis=0, descending=descending) expected = np.sort(arr, axis=0) np.testing.assert_equal(result.todense(), expected) @pytest.mark.parametrize("axis", [None, -1, 0, 1, 2, (0, 1), (2, 0)]) def test_flip(axis): arr = np.arange(24).reshape((2, 3, 4)) s_arr = sparse.COO.from_numpy(arr) result = sparse.flip(s_arr, axis=axis) expected = np.flip(arr, axis=axis) np.testing.assert_equal(result.todense(), expected) @pytest.mark.parametrize("fill_value", [-1, 0, 1, 3]) @pytest.mark.parametrize( "indices,axis", [ ( [1], 0, ), ([2, 1], 1), ([1, 2, 3], 2), ([2, 3], -1), ([5, 3, 7, 8], None), ], ) def test_take(fill_value, indices, axis): arr = np.arange(24).reshape((2, 3, 4)) s_arr = sparse.COO.from_numpy(arr, fill_value) result = sparse.take(s_arr, np.array(indices), axis=axis) expected = np.take(arr, indices, axis) np.testing.assert_equal(result.todense(), expected) @pytest.mark.parametrize("ndim", [2, 3, 4, 5]) @pytest.mark.parametrize("density", [0.0, 0.1, 0.25, 1.0]) def test_matrix_transpose(ndim, density): shape = tuple(range(2, 34)[:ndim]) xs = sparse.random(shape, density=density) xd = xs.todense() transpose_axes = list(range(ndim)) transpose_axes[-2:] = transpose_axes[-2:][::-1] expected = np.transpose(xd, axes=transpose_axes) actual = sparse.matrix_transpose(xs) assert_eq(actual, expected) assert_eq(xs.mT, expected) @pytest.mark.parametrize( ("shape1", "shape2", "axis"), [ ((2, 3, 4), (3, 4), -2), ((3, 4), (2, 3, 4), -1), ((3, 1, 4), (3, 2, 4), 2), ((1, 3, 4), (3, 4), -2), ((3, 4, 1), (3, 4, 2), 0), ((3, 1), (3, 4), -2), ((1, 4), (3, 4), 1), ], ) @pytest.mark.parametrize("density", [0.0, 0.1, 0.25, 1.0]) @pytest.mark.parametrize("is_complex", [False, True]) def test_vecdot(shape1, shape2, axis, density, rng, is_complex): def data_rvs(size): data = rng.random(size) if is_complex: data = data + rng.random(size) * 1j return data s1 = sparse.random(shape1, density=density, data_rvs=data_rvs) s2 = sparse.random(shape2, density=density, data_rvs=data_rvs) x1 = s1.todense() x2 = s2.todense() def np_vecdot(x1, x2, /, *, axis=-1): if np.issubdtype(x1.dtype, np.complexfloating): x1 = np.conjugate(x1) return np.sum(x1 * x2, axis=axis) actual = sparse.vecdot(s1, s2, axis=axis) assert s1.dtype == s2.dtype == actual.dtype expected = np_vecdot(x1, x2, axis=axis) np.testing.assert_allclose(actual.todense(), expected) @pytest.mark.parametrize( ("shape1", "shape2", "axis"), [ ((2, 3, 4), (3, 4), 0), ((3, 4), (2, 3, 4), 0), ((3, 1, 4), (3, 2, 4), -2), ((1, 3, 4), (3, 4), -3), ((3, 4, 1), (3, 4, 2), -1), ((3, 1), (3, 4), 1), ((1, 4), (3, 4), -2), ], ) def test_vecdot_invalid_axis(shape1, shape2, axis): s1 = sparse.random(shape1, density=0.5) s2 = sparse.random(shape2, density=0.5) with pytest.raises(ValueError, match=r"Shapes must match along"): sparse.vecdot(s1, s2, axis=axis) @pytest.mark.parametrize( ("func", "args", "kwargs"), [ (sparse.eye, (5,), {}), (sparse.zeros, ((5,)), {}), (sparse.ones, ((5,)), {}), (sparse.full, ((5,), 5), {}), (sparse.empty, ((5,)), {}), (sparse.full_like, (5,), {}), (sparse.ones_like, (), {}), (sparse.zeros_like, (), {}), (sparse.empty_like, (), {}), (sparse.asarray, (), {}), ], ) def test_invalid_device(func, args, kwargs): if func.__name__.endswith("_like") or func is sparse.asarray: like = sparse.random((5, 5), density=0.5) args = (like,) + args with pytest.raises(ValueError, match="Device must be"): func(*args, device="invalid_device", **kwargs) def test_device(): s = sparse.random((5, 5), density=0.5) data = getattr(s, "data", None) device = getattr(data, "device", "cpu") assert s.device == device def test_to_device(): s = sparse.random((5, 5), density=0.5) s2 = s.to_device(s.device) assert s is s2 def test_to_invalid_device(): s = sparse.random((5, 5), density=0.5) with pytest.raises(ValueError, match=r"Only .* is supported."): s.to_device("invalid_device") # regression test for gh-869 def test_xH_x(): Y = np.array([[0, -1j], [+1j, 0]]) Ysp = COO.from_numpy(Y) assert_eq(Ysp.conj().T @ Y, Y.conj().T @ Y) assert_eq(Ysp.conj().T @ Ysp, Y.conj().T @ Y) assert_eq(Y.conj().T @ Ysp.conj().T, Y.conj().T @ Y.conj().T) sparse-0.17.0/sparse/numba_backend/tests/test_coo_numba.py000066400000000000000000000033301501262445000236550ustar00rootroot00000000000000import sparse import numba import numpy as np @numba.njit def identity(x): """Pass an object through numba and back""" return x def identity_constant(x): @numba.njit def get_it(): """Pass an object through numba and back as a constant""" return x return get_it() def assert_coo_equal(c1, c2): assert c1.shape == c2.shape assert sparse.all(c1 == c2) assert c1.data.dtype == c2.data.dtype assert c1.fill_value == c2.fill_value def assert_coo_same_memory(c1, c2): assert_coo_equal(c1, c2) assert c1.coords.data == c2.coords.data assert c1.data.data == c2.data.data class TestBasic: """Test very simple construction and field access""" def test_roundtrip(self): c1 = sparse.COO(np.eye(3), fill_value=1) c2 = identity(c1) assert type(c1) is type(c2) assert_coo_same_memory(c1, c2) def test_roundtrip_constant(self): c1 = sparse.COO(np.eye(3), fill_value=1) c2 = identity_constant(c1) # constants are always copies assert_coo_equal(c1, c2) def test_unpack_attrs(self): @numba.njit def unpack(c): return c.coords, c.data, c.shape, c.fill_value c1 = sparse.COO(np.eye(3), fill_value=1) coords, data, shape, fill_value = unpack(c1) c2 = sparse.COO(coords, data, shape, fill_value=fill_value) assert_coo_same_memory(c1, c2) def test_repack_attrs(self): @numba.njit def pack(coords, data, shape): return sparse.COO(coords, data, shape) # repacking fill_value isn't possible yet c1 = sparse.COO(np.eye(3)) c2 = pack(c1.coords, c1.data, c1.shape) assert_coo_same_memory(c1, c2) sparse-0.17.0/sparse/numba_backend/tests/test_dask_interop.py000066400000000000000000000006371501262445000244040ustar00rootroot00000000000000import sparse from dask.base import tokenize def test_deterministic_token(): a = sparse.COO(data=[1, 2, 3], coords=[10, 20, 30], shape=(40,)) b = sparse.COO(data=[1, 2, 3], coords=[10, 20, 30], shape=(40,)) assert tokenize(a) == tokenize(b) # One of these things is not like the other.... c = sparse.COO(data=[1, 2, 4], coords=[10, 20, 30], shape=(40,)) assert tokenize(a) != tokenize(c) sparse-0.17.0/sparse/numba_backend/tests/test_dok.py000066400000000000000000000176031501262445000225000ustar00rootroot00000000000000import sparse from sparse import DOK from sparse.numba_backend._utils import assert_eq import pytest import numpy as np @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4)]) @pytest.mark.parametrize("density", [0.1, 0.3, 0.5, 0.7]) def test_random_shape_nnz(shape, density): s = sparse.random(shape, density, format="dok") assert isinstance(s, DOK) assert s.shape == shape expected_nnz = density * np.prod(shape) assert np.floor(expected_nnz) <= s.nnz <= np.ceil(expected_nnz) def test_convert_to_coo(): s1 = sparse.random((2, 3, 4), 0.5, format="dok") s2 = sparse.COO(s1) assert_eq(s1, s2) def test_convert_from_coo(): s1 = sparse.random((2, 3, 4), 0.5, format="coo") s2 = DOK(s1) assert_eq(s1, s2) def test_convert_from_numpy(rng): x = rng.random((2, 3, 4)) s = DOK(x) assert_eq(x, s) def test_convert_to_numpy(): s = sparse.random((2, 3, 4), 0.5, format="dok") x = s.todense() assert_eq(x, s) def test_convert_from_scipy_sparse(): import scipy.sparse x = scipy.sparse.rand(6, 3, density=0.2) s = DOK(x) assert_eq(x, s) @pytest.mark.parametrize( "shape, data", [ (2, {0: 1}), ((2, 3), {(0, 1): 3, (1, 2): 4}), ((2, 3, 4), {(0, 1): 3, (1, 2, 3): 4, (1, 1): [6, 5, 4, 1]}), ], ) def test_construct(shape, data): s = DOK(shape, data) x = np.zeros(shape, dtype=s.dtype) for c, d in data.items(): x[c] = d assert_eq(x, s) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4)]) @pytest.mark.parametrize("density", [0.1, 0.3, 0.5, 0.7]) def test_getitem_single(shape, density, rng): s = sparse.random(shape, density, format="dok") x = s.todense() for _ in range(s.nnz): idx = rng.integers(np.prod(shape)) idx = np.unravel_index(idx, shape) assert np.isclose(s[idx], x[idx]) @pytest.mark.parametrize( "shape, density, indices", [ ((2, 3), 0.5, (slice(1),)), ((5, 5), 0.2, (slice(0, 4, 2),)), ((10, 10), 0.2, (slice(5), slice(0, 10, 3))), ((5, 5), 0.5, (slice(0, 4, 4), slice(0, 4, 4))), ((5, 5), 0.4, (1, slice(0, 4, 1))), ((10, 10), 0.8, ([0, 4, 5], [3, 2, 4])), ((10, 10), 0, (slice(10), slice(10))), ], ) def test_getitem(shape, density, indices): s = sparse.random(shape, density, format="dok") x = s.todense() sparse_sliced = s[indices] dense_sliced = x[indices] assert_eq(sparse_sliced.todense(), dense_sliced) @pytest.mark.parametrize( "shape, density, indices", [ ((10, 10), 0.8, ([0, 4, 5],)), ((5, 5, 5), 0.5, ([1, 2, 3], [0, 2, 2])), ], ) def test_getitem_notimplemented_error(shape, density, indices): s = sparse.random(shape, density, format="dok") with pytest.raises(NotImplementedError): s[indices] @pytest.mark.parametrize( "shape, density, indices", [ ((10, 10), 0.8, ([0, 4, 5], [0, 2])), ((5, 5, 5), 0.5, ([1, 2, 3], [0], [2, 3, 4])), ((10,), 0.5, (5, 6)), ], ) def test_getitem_index_error(shape, density, indices): s = sparse.random(shape, density, format="dok") with pytest.raises(IndexError): s[indices] @pytest.mark.parametrize( "shape, index, value_shape", [ ((2,), slice(None), ()), ((2,), slice(1, 2), ()), ((2,), slice(0, 2), (2,)), ((2,), 1, ()), ((2, 3), (0, slice(None)), ()), ((2, 3), (0, slice(1, 3)), ()), ((2, 3), (1, slice(None)), (3,)), ((2, 3), (0, slice(1, 3)), (2,)), ((2, 3), (0, slice(2, 0, -1)), (2,)), ((2, 3), (slice(None), 1), ()), ((2, 3), (slice(None), 1), (2,)), ((2, 3), (slice(1, 2), 1), ()), ((2, 3), (slice(1, 2), 1), (1,)), ((2, 3), (0, 2), ()), ((2, 3), ([0, 1], [1, 2]), (2,)), ((2, 3), ([0, 1], [1, 2]), ()), ((4,), ([1, 3]), ()), ], ) def test_setitem(shape, index, value_shape, rng): s = sparse.random(shape, 0.5, format="dok") x = s.todense() value = rng.random(value_shape) s[index] = value x[index] = value assert_eq(x, s) def test_setitem_delete(): shape = (2, 3) index = [0, 1], [1, 2] value = 0 s = sparse.random(shape, 1.0, format="dok") x = s.todense() s[index] = value x[index] = value assert_eq(x, s) assert s.nnz < s.size @pytest.mark.parametrize( "shape, index, value_shape", [ ((2, 3), ([0, 1.5], [1, 2]), ()), ((2, 3), ([0, 1], [1]), ()), ((2, 3), ([[0], [1]], [1, 2]), ()), ], ) def test_setitem_index_error(shape, index, value_shape, rng): s = sparse.random(shape, 0.5, format="dok") value = rng.random(value_shape) with pytest.raises(IndexError): s[index] = value @pytest.mark.parametrize( "shape, index, value_shape", [ ((2, 3), ([0, 1],), ()), ], ) def test_setitem_notimplemented_error(shape, index, value_shape, rng): s = sparse.random(shape, 0.5, format="dok") value = rng.random(value_shape) with pytest.raises(NotImplementedError): s[index] = value @pytest.mark.parametrize( "shape, index, value_shape", [ ((2, 3), ([0, 1], [1, 2]), (1, 2)), ((2, 3), ([0, 1], [1, 2]), (3,)), ((2,), 1, (2,)), ], ) def test_setitem_value_error(shape, index, value_shape, rng): s = sparse.random(shape, 0.5, format="dok") value = rng.random(value_shape) with pytest.raises(ValueError): s[index] = value def test_default_dtype(): s = DOK((5,)) assert s.dtype == np.float64 def test_int_dtype(): data = {1: np.uint8(1), 2: np.uint16(2)} s = DOK((5,), data) assert s.dtype == np.uint16 def test_float_dtype(): data = {1: np.uint8(1), 2: np.float32(2)} s = DOK((5,), data) assert s.dtype == np.float32 def test_set_zero(): s = DOK((1,), dtype=np.uint8) s[0] = 1 s[0] = 0 assert s[0] == 0 assert s.nnz == 0 @pytest.mark.parametrize("format", ["coo", "dok"]) def test_asformat(format): s = sparse.random((2, 3, 4), density=0.5, format="dok") s2 = s.asformat(format) assert_eq(s, s2) def test_coo_fv_interface(rng): s1 = sparse.full((5, 5), fill_value=1 + rng.random()) s2 = sparse.DOK(s1) assert_eq(s1, s2) s3 = sparse.COO(s2) assert_eq(s1, s3) def test_empty_dok_dtype(): d = sparse.DOK(5, dtype=np.uint8) s = sparse.COO(d) assert s.dtype == d.dtype def test_zeros_like(): s = sparse.random((2, 3, 4), density=0.5) s2 = sparse.zeros_like(s, format="dok") assert s.shape == s2.shape assert s.dtype == s2.dtype assert isinstance(s2, sparse.DOK) @pytest.mark.parametrize( "pad_width", [ 2, (2, 1), ((2), (1)), ((1, 2), (4, 5), (7, 8)), ], ) @pytest.mark.parametrize("constant_values", [0, 1, 150, np.nan]) def test_pad_valid(pad_width, constant_values): y = sparse.random((50, 50, 3), density=0.15, fill_value=constant_values, format="dok") x = y.todense() xx = np.pad(x, pad_width=pad_width, constant_values=constant_values) yy = np.pad(y, pad_width=pad_width, constant_values=constant_values) assert_eq(xx, yy) @pytest.mark.parametrize( "pad_width", [ ((2, 1), (5, 7)), ], ) @pytest.mark.parametrize("constant_values", [150, 2, (1, 2)]) def test_pad_invalid(pad_width, constant_values, fill_value=0): y = sparse.random((50, 50, 3), density=0.15, format="dok") with pytest.raises(ValueError): np.pad(y, pad_width, constant_values=constant_values) @pytest.mark.parametrize("func", [np.concatenate, np.stack]) def test_dok_concat_stack(func): s1 = sparse.random((4, 4), density=0.25, format="dok") s2 = sparse.random((4, 4), density=0.25, format="dok") x1 = s1.todense() x2 = s2.todense() assert_eq(func([s1, s2]), func([x1, x2])) def test_dok_indexing(): s = sparse.DOK((3, 3)) s[1, 2] = 0.5 x = s.todense() assert_eq(x[1::-1], s[1::-1]) sparse-0.17.0/sparse/numba_backend/tests/test_dot.py000066400000000000000000000253661501262445000225160ustar00rootroot00000000000000import operator import sparse from sparse import COO from sparse.numba_backend._compressed import GCXS from sparse.numba_backend._utils import assert_eq, assert_gcxs_slicing, default_rng import pytest import numpy as np import scipy.sparse import scipy.stats @pytest.mark.parametrize( "a_shape,b_shape,axes", [ [(3, 4), (4, 3), (1, 0)], [(3, 4), (4, 3), (0, 1)], [(3, 4, 5), (4, 3), (1, 0)], [(3, 4), (5, 4, 3), (1, 1)], [(3, 4), (5, 4, 3), ((0, 1), (2, 1))], [(3, 4), (5, 4, 3), ((1, 0), (1, 2))], [(3, 4, 5), (4,), (1, 0)], [(4,), (3, 4, 5), (0, 1)], [(4,), (4,), (0, 0)], [(4,), (4,), 0], ], ) @pytest.mark.parametrize( "a_format, b_format", [("coo", "coo"), ("coo", "gcxs"), ("gcxs", "coo"), ("gcxs", "gcxs")], ) def test_tensordot(a_shape, b_shape, axes, a_format, b_format): sa = sparse.random(a_shape, density=0.5, format=a_format) sb = sparse.random(b_shape, density=0.5, format=b_format) a = sa.todense() b = sb.todense() a_b = np.tensordot(a, b, axes) # tests for return_type=None sa_sb = sparse.tensordot(sa, sb, axes) sa_b = sparse.tensordot(sa, b, axes) a_sb = sparse.tensordot(a, sb, axes) assert_eq(a_b, sa_sb) assert_eq(a_b, sa_b) assert_eq(a_b, a_sb) if all(isinstance(arr, COO) for arr in [sa, sb]): assert isinstance(sa_sb, COO) else: assert isinstance(sa_sb, GCXS) assert isinstance(sa_b, np.ndarray) assert isinstance(a_sb, np.ndarray) # tests for return_type=COO sa_b = sparse.tensordot(sa, b, axes, return_type=COO) a_sb = sparse.tensordot(a, sb, axes, return_type=COO) assert_eq(a_b, sa_b) assert_eq(a_b, a_sb) assert isinstance(sa_b, COO) assert isinstance(a_sb, COO) # tests form return_type=GCXS sa_b = sparse.tensordot(sa, b, axes, return_type=GCXS) a_sb = sparse.tensordot(a, sb, axes, return_type=GCXS) assert_eq(a_b, sa_b) assert_eq(a_b, a_sb) assert isinstance(sa_b, GCXS) assert isinstance(a_sb, GCXS) # tests for return_type=np.ndarray sa_sb = sparse.tensordot(sa, sb, axes, return_type=np.ndarray) assert_eq(a_b, sa_sb) assert isinstance(sa_sb, np.ndarray) def test_tensordot_empty(): x1 = np.empty((0, 0, 0)) x2 = np.empty((0, 0, 0)) s1 = sparse.COO.from_numpy(x1) s2 = sparse.COO.from_numpy(x2) assert_eq(np.tensordot(x1, x2), sparse.tensordot(s1, s2)) def test_tensordot_valueerror(): x1 = sparse.COO(np.array(1)) x2 = sparse.COO(np.array(1)) with pytest.raises(ValueError): x1 @ x2 def gen_kwargs(format): from sparse.numba_backend._utils import convert_format format = convert_format(format) if format == "gcxs": return [{"compressed_axes": c} for c in [(0,), (1,)]] return [{}] def gen_for_format(format): return [(format, g) for g in gen_kwargs(format)] @pytest.mark.parametrize( "a_shape, b_shape", [ ((3, 1, 6, 5), (2, 1, 4, 5, 6)), ((2, 1, 4, 5, 6), (3, 1, 6, 5)), ((1, 1, 5), (3, 5, 6)), ((3, 4, 5), (1, 5, 6)), ((3, 4, 5), (3, 5, 6)), ((3, 4, 5), (5, 6)), ((4, 5), (5, 6)), ((5,), (5, 6)), ((4, 5), (5,)), ((5,), (5,)), ((3, 4), (1, 2, 4, 3)), ], ) @pytest.mark.parametrize( "a_format, a_kwargs", [*gen_for_format("coo"), *gen_for_format("gcxs")], ) @pytest.mark.parametrize( "b_format, b_kwargs", [*gen_for_format("coo"), *gen_for_format("gcxs")], ) def test_matmul(a_shape, b_shape, a_format, b_format, a_kwargs, b_kwargs): if len(a_shape) == 1: a_kwargs = {} if len(b_shape) == 1: b_kwargs = {} sa = sparse.random(a_shape, density=0.5, format=a_format, **a_kwargs) sb = sparse.random(b_shape, density=0.5, format=b_format, **b_kwargs) a = sa.todense() b = sb.todense() assert_eq(np.matmul(a, b), sparse.matmul(sa, sb)) assert_eq(sparse.matmul(sa, b), sparse.matmul(a, sb)) assert_eq(np.matmul(a, b), sparse.matmul(sa, sb)) if a.ndim == 2 or b.ndim == 2: assert_eq( np.matmul(a, b), sparse.matmul( scipy.sparse.coo_matrix(a) if a.ndim == 2 else sa, scipy.sparse.coo_matrix(b) if b.ndim == 2 else sb, ), ) if hasattr(operator, "matmul"): assert_eq(operator.matmul(a, b), operator.matmul(sa, sb)) def test_matmul_errors(): with pytest.raises(ValueError): sa = sparse.random((3, 4, 5, 6), 0.5) sb = sparse.random((3, 6, 5, 6), 0.5) sparse.matmul(sa, sb) @pytest.mark.parametrize( "a, b", [ ( sparse.GCXS.from_numpy(default_rng.choice([0, np.nan, 2], size=[100, 100], p=[0.99, 0.001, 0.009])), sparse.random((100, 100), density=0.01), ), ( sparse.COO.from_numpy(default_rng.choice([0, np.nan, 2], size=[100, 100], p=[0.99, 0.001, 0.009])), sparse.random((100, 100), density=0.01), ), ( sparse.GCXS.from_numpy(default_rng.choice([0, np.nan, 2], size=[100, 100], p=[0.99, 0.001, 0.009])), scipy.sparse.random(100, 100), ), ( default_rng.choice([0, np.nan, 2], size=[100, 100], p=[0.99, 0.001, 0.009]), sparse.random((100, 100), density=0.01), ), ], ) def test_matmul_nan_warnings(a, b): with pytest.warns(RuntimeWarning): a @ b @pytest.mark.parametrize( "a_shape, b_shape", [ ((1, 4, 5), (3, 5, 6)), ((3, 4, 5), (1, 5, 6)), ((3, 4, 5), (3, 5, 6)), ((3, 4, 5), (5, 6)), ((4, 5), (5, 6)), ((5,), (5, 6)), ((4, 5), (5,)), ((5,), (5,)), ], ) @pytest.mark.parametrize( "a_format, a_kwargs", [*gen_for_format("coo"), *gen_for_format("gcxs")], ) @pytest.mark.parametrize( "b_format, b_kwargs", [*gen_for_format("coo"), *gen_for_format("gcxs")], ) def test_dot(a_shape, b_shape, a_format, b_format, a_kwargs, b_kwargs): if len(a_shape) == 1: a_kwargs = {} if len(b_shape) == 1: b_kwargs = {} sa = sparse.random(a_shape, density=0.5, format=a_format, **a_kwargs) sb = sparse.random(b_shape, density=0.5, format=b_format, **b_kwargs) a = sa.todense() b = sb.todense() e = np.dot(a, b) assert_eq(e, sa.dot(sb)) assert_eq(e, sparse.dot(sa, sb)) assert_eq(e, sparse.dot(a, sb)) assert_eq(e, sparse.dot(a, sb)) # Basic equivalences e = operator.matmul(a, b) assert_eq(e, operator.matmul(sa, sb)) assert_eq(e, operator.matmul(a, sb)) assert_eq(e, operator.matmul(sa, b)) @pytest.mark.parametrize( "a_dense, b_dense, o_type", [ (False, False, sparse.SparseArray), (False, True, np.ndarray), (True, False, np.ndarray), ], ) def test_dot_type(a_dense, b_dense, o_type): a = sparse.random((3, 4), density=0.8) b = sparse.random((4, 5), density=0.8) if a_dense: a = a.todense() if b_dense: b = b.todense() assert isinstance(sparse.dot(a, b), o_type) @pytest.mark.xfail def test_dot_nocoercion(): sa = sparse.random((3, 4, 5), density=0.5) sb = sparse.random((5, 6), density=0.5) a = sa.todense() b = sb.todense() la = a.tolist() lb = b.tolist() if hasattr(operator, "matmul"): # Operations with naive collection (list) assert_eq(operator.matmul(la, b), operator.matmul(la, sb)) assert_eq(operator.matmul(a, lb), operator.matmul(sa, lb)) dot_formats = [ lambda x: x.asformat("coo"), lambda x: x.asformat("gcxs"), lambda x: x.todense(), ] @pytest.mark.parametrize("format1", dot_formats) @pytest.mark.parametrize("format2", dot_formats) def test_small_values(format1, format2): s1 = format1(sparse.COO(coords=[[0, 10]], data=[3.6e-100, 7.2e-009], shape=(20,))) s2 = format2(sparse.COO(coords=[[0, 0], [4, 28]], data=[3.8e-25, 4.5e-225], shape=(20, 50))) def dense_convertor(x): return x.todense() if isinstance(x, sparse.SparseArray) else x x1, x2 = dense_convertor(s1), dense_convertor(s2) assert_eq(x1 @ x2, s1 @ s2) dot_dtypes = [np.complex64, np.complex128] @pytest.mark.parametrize("dtype1", dot_dtypes) @pytest.mark.parametrize("dtype2", dot_dtypes) @pytest.mark.parametrize("format1", dot_formats) @pytest.mark.parametrize("format2", dot_formats) @pytest.mark.parametrize("ndim1", (1, 2)) @pytest.mark.parametrize("ndim2", (1, 2)) def test_complex(dtype1, dtype2, format1, format2, ndim1, ndim2): s1 = format1(sparse.random((20,) * ndim1, density=0.5).astype(dtype1)) s2 = format2(sparse.random((20,) * ndim2, density=0.5).astype(dtype2)) def dense_convertor(x): return x.todense() if isinstance(x, sparse.SparseArray) else x x1, x2 = dense_convertor(s1), dense_convertor(s2) assert_eq(x1 @ x2, s1 @ s2) @pytest.mark.parametrize("dtype1", dot_dtypes) @pytest.mark.parametrize("dtype2", dot_dtypes) @pytest.mark.parametrize("ndim1", (1, 2)) @pytest.mark.parametrize("ndim2", (1, 2)) def test_dot_dense(dtype1, dtype2, ndim1, ndim2): a = sparse.random((20,) * ndim1, density=0.5).astype(dtype1).todense() b = sparse.random((20,) * ndim2, density=0.5).astype(dtype2).todense() assert_eq(sparse.dot(a, b), np.dot(a, b)) assert_eq(sparse.matmul(a, b), np.matmul(a, b)) if ndim1 == 2 and ndim2 == 2: assert_eq(sparse.tensordot(a, b), np.tensordot(a, b)) @pytest.mark.parametrize( "a_shape, b_shape", [((3, 4, 5), (5, 6)), ((2, 8, 6), (6, 3))], ) def test_dot_GCXS_slicing(a_shape, b_shape): sa = sparse.random(shape=a_shape, density=1, format="gcxs") sb = sparse.random(shape=b_shape, density=1, format="gcxs") a = sa.todense() b = sb.todense() # tests dot sa_sb = sparse.dot(sa, sb) a_b = np.dot(a, b) assert_gcxs_slicing(sa_sb, a_b) @pytest.mark.parametrize( "a_shape,b_shape,axes", [ [(3, 4, 5), (4, 3), (1, 0)], [(3, 4), (5, 4, 3), (1, 1)], [(5, 9), (9, 5, 6), (0, 1)], ], ) def test_tensordot_GCXS_slicing(a_shape, b_shape, axes): sa = sparse.random(shape=a_shape, density=1, format="gcxs") sb = sparse.random(shape=b_shape, density=1, format="gcxs") a = sa.todense() b = sb.todense() sa_sb = sparse.tensordot(sa, sb, axes) a_b = np.tensordot(a, b, axes) assert_gcxs_slicing(sa_sb, a_b) @pytest.mark.parametrize( "a_shape, b_shape", [ [(1, 1, 5), (3, 5, 6)], [(3, 4, 5), (1, 5, 6)], [(3, 4, 5), (3, 5, 6)], [(3, 4, 5), (5, 6)], ], ) def test_matmul_GCXS_slicing(a_shape, b_shape): sa = sparse.random(shape=a_shape, density=1, format="gcxs") sb = sparse.random(shape=b_shape, density=1, format="gcxs") a = sa.todense() b = sb.todense() sa_sb = sparse.matmul(sa, sb) a_b = np.matmul(a, b) assert_gcxs_slicing(sa_sb, a_b) sparse-0.17.0/sparse/numba_backend/tests/test_einsum.py000066400000000000000000000126021501262445000232150ustar00rootroot00000000000000import sparse import pytest import numpy as np einsum_cases = [ "a,->a", "ab,->ab", ",ab,->ab", ",,->", "a,ab,abc->abc", "a,b,ab->ab", "ea,fb,gc,hd,abcd->efgh", "ea,fb,abcd,gc,hd->efgh", "abcd,ea,fb,gc,hd->efgh", "acdf,jbje,gihb,hfac,gfac,gifabc,hfac", "cd,bdhe,aidb,hgca,gc,hgibcd,hgac", "abhe,hidj,jgba,hiab,gab", "bde,cdh,agdb,hica,ibd,hgicd,hiac", "chd,bde,agbc,hiad,hgc,hgi,hiad", "chd,bde,agbc,hiad,bdi,cgh,agdb", "bdhe,acad,hiab,agac,hibd", "ab,ab,c->", "ab,ab,c->c", "ab,ab,cd,cd->", "ab,ab,cd,cd->ac", "ab,ab,cd,cd->cd", "ab,ab,cd,cd,ef,ef->", "ab,cd,ef->abcdef", "ab,cd,ef->acdf", "ab,cd,de->abcde", "ab,cd,de->be", "ab,bcd,cd->abcd", "ab,bcd,cd->abd", "eb,cb,fb->cef", "dd,fb,be,cdb->cef", "bca,cdb,dbf,afc->", "dcc,fce,ea,dbf->ab", "fdf,cdd,ccd,afe->ae", "abcd,ad", "ed,fcd,ff,bcf->be", "baa,dcf,af,cde->be", "bd,db,eac->ace", "fff,fae,bef,def->abd", "efc,dbc,acf,fd->abe", "ab,ab", "ab,ba", "abc,abc", "abc,bac", "abc,cba", "ab,bc", "ab,cb", "ba,bc", "ba,cb", "abcd,cd", "abcd,ab", "abcd,cdef", "abcd,cdef->feba", "abcd,efdc", "aab,bc->ac", "ab,bcc->ac", "aab,bcc->ac", "baa,bcc->ac", "aab,ccb->ac", "aab,fa,df,ecc->bde", "ecb,fef,bad,ed->ac", "bcf,bbb,fbf,fc->", "bb,ff,be->e", "bcb,bb,fc,fff->", "fbb,dfd,fc,fc->", "afd,ba,cc,dc->bf", "adb,bc,fa,cfc->d", "bbd,bda,fc,db->acf", "dba,ead,cad->bce", "aef,fbc,dca->bde", "abab->ba", "...ab,...ab", "...ab,...b->...a", "a...,a...", "a...,a...", ] @pytest.mark.parametrize("subscripts", einsum_cases) @pytest.mark.parametrize("density", [0.1, 1.0]) def test_einsum(subscripts, density): d = 4 terms = subscripts.split("->")[0].split(",") arrays = [sparse.random((d,) * len(term), density=density) for term in terms] sparse_out = sparse.einsum(subscripts, *arrays) numpy_out = np.einsum(subscripts, *(s.todense() for s in arrays)) if not numpy_out.shape: # scalar output assert np.allclose(numpy_out, sparse_out) else: # array output assert np.allclose(numpy_out, sparse_out.todense()) @pytest.mark.parametrize("input", [[[0, 0]], [[0, Ellipsis]], [[Ellipsis, 1], [Ellipsis]], [[0, 1], [0]]]) @pytest.mark.parametrize("density", [0.1, 1.0]) def test_einsum_nosubscript(input, density): d = 4 arrays = [sparse.random((d, d), density=density)] sparse_out = sparse.einsum(*arrays, *input) numpy_out = np.einsum(*(s.todense() for s in arrays), *input) if not numpy_out.shape: # scalar output assert np.allclose(numpy_out, sparse_out) else: # array output assert np.allclose(numpy_out, sparse_out.todense()) def test_einsum_input_fill_value(): x = sparse.random(shape=(2,), density=0.5, format="coo", fill_value=2) with pytest.raises(ValueError): sparse.einsum("cba", x) def test_einsum_no_input(): with pytest.raises(ValueError): sparse.einsum() @pytest.mark.parametrize("subscript", ["a+b->c", "i->&", "i->ij", "ij->jij", "a..,a...", ".i...", "a,a->->"]) def test_einsum_invalid_input(subscript): x = sparse.random(shape=(2,), density=0.5, format="coo") y = sparse.random(shape=(2,), density=0.5, format="coo") with pytest.raises(ValueError): sparse.einsum(subscript, x, y) @pytest.mark.parametrize("subscript", [0, [0, 0]]) def test_einsum_type_error(subscript): x = sparse.random(shape=(2,), density=0.5, format="coo") y = sparse.random(shape=(2,), density=0.5, format="coo") with pytest.raises(TypeError): sparse.einsum(subscript, x, y) format_test_cases = [ (("coo",), "coo"), (("dok",), "dok"), (("gcxs",), "gcxs"), (("dense",), "dense"), (("coo", "coo"), "coo"), (("dok", "coo"), "coo"), (("coo", "dok"), "coo"), (("coo", "dense"), "coo"), (("dense", "coo"), "coo"), (("dok", "dense"), "dok"), (("dense", "dok"), "dok"), (("gcxs", "dense"), "gcxs"), (("dense", "gcxs"), "gcxs"), (("dense", "dense"), "dense"), (("dense", "dok", "gcxs"), "coo"), ] @pytest.mark.parametrize("formats,expected", format_test_cases) def test_einsum_format(formats, expected, rng): inputs = [ rng.standard_normal((2, 2, 2)) if format == "dense" else sparse.random((2, 2, 2), density=0.5, format=format) for format in formats ] if len(inputs) == 1: eq = "abc->bc" elif len(inputs) == 2: eq = "abc,cda->abd" elif len(inputs) == 3: eq = "abc,cad,dea->abe" out = sparse.einsum(eq, *inputs) assert { sparse.COO: "coo", sparse.DOK: "dok", sparse.GCXS: "gcxs", np.ndarray: "dense", }[out.__class__] == expected def test_einsum_shape_check(): x = sparse.random((2, 3, 4), density=0.5) with pytest.raises(ValueError): sparse.einsum("aab", x) y = sparse.random((2, 3, 4), density=0.5) with pytest.raises(ValueError): sparse.einsum("abc,acb", x, y) @pytest.mark.parametrize("dtype", [np.int64, np.complex128]) def test_einsum_dtype(dtype): x = sparse.random((3, 3), density=0.5) * 10.0 x = x.astype(np.float64) y = sparse.COO.from_numpy(np.ones((3, 1), dtype=np.float64)) result = sparse.einsum("ij,i->j", x, y, dtype=dtype) assert result.dtype == dtype sparse-0.17.0/sparse/numba_backend/tests/test_elemwise.py000066400000000000000000000471371501262445000235420ustar00rootroot00000000000000import operator import sparse from sparse import COO, DOK from sparse.numba_backend._compressed import GCXS from sparse.numba_backend._utils import assert_eq, random_value_array import pytest import numpy as np @pytest.mark.parametrize( "func", [ np.expm1, np.log1p, np.sin, np.tan, np.sinh, np.tanh, np.floor, np.ceil, np.sqrt, np.conj, np.round, np.rint, lambda x: x.astype("int32"), np.conjugate, np.conj, lambda x: x.round(decimals=2), abs, ], ) @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_elemwise(func, format): s = sparse.random((2, 3, 4), density=0.5, format=format) x = s.todense() fs = func(s) assert isinstance(fs, format) assert fs.nnz <= s.nnz assert_eq(func(x), fs) @pytest.mark.parametrize( "func", [ np.expm1, np.log1p, np.sin, np.tan, np.sinh, np.tanh, np.floor, np.ceil, np.sqrt, np.conj, np.round, np.rint, np.conjugate, np.conj, lambda x, out: x.round(decimals=2, out=out), ], ) @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_elemwise_inplace(func, format): s = sparse.random((2, 3, 4), density=0.5, format=format) x = s.todense() func(s, out=s) func(x, out=x) assert isinstance(s, format) assert_eq(x, s) @pytest.mark.parametrize( "shape1, shape2", [ ((2, 3, 4), (3, 4)), ((3, 4), (2, 3, 4)), ((3, 1, 4), (3, 2, 4)), ((1, 3, 4), (3, 4)), ((3, 4, 1), (3, 4, 2)), ((1, 5), (5, 1)), ((3, 1), (3, 4)), ((3, 1), (1, 4)), ((1, 4), (3, 4)), ((2, 2, 2), (1, 1, 1)), ], ) @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_elemwise_mixed(shape1, shape2, format, rng): s1 = sparse.random(shape1, density=0.5, format=format) x2 = rng.random(shape2) x1 = s1.todense() assert_eq(s1 * x2, x1 * x2) @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_elemwise_mixed_empty(format, rng): s1 = sparse.random((2, 0, 4), density=0.5, format=format) x2 = rng.random((2, 0, 4)) x1 = s1.todense() assert_eq(s1 * x2, x1 * x2) @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_elemwise_unsupported(format): class A: pass s1 = sparse.random((2, 3, 4), density=0.5, format=format) x2 = A() with pytest.raises(TypeError): s1 + x2 assert sparse.elemwise(operator.add, s1, x2) is NotImplemented @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_elemwise_mixed_broadcast(format, rng): s1 = sparse.random((2, 3, 4), density=0.5, format=format) s2 = sparse.random(4, density=0.5) x3 = rng.random((3, 4)) x1 = s1.todense() x2 = s2.todense() def func(x1, x2, x3): return x1 * x2 * x3 assert_eq(sparse.elemwise(func, s1, s2, x3), func(x1, x2, x3)) @pytest.mark.parametrize( "func", [operator.mul, operator.add, operator.sub, operator.gt, operator.lt, operator.ne], ) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_elemwise_binary(func, shape, format): xs = sparse.random(shape, density=0.5, format=format) ys = sparse.random(shape, density=0.5, format=format) x = xs.todense() y = ys.todense() assert_eq(func(xs, ys), func(x, y)) @pytest.mark.parametrize("func", [operator.imul, operator.iadd, operator.isub]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_elemwise_binary_inplace(func, shape, format): xs = sparse.random(shape, density=0.5, format=format) ys = sparse.random(shape, density=0.5, format=format) x = xs.todense() y = ys.todense() xs = func(xs, ys) x = func(x, y) assert_eq(xs, x) @pytest.mark.parametrize( "func", [ lambda x, y, z: x + y + z, lambda x, y, z: x * y * z, lambda x, y, z: x + y * z, lambda x, y, z: (x + y) * z, ], ) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) @pytest.mark.parametrize( "formats", [ [COO, COO, COO], [GCXS, GCXS, GCXS], [COO, GCXS, GCXS], ], ) def test_elemwise_trinary(func, shape, formats): xs = sparse.random(shape, density=0.5, format=formats[0]) ys = sparse.random(shape, density=0.5, format=formats[1]) zs = sparse.random(shape, density=0.5, format=formats[2]) x = xs.todense() y = ys.todense() z = zs.todense() fs = sparse.elemwise(func, xs, ys, zs) assert_eq(fs, func(x, y, z)) @pytest.mark.parametrize("func", [operator.add, operator.mul]) @pytest.mark.parametrize( "shape1,shape2", [ ((2, 3, 4), (3, 4)), ((3, 4), (2, 3, 4)), ((3, 1, 4), (3, 2, 4)), ((1, 3, 4), (3, 4)), ((3, 4, 1), (3, 4, 2)), ((1, 5), (5, 1)), ((3, 1), (3, 4)), ((3, 1), (1, 4)), ((1, 4), (3, 4)), ((2, 2, 2), (1, 1, 1)), ], ) def test_binary_broadcasting(func, shape1, shape2): density1 = 1 if np.prod(shape1) == 1 else 0.5 density2 = 1 if np.prod(shape2) == 1 else 0.5 xs = sparse.random(shape1, density=density1) x = xs.todense() ys = sparse.random(shape2, density=density2) y = ys.todense() expected = func(x, y) actual = func(xs, ys) assert isinstance(actual, COO) assert_eq(expected, actual) assert np.count_nonzero(expected) == actual.nnz @pytest.mark.parametrize( "shape1,shape2", [((3, 4), (2, 3, 4)), ((3, 1, 4), (3, 2, 4)), ((3, 4, 1), (3, 4, 2))], ) def test_broadcast_to(shape1, shape2): a = sparse.random(shape1, density=0.5) x = a.todense() assert_eq(np.broadcast_to(x, shape2), a.broadcast_to(shape2)) @pytest.mark.parametrize( "shapes", [ [(2,), (3, 2), (4, 3, 2)], [(3,), (2, 3), (2, 2, 3)], [(2,), (2, 2), (2, 2, 2)], [(4,), (4, 4), (4, 4, 4)], [(4,), (4, 4), (4, 4, 4)], [(4,), (4, 4), (4, 4, 4)], [(1, 1, 2), (1, 3, 1), (4, 1, 1)], [(2,), (2, 1), (2, 1, 1)], ], ) @pytest.mark.parametrize( "func", [ lambda x, y, z: (x + y) * z, lambda x, y, z: x * (y + z), lambda x, y, z: x * y * z, lambda x, y, z: x + y + z, lambda x, y, z: x + y - z, lambda x, y, z: x - y + z, ], ) def test_trinary_broadcasting(shapes, func): args = [sparse.random(s, density=0.5) for s in shapes] dense_args = [arg.todense() for arg in args] fs = sparse.elemwise(func, *args) assert isinstance(fs, COO) assert_eq(fs, func(*dense_args)) @pytest.mark.parametrize( "shapes, func", [ ([(2,), (3, 2), (4, 3, 2)], lambda x, y, z: (x + y) * z), ([(3,), (2, 3), (2, 2, 3)], lambda x, y, z: x * (y + z)), ([(2,), (2, 2), (2, 2, 2)], lambda x, y, z: x * y * z), ([(4,), (4, 4), (4, 4, 4)], lambda x, y, z: x + y + z), ], ) @pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf]) @pytest.mark.parametrize("fraction", [0.25, 0.5, 0.75, 1.0]) @pytest.mark.filterwarnings("ignore:invalid value") def test_trinary_broadcasting_pathological(shapes, func, value, fraction): args = [sparse.random(s, density=0.5, data_rvs=random_value_array(value, fraction)) for s in shapes] dense_args = [arg.todense() for arg in args] fs = sparse.elemwise(func, *args) assert isinstance(fs, COO) assert_eq(fs, func(*dense_args)) def test_sparse_broadcasting(monkeypatch): orig_unmatch_coo = sparse.numba_backend._umath._Elemwise._get_func_coords_data state = {"num_matches": 0} xs = sparse.random((3, 4), density=0.5) ys = sparse.random((3, 4), density=0.5) def mock_unmatch_coo(*args, **kwargs): result = orig_unmatch_coo(*args, **kwargs) if result is not None: state["num_matches"] += 1 return result monkeypatch.setattr(sparse.numba_backend._umath._Elemwise, "_get_func_coords_data", mock_unmatch_coo) xs * ys # Less than in case there's absolutely no overlap in some cases. assert state["num_matches"] <= 1 def test_dense_broadcasting(monkeypatch): orig_unmatch_coo = sparse.numba_backend._umath._Elemwise._get_func_coords_data state = {"num_matches": 0} xs = sparse.random((3, 4), density=0.5) ys = sparse.random((3, 4), density=0.5) def mock_unmatch_coo(*args, **kwargs): result = orig_unmatch_coo(*args, **kwargs) if result is not None: state["num_matches"] += 1 return result monkeypatch.setattr(sparse.numba_backend._umath._Elemwise, "_get_func_coords_data", mock_unmatch_coo) xs + ys # Less than in case there's absolutely no overlap in some cases. assert state["num_matches"] <= 3 @pytest.mark.parametrize("format", ["coo", "dok", "gcxs"]) def test_sparsearray_elemwise(format): xs = sparse.random((3, 4), density=0.5, format=format) ys = sparse.random((3, 4), density=0.5, format=format) x = xs.todense() y = ys.todense() fs = sparse.elemwise(operator.add, xs, ys) if format == "gcxs": assert isinstance(fs, GCXS) elif format == "dok": assert isinstance(fs, DOK) else: assert isinstance(fs, COO) assert_eq(fs, x + y) def test_ndarray_densification_fails(rng): xs = sparse.random((2, 3, 4), density=0.5) y = rng.random((3, 4)) with pytest.raises(ValueError): xs + y def test_elemwise_noargs(): def func(): return np.float64(5.0) with pytest.raises(ValueError, match=r"None of the args is sparse:"): sparse.elemwise(func) @pytest.mark.parametrize( "func", [ operator.pow, operator.truediv, operator.floordiv, operator.ge, operator.le, operator.eq, operator.mod, ], ) @pytest.mark.filterwarnings("ignore:divide by zero") @pytest.mark.filterwarnings("ignore:invalid value") @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_nonzero_outout_fv_ufunc(func, format): xs = sparse.random((2, 3, 4), density=0.5, format=format) ys = sparse.random((2, 3, 4), density=0.5, format=format) x = xs.todense() y = ys.todense() f = func(x, y) fs = func(xs, ys) assert isinstance(fs, format) assert_eq(f, fs) @pytest.mark.parametrize( "func, scalar", [ (operator.mul, 5), (operator.add, 0), (operator.sub, 0), (operator.pow, 5), (operator.truediv, 3), (operator.floordiv, 4), (operator.gt, 5), (operator.lt, -5), (operator.ne, 0), (operator.ge, 5), (operator.le, -3), (operator.eq, 1), (operator.mod, 5), ], ) @pytest.mark.parametrize("convert_to_np_number", [True, False]) @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_elemwise_scalar(func, scalar, convert_to_np_number, format): xs = sparse.random((2, 3, 4), density=0.5, format=format) if convert_to_np_number: scalar = np.float32(scalar) y = scalar x = xs.todense() fs = func(xs, y) assert isinstance(fs, format) assert xs.nnz >= fs.nnz assert_eq(fs, func(x, y)) @pytest.mark.parametrize( "func, scalar", [ (operator.mul, 5), (operator.add, 0), (operator.sub, 0), (operator.gt, -5), (operator.lt, 5), (operator.ne, 0), (operator.ge, -5), (operator.le, 3), (operator.eq, 1), ], ) @pytest.mark.parametrize("convert_to_np_number", [True, False]) def test_leftside_elemwise_scalar(func, scalar, convert_to_np_number): xs = sparse.random((2, 3, 4), density=0.5) if convert_to_np_number: scalar = np.float32(scalar) y = scalar x = xs.todense() fs = func(y, xs) assert isinstance(fs, COO) assert xs.nnz >= fs.nnz assert_eq(fs, func(y, x)) @pytest.mark.parametrize( "func, scalar", [ (operator.add, 5), (operator.sub, -5), (operator.pow, -3), (operator.truediv, 0), (operator.floordiv, 0), (operator.gt, -5), (operator.lt, 5), (operator.ne, 1), (operator.ge, -3), (operator.le, 3), (operator.eq, 0), ], ) @pytest.mark.filterwarnings("ignore:divide by zero") @pytest.mark.filterwarnings("ignore:invalid value") def test_scalar_output_nonzero_fv(func, scalar): xs = sparse.random((2, 3, 4), density=0.5) y = scalar x = xs.todense() f = func(x, y) fs = func(xs, y) assert isinstance(fs, COO) assert fs.nnz <= xs.nnz assert_eq(f, fs) @pytest.mark.parametrize("func", [operator.and_, operator.or_, operator.xor]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_bitwise_binary(func, shape, format): # Small arrays need high density to have nnz entries # Casting floats to int will result in all zeros, hence the * 100 xs = (sparse.random(shape, density=0.5, format=format) * 100).astype(np.int64) ys = (sparse.random(shape, density=0.5, format=format) * 100).astype(np.int64) x = xs.todense() y = ys.todense() assert_eq(func(xs, ys), func(x, y)) @pytest.mark.parametrize("func", [operator.iand, operator.ior, operator.ixor]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) @pytest.mark.parametrize("format", [COO, GCXS, DOK]) def test_bitwise_binary_inplace(func, shape, format): # Small arrays need high density to have nnz entries # Casting floats to int will result in all zeros, hence the * 100 xs = (sparse.random(shape, density=0.5, format=format) * 100).astype(np.int64) ys = (sparse.random(shape, density=0.5, format=format) * 100).astype(np.int64) x = xs.todense() y = ys.todense() xs = func(xs, ys) x = func(x, y) assert_eq(xs, x) @pytest.mark.parametrize("func", [operator.lshift, operator.rshift]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) def test_bitshift_binary(func, shape): # Small arrays need high density to have nnz entries # Casting floats to int will result in all zeros, hence the * 100 xs = (sparse.random(shape, density=0.5) * 100).astype(np.int64) # Can't merge into test_bitwise_binary because left/right shifting # with something >= 64 isn't defined. ys = (sparse.random(shape, density=0.5) * 64).astype(np.int64) x = xs.todense() y = ys.todense() assert_eq(func(xs, ys), func(x, y)) @pytest.mark.parametrize("func", [operator.ilshift, operator.irshift]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) def test_bitshift_binary_inplace(func, shape): # Small arrays need high density to have nnz entries # Casting floats to int will result in all zeros, hence the * 100 xs = (sparse.random(shape, density=0.5) * 100).astype(np.int64) # Can't merge into test_bitwise_binary because left/right shifting # with something >= 64 isn't defined. ys = (sparse.random(shape, density=0.5) * 64).astype(np.int64) x = xs.todense() y = ys.todense() xs = func(xs, ys) x = func(x, y) assert_eq(xs, x) @pytest.mark.parametrize("func", [operator.and_]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) def test_bitwise_scalar(func, shape, rng): # Small arrays need high density to have nnz entries # Casting floats to int will result in all zeros, hence the * 100 xs = (sparse.random(shape, density=0.5) * 100).astype(np.int64) y = rng.integers(100) x = xs.todense() assert_eq(func(xs, y), func(x, y)) assert_eq(func(y, xs), func(y, x)) @pytest.mark.parametrize("func", [operator.lshift, operator.rshift]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) def test_bitshift_scalar(func, shape, rng): # Small arrays need high density to have nnz entries # Casting floats to int will result in all zeros, hence the * 100 xs = (sparse.random(shape, density=0.5) * 100).astype(np.int64) # Can't merge into test_bitwise_binary because left/right shifting # with something >= 64 isn't defined. y = rng.integers(64) x = xs.todense() assert_eq(func(xs, y), func(x, y)) @pytest.mark.parametrize("func", [operator.invert]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) def test_unary_bitwise_nonzero_output_fv(func, shape): # Small arrays need high density to have nnz entries # Casting floats to int will result in all zeros, hence the * 100 xs = (sparse.random(shape, density=0.5) * 100).astype(np.int64) x = xs.todense() f = func(x) fs = func(xs) assert isinstance(fs, COO) assert fs.nnz <= xs.nnz assert_eq(f, fs) @pytest.mark.parametrize("func", [operator.or_, operator.xor]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) def test_binary_bitwise_nonzero_output_fv(func, shape, rng): # Small arrays need high density to have nnz entries # Casting floats to int will result in all zeros, hence the * 100 xs = (sparse.random(shape, density=0.5) * 100).astype(np.int64) y = rng.integers(1, 100) x = xs.todense() f = func(x, y) fs = func(xs, y) assert isinstance(fs, COO) assert fs.nnz <= xs.nnz assert_eq(f, fs) @pytest.mark.parametrize( "func", [operator.mul, operator.add, operator.sub, operator.gt, operator.lt, operator.ne], ) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) def test_elemwise_nonzero_input_fv(func, shape, rng): xs = sparse.random(shape, density=0.5, fill_value=rng.random()) ys = sparse.random(shape, density=0.5, fill_value=rng.random()) x = xs.todense() y = ys.todense() assert_eq(func(xs, ys), func(x, y)) @pytest.mark.parametrize("func", [operator.lshift, operator.rshift]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) def test_binary_bitshift_densification_fails(func, shape, rng): # Small arrays need high density to have nnz entries # Casting floats to int will result in all zeros, hence the * 100 x = rng.integers(1, 100) ys = (sparse.random(shape, density=0.5) * 64).astype(np.int64) y = ys.todense() f = func(x, y) fs = func(x, ys) assert isinstance(fs, COO) assert fs.nnz <= ys.nnz assert_eq(f, fs) @pytest.mark.parametrize("func", [operator.and_, operator.or_, operator.xor]) @pytest.mark.parametrize("shape", [(2,), (2, 3), (2, 3, 4), (2, 3, 4, 5)]) def test_bitwise_binary_bool(func, shape): # Small arrays need high density to have nnz entries xs = sparse.random(shape, density=0.5).astype(bool) ys = sparse.random(shape, density=0.5).astype(bool) x = xs.todense() y = ys.todense() assert_eq(func(xs, ys), func(x, y)) def test_elemwise_binary_empty(): x = COO({}, shape=(10, 10)) y = sparse.random((10, 10), density=0.5) for z in [x * y, y * x]: assert z.nnz == 0 assert z.coords.shape == (2, 0) assert z.data.shape == (0,) @pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) def test_nanmean_regression(dtype): array = np.array([0.0 + 0.0j, 0.0 + np.nan * 1j], dtype=dtype) sparray = sparse.COO.from_numpy(array) assert_eq(array, sparray) # Regression test for gh-580 @pytest.mark.filterwarnings("error") def test_no_deprecation_warning(): a = np.array([1, 2]) s = sparse.COO(a, a, shape=(3,)) assert_eq(s == s, np.broadcast_to(True, s.shape)) # Regression test for gh-587 def test_no_out_upcast(): a = sparse.COO([[0, 1], [0, 1]], [1, 1], shape=(2, 2)) with pytest.raises(TypeError): a *= 0.5 sparse-0.17.0/sparse/numba_backend/tests/test_io.py000066400000000000000000000013721501262445000223260ustar00rootroot00000000000000import sparse from sparse import load_npz, save_npz from sparse.numba_backend._utils import assert_eq import pytest import numpy as np @pytest.mark.parametrize("compression", [True, False]) @pytest.mark.parametrize("format", ["coo", "gcxs"]) def test_save_load_npz_file(tmp_path, compression, format): x = sparse.random((2, 3, 4, 5), density=0.25, format=format) y = x.todense() filename = tmp_path / "mat.npz" save_npz(filename, x, compressed=compression) z = load_npz(filename) assert_eq(x, z) assert_eq(y, z.todense()) def test_load_wrong_format_exception(tmp_path): x = np.array([1, 2, 3]) filename = tmp_path / "mat.npz" np.savez(filename, x) with pytest.raises(RuntimeError): load_npz(filename) sparse-0.17.0/sparse/numba_backend/tests/test_namespace.py000066400000000000000000000064631501262445000236610ustar00rootroot00000000000000import sparse def test_namespace(): from sparse.numba_backend._settings import IS_NUMPY2 all_set = { "COO", "DOK", "GCXS", "SparseArray", "abs", "acos", "acosh", "add", "all", "any", "argmax", "argmin", "argwhere", "asCOO", "as_coo", "asarray", "asin", "asinh", "asnumpy", "astype", "atan", "atan2", "atanh", "bitwise_and", "bitwise_invert", "bitwise_left_shift", "bitwise_not", "bitwise_or", "bitwise_right_shift", "bitwise_xor", "bool", "broadcast_arrays", "broadcast_to", "can_cast", "ceil", "clip", "complex128", "complex64", "concat", "concatenate", "conj", "copysign", "cos", "cosh", "diagonal", "diagonalize", "divide", "dot", "e", "einsum", "elemwise", "empty", "empty_like", "equal", "exp", "expand_dims", "expm1", "eye", "finfo", "flip", "float16", "float32", "float64", "floor", "floor_divide", "full", "full_like", "greater", "greater_equal", "hypot", "iinfo", "imag", "inf", "int16", "int32", "int64", "int8", "isfinite", "isinf", "isnan", "isneginf", "isposinf", "kron", "less", "less_equal", "load_npz", "log", "log10", "log1p", "log2", "logaddexp", "logical_and", "logical_not", "logical_or", "logical_xor", "matrix_transpose", "matmul", "max", "maximum", "mean", "min", "minimum", "moveaxis", "multiply", "nan", "nanmax", "nanmean", "nanmin", "nanprod", "nanreduce", "nansum", "negative", "newaxis", "nextafter", "nonzero", "not_equal", "ones", "ones_like", "outer", "pad", "permute_dims", "pi", "positive", "pow", "prod", "random", "real", "reciprocal", "remainder", "reshape", "result_type", "roll", "round", "save_npz", "sign", "signbit", "sin", "sinh", "sort", "sqrt", "square", "squeeze", "stack", "std", "subtract", "sum", "take", "tan", "tanh", "tensordot", "tril", "triu", "trunc", "uint16", "uint32", "uint64", "uint8", "unique_counts", "unique_values", "var", "vecdot", "where", "zeros", "zeros_like", } if IS_NUMPY2: all_set.update({"isdtype"}) assert set(sparse.__all__) == all_set for attr in sparse.__all__: assert hasattr(sparse, attr) assert sorted(sparse.__all__) == sparse.__all__ sparse-0.17.0/sparse/tests/000077500000000000000000000000001501262445000155325ustar00rootroot00000000000000sparse-0.17.0/sparse/tests/__init__.py000066400000000000000000000000001501262445000176310ustar00rootroot00000000000000sparse-0.17.0/sparse/tests/conftest.py000066400000000000000000000005541501262445000177350ustar00rootroot00000000000000import sparse import pytest import numpy as np @pytest.fixture(scope="session") def backend(): yield sparse._BACKEND @pytest.fixture(scope="module") def graph(): return np.array( [ [0, 1, 1, 0, 0], [0, 0, 1, 0, 1], [0, 0, 0, 0, 0], [0, 0, 0, 0, 1], [0, 1, 0, 1, 0], ] ) sparse-0.17.0/sparse/tests/test_backends.py000066400000000000000000000176441501262445000207310ustar00rootroot00000000000000import warnings import sparse import pytest import numpy as np import scipy as sp import scipy.sparse as sps import scipy.sparse.csgraph as spgraph import scipy.sparse.linalg as splin from numpy.testing import assert_almost_equal, assert_equal def test_backends(backend): rng = np.random.default_rng(0) x = sparse.random((100, 10, 100), density=0.01, random_state=rng) y = sparse.random((100, 10, 100), density=0.01, random_state=rng) if backend == sparse._BackendType.Finch: import finch def storage(): return finch.Storage(finch.Dense(finch.SparseList(finch.SparseList(finch.Element(0.0)))), order="C") x = x.to_storage(storage()) y = y.to_storage(storage()) else: x.asformat("gcxs") y.asformat("gcxs") z = x + y result = sparse.sum(z) assert result.shape == () def test_finch_lazy_backend(backend): if backend != sparse._BackendType.Finch: pytest.skip("Tested only for Finch backend") import finch np_eye = np.eye(5) sp_arr = sps.csr_matrix(np_eye) finch_dense = finch.Tensor(np_eye) assert np.shares_memory(finch_dense.todense(), np_eye) finch_arr = finch.Tensor(sp_arr) assert_equal(finch_arr.todense(), np_eye) transposed = sparse.permute_dims(finch_arr, (1, 0)) assert_equal(transposed.todense(), np_eye.T) @sparse.compiled() def my_fun(tns1, tns2): tmp = sparse.add(tns1, tns2) return sparse.sum(tmp, axis=0) result = my_fun(finch_dense, finch_arr) assert_equal(result.todense(), np.sum(2 * np_eye, axis=0)) @pytest.mark.parametrize("format, order", [("csc", "F"), ("csr", "C"), ("coo", "F"), ("coo", "C")]) def test_asarray(backend, format, order): arr = np.eye(5, order=order) result = sparse.asarray(arr, format=format) assert_equal(result.todense(), arr) @pytest.mark.parametrize("format, order", [("csc", "F"), ("csr", "C"), ("coo", "F"), ("coo", "C")]) def test_scipy_spsolve(backend, format, order): x = np.eye(10, order=order) * 2 y = np.ones((10, 1), order=order) x_pydata = sparse.asarray(x, format=format) y_pydata = sparse.asarray(y, format="coo") actual = splin.spsolve(x_pydata, y_pydata) expected = np.linalg.solve(x, y.ravel()) assert_almost_equal(actual, expected) @pytest.mark.parametrize("format, order", [("csc", "F"), ("csr", "C"), ("coo", "F"), ("coo", "C")]) def test_scipy_inv(backend, format, order): x = np.eye(10, order=order) * 2 x_pydata = sparse.asarray(x, format=format) with warnings.catch_warnings(): warnings.simplefilter("ignore", category=sps.SparseEfficiencyWarning) actual = splin.inv(x_pydata) expected = np.linalg.inv(x) assert_almost_equal(actual.todense(), expected) @pytest.mark.skip(reason="https://github.com/scipy/scipy/pull/20759") @pytest.mark.parametrize("format, order", [("csc", "F"), ("csr", "C"), ("coo", "F"), ("coo", "C")]) def test_scipy_norm(backend, format, order): x = np.eye(10, order=order) * 2 x_pydata = sparse.asarray(x, format=format) actual = splin.norm(x_pydata) expected = sp.linalg.norm(x) assert_almost_equal(actual, expected) @pytest.mark.skip(reason="https://github.com/scipy/scipy/pull/20759") @pytest.mark.parametrize("format, order", [("csc", "F"), ("csr", "C"), ("coo", "F"), ("coo", "C")]) def test_scipy_lsqr(backend, format, order): x = np.eye(10, order=order) * 2 y = np.ones((10, 1), order=order) x_pydata = sparse.asarray(x, format=format) actual_x, _ = splin.lsqr(x_pydata, y)[:2] expected_x, _ = sp.linalg.lstsq(x, y)[:2] assert_almost_equal(actual_x, expected_x.ravel()) @pytest.mark.skip(reason="https://github.com/scipy/scipy/pull/20759") @pytest.mark.parametrize("format, order", [("csc", "F"), ("csr", "C"), ("coo", "F"), ("coo", "C")]) def test_scipy_eigs(backend, format, order): x = np.eye(10, order=order) * 2 x_pydata = sparse.asarray(x, format=format) x_sp = sps.coo_matrix(x) actual_vals, _ = splin.eigs(x_pydata, k=3) expected_vals, _ = splin.eigs(x_sp, k=3) assert_almost_equal(actual_vals, expected_vals) @pytest.mark.parametrize( "matrix_fn, format, order", [(sps.csc_matrix, "csc", "F"), (sps.csr_matrix, "csr", "C"), (sps.coo_matrix, "coo", "F")], ) def test_scipy_connected_components(backend, graph, matrix_fn, format, order): graph = matrix_fn(np.array(graph, order=order)) sp_graph = sparse.asarray(graph, format=format) actual_n_components, actual_labels = spgraph.connected_components(sp_graph) expected_n_components, expected_labels = spgraph.connected_components(graph) assert actual_n_components == expected_n_components assert_equal(actual_labels, expected_labels) @pytest.mark.parametrize( "matrix_fn, format, order", [(sps.csc_matrix, "csc", "F"), (sps.csr_matrix, "csr", "C"), (sps.coo_matrix, "coo", "F")], ) def test_scipy_laplacian(backend, graph, matrix_fn, format, order): graph = matrix_fn(np.array(graph, order=order)) sp_graph = sparse.asarray(graph, format=format) actual_lap = spgraph.laplacian(sp_graph) expected_lap = spgraph.laplacian(graph) assert_equal(actual_lap.todense(), expected_lap.toarray()) @pytest.mark.parametrize("matrix_fn, format, order", [(sps.csc_matrix, "csc", "F"), (sps.csr_matrix, "csr", "C")]) def test_scipy_shortest_path(backend, graph, matrix_fn, format, order): graph = matrix_fn(np.array(graph, order=order)) sp_graph = sparse.asarray(graph, format=format) actual_dist_matrix, actual_predecessors = spgraph.shortest_path(sp_graph, return_predecessors=True) expected_dist_matrix, expected_predecessors = spgraph.shortest_path(graph, return_predecessors=True) assert_equal(actual_dist_matrix, expected_dist_matrix) assert_equal(actual_predecessors, expected_predecessors) @pytest.mark.parametrize( "matrix_fn, format, order", [(sps.csc_matrix, "csc", "F"), (sps.csr_matrix, "csr", "C"), (sps.coo_matrix, "coo", "F")], ) def test_scipy_breadth_first_tree(backend, graph, matrix_fn, format, order): graph = matrix_fn(np.array(graph, order=order)) sp_graph = sparse.asarray(graph, format=format) actual_bft = spgraph.breadth_first_tree(sp_graph, 0, directed=False) expected_bft = spgraph.breadth_first_tree(graph, 0, directed=False) assert_equal(actual_bft.todense(), expected_bft.toarray()) @pytest.mark.parametrize( "matrix_fn, format, order", [(sps.csc_matrix, "csc", "F"), (sps.csr_matrix, "csr", "C"), (sps.coo_matrix, "coo", "F")], ) def test_scipy_dijkstra(backend, graph, matrix_fn, format, order): graph = matrix_fn(np.array(graph, order=order)) sp_graph = sparse.asarray(graph, format=format) actual_dist_matrix = spgraph.dijkstra(sp_graph, directed=False) expected_dist_matrix = spgraph.dijkstra(graph, directed=False) assert_equal(actual_dist_matrix, expected_dist_matrix) @pytest.mark.parametrize( "matrix_fn, format, order", [(sps.csc_matrix, "csc", "F"), (sps.csr_matrix, "csr", "C"), (sps.coo_matrix, "coo", "F")], ) def test_scipy_minimum_spanning_tree(backend, graph, matrix_fn, format, order): graph = matrix_fn(np.array(graph, order=order)) sp_graph = sparse.asarray(graph, format=format) actual_span_tree = spgraph.minimum_spanning_tree(sp_graph) expected_span_tree = spgraph.minimum_spanning_tree(graph) assert_equal(actual_span_tree.todense(), expected_span_tree.toarray()) @pytest.mark.skip(reason="https://github.com/scikit-learn/scikit-learn/pull/29031") @pytest.mark.parametrize("matrix_fn, format, order", [(sps.csc_matrix, "csc", "F")]) def test_scikit_learn_dispatch(backend, graph, matrix_fn, format, order): from sklearn.cluster import KMeans graph = matrix_fn(np.array(graph, order=order)) sp_graph = sparse.asarray(graph, format=format) neigh = KMeans(n_clusters=2) actual_labels = neigh.fit_predict(sp_graph) neigh = KMeans(n_clusters=2) expected_labels = neigh.fit_predict(graph) assert_equal(actual_labels, expected_labels) sparse-0.17.0/tox.ini000066400000000000000000000001361501262445000144060ustar00rootroot00000000000000[tox] envlist = py36, py37 [testenv] commands= pytest {posargs} extras= tests tox