diff --git a/.devcontainer/.dockerignore b/.devcontainer/.dockerignore deleted file mode 100644 index b750bafd..00000000 --- a/.devcontainer/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -../.venv \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 0c49925c..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,69 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu -{ - "name": "soundscapy-dev", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/base:jammy", - "features": { - "ghcr.io/jsburckhardt/devcontainer-features/uv:1": {}, - "ghcr.io/schlich/devcontainer-features/powerlevel10k:1": {}, - "ghcr.io/devcontainers-extra/features/zsh-plugins:0": {} - }, - "remoteEnv": { - "UV_LINK_MODE": "copy" - }, - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "uname -a", - - // Configure tool-specific properties. - "customizations": { - "vscode": { - "extensions": [ - "ms-python.python", - "charliermarsh.ruff", - "REditorSupport.r", - "quarto.quarto-vscode", - "James-Yu.latex-workshop", - "nvarner.typst-lsp", - "ms-toolsai.jupyter", - "ms-toolsai.jupyter-renderers", - "ms-azuretools.vscode-docker", - "GitHub.copilot", - "ms-vscode-remote.remote-containers", - "quarto.quarto", - "flyfly6.terminal-in-status-bar" - ], - "settings": { - "r.rpath.linux": "/usr/local/bin/R", - "r.rterm.linux": "/usr/local/bin/R", - "editor.formatOnSave": true, - "r.lsp.enabled": true, - "terminal.integrated.defaultProfile.linux": "zsh", - // ruff settings - "[python]": { - "defaultInterpreterPath": "/workspace/.venv/bin/python", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - }, - "editor.defaultFormatter": "charliermarsh.ruff" - }, - "notebook.formatOnSave.enabled": true, - "notebook.codeActionsOnSave": { - "notebook.source.fixAll": "explicit", - "notebook.source.organizeImports": "explicit" - } - } - } - }, - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..fabfee0f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,63 @@ +--- +name: Bug Report +description: Create a Report to Help us Improve +labels: + - bug +body: + - id: describe + type: textarea + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. + validations: + required: true + - id: reproduce + type: textarea + attributes: + label: To Reproduce + description: >- + A minimal working example of code to reproduce the unexpected behaviour, + this will render as `Python` code so no need for backticks. + value: |- + import soundscapy + + ... + render: Python + validations: + required: true + - id: expected + type: textarea + attributes: + label: Expected Behaviour + description: >- + A clear and concise description of what you expected to happen. + validations: + required: true + - id: actual + type: textarea + attributes: + label: Actual Behaviour + description: >- + Be a specific and detailed as you can. Paste any output or stack traces + of errors you receive. + validations: + required: true + - id: version + type: input + attributes: + label: Version In Use + description: |- + Can be found by + ```sh + python -c "import soundscapy; print(soundscapy.__version__)" + ``` + validations: + required: true + - id: additional + type: textarea + attributes: + label: Additional Context + value: |- + - Python version: + - Operating system: + render: Markdown diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..bd9dfe4e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +--- +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 00000000..582c191c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,27 @@ +--- +name: Documentation +description: How Can We Improve the Documentation +labels: + - documentation +body: + - id: section + type: textarea + attributes: + label: Which Section of the Documentation Needs Improving? + description: Please provide a link (if it is a specific page). + validations: + required: true + - id: problem + type: textarea + attributes: + label: What Can be Improved About This Section + description: Is it incomplete, incorrect or difficult to understand? + validations: + required: true + - id: suggestions + type: textarea + attributes: + label: How to Improve This Section + description: >- + Do you have any specific suggestions we could take to improve the + documentation? diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..8e1ad7fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,34 @@ +--- +name: Feature Request +description: Suggest a Way to Improve This Project +labels: + - enhancement +body: + - id: problem + type: textarea + attributes: + label: Is Your Feature Request Related to a Problem? Please Describe + description: A clear and concise description of what the problem is. + placeholder: I'm always frustrated when [...] + validations: + required: true + - id: solution + type: textarea + attributes: + label: Describe the Solution You'd Like + description: A clear and concise description of what you want to happen. + validations: + required: true + - id: alternatives + type: textarea + attributes: + label: Describe Alternatives You've Considered + description: >- + A clear and concise description of any alternative solutions or features + you've considered. + - id: additional + type: textarea + attributes: + label: Additional Context + description: >- + Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 00000000..0aa99120 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,24 @@ +--- +name: Question +description: General Questions About Using Soundscapy +labels: + - question +body: + - id: topic + type: dropdown + attributes: + label: What is the Topic of Your Question + description: Please indicate the topic in the title of your question. + options: + - Documentation + - Installation + - Usage + - Other + validations: + required: true + - id: question + type: textarea + attributes: + label: Add Your Question Below + validations: + required: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..fa2746c2 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,37 @@ +name: Documentation + +on: + workflow_call: + outputs: + docs-pass: + description: "Indicates if documentation build passed" + value: ${{ github.event.inputs.docs-pass }} + push: + branches: + - main + pull_request: + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Cache tox + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: .tox + key: tox-${{ hashFiles('pyproject.toml') }} + + - name: Set up Python + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + with: + python-version: "3.x" + cache: "pip" + cache-dependency-path: "pyproject.toml" + + - name: Install tox + run: python -m pip install tox + - name: Build HTML documentation with tox + run: tox -e docs diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 00000000..9c410b81 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,33 @@ +name: Linting + +on: + push: + branches: + - main + pull_request: + +jobs: + linting: + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Cache pre-commit + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - name: Set up python + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install dependencies + run: python -m pip install pre-commit + + - name: Run pre-commit + run: pre-commit run --all-files --color always --verbose diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index 71d9030f..06bbf5e5 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -4,9 +4,9 @@ name: Tagged Release on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+" # v1.2.3 - - "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+" # v1.2.3rc1 - - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" # v1.2.3-rc1 + - "v[0-9]+.[0-9]+.[0-9]+" # v1.2.3 + - "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+" # v1.2.3rc1 + - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" # v1.2.3-rc1 jobs: details: @@ -37,7 +37,7 @@ jobs: echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" echo "suffix=$SUFFIX" >> "$GITHUB_OUTPUT" echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" - + echo "Version is $VERSION_STR" echo "Suffix is $SUFFIX" echo "Tag name is $TAG_NAME" @@ -46,29 +46,28 @@ jobs: exit 1 fi - - name: Verify version matches pyproject.toml - run: | - VERSION_STR=${{ steps.release.outputs.version_str }} - TOML_VERSION=$(grep -oP '^version = "\K[^"]+' pyproject.toml) - - if [ "${VERSION_STR}" != "${TOML_VERSION}" ]; then - echo "Error: Tag version (${VERSION_STR}) does not match pyproject.toml version (${TOML_VERSION})" - exit 1 - fi + run: | + VERSION_STR=${{ steps.release.outputs.version_str }} + TOML_VERSION=$(grep -oP '^version = "\K[^"]+' pyproject.toml) + + if [ "${VERSION_STR}" != "${TOML_VERSION}" ]; then + echo "Error: Tag version (${VERSION_STR}) does not match pyproject.toml version (${TOML_VERSION})" + exit 1 + fi setup_and_build: - needs: + needs: - details runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v5 with: - version: "0.4.29" + version: "0.7.2" enable-cache: true cache-dependency-glob: "uv.lock" @@ -85,17 +84,17 @@ jobs: name: dist path: dist/ + docs-pass: + needs: [details] + uses: ./.github/workflows/docs.yml + tests-pass: needs: [details] uses: ./.github/workflows/test.yml - - tutorial-tests-pass: - needs: [details] - uses: ./.github/workflows/test-tutorials.yml pypi_publish: name: Upload release to PyPI - needs: [setup_and_build, details, tests-pass, tutorial-tests-pass] + needs: [setup_and_build, details, tests-pass, docs-pass] runs-on: ubuntu-latest permissions: id-token: write @@ -138,7 +137,7 @@ jobs: github_release: name: Create GitHub Release - needs: [setup_and_build, details, tests-pass, tutorial-tests-pass] + needs: [setup_and_build, details, tests-pass, docs-pass] runs-on: ubuntu-latest permissions: contents: write @@ -163,4 +162,4 @@ jobs: gh release create ${{ needs.details.outputs.tag_name }} dist/* --title ${{ needs.details.outputs.tag_name }} --generate-notes --prerelease else gh release create ${{ needs.details.outputs.tag_name }} dist/* --title ${{ needs.details.outputs.tag_name }} --generate-notes - fi \ No newline at end of file + fi diff --git a/.github/workflows/test-tag-release.yml b/.github/workflows/test-tag-release.yml index 762ff35a..87928429 100644 --- a/.github/workflows/test-tag-release.yml +++ b/.github/workflows/test-tag-release.yml @@ -3,9 +3,8 @@ name: Test Tagged Release on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+-dev[0-9]+" # v1.2.3-dev1 - - "v[0-9]+.[0-9]+.[0-9]+dev[0-9]+" # v1.2.3dev1 - + - "v[0-9]+.[0-9]+.[0-9]+-dev[0-9]+" # v1.2.3-dev1 + - "v[0-9]+.[0-9]+.[0-9]+dev[0-9]+" # v1.2.3dev1 jobs: # Reuse the details job with no changes @@ -34,7 +33,7 @@ jobs: echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" echo "suffix=$SUFFIX" >> "$GITHUB_OUTPUT" echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" - + echo "Version is $VERSION_STR" echo "Suffix is $SUFFIX" echo "Tag name is $TAG_NAME" @@ -44,14 +43,14 @@ jobs: fi - name: Verify version matches pyproject.toml - run: | - VERSION_STR=${{ steps.release.outputs.version_str }} - TOML_VERSION=$(grep -oP '^version = "\K[^"]+' pyproject.toml) - - if [ "${VERSION_STR}" != "${TOML_VERSION}" ]; then - echo "Error: Tag version (${VERSION_STR}) does not match pyproject.toml version (${TOML_VERSION})" - exit 1 - fi + run: | + VERSION_STR=${{ steps.release.outputs.version_str }} + TOML_VERSION=$(grep -oP '^version = "\K[^"]+' pyproject.toml) + + if [ "${VERSION_STR}" != "${TOML_VERSION}" ]; then + echo "Error: Tag version (${VERSION_STR}) does not match pyproject.toml version (${TOML_VERSION})" + exit 1 + fi # Reuse setup_and_build job with no changes setup_and_build: @@ -59,9 +58,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v3 + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - version: "0.4.29" + version: "0.7.2" enable-cache: true cache-dependency-glob: "uv.lock" - run: uv python install 3.12 @@ -75,15 +75,15 @@ jobs: tests-pass: needs: [details] uses: ./.github/workflows/test.yml - - tutorial-tests-pass: + + docs-pass: needs: [details] - uses: ./.github/workflows/test-tutorials.yml + uses: ./.github/workflows/docs.yml # Modified to publish to TestPyPI testpypi_publish: name: Upload release to TestPyPI - needs: [setup_and_build, details, tests-pass, tutorial-tests-pass] + needs: [setup_and_build, details, tests-pass, docs-pass] runs-on: ubuntu-latest permissions: id-token: write @@ -103,7 +103,7 @@ jobs: steps: - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - name: Install soundscapy from TestPyPI uses: nick-fields/retry@v3 with: @@ -129,4 +129,4 @@ jobs: max_attempts: 3 retry_wait_seconds: 30 command: python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "soundscapy[all]==${{ needs.details.outputs.version_str }}" - - run: python -c "import soundscapy; print(soundscapy.__version__); from soundscapy import Binaural" \ No newline at end of file + - run: python -c "import soundscapy; print(soundscapy.__version__); from soundscapy import Binaural" diff --git a/.github/workflows/test-tutorials.yml b/.github/workflows/test-tutorials.yml deleted file mode 100644 index a6ab48be..00000000 --- a/.github/workflows/test-tutorials.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Test Tutorial Notebooks - -on: - workflow_call: - outputs: - tutorial-tests-pass: - description: "Indicates if all tutorial tests passed" - value: ${{ github.event.inputs.tutorial-tests-pass }} - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test: - name: Run tests on Tutorial notebooks - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.10', '3.11', '3.12'] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - # install a specific version of uv - version: "0.4.29" - enable-cache: true - cache-dependency-glob: "uv.lock" - - - name: Setup Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} - - - name: Install optional dependencies - run: uv sync --all-extras - - - name: Run tests for tutorial notebooks - run: uv run pytest --nbmake -n=auto docs --ignore=docs/tutorials/BinauralAnalysis.ipynb --no-cov # BinauralAnalysis is too slow - \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e458e2f0..edee3029 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,52 +1,85 @@ name: Test on: + workflow_run: + workflows: ["Linting"] + types: + - completed + branches: [main, dev] workflow_call: outputs: tests-pass: description: "Indicates if all tests passed" value: ${{ github.event.inputs.tests-pass }} push: - branches: [ main, dev ] + branches: [main, dev] + paths-ignore: + - "**.md" pull_request: - branches: [ main, dev ] + paths-ignore: + - "**.md" + branches: [main, dev] + +concurrency: + group: test-${{ github.ref }} + cancel-in-progress: true jobs: test: - name: Run tests runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'pull_request' || github.event_name == 'push' }} + name: Test with ${{ matrix.factor }} on Python ${{ matrix.python-version }} + + outputs: + tests-pass: ${{ steps.test-results.outputs.result }} + strategy: + fail-fast: false matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ["3.10", "3.11", "3.12"] + factor: [core, audio, spi, all, tutorials] steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.7.2" + enable-cache: true + cache-dependency-glob: "uv.lock" - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - # install a specific version of uv - version: "0.4.29" - enable-cache: true - cache-dependency-glob: "uv.lock" + - name: Setup R environment + uses: r-lib/actions/setup-r@v2 + with: + r-version: "4.4" + use-public-rspm: true - - name: Setup Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} + - name: Install R dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + cache-version: 1 + dependencies: '"all"' - - name: Lint and format check # Run linting before wasting time on testing - run: | - uvx ruff check . - uvx ruff format --check + - name: Restore tox cache + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: .tox + key: tox-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.factor }}-${{ hashFiles('pyproject.toml') }} - - name: Install core dependencies - run: uv sync --extra test + - name: Setup Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} - - name: Run tests for core deps only - run: uv run pytest + - name: Install tox + run: uv tool install tox --with tox-uv - - name: Install optional dependencies - run: uv sync --all-extras + # - uses: fawazahmed0/action-debug-vscode@main - - name: Run tests for all dependencies - run: uv run pytest + # Convert Python version (e.g., 3.10) to format needed for tox (e.g., py310) + - name: Run tox environment + id: test-results + run: | + py_version="py$(echo '${{ matrix.python-version }}' | tr -d '.')" + tox r -e ${py_version}-${{ matrix.factor }} + echo "result=true" >> $GITHUB_OUTPUT diff --git a/.gitignore b/.gitignore index fe2475d1..8912960a 100644 --- a/.gitignore +++ b/.gitignore @@ -162,26 +162,23 @@ dmypy.json *.code-workspace # End of https://www.toptal.com/developers/gitignore/api/python,jupyternotebooks,vscode -/.vscode -/.idea +/.vscode/ +/.idea/ .vscode/settings.json -Cultural-comparison-analysis.ipynb -soundscapy_old/archive -/notebooks/ -.idea/.name -.idea/misc.xml -.idea/Soundscapy.iml -.idea/vcs.xml -.idea/workspace.xml -soundscapy_old/.DS_Store *.DS_Store -/pyproject-poetry.toml -/docs/_autosummary/ -!/.pytest_cache/ -!/.vscode/ -.pdm-python + # Ignore everything in tests/test_audio_files except .wav files tests/test_audio_files/* !tests/test_audio_files/*.wav /docs/tutorials/updated_config.yaml docs/tutorials/acoustic_analysis_results.xlsx + +/scratch/ +CLAUDE.local.md +/.pytest_cache/ +/.ruff_cache/ + +# package version +*_version.py + +**/.claude/settings.local.json diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 00000000..dcd84a13 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,4 @@ +--- +MD013: false +MD024: + siblings_only: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..dbddb0fe --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,56 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.2 + hooks: + # TODO(MitchellAcoustics): Enable linting pre-commit hooks + # https://github.com/MitchellAcoustics/Soundscapy/issues/114 + # - id: ruff + - id: ruff-format + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.44.0 + hooks: + - id: markdownlint-fix + args: + - --dot + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.5 + hooks: + - id: forbid-tabs + - repo: https://github.com/pappasam/toml-sort + rev: v0.24.2 + hooks: + - id: toml-sort-fix + # TODO(MitchellAcoustics): Enable linting pre-commit hooks + # https://github.com/MitchellAcoustics/Soundscapy/issues/114 + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v1.15.0 + # hooks: + # - id: mypy + - repo: https://github.com/rbubley/mirrors-prettier + rev: v3.5.3 + hooks: + - id: prettier + args: + - --quote-props=as-needed + exclude: ^docs/.*\.md$ + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-case-conflict + - id: check-docstring-first + - id: check-merge-conflict + - id: check-toml + - id: end-of-file-fixer + - id: mixed-line-ending + args: + - --fix=lf + - id: trailing-whitespace + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.32.1 + hooks: + # Schemas taken from https://www.schemastore.org/json/ + - id: check-jsonschema + name: "Validate GitHub issue templates" + files: ^\.github/ISSUE_TEMPLATE/.*\.yml$ + exclude: ^\.github/ISSUE_TEMPLATE/config\.yml$ + args: ["--verbose", "--schemafile", "schemas/github-issue-forms.json"] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c02a9d2a..5f6905be 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -21,7 +21,7 @@ build: # Build documentation in the "docs/" directory with Sphinx mkdocs: - configuration: mkdocs.yml + configuration: mkdocs.yml # Optionally build your docs in additional formats such as PDF and ePub formats: diff --git a/CHANGELOG.md b/CHANGELOG.md index cf69895f..bb053ee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved handling of optional dependencies to provide better error messages and IDE support -- Audio components (like `Binaural`) can now be imported directly from the top-level package - (`from soundscapy import Binaural`) while maintaining helpful error messages when +- Audio components (like `Binaural`) can now be imported directly from the top-level package + (`from soundscapy import Binaural`) while maintaining helpful error messages when dependencies are missing - Centralized optional dependency configuration in `_optionals.py` for better maintainability @@ -19,8 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - No changes required to existing code using audio components - The new system provides better IDE completion support while maintaining the same runtime behavior -- Optional components can still be imported from their original location - (`from soundscapy.audio import Binaural`) or from the top level +- Optional components can still be imported from their original location + (`from soundscapy.audio import Binaural`) or from the top level (`from soundscapy import Binaural`) ## [0.7.5] @@ -47,15 +47,15 @@ pip install soundscapy[all] #### Dev Container Configuration -* Added a new `devcontainer.json` file to configure the development container with specific features and VSCode extensions. (`.devcontainer/devcontainer.json` [.devcontainer/devcontainer.jsonR1-R69](diffhunk://#diff-24ad71c8613ddcf6fd23818cb3bb477a1fb6d83af4550b0bad43099813088686R1-R69)) -* Updated `.dockerignore` to exclude the virtual environment directory. (`.devcontainer/.dockerignore` [.devcontainer/.dockerignoreR1](diffhunk://#diff-7691e653179b9ed2292151d962426f76e6f5378e4989e741859bdfcbcef16b97R1)) +- Added a new `devcontainer.json` file to configure the development container with specific features and VSCode extensions. (`.devcontainer/devcontainer.json` [.devcontainer/devcontainer.jsonR1-R69](diffhunk://#diff-24ad71c8613ddcf6fd23818cb3bb477a1fb6d83af4550b0bad43099813088686R1-R69)) +- Updated `.dockerignore` to exclude the virtual environment directory. (`.devcontainer/.dockerignore` [.devcontainer/.dockerignoreR1](diffhunk://#diff-7691e653179b9ed2292151d962426f76e6f5378e4989e741859bdfcbcef16b97R1)) -#### GitHub Workflows: +#### GitHub Workflows -* Removed old CI, release, and test-release workflows. (`.github/workflows/ci.yml` [[1]](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fL1-L40) `.github/workflows/release.yml` [[2]](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L1-L33) `.github/workflows/test-release.yml` [[3]](diffhunk://#diff-191bb5b4e97db48c9d0bdb945dd00e17b53249422f60a642e9e8d73250b5913aL1-L53) -* Added a new workflow for tagged releases to automate the release process, including building and publishing to PyPI and TestPyPI. (`.github/workflows/tag-release.yml` [.github/workflows/tag-release.ymlR1-R138](diffhunk://#diff-21e1251c1676ed10064d2d98ab1a8f6471a9718058bd316970abe934169f2b60R1-R138)) -* Added a new workflow for testing tagged releases, including installation from TestPyPI and running tests. (`.github/workflows/test-tag-release.yml` [.github/workflows/test-tag-release.ymlR1-R114](diffhunk://#diff-11b7dedbf7b09ab5a0bd90aa70d8a2eda1918dab64a511c82104706cfa09f3b7R1-R114)) -* Added new workflows for running tests on the main codebase and tutorial notebooks. (`.github/workflows/test.yml` [[1]](diffhunk://#diff-faff1af3d8ff408964a57b2e475f69a6b7c7b71c9978cccc8f471798caac2c88R1-R52) `.github/workflows/test-tutorials.yml` [[2]](diffhunk://#diff-01bd86ab14c3e8d7d1382e5ed2172404eb7d3c46bbffeffe09fc11431885e2a0R1-R42) +- Removed old CI, release, and test-release workflows. (`.github/workflows/ci.yml` [[1]](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fL1-L40) `.github/workflows/release.yml` [[2]](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L1-L33) `.github/workflows/test-release.yml` [[3]](diffhunk://#diff-191bb5b4e97db48c9d0bdb945dd00e17b53249422f60a642e9e8d73250b5913aL1-L53) +- Added a new workflow for tagged releases to automate the release process, including building and publishing to PyPI and TestPyPI. (`.github/workflows/tag-release.yml` [.github/workflows/tag-release.ymlR1-R138](diffhunk://#diff-21e1251c1676ed10064d2d98ab1a8f6471a9718058bd316970abe934169f2b60R1-R138)) +- Added a new workflow for testing tagged releases, including installation from TestPyPI and running tests. (`.github/workflows/test-tag-release.yml` [.github/workflows/test-tag-release.ymlR1-R114](diffhunk://#diff-11b7dedbf7b09ab5a0bd90aa70d8a2eda1918dab64a511c82104706cfa09f3b7R1-R114)) +- Added new workflows for running tests on the main codebase and tutorial notebooks. (`.github/workflows/test.yml` [[1]](diffhunk://#diff-faff1af3d8ff408964a57b2e475f69a6b7c7b71c9978cccc8f471798caac2c88R1-R52) `.github/workflows/test-tutorials.yml` [[2]](diffhunk://#diff-01bd86ab14c3e8d7d1382e5ed2172404eb7d3c46bbffeffe09fc11431885e2a0R1-R42) ## [0.7.3] @@ -70,6 +70,7 @@ Complete refactoring of `Soundscapy`, splitting it into multiple modules (`surve ### General Changes #### Added + - New `soundscapy/surveys/survey_utils.py` for shared utilities - Implemented `PAQ` enum for Perceptual Attribute Questions - Added `return_paqs` function for filtering PAQ columns @@ -95,6 +96,7 @@ Complete refactoring of `Soundscapy`, splitting it into multiple modules (`surve - New test cases in `test_isd.py` to cover refactored functionality #### Changed + - Modified default logging level to WARNING for better control over log output - Refactored `isd.py` to use new processing and survey utility functions - Updated `load`, `load_zenodo`, and `validate` functions @@ -109,6 +111,7 @@ Complete refactoring of `Soundscapy`, splitting it into multiple modules (`surve - Changed to Rye as the dependency and environment manager for the project #### Improved + - Enhanced error handling and input validation in database modules - Added type hints to all functions for better code readability and IDE support - Implemented more specific exception handling @@ -119,21 +122,25 @@ Complete refactoring of `Soundscapy`, splitting it into multiple modules (`surve - Standardized coding style across all modules (using Black formatter) #### Deprecated + - Removed `remove_lockdown` function in `isd.py` (redundant since the release of ISD v1.0) #### Removed + - Eliminated redundant code and unused functions across modules #### Fixed + - Resolved issues with inconsistent PAQ naming conventions - Fixed bugs in ISO coordinate calculations and SSM metric computations - Resolved issue where Jupyter notebooks were overriding the default log level - #### Security + - Implemented input validation to prevent potential security vulnerabilities #### Development + - Implemented a more robust logging system using loguru - Added ability to easily change log levels for debugging and development - Enabled file logging for persistent log storage @@ -142,6 +149,7 @@ Complete refactoring of `Soundscapy`, splitting it into multiple modules (`surve - Implemented consistent error messages and logging across the package #### Documentation + - Added comprehensive docstrings to all functions and classes - Included usage examples in function docstrings - Updated README with new package structure and usage instructions @@ -149,39 +157,39 @@ Complete refactoring of `Soundscapy`, splitting it into multiple modules (`surve ### Changes to Plotting Module -#### Code Structure: +#### Code Structure - Split the original circumplex.py into multiple files: backends.py, circumplex_plot.py, plot_functions.py, stylers.py, and plotting_utils.py (implied). - Introduced abstract base class PlotBackend and concrete implementations SeabornBackend and PlotlyBackend. -#### New Features: +#### New Features - Added support for Plotly backend alongside Seaborn. - Introduced CircumplexPlot class for creating and managing plots. - Added StyleOptions dataclass for better style management. - Implemented simple_density plot type. -#### Improved Customization: +#### Improved Customization - Created CircumplexPlotParams dataclass for better parameter management. - Added more customization options for plots (e.g., incl_outline, fill, alpha). -#### Enhancements: +#### Enhancements - Improved type hinting throughout the codebase. - Added docstrings to classes and functions. - Implemented PlotType and Backend enums for better type safety. -#### Refactoring: +#### Refactoring - Moved plotting logic from functions to methods in backend classes. - Simplified scatter and density functions by leveraging CircumplexPlot class. -#### Removed Features: +#### Removed Features - Removed jointplot function (marked as TODO in CircumplexPlot class). -#### Constants and Utilities: +#### Constants and Utilities - Moved constants (e.g., DEFAULT_XLIM, DEFAULT_YLIM) to a separate utilities file. - Created ExtraParams TypedDict for additional plotting parameters. diff --git a/CITATION.cff b/CITATION.cff index e9c017db..39ccb3d9 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,17 +12,17 @@ authors: family-names: Mitchell email: a.j.mitchell@ucl.ac.uk affiliation: University College London - orcid: 'https://orcid.org/0000-0003-0978-5046' -repository-code: 'https://github.com/MitchellAcoustics/Soundscapy' -url: 'https://soundscapy.readthedocs.io/en/latest/' -repository: 'https://pypi.org/project/soundscapy/' + orcid: "https://orcid.org/0000-0003-0978-5046" +repository-code: "https://github.com/MitchellAcoustics/Soundscapy" +url: "https://soundscapy.readthedocs.io/en/latest/" +repository: "https://pypi.org/project/soundscapy/" keywords: - soundscape - acoustics - psychoacoustics license: BSD-3-Clause version: 0.7.4 -date-released: '2024-10-17' +date-released: "2024-10-17" # preferred-citation: # type: article # authors: diff --git a/CLAUDE.local.md b/CLAUDE.local.md new file mode 100644 index 00000000..0150e1c2 --- /dev/null +++ b/CLAUDE.local.md @@ -0,0 +1,51 @@ +# Soundscapy Development Guide + +## Build & Test Commands + +- Run all tests: `uv run pytest` +- Run single test: `uv run pytest test/path/to/test.py::test_function` +- Run tests with specific marker: `uv run pytest -m "optional_deps('audio')"` +- Run with parallel execution: `uv run pytest -xvs` +- Code formatter: `uv run ruff format .` +- Code linting: `uv run ruff check .` +- Build package: `uv build` + +## Code Style + +- Use python 3.10+ type hints for all function parameters and return values (e.g. `str | None = None` rather than `Optional[str]`) +- Type annotations should follow Python 3.9+ conventions and use the built in datatypes dict, tuple, etc., rather than Dict, Tuple, etc. +- Use docstrings in NumPy format for all public functions and classes +- Variable naming: snake_case for variables/functions, PascalCase for classes +- Prefer explicit error handling with try/except blocks +- Prefer pathlib.Path over string paths +- Use loguru for logging through the soundscapy.logging module +- Use optional dependencies system for features with heavy dependencies + +## Scientific Python Design Principles + +- Detailed design principles are in `ai_dev_docs/design.md` +- Keep I/O separate from scientific logic +- Take a layered approach to complexity and permissiveness: + - Separate into two layers: a thin "friendly" layer on top of a "cranky" layer that takes in only exactly what it needs and does the actual work. + - The cranky layer should be easy to test; it should be constrained about what it accepts and what it returns. + - This layered design makes it possible to write _many_ friendly layers with different opinions and different defaults. +- Prefer duck typing and protocols over explicit type checks +- Prefer functions over classes when possible +- Avoid changing state; use immutable objects where appropriate +- Use standard scientific types (numpy arrays, pandas DataFrames) over custom classes +- Write specific, helpful error messages +- Prefer small, focused functions over complex multi-purpose ones +- Functions should have stable return types regardless of inputs +- Use keyword-only arguments for optional parameters +- Make complexity explicit rather than hiding it + +## SPI Feature Development + +- Detailed development plans are in `ai_dev_docs/spi/` +- Using test-driven development (TDD) approach +- The feature relies on R integration through rpy2 as an optional dependency +- Implementation is based on bivariate skew-normal distributions for soundscape perception indices +- Follow layered design with "friendly" and "cranky" layers +- This feature is based on research from J2401_JASA_SSID-Single-Index repository +- Reference the original paper for mathematical background +- Use Soundscapy's optional dependency system for R/rpy2 integration \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abe9ea8f..82203767 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ - Use the `uv` tool for managing dependencies and other project tasks. `uv add` and `uv remove` should be used to add or remove dependencies. `uv add --optional ` should be used to add an optional dependency. `uv sync # add --all-extras, etc as needed` should be used to install dependencies and sync with lock file. `uv build` should be used to build the package. - Try to keep all necessary configurations to `pyproject.toml` where possible. This includes versioning, optional dependencies, tool settings (e.g. `bumpver`) and other project settings. -- Wherever possible, centralise operations and metadata. For instance, version is defined in `pyproject.toml` and automatically brought into `soundscapy` metadata in `__init__.py`; optional dependency groups are defined in `_optionals.py` and checked once at the `.__init__.py` level, rather than for each individual function or at the `soundscapy.__init__.py` level. +- Wherever possible, centralise operations and metadata. For instance, version is defined in `pyproject.toml` and automatically brought into `soundscapy` metadata in `__init__.py`; optional dependency checks are performed at the `.__init__.py` level, rather than for each individual function. Changes should be made in a feature branch and submitted to `dev` via a pull request. The pull request should be reviewed by at least one other developer before being merged. The `main` branch should only contain stable releases. Docs can be updated directly on `dev` or `main` as needed. @@ -41,24 +41,24 @@ The settings for `bumpver` are stored in `pyproject.toml`. 2. **Minor Version**: - - Incremented for new features or significant changes - - Reset to 0 for major versions + - Incremented for new features or significant changes + - Reset to 0 for major versions 3. **Patch Version**: - - Incremented for bug fixes or minor changes - - Reset to 0 for new minor versions + - Incremented for bug fixes or minor changes + - Reset to 0 for new minor versions 4. **Pre-release Versions**: - - Use `rc` for release candidates - - Use `dev` for development versions + - Use `rc` for release candidates + - Use `dev` for development versions ### Commit messages Try to use the [Angular commit message format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) for commit messages. Mostly this means starting the commit message with a type, followed by a colon and a short description. For example: -``` txt +```txt feat: add new feature fix: correct bug in feature docs: update documentation @@ -77,29 +77,11 @@ The avilable types are: ## Optional Dependencies System -### Core Components - -1. **Dependency Definitions** (`_optionals.py`): +Soundscapy uses a simple and standard approach to handle optional dependencies. - ```python - # Package dependencies - OPTIONAL_DEPENDENCIES = { - "audio": { - "packages": ("mosqito", "maad", "acoustic_toolbox"), - "install": "soundscapy[audio]", - "description": "audio analysis functionality", - }, - } - - # Top-level imports available when dependencies are installed - OPTIONAL_IMPORTS = { - 'Binaural': ('soundscapy.audio', 'Binaural'), - 'AudioAnalysis': ('soundscapy.audio', 'AudioAnalysis'), - # ... other optional components - } - ``` +### Core Components -2. **Package Configuration** (`pyproject.toml`): +1. **Package Configuration** (`pyproject.toml`): ```toml [project.optional-dependencies] @@ -110,18 +92,45 @@ The avilable types are: ] ``` -3. **Module-Level Dependency Check** (`audio/__init__.py`): +2. **Module-Level Dependency Check** (`audio/__init__.py`): ```python - from soundscapy._optionals import require_dependencies - - # This will raise an ImportError if dependencies are missing - required = require_dependencies("audio") + # Check for required dependencies directly + try: + import mosqito + import maad + import tqdm + import acoustic_toolbox + except ImportError as e: + raise ImportError( + "Audio analysis functionality requires additional dependencies. " + "Install with: pip install soundscapy[audio]" + ) from e # Now import module components from .binaural import Binaural ``` +3. **Top-Level Imports** (`soundscapy/__init__.py`): + + ```python + # Try to import optional audio module + try: + from soundscapy import audio + from soundscapy.audio import ( + Binaural, AudioAnalysis, AnalysisSettings, ConfigManager, + process_all_metrics, prep_multiindex_df, add_results, parallel_process, + ) + __all__.extend([ + "audio", "Binaural", "AudioAnalysis", "AnalysisSettings", + "ConfigManager", "process_all_metrics", "prep_multiindex_df", + "add_results", "parallel_process", + ]) + except ImportError: + # Audio module not available - this is expected if dependencies aren't installed + pass + ``` + ### Adding New Optional Features #### 1. Add New Dependencies @@ -136,40 +145,21 @@ uv add new-package --optional audio uv add package1 package2 --optional new_group ``` -#### 2. Update Dependency Definitions - -In `_optionals.py`, update both dependency mappings: - -```python -OPTIONAL_DEPENDENCIES = { - "audio": { - "packages": ("mosqito", "maad", "acoustic_toolbox", "new_package"), # Add to existing - "install": "soundscapy[audio]", - "description": "audio analysis functionality", - }, - "new_group": { # Or create new group - "packages": ("package1", "package2"), - "install": "soundscapy[new_group]", - "description": "description of functionality", - }, -} - -OPTIONAL_IMPORTS = { - # Existing imports... - 'NewFeature': ('soundscapy.new_group', 'NewFeature'), # Add new top-level imports -} -``` - -#### 3. Implement Feature Code +#### 2. Implement Feature Code Create a new module directory if needed (e.g., `new_group/`) with an `__init__.py`: ```python """Module docstring describing the new functionality.""" -from soundscapy._optionals import require_dependencies - -# This will raise an ImportError if dependencies are missing -required = require_dependencies("new_group") +# Check dependencies directly +try: + import package1 + import package2 +except ImportError as e: + raise ImportError( + "This functionality requires additional dependencies. " + "Install with: pip install soundscapy[new_group]" + ) from e # Now import your feature code from .feature import NewFeature @@ -177,39 +167,36 @@ from .feature import NewFeature __all__ = ["NewFeature"] ``` -#### 4. Add to Top-Level Exports +#### 3. Add to Top-Level Exports -If you want the new feature to be available at the top level, add it to `__all__` in `soundscapy/__init__.py`: +Update the main `__init__.py` to import and expose the new module: ```python -__all__ = [ - # Core modules... - # Optional modules - "NewFeature", # Add new feature here -] +# Try to import optional new_group module +try: + from soundscapy import new_group + from soundscapy.new_group import NewFeature + __all__.extend(["new_group", "NewFeature"]) +except ImportError: + # new_group module not available - expected if dependencies aren't installed + pass ``` ### How It Works -The system provides three levels of dependency handling: +The system uses standard Python try/except patterns at two levels: -1. **Module Level**: The `require_dependencies()` check in the optional module's `__init__.py` ensures dependencies are available before the module is imported. +1. **Module Level**: Each optional module checks for its dependencies on import and raises a helpful error if they're missing. -2. **Top Level Imports**: `__getattr__` in the main `__init__.py` enables importing optional components directly from `soundscapy` with proper error handling: - - ```python - from soundscapy import Binaural # Works with deps, helpful error without - ``` - -3. **IDE Support**: The explicit `__all__` list in `__init__.py` provides IDE autocompletion while maintaining proper runtime behavior. +2. **Top Level**: The main package tries to import optional modules and their components, extending **all** only when available. Benefits: - Clear error messages when dependencies are missing -- Optional components available at both module and package level +- Standard Python import patterns that are easy to understand - Good IDE support through explicit exports -- Centralized dependency configuration - No runtime overhead for unused optional features +- Simpler to maintain and extend ### Testing Optional Dependencies @@ -217,41 +204,93 @@ Soundscapy uses a flexible system for testing optional dependencies that allows #### Test Structure -Optional dependency tests exist at three levels: +Optional dependency tests exist at several levels: 1. **Optional Module Tests**: Tests within optional modules (e.g., `audio/`) - - Only collected when dependencies are available + + - Only collected when dependencies are available using pytest_ignore_collect - Test actual functionality - - No need for special markers or mocking + - No need for special markers 2. **Integration Tests**: Tests that use optional features from other modules + - Use `@pytest.mark.optional_deps('group')` marker - - Skip when dependencies unavailable + - Expected to fail when dependencies are unavailable - Test actual integration between components +3. **In-Development Module Tests**: For modules under active development (e.g., `spi/`) + - Use module-level `pytestmark = pytest.mark.skip(reason="...")` to skip all tests + - Prevent pytest collection errors by using `--ignore=src/soundscapy/module/` flag + - Simplifies testing during development when module imports might fail + +#### Testing with Tox + +Soundscapy's tox configuration provides separate environments for different dependency groups: + +```bash +# Run core tests (no optional dependencies) +tox -e py310-core + +# Run with audio dependencies +tox -e py310-audio + +# Run with SPI dependencies +tox -e py310-spi + +# Run with all dependencies +tox -e py310-all +``` + +Each environment is configured to run the appropriate tests: + +- Core: Run only tests with no optional dependency requirements +- Audio: Run core tests and audio-specific tests, skipping SPI tests +- SPI: Run core tests and SPI-specific tests +- All: Run all tests + +Test selection is implemented using pytest's keyword-based filtering to precisely target the right tests: + +```python +# Core tests only +pytest -k "not optional_deps" + +# Core + audio tests +pytest -k "not optional_deps or optional_deps and audio" + +# Core + SPI tests +pytest -k "not optional_deps or optional_deps and spi" +``` #### When to Use Each Testing Approach 1. **Use `@pytest.mark.optional_deps` when**: + - Testing actual functionality that requires dependencies - Writing integration tests - Testing with real package interactions 2. **No special handling needed when**: - - Writing tests within an optional module + + - Writing tests within an optional module directory - Testing core functionality that doesn't use optional features +3. **Use module-level skip markers when**: + - Working on a module that is not yet ready for testing + - Dependencies might cause import errors during collection + - You want to include tests in the codebase but skip their execution + ### Adding Tests for New Optional Features When adding new optional features: 1. **Inside Optional Module**: - - Put tests in the module's test directory - - No special handling needed - - Tests will only run when dependencies are available + + - Put tests in the module's test directory (e.g., `test/new_group/`) + - Tests will only be collected when dependencies are available + - No markers needed for tests within the module's directory ```python - # soundscapy/new_group/tests/test_feature.py + # test/new_group/test_feature.py def test_new_feature(): """Regular test, no special handling needed.""" from soundscapy.new_group import NewFeature @@ -259,6 +298,7 @@ When adding new optional features: ``` 2. **Integration Tests**: + - Use the optional_deps marker - Put in main test directory @@ -266,7 +306,7 @@ When adding new optional features: # test/test_integration.py @pytest.mark.optional_deps('new_group') def test_new_feature_integration(): - """Will skip if dependencies missing.""" + """Will be marked as expected to fail if dependencies missing.""" from soundscapy import NewFeature assert NewFeature.integrate() == expected ``` @@ -277,7 +317,16 @@ Soundscapy has three primary workflows: `test.yml`, `test-tutorials.yml` and `ta In all cases, python and dependencies are managed and installed with `uv`. -`test.yml` will test on multiple python versions, defined by the `python-version` matrix. First, it will install the core dependencies with `uv sync`, then run the test suite (which **should** ignore the tests requiring optional dependencies). Then, it will install all optional dependencies `uv sync --all-extras` and run the tests again. This ensures that the tests run with and without optional dependencies. +`test.yml` uses tox to test across multiple Python versions and dependency combinations. The workflow has a two-stage approach: + +1. **Linting**: First, it runs ruff checks for code quality and formatting. + +2. **Testing**: Then, it runs tox with different environments: + - **Core**: Tests with just core dependencies using `py{310,311,312}-core` + - **Audio**: Tests with audio dependencies using `py{310,311,312}-audio` + - **All**: Tests with all dependencies using `py{310,311,312}-all` + +This approach ensures consistent testing between local development (using tox locally) and CI environments, while verifying that the package works correctly with different Python versions and dependency combinations. `test-tutorials.yml` uses `--nbmake` to convert the notebooks to python files and run them. This is useful for testing the tutorials and ensuring they are up to date. It does not test the veracity of the outputs, just whether the notebooks run without errors. diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 00000000..a96a5ac2 --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,12 @@ +Package: Soundscapy +Type: Package +Title: Soundscapy +Version: 0.8.0 +Author: Andrew Mitchell +Maintainer: Andrew Mitchell +Description: A python library for analysing and visualising soundscape assessments. +License: BSD 3-Clause +Encoding: UTF-8 +LazyData: true +Imports: + sn diff --git a/LICENSE b/LICENSE.md similarity index 95% rename from LICENSE rename to LICENSE.md index 089e1dcf..c3ee14c0 100644 --- a/LICENSE +++ b/LICENSE.md @@ -1,6 +1,8 @@ + + BSD 3-Clause License -Copyright (c) 2024, Andrew Mitchell +Copyright (c) 2025, Andrew Mitchell All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index ae6f4d85..580f4a3b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ - +![Soundscapy Logo](https://raw.githubusercontent.com/MitchellAcoustics/Soundscapy/main/docs/img/LightLogo.png) # Soundscapy -[![PyPI version](https://badge.fury.io/py/soundscapy.svg)](https://badge.fury.io/py/soundscapy) +[![PyPI version](https://badge.fury.io/py/soundscapy.svg)](https://badge.fury.io/py/soundscapy) [![Tests](https://github.com/MitchellAcoustics/Soundscapy/actions/workflows/test.yml/badge.svg)](https://github.com/MitchellAcoustics/Soundscapy/actions/workflows/test.yml) [![Documentation Status](https://readthedocs.org/projects/soundscapy/badge/?version=latest)](https://soundscapy.readthedocs.io/en/latest/?badge=latest) ![License](https://img.shields.io/github/license/MitchellAcoustics/Soundscapy) @@ -35,8 +35,6 @@ To install all optional dependencies, use the following command: pip install "soundscapy[all]" ``` - - ## Examples We are currently working on writing more comprehensive examples and documentation, please bear with us in the meantime. @@ -47,15 +45,15 @@ Tutorials for using Soundscapy can be found in the [documentation](https://sound The newly added Binaural analysis functionality relies directly on three acoustic analysis libraries: -* [Acoustic Toolbox](https://github.com/Universite-Gustave-Eiffel/acoustic-toolbox) for the standard environmental and building acoustics metrics, -* [scikit-maad](https://github.com/scikit-maad/scikit-maad) for the bioacoustics and ecological soundscape metrics, and -* [MoSQITo](https://github.com/Eomys/MoSQITo) for the psychoacoustics metrics. We thank each of these packages for their great work in making advanced acoustic analysis more accessible. +- [Acoustic Toolbox](https://github.com/Universite-Gustave-Eiffel/acoustic-toolbox) for the standard environmental and building acoustics metrics, +- [scikit-maad](https://github.com/scikit-maad/scikit-maad) for the bioacoustics and ecological soundscape metrics, and +- [MoSQITo](https://github.com/Eomys/MoSQITo) for the psychoacoustics metrics. We thank each of these packages for their great work in making advanced acoustic analysis more accessible. ## Citation If you are using Soundscapy in your research, please help our scientific visibility by citing our work! Please include a citation to our accompanying paper: -Mitchell, A., Aletta, F., & Kang, J. (2022). How to analyse and represent quantitative soundscape data. *JASA Express Letters, 2*, 37201. [https://doi.org/10.1121/10.0009794](https://doi.org/10.1121/10.0009794) +Mitchell, A., Aletta, F., & Kang, J. (2022). How to analyse and represent quantitative soundscape data. _JASA Express Letters, 2_, 37201. [https://doi.org/10.1121/10.0009794](https://doi.org/10.1121/10.0009794) + ![Image title](img/LightLogoSmall.png#only-light) ![Image title](img/DarkLogoSmall.png#only-dark) @@ -10,7 +12,7 @@ _Soundscapy_ is a Python library for analysing and visualising soundscape assessments. This package was designed to (1) load and process soundscape assessment data, (2) visualise the data, and (3) enable psychoacoustic analysis of soundscape recordings. !!! note - This project is still under development. We're working hard to make it as good as possible, but there may be bugs or missing features. If you find any issues, please let us know by submitting an issue on Github. +This project is still under development. We're working hard to make it as good as possible, but there may be bugs or missing features. If you find any issues, please let us know by submitting an issue on Github. ## Getting Started @@ -43,22 +45,24 @@ We welcome contributions from the community. If you're interested in contributin ## Citing Soundscapy !!! note - If you use _Soundscapy_ in your research, please include a citation to our accompanying paper: - - Mitchell, A., Aletta, F., & Kang, J. (2022). How to analyse and represent quantitative soundscape data. _JASA Express Letters, 2_, 37201. [https://doi.org/10.1121/10.0009794](https://doi.org/10.1121/10.0009794) +If you use _Soundscapy_ in your research, please include a citation to our accompanying paper: + + Mitchell, A., Aletta, F., & Kang, J. (2022). How to analyse and represent quantitative soundscape data. _JASA Express Letters, 2_, 37201. [https://doi.org/10.1121/10.0009794](https://doi.org/10.1121/10.0009794) ## License -This project is licensed under the GNU GPLv3 License. For more information, please see the `license.md` file. +This project is licensed under the BSD 3-Clause License. For more information, please see the `license.md` file. ## Project layout - mkdocs.yml # The configuration file. - docs/ - index.md # The documentation homepage. - about.md # The about page. - license.md # The license page. - tutorials/ # Tutorial pages. - Introduction to SSM Analysis.ipynb - ... # Other markdown pages, images and other files. - src/soundscapy/ +```plaintext +mkdocs.yml # The configuration file. +docs/ + index.md # The documentation homepage. + about.md # The about page. + license.md # The license page. + tutorials/ # Tutorial pages. + Introduction to SSM Analysis.ipynb + ... # Other markdown pages, images and other files. +src/soundscapy/ +``` diff --git a/docs/javascripts/katex.js b/docs/javascripts/katex.js index 2ab434fc..0946ce0a 100644 --- a/docs/javascripts/katex.js +++ b/docs/javascripts/katex.js @@ -1,10 +1,10 @@ document$.subscribe(({ body }) => { renderMathInElement(body, { delimiters: [ - { left: "$$", right: "$$", display: true }, - { left: "$", right: "$", display: false }, + { left: "$$", right: "$$", display: true }, + { left: "$", right: "$", display: false }, { left: "\\(", right: "\\)", display: false }, - { left: "\\[", right: "\\]", display: true } + { left: "\\[", right: "\\]", display: true }, ], - }) -}) \ No newline at end of file + }); +}); diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js index 0b00d2ff..5209b3c1 100644 --- a/docs/javascripts/mathjax.js +++ b/docs/javascripts/mathjax.js @@ -3,17 +3,17 @@ window.MathJax = { inlineMath: [["\\(", "\\)"]], displayMath: [["\\[", "\\]"]], processEscapes: true, - processEnvironments: true + processEnvironments: true, }, options: { ignoreHtmlClass: ".*|", - processHtmlClass: "arithmatex" - } + processHtmlClass: "arithmatex", + }, }; document$.subscribe(() => { - MathJax.startup.output.clearCache() - MathJax.typesetClear() - MathJax.texReset() - MathJax.typesetPromise() -}) \ No newline at end of file + MathJax.startup.output.clearCache(); + MathJax.typesetClear(); + MathJax.texReset(); + MathJax.typesetPromise(); +}); diff --git a/docs/license.md b/docs/license.md index da3d61b8..75b899af 100644 --- a/docs/license.md +++ b/docs/license.md @@ -1,29 +1,3 @@ -## BSD 3-Clause License + -Copyright (c) 2024, Andrew Mitchell -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. 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. - -3. 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. \ No newline at end of file +{! include-markdown "../LICENSE.md" !} diff --git a/docs/news.md b/docs/news.md index 47b25909..aa8a1e98 100644 --- a/docs/news.md +++ b/docs/news.md @@ -1,4 +1,4 @@ - +# News ## [2024-08-15] Significant Enhancements to Soundscapy's Plotting Module @@ -21,7 +21,7 @@ We have introduced a new `CircumplexPlot` class that serves as the central mecha While we've significantly expanded the capabilities of our plotting module, we've maintained a focus on user accessibility. We now offer two primary interfaces for plot creation: - Function-based Interface: The `scatter_plot()` and `density_plot()` functions remain available and have been optimized to leverage the new CircumplexPlot class internally. These functions offer a straightforward method for creating standard plots with minimal code. -- Class-based Interface: For users requiring more advanced customization, the `CircumplexPlot` class provides direct access to a wide array of plotting options and methods. +- Class-based Interface: For users requiring more advanced customization, the `CircumplexPlot` class provides direct access to a wide array of plotting options and methods. This dual approach ensures that both newcomers and advanced users can efficiently create the visualizations they need. @@ -42,12 +42,14 @@ Our development roadmap includes several exciting features: It's important to note that this update introduces breaking changes that will require modifications to existing code. The primary areas affected are: - Import statements: The module structure has changed, necessitating updates to import statements. For example: - ```python - from soundscapy.plotting import scatter_plot, density_plot, Backend - ``` + + ```python + from soundscapy.plotting import scatter_plot, density_plot, Backend + ``` + - Function names and parameters: Some function names and their parameters have been modified for consistency and clarity. Please refer to the updated documentation for specific changes. - Class-based interface: If you were previously using lower-level plotting functions, you may need to transition to the new CircumplexPlot class for advanced customizations. We strongly recommend reviewing the updated documentation thoroughly when upgrading to this new version. While these changes may require some code adjustments, we believe the improved functionality and flexibility justify the effort. -We are confident that these improvements to the Soundscapy plotting module will significantly enhance your ability to create insightful and visually appealing soundscape visualizations. We look forward to seeing the innovative ways in which our user community will leverage these new capabilities. \ No newline at end of file +We are confident that these improvements to the Soundscapy plotting module will significantly enhance your ability to create insightful and visually appealing soundscape visualizations. We look forward to seeing the innovative ways in which our user community will leverage these new capabilities. diff --git a/docs/reference/api.md b/docs/reference/api.md index 10919bea..572af2da 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1,8 +1,4 @@ # Core functions ::: soundscapy - show_submodules: true - - - - +show_submodules: true diff --git a/docs/reference/audio.md b/docs/reference/audio.md index 2d2bcf53..7d11610e 100644 --- a/docs/reference/audio.md +++ b/docs/reference/audio.md @@ -2,21 +2,22 @@ This section provides an overview of the binaural analysis tools available in Soundscapy. It includes a brief description of each tool, as well as information on how to access and use them. -::: soundscapy.audio.analysis_settings - options: - show_root_heading: false - show_root_toc_entry: false - ## Binaural Metrics ::: soundscapy.audio.binaural - show_submodules: true +show_submodules: true ::: soundscapy.audio.metrics - show_submodules: true +show_submodules: true + +## Analysis Settings + +::: soundscapy.audio.analysis_settings +options: +show_root_heading: false +show_root_toc_entry: false ## Parallel Processing ::: soundscapy.audio.parallel_processing - show_submodules: true - +show_submodules: true diff --git a/docs/reference/databases.md b/docs/reference/databases.md index 075201d3..7024a15a 100644 --- a/docs/reference/databases.md +++ b/docs/reference/databases.md @@ -3,8 +3,8 @@ ## International Soundscape Database (ISD) ::: soundscapy.databases.isd - options: - filters: ["!ISDAccessor", "!_"] +options: +filters: ["!ISDAccessor", "!_"] ## Soundscape Attributes Translation Project (SATP) diff --git a/docs/reference/plotting.md b/docs/reference/plotting.md index ce594e77..70777d79 100644 --- a/docs/reference/plotting.md +++ b/docs/reference/plotting.md @@ -1,28 +1,16 @@ # Plotting -::: soundscapy.plotting.plot_functions - -## Circumplex Plotting - -::: soundscapy.plotting.circumplex_plot - show_submodules: true - -## Backends - -::: soundscapy.plotting.backends - show_submodules: true +::: soundscapy.plotting + options: + members: + - backends + - circumplex_plot + - likert + - plotting_utils + - stylers -## Stylers +## Plotting Functions -::: soundscapy.plotting.stylers - show_submodules: true - -## Plotting Utilities - -::: soundscapy.plotting.plotting_utils - show_submodules: true - -## Likert Scale Plotting - -::: soundscapy.plotting.likert - show_submodules: true \ No newline at end of file +::: soundscapy.plotting.plot_functions + options: + show_submodules: true diff --git a/docs/reference/plotting/backends.md b/docs/reference/plotting/backends.md new file mode 100644 index 00000000..620177b9 --- /dev/null +++ b/docs/reference/plotting/backends.md @@ -0,0 +1,4 @@ +# Backends + +::: soundscapy.plotting.backends +show_submodules: true diff --git a/docs/reference/spi.md b/docs/reference/spi.md new file mode 100644 index 00000000..7fdd791d --- /dev/null +++ b/docs/reference/spi.md @@ -0,0 +1,9 @@ +# Soundscape Perception Indices (SPI) Reference + +This section provides an overview of the tools for using the Soundscape Perception Indices (SPI) framework. It includes a brief description of each tool, as well as information on how to access and use them. + +::: soundscapy.spi +options: +show_root_heading: false +show_root_toc_entry: false +show_submodules: true diff --git a/docs/reference/surveys.md b/docs/reference/surveys.md index 8748ed30..5b6acd1b 100644 --- a/docs/reference/surveys.md +++ b/docs/reference/surveys.md @@ -1,11 +1,11 @@ # Survey Analysis -This section provides an overview of the survey instruments used in soundscape research. It includes a brief description of each instrument, as well as information on how to access and use them. +This section provides an overview of the survey instruments used in soundscape research. It includes a brief description of each instrument, as well as information on how to access and use them. ::: soundscapy.surveys.processing - options: - show_submodules: true +options: +show_submodules: true ::: soundscapy.surveys.survey_utils - options: - show_submodules: true \ No newline at end of file +options: +show_submodules: true diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 18b5dd80..05ae4bd3 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -4,7 +4,6 @@ --md-accent-color: #71e5e9; } - /* This will only apply if the user has chosen a light color scheme */ @media (prefers-color-scheme: light) { .only-dark { @@ -25,4 +24,4 @@ display: block; margin: auto; } -} \ No newline at end of file +} diff --git a/docs/tutorials/HowToAnalyseAndRepresentSoundscapes.ipynb b/docs/tutorials/HowToAnalyseAndRepresentSoundscapes.ipynb index e4214483..e01f0776 100644 --- a/docs/tutorials/HowToAnalyseAndRepresentSoundscapes.ipynb +++ b/docs/tutorials/HowToAnalyseAndRepresentSoundscapes.ipynb @@ -37,12 +37,13 @@ "outputs": [], "source": [ "# imports\n", - "import soundscapy as sspy\n", - "import pandas as pd\n", + "import warnings\n", + "\n", "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", "import seaborn as sns\n", "\n", - "import warnings\n", + "import soundscapy as sspy\n", "\n", "warnings.simplefilter(\"ignore\")" ] @@ -97,6 +98,85 @@ "outputs": [ { "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "RecordID", + "rawType": "object", + "type": "string" + }, + { + "name": "pleasant", + "rawType": "int64", + "type": "integer" + }, + { + "name": "vibrant", + "rawType": "int64", + "type": "integer" + }, + { + "name": "eventful", + "rawType": "int64", + "type": "integer" + }, + { + "name": "chaotic", + "rawType": "int64", + "type": "integer" + }, + { + "name": "annoying", + "rawType": "int64", + "type": "integer" + }, + { + "name": "monotonous", + "rawType": "int64", + "type": "integer" + }, + { + "name": "uneventful", + "rawType": "int64", + "type": "integer" + }, + { + "name": "calm", + "rawType": "int64", + "type": "integer" + } + ], + "conversionMethod": "pd.DataFrame", + "ref": "86fc413c-dfbb-4845-baf5-525f040cbcb4", + "rows": [ + [ + "EX1", + "4", + "4", + "4", + "2", + "1", + "3", + "3", + "4" + ], + [ + "EX2", + "2", + "3", + "5", + "5", + "5", + "5", + "3", + "1" + ] + ], + "shape": { + "columns": 8, + "rows": 2 + } + }, "text/html": [ "
\n", "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
LocationIDSessionIDGroupIDRecordIDstart_timeend_timelatitudelongitudeLanguageSurvey_Version...THD_THD_MaxTHD_Min_MaxTHD_Max_MaxTHD_L5_MaxTHD_L10_MaxTHD_L50_MaxTHD_L90_MaxTHD_L95_MaxISOPleasantISOEventful
0CarloVCarloV22CV1214342019-05-16 18:46:002019-05-16 18:56:0037.17685-3.590392engengISO2018...-0.09-11.7654.1834.8226.535.57-9.0-10.290.315-0.083
1CarloVCarloV22CV1214352019-05-16 18:46:002019-05-16 18:56:0037.17685-3.590392engengISO2018...-0.09-11.7654.1834.8226.535.57-9.0-10.29-0.3370.529
2CarloVCarloV22CV1314302019-05-16 19:02:002019-05-16 19:12:0037.17685-3.590392engengISO2018...-2.10-19.3272.5232.3324.520.25-16.3-17.330.7640.075
3CarloVCarloV22CV1314312019-05-16 19:02:002019-05-16 19:12:0037.17685-3.590392engengISO2018...-2.10-19.3272.5232.3324.520.25-16.3-17.330.716-0.019
4CarloVCarloV22CV1314322019-05-16 19:02:002019-05-16 19:12:0037.17685-3.590392engengISO2018...-2.10-19.3272.5232.3324.520.25-16.3-17.330.594-0.042
\n", + "

5 rows × 144 columns

\n", + "
" + ], + "text/plain": [ + " LocationID SessionID GroupID RecordID start_time \\\n", + "0 CarloV CarloV2 2CV12 1434 2019-05-16 18:46:00 \n", + "1 CarloV CarloV2 2CV12 1435 2019-05-16 18:46:00 \n", + "2 CarloV CarloV2 2CV13 1430 2019-05-16 19:02:00 \n", + "3 CarloV CarloV2 2CV13 1431 2019-05-16 19:02:00 \n", + "4 CarloV CarloV2 2CV13 1432 2019-05-16 19:02:00 \n", + "\n", + " end_time latitude longitude Language Survey_Version ... \\\n", + "0 2019-05-16 18:56:00 37.17685 -3.590392 eng engISO2018 ... \n", + "1 2019-05-16 18:56:00 37.17685 -3.590392 eng engISO2018 ... \n", + "2 2019-05-16 19:12:00 37.17685 -3.590392 eng engISO2018 ... \n", + "3 2019-05-16 19:12:00 37.17685 -3.590392 eng engISO2018 ... \n", + "4 2019-05-16 19:12:00 37.17685 -3.590392 eng engISO2018 ... \n", + "\n", + " THD_THD_Max THD_Min_Max THD_Max_Max THD_L5_Max THD_L10_Max \\\n", + "0 -0.09 -11.76 54.18 34.82 26.53 \n", + "1 -0.09 -11.76 54.18 34.82 26.53 \n", + "2 -2.10 -19.32 72.52 32.33 24.52 \n", + "3 -2.10 -19.32 72.52 32.33 24.52 \n", + "4 -2.10 -19.32 72.52 32.33 24.52 \n", + "\n", + " THD_L50_Max THD_L90_Max THD_L95_Max ISOPleasant ISOEventful \n", + "0 5.57 -9.0 -10.29 0.315 -0.083 \n", + "1 5.57 -9.0 -10.29 -0.337 0.529 \n", + "2 0.25 -16.3 -17.33 0.764 0.075 \n", + "3 0.25 -16.3 -17.33 0.716 -0.019 \n", + "4 0.25 -16.3 -17.33 0.594 -0.042 \n", + "\n", + "[5 rows x 144 columns]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "for lang in data.Language.unique():\n", + " angles = LANGUAGE_ANGLES[lang]\n", + "\n", + " lang_idx = data.query(f\"Language == '{lang}'\").index\n", + " iso_pl, iso_ev = sspy.surveys.processing.calculate_iso_coords(\n", + " data.loc[lang_idx, PAQ_IDS], (1, 5), angles\n", + " )\n", + " data.loc[lang_idx, \"ISOPleasant\"] = round(iso_pl, 3)\n", + " data.loc[lang_idx, \"ISOEventful\"] = round(iso_ev, 3)\n", + "\n", + "data.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9a561944", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fitted from data. n = 96\n", + "Direct Parameters:\n", + "xi: [0.06 0.597]\n", + "omega: [[ 0.15 -0.058]\n", + " [-0.058 0.093]]\n", + "alpha: [ 0.87 -0.558]\n", + "\n", + "\n", + "Centred Parameters:\n", + "mean: [0.281 0.447]\n", + "sigma: [[ 0.101 -0.025]\n", + " [-0.025 0.07 ]]\n", + "skew: [ 0.146 -0.078]\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAe4AAAHWCAYAAACxPmqWAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsvQm8ZGlZ3/9W1d3v7b1v793Tw7AODosgCCIiEBBNAm4BlyCouBuRv4KQALIoirLEFaOCmpiIGqNJjGBEERNgEFQWw+AwMz09vd/e+3b33arq//m+T/3qvOetc06de+/p6u6Zej6f6ttV9da7nXPeZ/89tXa73XZDGtKQhjSkIQ3ppqD69Z7AkIY0pCENaUhDKk9Dxj2kIQ1pSEMa0k1EQ8Y9pCENaUhDGtJNREPGPaQhDWlIQxrSTURDxj2kIQ1pSEMa0k1EQ8Y9pCENaUhDGtJNREPGPaQhDWlIQxrSTURDxj2kIQ1pSEMa0k1EQ8Y9pCENaUhDGtJNREPGPaQhXQc6ePCge9nLXna9pzGkDn34wx92tVrN/71RaHiPDCmPhox7SDctffazn3Xf9E3f5G655RY3MTHh9u7d6/7ZP/tn7hd/8Rev99SG1CEYDwxRr5mZGfewhz3MX7f/+l//q2u1Wu5Gpf/8n/+ze/e73115v+F+1Ot1t2fPHve85z2vMqHh2LFj7id/8ifdP/zDP1TS35BuPBq53hMY0pDWQh/96EfdV3/1V7sDBw64V7ziFW7Xrl3ugQcecB//+Mfdv//3/9798A//8PWe4pA6ND4+7n7jN37D///q1avu/vvvd//jf/wPz7yf9axnuT/5kz9xGzduvK5zfOYzn+nnNjY2lmLcn/vc59wrX/nKysdDwHzpS1/qKBVx3333uV/5lV9xz372s92f/umfuhe84AXrZtxvetObvMb+hCc8obI5D+nGoSHjHtJNST/1Uz/lNm3a5P72b//Wbd68OfXdqVOnrtu8htRLIyMj7tu//dtTn731rW91P/MzP+Ne+9rXesHr/e9/v7uehOaL1WZQ9MhHPjK1J1//9V/vHve4x3kNf72Me0gPfhqayod0U9I999zjHvvYx/YwbWjHjh2p9ysrK+4tb3mLu+2227z2hybyute9zi0uLqbaYbrExNjP1/hbv/Vbvu3//b//173qVa9ys7Ozbnp62h++c3Nzqd+iUcGk9u3b56ampryV4B//8R97xlheXvZa0iMe8QjPQLZt2+ae8YxnuP/9v/93qt1dd93l/tW/+ld+zMnJSfeoRz3K/dt/+2+736PN/sAP/ID/nO/p55u/+ZvdoUOHUv1oDR/5yEfc937v9/p2aL1ogefOneu2+47v+A63fft2P7+YMO8yzlrpJ37iJ3wff/AHf+D+6Z/+KfXdn/3Zn7mv/Mqv9Pu6YcMG93Vf93U9+8Y1wfR+9OhR96IXvcj/n335sR/7MddsNlNtf+/3fs896UlP8n2xzjvuuMNbZvJ83FgC0H7ZT5m1uQ/m5+f9nH7kR36kZz1HjhxxjUbDve1tb1v1XjAf9hntu4juvfdefz23bt3q76cv//Iv9/MM1/FlX/Zl/v8vf/nLu3Pneg/pwUNDxj2km5Lwa3/qU5/ypsx+9N3f/d3uDW94g/vSL/1S9653vct91Vd9lT9cX/KSl6xrDpjjP/3pT7s3vvGN7vu///u9+feHfuiHUm0Y9/Wvf717/OMf737u537O+3dhVpcvX061Q2CAccPYf+mXfskzY9wAf/d3f9dt85nPfMY99alPdX/5l3/ptVQYDwyLcUVYIHAjsLZf+IVfcN/3fd/nPvShD3lGdOXKlZ41MN/Pf/7zfnyY9u/+7u/6PlXt91//63/tzpw54z74wQ+mfnfixAk/j1iTXi3RP2OFAsp//I//0TNqGPHP/uzP+v37f//v/3lBJhZAYNDPf/7zveDx8z//8/7avuMd73D/4T/8h24b+v6Wb/kWt2XLFt8fmj77geCVR+w/ZmaYKfPhhTbMnBDQsBDEwsF/+S//xa/l277t21a9DwhLvFhHHp08edI9/elP99cC4Qyr08LCgvuX//Jfuv/23/6bb/OYxzzGvfnNb/b//57v+Z7u3HEFDOlBRNTjHtKQbjb68z//83aj0fCvpz3tae1Xv/rV7Q9+8IPtpaWlVLt/+Id/gAO1v/u7vzv1+Y/92I/5z//yL/+y+xnv3/jGN/aMdcstt7S/4zu+o/v+fe97n2/73Oc+t91qtbqf/+iP/qifz/nz5/37U6dOtcfGxtpf93Vfl2r3ute9zv8+7PPxj3+8b1dEz3zmM9sbNmxo33///anPw76vXLnS87uPfexjfrzf+Z3f6VnDk570pNSevf3tb/ef/8mf/Il/32w22/v27Wu/+MUvTvX5zne+s12r1dr33ntv4ZxZ4/T0dO73f//3f+/HY++gS5cutTdv3tx+xStekWp34sSJ9qZNm1Kf0ze/ffOb35xq+8QnPtGvS/QjP/Ij7Y0bN7ZXVlZy5/FXf/VXvi/+irgeXPuYuM9o+2d/9mepzx/3uMe1v+qrvqrdj/jtd33Xd7Xn5ub8PXLnnXe2n/Oc5/jP3/GOd+Ted6985St9m7/5m7/pfsZ+3Xrrre2DBw/6awX97d/+rW/HNR7Sg5OGGveQbkoiuOdjH/uY1zbQet/+9rd7zYvI8v/+3/97t93/+l//y//FpB3S//f//X/+b2hmXC2h0WCGFGHaRQvDvAr9xV/8hVtaWvKaedguK9gJkz+m4LvvvjtzLEzwmLW/8zu/02viIYV9Yx4XYd5GW374wx/u+w+193ANo6Oj3fdYDvBJa9/w/aJBsqeXLl3qtkMzR/u79dZb3XoIDRZS32jH58+f9xry6dOnuy9M0Fgb/uqv/qqnD6wKIXEdMCmLWDsWjtjtsFZ67nOf6yPB2QMRlh8sImUtEL/5m7/pzfq4dViX3C5FgXBck6c85Sne8hDuH9cQSwRWiSE9NGjIuId00xK+vD/6oz/yJsZPfOITPtAJBkC0sg4xmCjMB+YVElHoHOhismuhmIFiioXkI1bf+K1D4sBWWxHmTRgWQUv4O3/8x3/cMwKRGNGXfMmXFM6JyGjM8/v37/f+fEy9jEffFy5c6Gkfzw1GsHv37pRJGhM6/coc+4UvfMG7KTBzr5fwGUP4niEJLkRYM+/w9ed//uc9gYfEA/BdSOxt6KfHrMy+EvRFrAHCzwc+8IE1z1nCzB//8R933Q8wceaC/7kMvfCFL/SCBMLdnXfe6YUTTPz0nUfcT1kxBZjH9f2QHho0ZNxDuumJFB6Y+E//9E+7X/3VX/WaJgFPeVrpain2ZYrQArNI/uHVED5IAu7e+973euZM+hQ+eaVRlSW0e3yfBLD9/u//vmd2MAh8p2vNmb799tt9YNd/+k//yb/nL3vOGOslxShIsNIc8csy7/hF6liZaxASWi05zVgNsNCgtcPECbxbKyHMIHTAvLnepI7983/+z32mQxlCgEBzf85znuO1aALehjSksjRMBxvSg4qe/OQn+7/Hjx/vBrHBDNDkpJko0ActlO9DTY3PQsLUrb5WS+qbsQlKC83eoUYoIlKYSGBeMAWYOUFjBNfp9/2C8f7wD//QMyS0NxEBTPG6RMyNgDgR47Ler/3ar+1hVJhy+Q4mRfBYbDVYC8GgEapwfUBE/ovZwtiqIgSNf/Ev/oV/cT+ghf/ar/2aD3yLrTFlhD2Eqyc+8Yle04YJHz58+JoD/3A/Ye2IiUwDfb9eIXVINwcNNe4h3ZSE1pSl2co3K5OiGFCMgPXOd77T/4UBiWAa+JFDIjo5T+PuRzAe/Mcc6OFcs9C48EXHJmsYilLWMAfDyNHIYRIhhX2jgcb7wvh5a2B9YaoXFgvS5+JcYnzOMATSoDDbrzeaHCK6G4vAi1/84q7JnjgF0rWwnmSloMXpdmUo3lvM0eRMQ3FKYEhowVnuBRGuAubP9cSica3zr7mXcQkR2yHCd881JFUNy4jmDeUJa0O6+WmocQ/ppiRMwvgXSc159KMf7TVj0qBI0+EQQ2uFSMNCA+Vw4yAjXYjD77d/+7d92lOobaLZEuj0jd/4jV4DJOiN1Bv8xGsh5RSTeoYZlYP37//+732Octwnhy4pSpik0bw/+clPeu05TC8jvYvAJEzoBCQRGIYvmgA7wVsyDlosJlv65JDHj5qXZsS+Ya7F7I02B4IXY2BSjtfyNV/zNd4FQWxAKPD0IwQBmdnR/vHFYrbGh8/+h6lbMG2EB5gi6yStjbERVljnV3zFV/h0udUQ1/Xs2bPeb452zPgIM6R7hVaYmLgW3E9YGnDFIEyhsYu+9Vu/1b361a/2vn+C+sIgv2tB5L2TcoaA8G/+zb/x9wn3MbnfwMfKP44AyjV6z3ve42MHYOQEwK03kHBINxBd77D2IQ1pLUQqznd+53e2H/3oR7dnZmZ82tXDH/7w9g//8A+3T548mWq7vLzcftOb3uTTZkZHR9v79+9vv/a1r20vLCyk2pFO85rXvKa9ffv29tTUVPv5z39++4tf/GJuOhhpN/1SiuiTsXfv3t2enJxsP+tZz2p/7nOf6+nzrW99a/spT3mKT4WiHev6qZ/6qZ70Nn779V//9b7dxMRE+1GPelT79a9/fff7c+fOtV/+8pf7NbAvrOGuu+7KXcNf//Vft7/ne76nvWXLFt/+277t29pnzpzJ3PPf//3f97+hfVlSypZe7CupS9/4jd/Y/sM//MNuClNM7CFzJwWMdd52223tl73sZe1PfvKTfVPNSOkLjzbGed7zntfesWOHv08OHDjQ/t7v/d728ePHC6/d/Px8+1u/9Vv9XvNdVmrY137t1/rvPvrRj5beE9r/4A/+YN928TWD7rnnnvY3fdM3da8/98z//J//s+e3pPPdfvvt7ZGRkWFq2IOQavxzvYWHIQ1pSIMlkLSwSgDYoriAfkRgGFYK3AmkXA3JoEopdvPFL37xek9lSA8hGvq4hzSkIZWiX//1X/dBcmEe8UOZCNTDfF9FWtyQhrQaGvq4hzSkIRUSON/4o2FSwKw+1KOW8SkDmEKqHn5tsN6HNKRB0pBxD2lIQyokIsoJzPqu7/oun0b1UKe//uu/9m4GAHgIDgPMZ0hDGiTdVKZyfGtEdQI3iNQP+EE/oloO0amgSJFek1Ul55d/+Zd9JDLIR0RfEnU8pCE9mInKWoS3lPFv0w5EOjRM4FAf6qS9IzodlL4hDWnQdFMxbnIWSe+B0ZY1aZG2QsoJ6TLgAJMaElY6UroHFZ7AcqZ/ckmHNZ2HNKQhDWlINyLdtFHlaNzkTxLlmkevec1rvF8uRJsiL5R8XmEVo2GTo6ncUFCVwHkmT5i8ySENaUhDGtKQbiR6UNu9AJ+IYRPRplWBB/AJiiVQnEIEiAG/CdGJYgJtKURcgtkD8ADIxUM9cGdIQxrSkB6q1O64lXDnFhWMWS89qBn3iRMn3M6dO1Of8f7ixYu+2hF40UBBZrUR/m8WgYT1pje96ZrNe0g3Hv3yL//1QMer1epu69ZZNzW12bVaNVevt92VK+fd2bNzrt1urbrdjUxbtz6257PDh/Ofv5BY/969D3Nnz467K1cS4+HGjaNu69a6u3y56SYmGt29uXq16RYXW27LlgXXbvN53dXrLXf16gV35syp7p4dOPDo1Dhnz/6ju5Eove7kOk9NcT8suqNH763k+v/gD37Vuvt4KNIDDzzgUfquFT2oGfe1IjT0sL4zeMZEmALCUFR4AZ97GdjBKtuVaQMkJaUFcRv0Cz6qasyq25Vdw2rHvOuu4v1YXFxy4+Nj626DpE4MB/CUWG2WlxuO2ianTydtJidn3O2373Ot1tVuf3ntHv3oPW58vL8XrMzc+rXD2Yb1CjhTIDbr9Xyr0+nTI67VaqfaTEz0znPr1ieUmtvSUsudODHq5xCUIndLSzUHVPfUVMMdOZJ8Pj3dcHv2gF8+kWL0U1Ps7V63uHixcw3S40xNJUF827evVHbd19sO4WR5ueaAoqdQWqu14CYmpt327Y+rZEygdwf5vF+r57gfVXWuoQwSBK0ytdeKHtSMmzQNqkCFxHvwkCcnJ31BBl5ZbYpSPIhQ5xUTTBv84DziYCv6/lq0K9OGh4UbjXb9GHdVY1bdruwaVjPm3XdP+MOwiEZH267RGFl3Gxh3vc79CGOruRMnmAOaVTgnrEjO7dkz7u9bDuu8didP1t2BA7VK5t+vHa6iCxesoAUY6Sq1efJkNgOfmGi50dH1jZmM3XYLC7UeRgsjv3iROt+2N7znLx4uBB3qcIRuratXbS937OCaN3pcXmHVzdOna67VMlxy0FDHxoxprmX+623HuIJI5x6an19xjcZEX5dd2TGvXn2427p10039HG8d4FkqutYu05sqqny19LSnPc196EMfSn1GTV8+V6k/CgmEbTiEeK82VVLZYhVVtltrgYxBjFl1uyr7Ony4XF3lMulRq02hWloyRpJFfL6y0ihsB5O6cqXmv+NVVNys7Nz6tWs2d/jX6dMIwrUu056erve81jIma2At8/PpNSHk5BGlvdkLhd+KeV+6hPDd255+W61+FhYqlNXdkSP2OnWq5o4dQ9CqXbO9XW27qse8884rN+1zvP0GPksfMoybWsGkdakSEqYL/q8yh5iwqRssotITJQip4IPPmspHv//7v+9+9Ed/tNsGkzdQjgApfP7zn/dVfjBZqrpUlRSXYxxEu7J9laUqx6y6XVV9cVBh+i1DZdqV7UvUr4roykort52ZrY3BwIgOHcLfZu/XM7ewnRizXnNzJkhcvXrGTUykmXUVYzJ31qC1hGvCP51FYtbStkMGHn6/mr3nO7R19lXJOKdO1d2JE5irk31Z6zqranctxixi3jfqc3yjn6UPGVM5/pawDKP8zJRtBFgF7OBwc/FHkA4GowaqkWABQCSILBdRC5gav294wxt8MBul/kgViwPWhvTQoH7axSCon3lb38ftxLRDpgXBaGA4+/f37zsmMaJ2ezxl/guZMlaqpaVFNzm5zV25gkaNNWv1Y/VjliFpTbt3m287/F7rx7yN2yAmlpFnySyac5ElBOZ98KDthfZs82ZXObEfzEM+7ar2ueyz8dSnTg1msCE9eBg39YqL0s6zUNH4DTWQi4iax2Hd42tF1BUedLuyfZWlKsesut16+wqZdpXmy9WaODmMY2Yk4nO+z2oXapUwrVCpog3vwwCurLll+aVh0mRfNBrZGjTBUWfOTPlgL7qDKTIOTDXLJL2ave3nNmg2a36ckLmzB2j+uCPxW8dzmJlJ742IOdfrK+zsmiwhfD8zk+zR+fMMXHM7dxYHCZbdj3Z7zFsawv0o2ucqxozbZTHvG+05vlnO0ocM477ZicNv0O3K9lWWqhyz6nbr6atX0y6LS1Sm3eowjtCgYmYUHtJmHq7ntpuacm7btnSkOZS1dHzSMJeQsk3c7QKNuJaK0O6v5Zff2zLMkvUyjjRR/8s2AgV7lewN/9+40fZsbi7dj/Z2mR/lMO48zRZBhX4hfPDShImWt+BXM+fDwLM05jL7IctDbEEI93l1acNrv79j5n0jPcc301m6Hhoy7gESIC1lohKrbFe2r7JU5ZhVtytDWX1lmcdXVtAw+z8eZdqV7SskNKiQGYVm0cXFpL+wHTyHv7zm5oy5ZzGeUKvGgoWWGDIUmIHGok98yc3mSNcErihmqJ9GnKXlV7m35GYjeDBXjQNzu/9++0t2JpdbgWmsDwZHim28t/S1tNRelSWEPlHEzp5Fw05M8LTbubPu/0oQOnGi5fdzfj4ZI2xXRLbP7cxoZe0zVoayVOU1uFGe45vtLF0PDRn3OgjMdF6SxAiW4+LiWz969KgP7CDtbMeOHb4gwbFjx3xkIoflmTNn/G8oboJvnVQDUsx2797t20EgsYG+gw8eIlf89OnT7sqVKz4iHn8aueNKRaPEoDDWgW1lLvTF5/z2nnvu8d9t3rzZF1RhXAh/PoF/fE+/zJ//M09S58hrJX4AYp6MAYgNc6M+M+tmD0jjoL3mT0odQDfkuUPkNx46dMinfFBtiv3R/JkD78mD5HDasuVWd/fdS+7ChbabnJx1mzevuAsX7vdzwmTFeKwPYr5Hjhzx79l3UIvY7zASlH2DGJu22u9jx7b6nFZoZMS0T9osLy/5veD/7DNz4r0Q89CkyEWmHTQ2NuoPOWvL+/FuW+bK56bNwfhGXavVdM1m0hYfcbO54sejb7UdH0/arqww7nj3O/aftisry54ZTU2NOW4rDnhLL1MQFcy54bgM7CfvYRR8xz5cvTrl08fSec01t2OHc1xK0qpCMzOCwsjIcmdNqIxpZhK6szBlM99wDzX/cL9tD9P7bWtc8QAqMCc+U9/8H422Vlv2OdvhtWk2x3yKGI9lbHVgTzZt4j5Y9P8fHQWAxfabvnlpDrb/XJulzn7W3e7do+7YMUBumGPDbdzIs8warW/N78oVGHXN7dnD2pquXh9zc3MW6a/wmcuXW37PebQOHEAb133Iscw87FyxPUuupe2DgvJsn5aX+Zzft7trybpn+az8/U3Q3VL3ntV9zKW5806ereN+HCCkCejVGcHzx3kSnxGcCzojeE6zzgjOQ/rj/CG1kLOK5zU+I44dO+b75Ts+m5qa8s+6Ypx0Rugs4ozg/6yPs4++wjOC+eosuuWWW3xKsM4I1qP5w7jZm7wzmXkMgm5arPIbibhBuclgxkUSmfkJ+0eSVNmuTBse3v/zf/6Pe8YznlEi7aeaMYva3Xuvc29+s3MJxHzb3XFHzb3+9c497GHrW0M4ZnEgmmly/alMu/5tLAd33gs0/XNA8/tTnjIMAuYLGYNK+19FMLfYb2rzgYGYFteRMVPMm0As+lTket71pF0/bTKPZAWAackSgBAhsJU83y7mas5vftfhv6l9OHDA1rDWaxBaJyDGymuu9Yf7BO3YkUwM5t1vn+LfZ42DIFPFPbSadpjMqzyv1vIc30hnqaCvEUQQUK4V3VTpYDc7SfobZLuyfZWlKsfMaoe5Mc20OSSX3Wc/69xb3mLfr4c0Zr/occYsQ2Xale2rLBX1ByObnKy5zZvRoAwCc+PGeibThhYWgAHt/RzmSL6zzK+heA9jVHqZzMdZFAbSrTYVif4PH257ZoVyJv8uTBcmhdbPWrP64myFb8GkGV8v3vP5eqOw+X2jsdRl/nk8EkFAzD12j5JGxgtCwOrnPpXPfDX7PIj7m+eo6nPtoXKWroeGjHuAFBYmGVS7sn2VpSrHzGqH9Spk2pDMgjDvjnVrzcSYZVK+yhqiyrSr2qiV11+YRzw2tuw2bDDfaRGjajaz+wpBS7J/Z38VIBczFWnEWWP324+sFDChnuEJClOgsvqSMMFv0LJ51WqYjouZHGb9Wm3Kzc/X3OXLZu7OA7DRuGtN3RPBvLlmly6lc8Cz+tm1y1wcZfd5UPf3ffdlmC+u8Vm0+CA4S9dDQx/3AAnfyqDble2rLFU5Zla7jqurp6BC0feroRMntqeCq/KoCG97te3K9pVHcSRyHCwUHvgKhCJFqww1GtntpEWWyXceHW25zZvn3ZYtQLGSx23afh4z6bcfYcBbDJwCMw0D3rL66heVnzUvuRguX677WAJM7LgrCZFAWAABOTTLa9yyqXtYfemDvtlT1hCa/bGMLCwkOeBZKWQjIytu//6xSvK4q76/y+R4V3kWTTwIztL10JBxD5DKgrpU2a5qIJkqx8xqR+BQTCGuddb3ZYnDZWSkBNf2h2R17cr21c9nLZqcbHgGdP58NiDKasY003ov44G5gPOt9KOQgWMijn3LCwvzGNHdjh07+/oB+80t0XBrmX5qPhPjzuurNyo/X5iQho9QgMAjQYH3BLexD3Fqm8YtFhIs2l3X0PpPhAKi0XE77Npl7XQNCVqDgcfMmzFlNVgvXYv7ux/zrvIs2vkgOEvXQ0NT+QBJUYyDbFe2r7JU5ZhZ7W65xbk77kh/Jj8mn/P9Wkjm8esBQ7lWqEoYCm41zLYc9sLeJvAIjGwoD1q07JhEMgt9LKQwNxoSM4Npk0pVxmoRr0VY4wRjFfl0YWJCgQuZts2r4c6fb7ijR/WqB/9Pv06caLizZ4mmb/jIb95ntTt2jIItDTc21ugZD2YLw1fKVdb+SkiQ711/nSNTIGHqod8dzRvrEZXKaBeSrudaIVSv5/1d5Iaq8iy6/0Fwlq6Hhhr3kG4oAiaS6HEC0fBpi2DafL4WGMmqYEyzwTOuHaHtcriHpmKCzngviM0qqChnHEHJ8rjtPW1Xy7Rjq0G7XfOCQVZUOIwUwiqZNQ5WBoJ1uQ/oY2GBdLH+sGFF7TBZk90D056d7VXJ222TMvoJG7356mmzP4wbixF7G+aVZ7kj+mnfNzINoVGvPQ0Z9wDpwVDRZhDVwUj5ete7LBANxkUN5dtuq4ZprxXqMdtkjZlz7JpUdeJAh3GLacOwRWiZMLUqq35lMR6IcWyscmk1ZbHG0XCxJmQJPxs3WnAYaWpovCL5nGW+tvmVW2deO64rDJa/bMWRI1YiVMx0zx5yttHGqbWMxaG5KqjbkGljGmfu4Zpg5Hv25N9DMHAxb+fGuzng66VrDembxbyH1cGqoyHjHtINSTBpMerz5y97IIbrRUWFLsCwWUvxjn4UFgsR06aMZJhjPajiEushM0PHDLrta1pDWA2yMCvkY+esRBPGt82ewPh4X1UBE4QHtH7mAPOm38QlkWj4hkzW7loE8DLu3dsqtMysrADuYsyZvmKmXfYeSrTv5g2hfceWJ4IRs+Z+M2ve588nigNHD2mINxINGfeAkdMe97jH9UVO+8xnPuPRevohp/EbIYLlIafdfffdHgmoSuQ01lWEnKb590NOY36af4iKBIWoSOzVE5/4xB5UpH7IaceObetqA0LnYk0gkbVaQpYazUVOm56e8f9fWmr4+tYxYhWHMdoQKF0c/EXIaez36pDTYCQ1t3Vr3TOSBx6wwzuE1Gw0GK8W9dvy4/GZ0OmE+mVFOxJ0LiGn8bsYISxuy1pM48YPPOKRuhqNthsd5a/NaXFxpzt+fCSFcAbZ7WtAHnxne2RoXWZZWOqkbilimj5rXtOfm+N6mV+f37P/5Knz3fLyiltYWPT37Pi4oX4xLPMkul05yFxj1kk0Pt2wVtouL4/668o+S5OXu5Z5UaiFz4EohVmOjJjVgX4XFsy/znizsybR0e/Vqy2PmKbSn4wHYyOinMctrNxmr7a7fBmo1Zqr1+1+ie9ZQzgDDY3JTXrmvXnzQs89K2S1sshpur+LkNOuXr3iRkfHXKNh14YhGN+CFm083Bp79/L/pZ77+yMfWXTPfOYWf0Y88MAD7lGPelRlyGnPeMYz+iKnfe5zn/PnQT/kNM4lAde027e6n/zJpvv0p5s+o4X751GPWnKvfOWKu+22s0PktIcachqQejCvflRluzJtVoOcVtWYVbeL15Dn1+bB5YDtR2E7Aqow22YRjOTAAQBOyvWVRzFqF4U7jInAgJKALflF0QKKoovXss5+7TDVhpYHgrmERtZswiyY10iqihgaZoz4xZ4pxU8aN92j/YZWDRgCWOMnTyZBarFvHMZdzsfd2w6AGV1X1oGGPz5uDE8WDT5nrFijDPu7eDE5QpeXm4EvX/CkZiZn7+Te0L5ZvfC2O3CgVngPxdcKgREKte/VoO+t5bpjNWHPuKYmcGr+LZ/OVmQ1QPOu8iz64jU6Y9C0X/nKXiwJ6FGPWnS//MvjhS67QSGnDTXuARKa76Dble2rLFU5ZtXtygSjIRWXobBdkVkWBtTPbFt2TBEaDQf7/LwJBPh74+pfMLQqxizbDo0Lxceir5PPiW5nPvv2jbmrVy+5ej0dUSYzdGgiFtPmc76XyTp2RaDVcZBy+Q2nvZeJommXoax2YT8IBih9aJ9gl0uoYLw4OM/+jqV88tD58+3u/iwtoa3VUtq15eBrD8Jc+WxTc9G1Cn3fazGdr/a6c224F3UdFfVvzLueW1AmfCa/9EurO4tuuUZnTBYAlOiuu0b999fRa9elYTrYAAng+kG3K9tXWapyzKrbQZ/8ZDG6ESbHMhS2K4L1xO/Z7wwsGlNpUpcvY0bGDFrvHsyYV/EGoOXAuAQMAkPD7FoUnLaWdeYRft3jxy1lCsI/rVcIipJFynMOLYhohtKcleechZXOWumbv2jD/CZmcHIT9KOsdhIqsggzOcITllcsBmjm+st8MdP3rjXZE3LtSV3D1I62zbwZK0FyW909lHWtwrSx1VLZ+2NxMYHEjW2zAseR0bYfbOudd152VdHJa3TGFAE8cc3XCwBVFQ017gESfrZBtyvbV1mqcsyq262sPKZvG3zbZShsVwSygbaThz7Wb8w4Un1yEjOzoWjZ7wywIzzU7cDnMDVTep5AsZZ1hpQEYRnBbMSUsgi/dJ7XTelm0lzx6+KnFhMOD3zV0Q7zqdkfrKZZlt31rDO8rqFFwLIFzArA53JR8OI97QFNiUn1sNGgcYFu3ozvuuHm55vdvHg+j8cqcw/lrSFh3myYWT/Wuh9ZFF6bfOt7Ulq135iyhq03aG3hGp0xRQBPuATWAwBVJQ0Z9wCpjE+p6nZl+ypLVY5Zdbsy1L9qUna7vFznVgvNa2zVY8aR6hSagPGRn81hzljK8Y1/Lv5YpOGsdZ0hw56eJnBL49T6ArlcuTLvAxmz0sakcUJLSwTgZbsiYqYtipHLqoLujIUKfcZ75ovmzTXqxEx6vzSMt9lMGytpj0sDF4aix82KQroXQWG4FZpu716znKz2Hup3TWU6n5+f7usr79dXSOF+c+/Hbo9Ob6UKnYRjrjfifPwanTECgAoxJESPf3x9zQBQVdPQVD5AIrpx0O3K9lWWqhyzynb9TOQiIkTX2k65zhyMKt5BBO1a+gqBOWDaFJvoBPl3/YV5GowOwFBjFSqZimKs1sctFDH5bMfHaykzMeZ6tN4spZq9aLXKF2AgSjnLZC3zeEh8zl7ALLJqPMR9lR0zS6iYmSGinpQvMkRIv0tKnsJwWb9SxVqt5OiUjx63hoHCJH0bWI4xcFwNoLf13kPVxCOojKuqjhX3lX3fxvfS2Fi9K3ABVIPpX+8lVDJumUIn8ZjrAUbac43OGAFAxeiNvH/Tm0ZuCP82NNS4B0ikRJSJcKyyXdm+ylKVY1bVbjUHAKlf5aJp89uFeayYfScmis2EWX1JuxPTzhqDgz0LR9x8xIa9nQcMs3MnaVP9D3ADHKH0Z6INZQWLXbjQcrOzdc+cOllKnflb1apz5y530rsSLT0rmMz2Ix3hLZM1DDNkziHgiualtDiRBZP1XycpW/2iz5k7Fo94v9GkYcAwW7I4rWxoUqeaObNO/PAwdhg8DE1CCO3lZgDQBQFp797mNcoAWHBTUzM+Eh/KC1zLuiez7iX877gFyDJFeMLUT7D0jh0SOrBmrLjx8f6sJGvMtWre913DMyYGgMI8jqZ95sx9zrnb3I1AQ8Y9pJuaqoIzXTt8Z2+KUhmijCO/y9OOFIFcVOUKygeGqXnQiDyBIjGJt1JMW2uM+4QJwUA5tHlZvrTWbMxhamqLO3rUcphFZffGGHzdm5rlU2ZMTNQyKXegD7pCiwW2Lbvx8f5Vm06eHC9k8Pv3tzpWEFsLc6C5NGyYtzI9SVODiUlj5nu0UT5DM6cffst3CDlhMNrYmF137X/IwKuk1Uad54MMtX0fWSb+JH+/uS5WciMCtWwOAKBE3H9ZwCzXQwsfMu4BUlGO97VqV7avslTlmFW3K0MGWLG2dtmHmzGq2AcbauUAi+jAC9O9Ll3KZiSqJY25kt/Eh+bICMAnNi4HKJqgmJ3KReal54Q+bBj2ykrvoZ7tO6/7z9G48fsyZrjW8fFpd+oUwCzpXymYK11Zq9GjOfud9IA2Bq5iVmFbJxosGh99hwA0Zp4td4Rt3AiASrZflzQu5mMuBgN6mZ9vddK6rI20S9aBkME8pWmj3crvi5k89AObdp4wfK4T3xE/APCKXY/JXBS2tdy7ZZh33FfoukmT3Wfcf3mBkOt5pmIB/ElPKuc22Hodzpjl5f09Od6qoYCWPkgaMu4BIqddvnzZS/39kNNAGFIifxFyGkhlAAcUIafx/tKlS5UhpwHuQJ9FyGmafz/kNH6v+Rchp2EOpX0vKpKdiOTUggQF8hhpP+xDiPoVolAZKtlSX+Q0kJ8mJhopZKlmc8xduZJGThNa1dWrdZ8602jQ96hP6wrbTk0Z+taFC/UOelfT406jGYO8pohsDndMc/fdR5S2+bPNXAkTW+6gcy16H2urNe59z2iDIqF9gTiG6TdEQzt1atxHxo6Pr3jsbpg2KGAgvtGv9gVBQyhlRvVU0Bj/P3zY5gR6G5pyvT7uq36ZmTiJMMcMv7RUdw88QL+2HpDXpqdBYjOUNaUmGQKa+ZYRWqBt20B3S+pYKxoaxLNjx5hDy/vXDaluIYX6FSLKcW/wYl1CTuO6kbO9caO1BUnt+HHui4bbssUEC4GkwACZg0BUYM5cp4UFS5ci+E0WAllDuC7CEOEvlorDh5mvZQ6wf/U6KGwjHoVt+/arhchpXKdGo5V7f2vPQ+S06WlDQztxwtrNzjYz7+/l5fiap+/vlRUQ+lYykQF1nbOQAe35FDJg0yPtsedZaH/QJz/ZLoWcdvmywSD3Q07TWdQPOY3zT0iM5HSTHqYzGf/3vfeedT/5kxvcXXet+PXr2nzmM2PujW9suje84bwbH786RE67mWiInLb6NuttF5vIy6JGrcefmIWgFqKAoY2hlSigKW4zM2Mm3rAMZ6iZY7ZGAECmi59KRZqbeXjRjYyMd012MSnQautWi/YNg87KIIqZLznRGsPymvQNA8JkqBQn5n3hwop74AG0Ze4ftOS0Rs3aVRgEBptl3g7HVX4w/lXOYpi+kLpCOnCg6YvQ9KO8MUNCkIGxLiwgXBsjZk47dhh0KXPgvVDLMJEiJMFXwjQ22mHyZ+8FMIPwgSUkDL7T/i0vL7qlpUTTzDOf59276XvIGHLWYxyircV9cb/GCHfhvQsYTZ7GXaWPXs/xs56VwBXfCGfppz/t3Ld/e/499Ju/SdT54JDThlHlQ7rpaNB+bVG/qFm+zzM54pe1Q72eGak+PW2aDX7kLFG6tx60MYMst635X01IKGLaResQaEoY6a1gMRiQGCnzunLFsM1nZsbc6ChMwyZFgQ69yiCDybcuJonmrrEF9iFmKt8zVoWqiDkS1CegFMUZXLjA/A2GNqyBHu6/IEDhS/AbhDysHvwewxaCTlatbymtXB9doziHvoj4fQgOw18EiawI/CLAliKQoTKpXlUTWSLX6znPon7AK4MGZhmaygdImKsH3a5sX2WpyjGrbleG1gMFqsMtZMzStnW4ZTFtaYQEopmGmk39kKf0PXPDDAuzsGIbaaZgcKmN7lyKmHYeZKjym2EuZgpOqnNBivYGlrUzaqeghkVQh8KHNM94b1mPItClXJl5Vv+3WAD1m7XOMsAl4Zj9qNVacPv2UZwl2W/M30TZMzauDca1oiNmHZmaanWtE2HePcJTVk56b/ZAGj4V7POs4LV4DUVV6/Ly3uX3Pn8ec3F/kCFM+v1SvaqG1y0TuHZgwGeM1VHPn/+ggVmGGvcASZWwBtmubF9lqcox19IuTwq31KxJHzSkXOYskm+qH2W10+EWaiaY9pJAqd4DLs6tLToAy2j0mluIea1gLr1mZgD8aKe0uDzKgu4Mx7NUJrQ0fI/GvLAAnjsHo040a5jb6dOkhVmqEDCf27bB4JK9ETF/GDaaIX3yf3z1aLCk0ypSm7H4jkA4wb2GxL4bGErxvmnMsqS8bkz7/B/tWXuMAGK1udkPXBGWTke+c+geIFaBfeiXqUb/8TXI077jNYTWncQyYtXkimBoYd5jY1YiNNS+JaxhFtffPXuIfyhew3qeqTKU9cyfHvBZSjrY7bdn32gEqA0amGWocQ+QCCobdLuyfZWlKsesqp2laBEJnJhkxUzjQ4dAmDKU1y5GUIvzuEOtXEybwxEG28/kSD4sGNdZWnv4W+YWjhNqearYNTHRKiWXa52h9pukeRlDgAHht2bNly8ThGdMggIjab+zlaeEMA8nAB3pa4F5m0hsAunQWOPgOuBG8d9zzhOXScQu/w/3hXYIBpijidDuFxNU9rrHJHCYrPQ4PrfrYOVJL1wAc9xKfY6Pt7y/fNMmGHo6Il6kIivLy9lzi7Xv7dvT9m8JLKF/Hd+8XA18lg+J23LT06M9Uedy3YgWF2G2xRLlep+pMhRDpV4Z8BlDPMOrX73o3vnO0RSqmqLKB50SNmTcA6SqTUpl2q3FPDWoMVfbLkvyXq25cK1QoCGFh1sWfKdMjommndbK86jVWna7d6dLZ0Lxb1VNKjZtimlv2MDhXe6QJBo6LqlppmAzEaMFwxjQJvnc0NMMkCMsS0mUMP57FUMJ1xlfCxDE+G3MtCHeo/xgeuR7znoEBrRfGLXmJ7O9ma8tYh06eDD7upW97llBX1gPEDTYC3UTFkmB+LtpU837mSkNuryM5tvuoo1lAdfo90V+emneMPC5uUm3bVsrSAtMUgBjnqhYgDANMWs/+uV7l9m3Kp6psqQzYPfuwZ+lmzefc+9610wPMMswj/tBTvv27Rt4u7J9laUqx1xNuzwTeX7+aXYu8yCEJ7QotDAYGcyK/GEVDOFvCF4REgxwbKzt9u837TYL7CIcU9r/sWP2JTnIQiprt8vNn9SvOAoeU7gYlQqeQGfOWJQ6GnhWhLfMtFnmYfoX/nerZZp6zLRFKD+kz1lQLil0SbBV3DeuERirrvGhQ3lJMuxH8l0eg88C2mGdzIUDGmbJi72Og54VXEcBGGjzZlVTa2UC1+ialoE8BYIWdwLXmpKhCUKeVTCLSVYCTPZZWjf3kASTdhsrQTbzvh6Cehk6fny7Bz8Z5Fm0d+9ef//dCLCnQx/3AOnee+8deLuyfZWlKsesol3ZgC5RmHtaROtpxwGIJsOBiWZJbjBSelwaMo+yMNHzxgT72g4TMxcnaFbl5r+wQApa+jPVWBZkKr7cs2dNq6XbsGBITJiw83yruhb1uuUyh0Suul4AzMDgzpxpdTVVmDkaNlYAtFi9cAmE/ACtPOs1M7PQ/T90771t/yJfHmYfMvzYisNcEB4QZpiPpXgt9r3X2DNbb90H8dFPVnnSftdKELTEGRD4h2WFl4qfmCsjoTD6P+/5uHq1lbofjxwBLwK8/LTfu8x9VPUzVbavO++80jfy/EY+S9dDQ417gAAsAACQ7N8PgIV2UD8AFnxG/QBY6IvPqwJgYZ79AFg0/34ALOxPGQCWw4c3eQ0sDToBKETT1WoULjBQGz4T8ISAJDAFAwShwBgz6/YHYKGNAV8kACy81+FjIB+AUthv6Yf5nDkz6sbGrNoT/kE0JIBJaCvIBDQywEMI/KnVmh7MgrWxhxZ4lg9QYUAp9h2mU3zsY2OAeFhb1s08aEN+NmtjXEBSADzRHpJvzecAayhXV3t48aIdC9weMEUKb/B54g9HM2v5Qx6GKhOoQEkIUhNQin1njBjGbz7YZY8ahzYowldNW/OLtz1anF0vcsQxVxuiGbnWusYWAMbeLLtGIwFV6QVg4TqyfwDXjLhLlxoROI4JSTDvdls+VDHd5F6ysp4A5iB4LLuREdtvXRvmkKw7zbzJqT97Fqz3ltu50/KZdR8aeAr3S3htkmAugHaYj64PzHv7dgQWcNVbHooVy4tZO3hODGnOtPta6p41wJu2O37cYhVCGA/uy7k5LBhcW9tb3d8GYtPMBGAB2AbQoX4ALPRlYD3597dZApq+b+bLMwlIkPaF/Wo2ub8N95z7+SMfWfTX+ylPmewBYNFZVAaARWdRFgALZxy/B6eDeeWdyUMAlgchAAvfw4z7UZXtyrRZDQBLVWOupt3//b8Xc+cl0A6LJAdZqpEJWhKutd8a19pOmnYMaiFGE1MIalEWQEZjgr0N5UWNl50/sJv332994MdWlDrMWWZdGAGBV0LTUgStIpk5TFstDmU05AnP3GKydeqogQlZJLoQ0uIIeTR3oEi1F/zhVuFzxpQLod3uXWcISBK2q9VGetwCIs5bzMTHjhG0BbNI5oRpPzTRY1WdnMweN+w/vO66Fy9danex0cteK0zkBvyTFDaBtm+3vvjplSvG+MJnIOv+h5gfloas+4yPuC8VMLdpE0h7xXn4VT5TZZ+DlZy+4tSxQZ+lgwJgGWrcA6RhcNrq22EKK6q5HAZphUwgLxhsvfWb89plgVrITJl3AJVJY8qikydt34pSvcoGAuFTJ/odWFLLl0YztL2EQfIZzBI/Mp/zF20sLF2J5rt1a8tdvnzeazJo4mJeaM8q1kFfIIoROIfWhZaLQmRaexKBjt8WjTFcAmNwDsZZAgSBKf2Pa81Y9CeIVBgwyt2uXY1ufEEWtru5NKycKfcOZnHNFy3WgvCScqpZ+5sVMMiYiguAcGkIG10MvN+1Su7hdLuw4hhoaQsLyQ1VFAxp2RDZTJvryzVZXKx7jRvzOcy0qFBM1c/Uevq6M4o+X16e9qhn/YqCXI+zdD00ZNwDJMwvmJMH2a5sX2WpyjGL2qkKDwxEZtE8qZ8DZd8+MK2TlJa8ADByZsfH+yNTrabd+fP2GIXatuYB5WncZdDEYiI1CPMsaUb95hVaH0IKU7/OnjXzqVWyqvlIbhgfyowQyhRERpoWzDtk2oa7zm/H3cTErG+PmRtsclu7MVHBuHI9YcwwFq4p2ntWIB5aLdcTZpl1PbUGmC3z4TtFb/O5Irjl70VrZTwYNHOUf5h5wKwgq0CGKdn+z7qxiLIXFkdgQghCBRjpi4uNLniM5qeAQeYk5s36sOAyPgwQ5g2Jgc/Omlk4vjbqTylpRH8rd15k+eZWsASz8saNuF7y73/Ighd770kEI+IIVNGMjAiYN+2I08jS3q/FM1WGlvv0BQNnH3/t1xruC1/oXxTkepyl66Eh4x7SDUfEgLz5zc4961lXSpfOtMPoailTc5V07tx4RyvtZaTKtc5KEV0tjGQIXbqw0D+3No/C1C8DDAG3uuaZMgc2jE8R4wJCUZ4w72OmDVOEKaNl4nOlmhcaKgwSxhevPSw5CuVFz1u6E37MiR7Tt1VWS9LJwnkgGAiulPZinsCWAvgiKwdWBMBe2AuYFcIG9cDpj9/wHcR7MXL2gHuQsQ8fRoM3wULXM7w/Yfgw01AzjNPixMBVchQmGddB131v9cpNSIm/oy/uC5jxhQvm3y+qNqb7MuxLn1usRPKZIs15ppaWLK3xZqBmJ8Dwuc9ddl/4QnJokIP9lrdYve0bITp8rTRk3AOkYTpY/3Zo2jBtSuc961n2GVpGEYzjaoggsqraCVgli2mnTafpGtVlcrpDivHGy8wtq42ikxOmjeBha7BgpN45SQYyXHDXo6EZ8zTzstpyDWFgYfBZSFRDg0HA3LL2RczPggF7I7xhLAgalqqlCnGJeR2NWu5PTN2MEyOvyYrA9wgrqrmtMdgnviNkhfcwegNLkS/eNPNmMwk41P2ZYK7XCtPiRDBwTLlo30tLaYaL4MNcWO/OnRZ3kJVSJmo0rrp2e9rfM3nFSvjNnj023zR8b5Kbn3xGzn7NCxWGFNe+ps9UWRrr05fSRDk7fuiHTHr8pV+a6jJvrHkh474eZ+lDKh2MKG4isYkMfOpTn+o+8YlP5LZ91rOe1YlUTb++7uu+rtvmZS97Wc/3X/M1X3NN5k65ukG3K9tXWapyzKx2PFBhvVsoicbOTzUqSxZdvP528mkDHVlEHK5Ej4cwkqrpXIayioQQFcyBjuaXB/mpKN2QxFDEtM+etfxyQWQWA1tZwYyQZE4mhUumX/zFEP3lWRS4nKEpWSThTFNfXib6OW637KOMjx5ddpOTyz6intf585Q8TV6Nhv2lLVHnlM/kfciUmLsEDgW8CdgErZ214OtWjXHesy60+iNHLL9cwgm+fEz7MnNrz7JI3/NX/SEIEBA4OpqGTlUqGnMl6h2TO8JSFryuCNN5v2Il7EcMbxoKX8n47RRkb1YsR1XP1GpopU9fCapccg3EwLOKglyPs/Qho3G///3vd6961avce97zHs+03/3ud7vnP//57gtf+IJPuYrpj/7oj7rpL4oKfPzjH++++Zu/OdUORv2+972v+75Mibq1ENGSg25Xtq+yVOWYWe30QIUPWRhNu9aArirhGXV4oWkvLnZO9QIiohko09VSFtOGMVC7OctkGt62pOKEJk8dvjAlGHZWwQ7BeGaZ9gXviX9aPlAizTns0Wilralql2A38R/HgWBq0w8054EHmj0aKMIK37MO5qIgMkzdMHkxHuZCOz7HFM7e4J6EeaeJ9L2EYQPZKv848+f33JOq2ia3DRqrzOz0SxssEKC4UYoU0rpjYpzQkqD9YM7btlm6Fsx7ZYV0LEXuJ88B16ef9SnGOo+1b+5vxgtN31bwJDvqns8B4yEVLgZqWe8zlXaFENPQ34fU6jNmsi/pSHydK5s2TV33s/Qhw7jf+c53ule84hXu5S9/uX8PA//TP/1T9973vtf9xE/8RE/7ODXr937v93yeXcy4YdTkHF9rKpMyUXW7sn2VpSrHzGqXXWUnefDWYyb3PdWqaSfzeJWQkP2YdmjqDrvMOsjjMTHD8hGm8Zhph1jnmIdhRKofLUZFHjVjo/nhw5a5Vkyv0SA/PokehynQBg0xhAmFkdNvvuUEbVogNG0vZGTvqYISbRz+D+NUJTNIJTYtCtyC0eLzHm2d9ggAlmc+0t07jg/M6OG6sszsyhBiXWiup06ZdSKLt8AAmRNrjM3U6heN2aKgyb23oDRbU7IXXB9DPQtjBCzXPQ/vPGTeZaPiY9dOUhqUfPR0zno/ymoXu0Kg8fFRt3evoQCupq/s+JLs1MTLl2HgU9f1LF0P3Tgz6UNozp/61Kfca1/72u5nBGE897nPdR/72MdK9fGbv/mb7iUveYkHFAnpwx/+sNfYATF59rOf7d761rcW5uuRxB+iAJHHrdzCogo4+EjKVMipsl2ZNgIZGeSYee327au5L/mStAcnzIUdHTWm0UthjnD+mIBxlIEuyGsnk6G+M9CT4v7UBlMoTEAHLcwNhhcTGjXr4AAP+yaILM93qtrO8kHiA9RvjxyREGDm7DjADKQyq3ilKH4zASMIiAkT2HX1amIul2WE78gnxr9s4CNJFLgEDGmeCiQD0tXy23UAJ1ow48P8ML8vLPR68lTiE20bk7E0eMa87TYTbNDKRew362bsuDQoxwD9ISzqupCfre8nJ0c9Y9Z5HV5m+pNeoM91f/I50ensheW4py0jjBVaNQRSA3OmX44eQ46zMTZswLKTpI3RjmA8S9tK+rG0s4me+xFBjPxx7ivcNkX3LdeNZzC5T2vd+zRsTgohwg6+77U+UxZAlo7/gACbgZmTKZInqI/2GZM92r3baqhnCSEInx//+GX35CePV36WDoJuGsYNOg1+O/JEQ+L9XXfd1ff3+MI/97nPeeYdm8m/4Ru+oYsW9rrXvc694AUv8MJAXjrN2972NvemN72p5/M777yzMF2A5PwigJZr0a5MG5i2EIcQhgYxZl47xv+BH7jd3XMPQB4JM8ZMB6LTwgLBN9n9gc7Uz5oFyhMIYv0oq938PAIfB99Cd5wY9CW7r6YbHZ1xJ0/WgzXZ4QcCWbu94A8hhNMjR2B8pnrFa2k2p1Km6JjM3Hil6wMkPejMmRk/57Gxq/4A27Fj2i0vkz5kv+Fyc1ijPcIIrIiI1ZfmYN65E1S2ER+ABuoVYxswiiF5kUfNYcjvwWeXyZeDkVtKEd4QEdgwf5gNnyeHHL7xVpe54VqYn1/ygCnUC08zOZhZwwsZEgwYT/NH4OD/0pARUmDMzAuGrkIpwvkWhCraI8KH1sHULlzAf05f5nNI6oQnZnNLq7JqYHZ/ci1tHuRWX7062amwhta84n3urdakB3URKTWLHHpS8sw0btXdmPO2bW2//2bKX/Fa9OnTmNKZUKLWc01hVLt3M0bapMEcm81Jf39t3nyp1H0LLS6C5pff7sSJCTc5eWFNzxRleKnolw3C0uykA15d83Nc89kGtAH1jfsX4chQ12Tx+fCH593IyOcrO9cuhVLjNaSbhnGvl2DYd9xxh3vKU56S+hwNXMT3j3vc49xtt93mtfDnPOc5mX2h9eNrDzVuIEbxuxddWAQD+u5HVbYr00YH6NOf/vS+5qCqxixq98lPLvp0IZiCgUUIvYkDBEaURTzsgGvAXPPtaFhKysQwZLW7cqXeKdU5s6r+OGyPHx/p4nwnv8U/XHf79hlT57DeuBGTdu8aOXhgQqrxrBrWoTnWzIMz3XnNzU11qlbxiX0Os1OlLX7L1Im5gemJ2SlX2KphjXsfNv10UB47CGpWKESMn0AytHCZ3WFCeJ86iJOeERrjA1bTzNMKUMPEy7WFafObVovcctLsWm7vXvCzEw1VTFGR4MZoE8Q1CRwyI0P4oR/1KNO8tUY0ShAutX9EbPP/DhKx3xcYO/1YYBvzTTNwYcTPzLQ7Zu16z72BZgydOcOFb7hbbhlPmfOtvySnmjWwD7fcYtXYDKPd5rZlC5aRUY/PzprMNJ5mqoCwsHczM/l+4osXN/kxi1LGVnN/X7682e/VavsyYbC3XShUkN651nkl7cJB+H+8N1/mHvvYI5WcazD3QdBNw7jBk+VikgQfEu/7+acvX77s/dtvJs+oD4GRzVjg1uYxbm6YrJsGplfE+GDqZfwkVbYr2xeabr/5Vz1mXrtabalbhQkiSKc/VGL314U+Zfop43OO2ykgLf5tmf6azZHCCmbHj5N7i1nwqmfacX9h7rVKOApYRKUtVduZ3+LPbrcn3ZYtvfOijUGY2nu0OTPrGjMJ/d0QwVbsLX7bUCDiM/pBkOCRxI9qZKbV0A+MgEF7/MkQ3zNvGCkCBMxKaUjp1C/DEefxDtPRtAfkWnNO0he/hxkLeQ3Tvu4JzZV5JsJJWuhBsDCQk/RvEC5gnsxzYmLZf764OOrN7PyGuVGPPYuweOha8nuEgg48thcAk3siDY2qgDGuO9eLPWYeWDpIYQLnnbnyXGRRHjKaCI39wgVyxkdyU8ZWc38jSOFCyioLWtRXP4U/D6FuPc9xHp0+vds96lHrP9cG5Qe/adLByOl80pOe5D70oQ91P0ND4f3Tnva0wt/+wR/8gZe8vv3bv73vOBS2IPqcYh9VEwVHBt2ubF9lqcoxy7brZ75fDfXrS6AdV69a9aUwij0rX7vM3IrqLVPlKUzhyZpPCMphZuikWhbMLwTiEJTmpk3Zh6gCkLT1mj5/e3N4rQ8Y1PR0kkOsF3Pj0MakGQKNqKAITJC5obGKadOX+cWtLWuQ1o5mHQdsGWBLAlUKwXBhYDBtMVrNm32RbzjsxwLPjLnDkAWqEu6zsNBFmNBhmspXR9ihn+lpCozYeoqCm+N7I6xOhsab3P5pEBfmBjY5TJ4gNkHMHjgAFj4oggm2fN41DklpZ2H6oPayKGUsaw15bXg2slLFivpSAFkW9QMoqpc8E9ROzzVWufi5Vrt+lcaqPksfEowbwjz967/+6+63f/u33ec//3n3/d///V6bVpT5S1/60lTwWmgmf9GLXtQTcEZ4/4//+I+7j3/8475qFULAC1/4Ql/FijSzqknVuAbZrmxfZanKMcu2UyWhKqioLzScpNQhpR7tPUhfeSAr9Nf/UGgVMm0zeebPqRcko92BGTWmorxwMW3Mt0XrFCwncKMEs8EEZfa2McyHTRcwZhgYjFDBXHrxXkyCADcxb8GAoqGeO2fzoA/Wgpn+vvtsX2EkRJmrkElslcjK31dbGE9YzzusA678bJHVAE8DwvCe/WOeql2NK4D53Xqr7Q8vS4Oy/8NUTRAx1wC54ydP5u9z3jXgGDJBo+2vAUFY/LXa7b1R1mwD+4cVgXuM/1+6lM28YexWdMT2l71A6CDeICzjyR6GKWN59y/FRoru7XCd/Zh3vB+xECnCGtEPoGi55JlAu/RznV1aV/0VMe+qz9KHhKkcevGLX+zLqb3hDW/wm/iEJzzBfeADH+gGrBFgFUti5HhT+erP//zPe/rD9P6Zz3zGCwIk11O+7XnPe557y1vecs1yuYd0Y1Jcg1mEmS2MBI+JwKW44lSMAMZBKkhR5TOj0UOkqxQVHMrOW09KYCotK2Ta/dYpkBBLlWp6P7LWLi1b8J4wONpiZkZIgIGLSaIFqlRnq0WwEJCnYKmblg3JrwyT5/APfekwF7Rm2vQr1ax9QMC4cGG+G7kekiBaSXmjnKaEEcbqVLv1zJvxl5ZM5UQAgFmj+TMHFRxB22XOMFP2IsbeQKiAwdD3fffZWm+9tTwymK45Lgj6QliA2JOYaSsSXhHnzNVw2WtuwwbL+YYIkNu+vZZKNeNeY82hW8XSB2teWCB2hIhzys8uLTVT9y9r4/MwCyG+t7MozvMuIgmRIaRtu42FhotbBXRxI/O5rgqJ8XrRsKznAMt6Upu6jLmlynZl2qymrGdVYxa1i6VeXCL9TGNlywHm9aUSnDFxOOO/C0tw5pVxDEklFWHWHBAcljqAN26kzrBpTTB0Dq+8+fO7rHmJmNeZM71MO2udWTmzpF0BJsJ87r+/3WVmys9mjTAxzMSxWRjGpshsEwYUVGSMjN/qUEbLVsCYouJ5MY6YVtE6R0bmuxq1Bajhy09M5KFvHgsCwoCEE/m3+R2fMya42/odtGXLjP+e/Va+N9/B1AmUzMttDrHJY+ata5BVZjRkFmfOWPzAwYMwUdOskz5CoabtU7XQoJkba+FlzN2i2ym+QWUv7ZN+i6XA0gGTvtl3uSdUIjRk3lxfmH1McbnQ+F4j7xyKmfcgnuOYyOJQ2dos0nMd9xeXBy17rg3Lej5IGXwZhlZlu7J9laUqx6Td4uJklzGo7F5MrVazMj93Xl+xZhtXT8rSfDmMORjyMKlh1BYRbW0ESmIpVTUPHtKv0IgqQ4UpUZobn5MWxPCxpg0TDdeZZ1EgPQvtS3nEsfbP4YxwwVpD0zSHOjIqzPPhDzeN1mpCt7xmHYJ2WLCU6mKnGbeCwFhnjNrVapFaY7WwifoOmR17i4ChveU9fRGwBnMSc4PY99nZto/O5j4zAcq+5BrA9E6cSHLvWq0kkpm10D7WCrFUyNUBYfKHeYfaN/camRH9mP6mTdSWHvWQpiYcpeMFQgqFF6G1cR9Yjj/XcMTX0EYACFOKlc+ueuaWHpbMi72BebMmmDfX2sqf9t7fIbqd7Vf6XsNkDvPuRVi79s9xTP2QFvV93B/KQ8y8qz5L10NDxj1AIscvzkO/1u3K9lWWqhzz1Klp9453pLHJKbtHyEIM31lVsGZeX73mMssdz/++/6HAwRn7p0lNUR3lOEAqi0I0q4R52wFLf1mR4zqIyF/N85WnccPNrKzyllmky6mzDaahVC8r/2k5zxz2mGXjSl9av+YPc1J0OEyF9TGGab1hAjuHf+/+w4yBI2UOsgYgdMC0WSt9KuAOUy+R8zAu2kqA4AxmvsCcyn1pKVc2/sqKMXAx6/DMXlzsrdCm/RMDR4goY6ZFyIJxK/KcfGMFNIY8E6YbIs4p6E5WH+aJMLWyUnfbt7e67gHWS5Ada1Q2Ab9j/BATIGTepJUV2WLDez/rmcpi3oN4jmMiW0OCkIRGEevWfVWmv6rP0vXQkHGvs+AJLxV0uO+++7ypBDCXo0ePekANJDRQ2e6//37vlyfVDCkWszpEwRQ+B7gBvzrR7AqCwOSCFIhfHzpw4IAHorly5Yr3ASEJk7YGgfoGGtKpztNKXjlzoS8+57fkIUKbN2/2RVo0Djcj5im+p1+B0TBPzD0gzR3npOkEcTAG0idzI32OdbMHgM/QXv2Spod5CbMRRNAfQYCY5sfGdri3vW3Mffaz5kBjjqBMffKTK+75z2/7CNpmc6mDOLbi2u2xLu68mfPb3UIDgHQAqkA75kdf6bZpsA/aMpblIo+6xcUlV683OoAZ1kYeJA4eArgaDVShRhcxD5MwvwGshBhP+gq9TrzntuAzaS2UvRTT4jMANMA657ozB9aDqZM0IoA6IOZHcJv5yEnzqvucYHzRGzY0fc4z69Eeyiy7ssL/mTd7BKIfSFPJHJkPh7dhjrfcgQNUpmK/zWTIvhlYyIg7caLhGaKWh4YHgxDjIECr3SZ/eM6NjW33gB2UtLQKVmjKLTcxAfgM94+l6wl/G2bElmKqPHbsShfFDYZq+OFcNwMB4XcrK0opG/NgGjt3Nrw5Gw0aYQ9zuzR8zZd+GNuAWSyIbnKSPcU/3PJxCkllM3FKgvaMgTca0517pN69Nnafjfj+9PxzHdnDjRvJaR/zJm9+Z8ds0q8wAdCQbT6jHWa95Nd96ZJJc1gCIJuvpe7NzbW71xFBgfuTe9auqwkg3CvnztW95s1tz/5w3MgqAWn/+YxgR90T9E+/CJi1GveX+fEVTCcCUU3PAuvn+inAy55lGCH384if45YtC/574iB4buK22kPOH/5P3zxj4bMQ7vfysj03ds+2g3s2fUawhomJMV+9TYKirVXBlvTHPdDqroc5cJ995COL7tZb533sE2cc5xquUOaVdyYDqT0IGvq4B+jjvlFpNT7uqujTn3buu74r+zsKAWT5lKvwjRVR7APGv03ObF4wTj8fN1qO8nZlVpW2DbFGPedl518UhJblw5ZZVr5qkYLQVM2ryIcP0wsPPdsbBZxxGHNQNv174gFCfGjVvea39IXpWX5nmdthNo2GMUkO2H7XPhFOEpOv/s8aQ80KjVsaMxom/1fOu7QteAg+eEOqS+erW/Ca+Y9vuy3RwIt81iLM1fjLfa/t0R73gAqghNdJ9xl9C8sDN4HS3EJ4VRhsB/CwM0Y7uE7UPLfMABg4AWsIWdKUmQcavjT18JYzpDdznZw71+xJF4x93P0oz+d9rZ7jmLC46N4TCQeBz3CXFK0ly9+dR0Mf94OQ7r33Xq+hDrJd2b7KUlVjooQXoR+FpriyKEllqKivMMJVdaqLDig+B7IU1LMsZiloTcGcwrQ5AHmeifSF2XAgM27srgujv1V7+dgxazQ5iVaRXkPsw1awjcyyMAn5kEOmjYaNSyDLZM/4+Do5yFmLNDQIjYQD0eaFTxeEMwuuClUBaXlYGGHUaL2sh8+Zy8mT8x0cc9sbrSXvWkk4YV4K9pMFQBW+wkwhMUqsG5ifUZQQDMJrCkMitoI+gRxV/jj92L4ZA7nnnvnOXsykYhuyIq3NXG/gNibLU2J01I8lFDnzvSdWAKXdqWQn38Gs0YLBd5+aQjO0wiOMlVXFDYtFAptqsQto91gWJNyIECpYH9csjBw3rAD2AYtbw128iEZt+wEKX5yq1e/5lNlc1oAqaHEVyGnt9nj33gsr1XEvSIiq1/v3V/VZuh4aMu4BUhUlJVfbrmxfZamqMQ2GM18CX02KhmlAHFZTbmHBoDjXmuIhXyYlK2dm+ndidY3HczUwDjlSapRCZWk8dmiGVbMoiCBNL0RKE2Fm50DduhUTZe++5fmwIT7n0GYuRI4zF2naYRBZVn/yqQpUBYGjVpMZ3zQZW5sxnhhHXUFUMCT5VwkUO3qUSPb5rnk6FBzSJRl7hZMwQh9S0JyZeO3/fE8/BDwKUQ6mzn0XxyawVsGqqlAK1wRNLMz9pkLa0aO4N5g3jHtDjs/a3nO96Eea89attm+12mi3khnjas9kxQitrfJ7gyuwe/eSGx9PJqSqZ4qU13UQHC7WAoQVIujHx3GzYJJO+qY9zIvocgHq0B/X0SzBNY+/Djb81BSuEOtzLTI0zBv88dWkilVFzWZv/e34+6I4NwWqVX2WroeGjHuAVNZ0UmW7qs01VY1JDukTnjDiPv/5/qhJhgGdTaF5OAwiyjNvF/W1FsLfFQcuhcQcOMBHRwFxsbQkaYRhPjOMescOK4YQM23ydI3Z1jzjYcw4HxsK/bqxlRHGTdqRsLCtHwuEirVtMR6LKk5/PjpqsJ8wo7AEqwWoZRc/EdMXnT4970FnwpKd+l147eMiGAJpCeFP9TtMnggHzEnFRNDyMUXzYo8Eawozj9ealEpt+XHFzEOLi0XWS8tGACKffKbLOJkDf1XchBfCRJgjbm2X3ewsUeTpfeI+YEyejXDpQok7epS4Fov/kNbNGglrCSP+uW4IHdxrrGtmhoyGto8zoMpYWG9c+63x2LtknuwP2nrb4w5MT+N7FnxtGHxYTkom9ZDo9yqYd6Pkc0y7fvzWnoP+/V1L0/dqaci4B0h5gPnXsl3ZvspSVWNyGL3hDc799E8799nPpqPKYw2QYJYsyk9xygdXyOsrprL+tX4pKaBScdgqpUsm2PAwERwngWBZCGJh0BTfUzUrhgflEIdJ8D2CAmZS+uW9aVDZ6V5hlap0mpvtXYjfPT6eoKChiSrgS0AsRSTh5vJl07JhJjLFCyQl1v7ja2XBfum9C/3ZZ8/O+b2VsGSlRpPa4PKtnzo12703shHb7AUjZm0wKZgdbbXHCFFmfZjv5LHPdJm2/M9orWjLArKRAGOa9rLbvXvU+54JNstLtYrjFuAd99wD0ppZEAgalDAT3gsIf4qsN+AWgiUTzZv2CDnEH4TPCP8XWAvXV+UxuYfPnUOgIW0qLRyvBn40L1VstVQv+RzTTtCqebEodt2K+0PrvuOOas/S9dCQcQ+Qjh075iOrB9mubF9lqcox6/X73bvedVs3j5uDBG3jC19It1MEaUx5TC7rAOzXV0xlYzbL9Gc43eP+8I+ZNqSCF81moyvUqM402nYS6WwHNIU/cAmEBIPhOxhtGLCElo+2lcW0bf5p8yskJgXT4WA3DdeYNgc68yNSGaaDb5gx2C/gNrP8rnxvMKGUUDTwFOUVw+D4TNW9ZGpmfIt4Tj4XMlqyr3M9+3jhQsMzZUhgI/Ipqwb65ctz7t57bU7GUGeD/HKi6xN0OP4SuCYrCYyU9cDciJA3pLW2Gx3F/z3T9enzlz2ydRsTD3HVuU64HEyYsahzBa/JmhALpuwxAgP7hssDMBZVGgv3xvL7E+2fFxo7ePjj48a8t29vet99LNhyLWir64hgZu9rbu9etHbL8w5jJwSsUxSoFz4rVTDv5ZLPsdoppTIrFoX5KrujiKha+MxnDiZqvB8NGfeQrhtxEMEEQtMlhD+pH+D/asAVrhfFBRzECOKDUlHRHPCkEPEZjIGUnlh+EGRlWBNahzNmzrgcvKXPZDPtmMQwVXlLPlg0bTFOGLWwvWG6lg7Wdlu3ttzOnXV36lSaeSuq/L775jtzBwTFCmwINS4udcqYxrCtlCn7QT/SnK5cIe3M2sOoIebEb2inoDntOZ9DMqIsLjZ8xL+0MDR17e3S0mzX1wvTwtyM5q1CKNJgYbT0gWaLxrx1Kz5vE0wAcLGKYua7p01oyob03nzwK92UtNCtkCeYypR9/LilhoVAK1pH/H8TasySRMQ4QXMIyTHpPtS9ilVBke/HjhnzFkiL8NAJSgxdHf0gUe2+qEbzLktZ0KrriYW53jRk3AOkshXHqmxXdZWzKscs245czywqUxawbF9rNZWH/cVpQhY5jHY53tXgQhQ0aZeKjIZpCQMcps33YfoYh6JqWsfpWWYet/8bhKpFGEvzhEFk+eEbDXJc7bAWkhYHPHOgPYVC+A5mwaHOQY5wgBlakbqYbUdH0VSbbuvWES8kSKBgXg88YEybVC9F1at2tuarvYBp0y/avvzJEN6Xet0YLH2i+al/5sr6WCd+bjQrhAXV3ZY2qEsq0zxCkq3LggctMGyuG5A3PT3bBZmRqVvlQ2HmMG71PT9vVgQAXIyBm1mV70OmHd5WfM4jYNfdos7lmtB+5N2TzBvhLgQYCcdQ0xCwRUIMmja+5gceqLn9+9OSZIxyF895bs4YP8ybYLO4hGiRmyp+9tbDvEdLPsdhu6JYlDL90SYLUe160E1VHexmJyqZDbpd2b7KUpVjlm1nKGC9tJaygHl9rdZULiYHc+GvckUJOIIhKPfZKmG1/V+YEgyFw1mMTXnFFnXdTjHh8EAMzXpZ5nZp3wJVEdNW9HiW9cGqJtX83DAdM3dV7LL8Z6t8Rn9i2jBl09ySilrSXFqtq55xwQCFdX3q1LzHZIexQTAb3mPO56VKYcxF/nlj2n5Vnf2Yc4uLxlBhGGjZaMK8mB/mWkW8q4gJmq+Yj6wa/J6XMdqk2hljCqTlypWGL5RCf2jigMok9479XqZ9aW0yh1vQnq7ZfDciPayoFlpJ1GcHi8kLSewP72F+MaRCfEsiQDEHgr7i68ra4msufz8vwHtCXABRyGxDZLHwM4RJc8uY8BQ/K2E2Qnr83igxVd3rVxJ0rc9xq8J2ZfsaBA017gEip+H7BV2nH3LaXXfd5UFd+iGn8RvaFSGn3X333R69rCrkNMaEipDTNP8i5DQC2Jif5s8c2K9zQqxwe7qoSLRvNHpRkUAa27lz1OdRW660oZQBnEJ+taFspZHThGQWI6dZv5xS5Pry96prt6f97zhwrO2YzwvFrIl5ED8z323ejOZgGhdBPPjqlbMtPy1/mQIvGCOMWjnHMknbIYm/1E5LfKdEB6NVGQIa8xjv+EXTh6X8phzazIGz1HKq7bBMgD9srfX6mDd9ovHg1xUal8zARI8zV7RYqyKlqlLJeKyLfScVr1Zb7OwxGN6Yb1fc4cNLPo1N6WuYjhX0FCJ4sZZjx9oeaARGGMpzMG0Ihkpb+Y0ZShWzYNYwSTFAmBZ7K3Q3mLR4hoQkHhPmj/bP4yVmrNztixctqIlgLHzprZbhmUoxU7qZ+czT5wIYAKCcHT9ugXi12kymoKW889tus3myprNnlzsBbCP+Pftu8Qytbr4312tiguvWdFu2jHjhC+atXGwu8e7d7Gnary7Am6Ul+lpwmzZN++tLjMmuXcQ6gEa34q0Z1hf3uOX5K1DNnrFE856f595ZSSEHGsIdEeR2rxnC2aJHQJuYGO9BTqOqGWvkGSBKvhxy2pKbmRnti5zGeTU62vJrox9rS2BfGl3RyvO2Ushp8RnBmFgrPvrRS27HjpOZZ/IQOe1BiJwGM7yNp7QPVdmuTJvVIKdVNWa/dqGPux/YQqJNWeGHIt9VWeAGDpGsPO4YLY3HZ8cOq1QlUyQmVN7ze5ijVaOy9nxv+dSWskM7+Y853GmrgDQYZnwOWKSz+ShDhgFD49YjehwCxpQDHoLRw4xgBKEZ31DGDNQjBC0hmI55qKIWgCVoxqbJJxojTI++jZkBdLHoNm6c9gcuYCUcxpi4FSzHevl/aHq1lCP7i/8cBqHqWGLaly5ZNTXmSB+qxGX7b/2xV7IOKPWLfrBwGKRqEtGNbCihCpMu1yAUbpiH0sNowz6Tx2zX3wLZWBduAigrHQvhTAILfRB1HmcSIOChYYc49PStGuVbt5rfm+Awuz52TWM/MuuThQeiz8OHDfhFVgcJBsjJ3AtjYygKZhI4f97mKbO5ItktlqLlZmfr3Tno2iEoWclWGLQx7pCy0O/6PXtCWJuautgXOW1xFQAsVbUL2+SZy4fIaQ9CKsPMqm5Xtq+yVOWYRe3CALV+D5TMhSsrV9zERPEDXxaBLa+POGDINI20H9QOdmPaUCwa07Uqg+GbNcQu+061lTn0sqbKgY82z5mA2VoMSYyX/1s0teWvCnpVFbM0Pz4zRmLVxdIR8GlTM4yZuQg9i/YwRA5zmcpJUSNPeHKy7g4dmnebNpGnnQSMsUYxLpn0Q9Ox0rxM800Ydt410FrFpGXCVhlM/s8eoTnCwOPLKcEuvDZicCpAIrhU+jt/vuE2bSIVaq7DVGc9Y4OBwYDDa6E+iQBHCDh3jg/mXbOZpBMpOE9uFY3PC0HArhlWDwNroT33hq5LKJhybfjcXDfJdRYyWNhOJvulpYmuoAh8LswbsznMW4FchhNgsRbMIRT8GA8hAO11enrE53gr9ZA9yXJT9Xv25POen5/2wk8RjZd8jqtsF7a53r7uoY97gIRJedDtyvZVlqocs2w7zGxVUdm+8gxR2eU/058VvTf4TUulQuNFU47PDAKHwnxmxlQEL0QEt9CtDBJTjBkzu2mMHPy86Pvuu60dzAUmIx+2Ck0IOEVMG4arAx6mq5KbEi6UIqQcbNsqM6cSPQ7CGAxesJK05/di1iHTFYCM2p8+PdcpfEIBl6StXAEhNKc0YvWFaZiaO3iE8NkzP8u1TgsImrOKpIRGJuZJn/yONaKNMy6MDGEMCFB+c+rUnG8Dk5XQZAJkgkiG6ZUxtm+3tCuqjsGsubZcB6uE1ivYQYrkbreXfUwBFiCKzTC/LGuSrChzcwZPiiAbV8KSv5975/Dhlo9pUHyBsO/l81YO/9jYYuevved+VYwG/SB4GERww9+frC/ODV/Ns4ebCwLzvorneKnCdnGbMpkv14qGGvcASb7wQbYr21dZqnLMsu2qdOast6/sAykdLW7Yx2mmI+YgRggTxWSLOZr/Y94EIAOtCYarcbIKh1BxC1+gTNk2L2PaHKwIBGLyITa2Upn4jjGZE/0bU0kQ0ViLcuBhMvRB1C95y6yRz2EsocZrTHDBB6HRp3zQEAwV5s3hD8MKzdCMLUQzmLaV5zSmnfhVbc/oQ2Z0SMKF+fzT5moxdBg9jETpTQKlQdBBq+N7aZ685P/mO9wS3KJiosm4DZ8HTfDaLbfMdq+Nct/DfGppx4CYkDZ3/vy827fPwFr4ndwoYQS39lVWDsuht8pvRRRmEbBX09PELBgjVDob6wvXE0aBx5p33vPCPLimAqfBAgSMLS6S8+eJdVnvs4d0Nl0Ybd5uWyW/fqldZccs0+5GcioPGfcAaYictrp2MpeXRWYqQ+vtKwuFCc0DLVZ+QN6r1nMIb6pUKjQdDmUYCge9cJQBx1C0chEyHD5u+cBherSH2WB2p6+wCpKIgzzUDpkrvk7+whiEFAZjRWtSSlBYS5u5q9Y2Wr00deZHcKMONubEoR6ayFknY6lSmABjxHgVvY1Wy+8FX6qobQkiQmwTMQZCTKe6bYq0N1wPAdpAoY9Y/mqli+GDZ438XwUxLNc7HclP2Ut+d+LEXEeTTRi4xuD3YU3yPXuMiR47Zhdozx7cOqxRpToTl4FqirNuM1OTS1+c/yhsdH6LUMj9QInRjRsN5Y3ryzosHa+WEmQkqIl5i7KgQGNcfIIhibmgljcgQsSaZGV7rObZQ/Mm8DGLeS96LH8F0BXnj68G1W0tba6XyXzIuAdIBLANul3ZvspSlWOWbdfvwFpNkZGyuMp5Pm7TnkJ8aztwORTFzJgPBycBOooa14Ej3Gz+L7xvNCQzJVt6Tf/CIQZMwrgmCNjBphzyLJKWrKho5om5HoFDWjRaeJ4plj0VnCgCgFDJoIkJAx6hBCTR/ypUAQOEpAkzX36Hf591SZiB+VkEezIwDIj5CBWM3ykgTnni0kgF+RlfMpg/AoMsIDInK1dZ11DCDIyOa8b6MHXLl86eIHAY2IoLcskRHIhlIJ95zh08OJvSAPNCLXbtIoKaqO95Nzk54/PbIeXSSyBhbhJSWPfJkyvuYQ/LcB6nqtVRszsJwuN3MGLG1DVJ0u2SscJHEeYtrTvrecnLL0fzhnmfPg16XXPNz15RnreE2YUIOTAvf7zsmGXa5bW5Hsx76OMeIJEiNuh2ZfsqS1WOWbadUkeyyHKRTYsN//L5avsKqSjZQsE7HPJ797b8X5iRfMEGFpIc3idPtjwzkGat7ziEYLz8nv6I8I4LdxTNLfweZhbXVba22YhtogsXlr3JXdpp3vnVbC55DTL0bUP4bekbEzlpcTBcRZ4jKLA+BBr2h7+8Z/6yPigFK2TakOpPKxgrS4OTxh/7sPVijOXlQ65et1etZn9HRg65EycOeeFHv5dZnVtSEf7qV0yQPZblYWoK07DNY+dOS1U7csQC1yQ4kj6URzBSaGVlvuuPlvChtC1lRjI+DJh9u+++4vu33b7q9u1rd+8pRZYrWC9m2hDvlRMfEsxbqVghxfeIUg4hAQbFqIGrefaK8ryTQjOtnrZZ+eNlxyzTbi3zv1Y01LiHdNPSWoqMVEVCYQLxql+qWhYJQUxtrHhIbV3IcCqawcEvczkM2eorpyOe+T9QpnovBllEiipP0teSspwwFdKGDBs9cSfE5RQtWrvpxsft6Ln77rnc6HFp5WZmNmYOg5EWagUwEoz2BInNym4ljM9w4iHaKg1v0yZrJwGr1TroTdNo3gqak6+evi0Owcas1awmehKDYGu4914LrnvYw8j7LgYVkebdbM67W27Z0PX9y1ceQrUy782b7V6Ded96axrpK0Htm/LXKLQ6seenTpnJvAPdkCIJLaxF6YcymZ88Od4DjcrtHu53SBbARqoipVsbbu/e9cfYhJo3fvsbEeZ40Fr3UOMeIAFIMuh2ZfsqS1WOWaYdD0MeHGGZIiODhDzNIvOdph8z1eW26N7EUkDwVzhnafBZc1O6mMzkMvnK1xcGCAnVS6U85UfnMxgEFgIO3CJBAfALafRhZDjY4zAJCocQHCX4Uuaguctvy3cE3slXeM89CYRp7D6UVUKWApnLYaQKJBPKmgXPMd6hLtO+fHncbdw47uFm+W3YvzTbxcVxd+mSvWxM+/25c4e60f/SoPXCaoAmz3XPEhzBQWfOxsD732tE4DMWYC1YZWLLjIIbTVgyeFr29Z57DNUOgukSkHjPPWbivu++Wo/VCWsBjDhrn9m/sLiJCObNvZaFrsb1FZPX/W115e17hIS1PntFmjeoe/1cWeuFRl1Pm0HRUOMeIHIaQC0gjfVDTvunf/onn7zfDzktRD/LQ04D8Yx+qkJOY46MX4Scpvn3Q07jN+EcQuQ0+j1y5Ij/bGlpiweLiFGRlpetCIchfyXIaeZ3bnU0j3YKOY05N5srhchptAUS8tKlaTc21uxBTrN+Rj2jtcOd3FfGTFDWnJt0CwuL3m9NfvPCgpVThDZsqHszMgcu6GiMaUAoDW+qNX9ey42MtNyuXSMdMIx2d/6YVmljwCI1d+lS22tSKyuG1iYGiZlUudGYdmHcFKYwn6rtx+bN7U7ZSXygmH8JkONVT+03e8K6aMe6Gw3TtpeWDInKKpC1AuxqNHACzVSAApQtAwYxN8Ep//nWrQ0/rpXeNCZmwoFQuFgnTAqmxfq4Jy2yXsFVp04d6gokmzcbLjy+e+Vja59VPjN0CbA3YuKCNN2+fdGNjZkAsLx80P+lbwQTxuN+sWd+MqV1sg8EgwECdOVK0x06dMYdOABKS9PvIfemFZQhx77u14KZmsfD4jTn3dLSTIeR2v2cMK22O3zY0NGEUobp9pZbRr3vfX4eNMB0utvRo21vdWq1ljp9jXuhQ9YH7THfmWWj1r2/8edyr42PL7iFhQl/3XbvXuqmuHEO7Nq10lkLe8O1YU94XhBqWv7ZOXJkxJvuSaUC6ZDnLUZO45nRGcozlkZOS9DQJidHvKA4Pc093/L7reeeZ4iANpDfQLvTGcG1op9+yGk6H4qQ05gX/eiMSNrac/83f7PkHvOYPsW/K6IhctoAkdO++MUvlip3WWW7Mm1Wg5xW1ZirafeRj5zLNEejfaCpiniwwgCStaA3he3On5/oSvrp7xJNS4JCVlQrpkI0j6tXMfNZnjMEUyWtSZoOZJHJdc+ADxwAdjXRnhXMpsAn0KzIc+X3YtpKOZOWqNKTHKowBSkLMqkePbrsGTmHNybIkAFlrYX9qNfH/X4vLCQmchFj7N9vcJkECgp5S8VQmA9aHwILTJvDc/9+u1ZCopOfV4SwwV4IW5w9gyELNhUaHcVvzZ6O+9zt0JfPfnB7GbKZHdgQ88AkHprxFbyGUMG6kas3bEhU1unpg17bTtL0Fr02H6KuxXPbuJH70czm9Jm+b5JIbuBRwThnvVadK13bW8GQV64InjbZV767cgXml+TD8zsFGgqbXLjpqlFO6mEYJU3bLNfS4qIhrKGtxwVJRAioQJlmEUIZhMk869mLC/NY0Zh5ryzkadXz8y3/mxD2Ny+q/Fohp+URSsFXfMWmIXLakIa0mtSsfkVGrodfHT/4/v3mD1U1sHBuHMQcuJKZxHzCPmWW5PeY19G8YBQyb/IbMW0+o2AJ6GUIAAgwYtx8f+KElelkDuqrzFqUpoWgkBTTUJUwDlM0nbo7ftwKr8hEDikgirMMowo+4TBXHK0xTNlivZhqdaALvUt55ha8ZVIbAoWAXoRXDk+S1m0ocabN0w9zQWhRlS9dA0HDKmZyft4O6U2bFl2ziVZv2rco3BvmFjJtvltYwNrS9C6Bhz981u8be46QE15j1sRnIK3hwx0ZmXe7d5vmrTzvBGK3N+97ZGTZLSyM+uvDPYQHin1QBgNj4bdnn9k3xsP6oAC80MRdRGF+d1lCcIV5I8Qqy0CUhVHANZidTdeHj2lmpu4rk1F5b8OG9k1fonMtNGTcA6Qh5Ona2n3lV252n/jE1b6pWVmVtKqGQMyCPI396rGWr77EfEPtVswNBhVqnCG2dhg0xP+VChNHikvr0rzUV+i7lOCgdCPMz6HWr9/A3MJxR0bGPZM7cWK+W5c7yQNG62+6vXvNjB4ymZCMeRN5DWBHEoPAnPHJW8R5EqBFIBqaIgKEkLvk175wwZj2xYvjXeuC0tAgGDACghi5GBRMHCYPk4N5w+zDMQWmIxxy5jY5iWCw6L74RRtz166DrtEY77gg7HqGZUpl+TBtsuHdLocOzXmY1DCqm3HNElHrwpZiFbh6FSvIvHvEI3AnJdq1QpLCfTWUORjksltcHPX7yhiq2a7ANsZEIGGPVSUMU/LWrQgL+fWzde9mgbOI8rTtmHmfPj3VDVYrEoCxTmFpKTL+zcw0fMAaADO4jVStLwZkuVaQp9ebhox7gHT48GF3S1b1+mvYrmxfZanKMVfTzjmrzpSXmqVDV0FEedI3fi38Uv3I/F/jJSFPE+adXT7TKi+Fc9aBL4QvEZ8r0Ezm0l7Ga2bfLMwIHery5Sm9DKZLXzJfo3XB7LRntuY0U2AeAibBX0q0MIc+2raCmuRPtfHsABXFlk4FW12+3PDja1wEHWmsaexw88dLGGI88+UnTFvzZL9gSjBs2hkoTHrv1EaBZyHKmQjrhHC5VRGNwK9t28Z9+3p90R0+zPi3dKrQmfk5QbBLkOq0fqLmx8eb7uRJypOm72Mxe+GWQ/hw0SS/+EWr6402rmtjsQBJ36xTwhVZAuPjVMtL9lswp2LeyoEHxAcBirZ5ZvLkeRnvFIIxoeL+++tu3z5yvLPv7zzmfeFCsxtpXhRYSjxHaIHKoqWlJTc9PeaZt1XqM9TBWHgn+r/s896vXdmzYxA0jCofIFWZU3i9cg+rHLOqdioyUq9TZCTJr82isiEdtEOSV8WicKx+c+k3piwFwrUWcdAKClWBRnGf4f9VWzokHehKy+KwRu6BKcAsBaeJ5ikmIK0u3hqLdjaNjgP79On5LvNFI0W7Y75UR9u5c6QT3JfMXfnRovHxOX8gw6yUv22+SmPazDkPYlPzEdMmElz9w6yZIwyWdfKXtdrBnVgSZG2IMbzjsZgXzBLNHEEBAYf+mevCwnjn9/d7wQM3A+ZnuWZCpLlwnM2bGx7PWwVUwjYSqkRmZTE3weXLBKylq8SFlhGsIsxP7o5z5+xZiTHYVYiFvvHLKxuAGAdZdbKIe1dYCezrqVMGaRtGrWc9UyG+Pn95PzFhAV0w735pW/2+b3fGJPDT/PrZgCztdjkWV+ZcuJHCwYaMe4BEwMWg25XtqyxVOWbV7crQeiEQ88BA8vzqFqnd2xfaHNqawEms5jU1xNNVtOIqYdLWre8kEE1MTDnNaHCCOFX6GQfo6dPm31YQbYjjHZLwymGqdiYaI0HrRjsTA4BR8rp8mehiwxgXyhqk4CgzA5u2LVNyeA4yDmtT6lpIYoaao9K3IPlsVaxDZmWYIRqy/KpidkKJCyksqCLtnrWqaAZEKhrfMT+Y96ZN4+78+UPu6NFDXjDiWsTXXrxE14v1U+c7ZN6KAtf1kCXDKo9ZB+fOzft1SLDRrSmgFvafOatW+eTkcreIjGIetHf6raqImUUlbSlJr2HErw1BBiHN4gZwiRDLINN0+v6GobMnMSgS0eZKE1uLAJz1fC4tIVwpHa2XeQPBGpPM6twjMq+vFfL0etGNM5OHABVFnF+rdmX7KktVjrmadlWBG/SLmu/XLs5RlqSf5VeXPw8Y0Czip8rdhWLTYVbQkDCvGU8mXw7sW28lZoAIZjN7kp5joC5JUBdasiF8mYYmjZ328hFrXOWZQ6Y9J8hVwmRX8BTnGaUr8aESac5lVQAW7Qy0Za5r9lcEN1usdCwJHaFGrGh9vueQJe0r1uL5XubmMMc8FAbC/WPuMGPmFwoI8pMvL1tZS+aMmwHmlqRP2Zpl1r9wQQLEIa+Rs+4Qaz7cS5UdNdz1hHkrkloQrtpP7YXgUIktkCsAcz4v3qvYi2m0VljErCKW5x3GT0gwCfdHt7nquMe0smJuDVlt+ItAYxHs9U6RmpGeAiTxvcw1gsHyPcz77NlGrgAMA+6XMj3SmbgEDlUTi5l3s1krhbTYao1UdnYMgoaMe4D0gHJHBtiubF9lqcoxq25XhpS7uZ52WZCnvM+LXcnrS9ozhysH8MwMua+mgYd9hmZHOxBXuuPTHkZidayNkdGn8m2hMMWJsTj8pTFj/iSNCuYE4xcsaVjL2VDFLnkmYiloxthUFYzx9+2ruS1bzKfOXGFiCB1o9EqjmpqykpgykeslISA+F1XrmuqvzNOY+ngXnCU83MXMlWKnYC8F0IlUnpQ9wMoRQrHCyHbsMN+taa5JkBckSwXzUoCZtH+iztkzBAJB4HKNtJcwDNWp5gVQy9iYIFLJc06XPQ0FFDHvc+cueSEPv7QC6BgHoY21GAzrUheaVRXfQu1cMLIiCVeMJTAfkQLIsiBSKSpixW2SfP9ifH1LZwtN8rhWYubN+507uQbFZumlzpihkJTFvMEWiNeTFRCHsNHPPF/27BgE3TgixEMAgOXYsWNu+/btfQFYaAf1A2Ah6Z9c6CIAFvri86oAWJgnYxQBsGj+/QBY2B/NvwiAhb1i/gKHELgCgA4QgTHgQpNDiT+c9abBWhIAFgOuWOoLwEIb9mLr1mV35syoB5MIAVgM1AHz8VUPxlKvM4cEgEVt2+1Jt7Iy4pkdhwhgLWNjaB4G7LJzJ8FEBumI1grDIdBm1y4AWVbcwsJYDwDL+Hjd7d7d9OMAcEJtZYj3MAd+S6AYcrkBu6C51jrCgR1QHOKkCClyHY2Wy0Z5RoKQ7PA3YA4R86Uvg/IkbanmfeX0q1KRYqgwXKVVWeUvC15jHownJqU0NjE0mJ3ATogYRriw7w95BoO2HDMTmZdVpCNEdlMdcDEv5svtyP8FVqNiJwDeLC9fdSsrE51iKdYR86W9hJU4qvvKlXE3OWkpY+fP3+JmZmoeEIVrpj6ItofJAnGq4jSLi3U3OTnntm3b7k6fZq8FOkNOt2qlW741AoR9dslNT29wtRqAONyTyT3L9cY0LJQ8fsN1VKEbMadQ4GGfjVECOMQ9u9C9v5eWRjyjJc9cFh5Vgzt3LgHW0b0MwMnKCp0bYIz5hA19jf/zWlmxdRJIRwob4+zZ0+z45AHiAS71sgfESQOw4Bc333i9zr7a/Jk3fm7tqQqSwLztWSTa3s4Init8+wnQT9IP14rvSa3LA2DhTAjPiBiAhbaDMqcPAVgGCMBCuzJJ+VW2K9NmNQAsVY25lnZFheu5jRE+EAiK4EpjkJYYAEIBRmE7w0iul+ovq/ygadS1lPk7BFVRwBd50OfPj/r8a/kmBU4SrxUGCBM+c4YSn2ntGCazZw/Vp4xJKt+bg3vDhmUf/BaXxsSciyYHY/RB/J2+VObz0KFLPkebMcV4VU40LCMq/za/gxmazDbn06JgXKwZOVSMl37QSrkNGA+zLI8Q+4RMJ8EBoBXyqtk3tHgxXEWaY9YX0w61TLRS+lLdbf4vBqQiI1x/fs98Z2dX3NTUSBefHFMqbYhHYC3sNcIF10X55fLnb9y46NveeuvBjlZtjMsYcGIWZ56631j/8eMIYbOdcpvWF4yStbK2xBXQ7hawIc87vF+VE01+M0yOObFWrgO+ZZVFlblf+yNBSYQ+cfCg3atcV5gZ924oKKlyG+Mi/F282HIHDtima8/yCGuESo9yPwp7XKVAuddMIG9015dlfp8MAFey8sERXlkX11PPJ9cY61IcnGh7bmA/RZWGi571pM0QgOVBR8Oo8mvbrgyFcmoeAIT5lctHn2dRUn4QrQimb4chzILDDQYQj7tjB4cHMJENnzdbZHbkIFWwUpLnawcR2wWzVo4733PAwRDDUpFhXzJ5W9pP0hfzhaErxQqmT/+Mh4lZfvQQalNBYIbMZu/pGyYEA2EevMRAVb+bPUCAgLGH/mqYtoi2CBaY9ekfxgLeN3vFOsIKW6pBTlvWL22Z9cCUJBzJD43VAHMrZnQFyTEn5UBjkmYdtOf/chWwVvaFoLXp6UV37Nghb0njmof3F3tGfwgw/JWbYMeOujt71lLF6EdrFyNTRDz7ixAJA7//fiL8Z1LIahon0SKV+77sZmZGu8zX9qftYyWyfMmYzGHe7I9ywkOS+yBEklN+d1EBkslJNO1aihHjBqAUqGBiEYimpurd+ArWZ9c5nkPbA/3InaSU0EQAB2a4laqkF1tLID0/ZQBcbiQdd+jjHiDJDDzIdmX7KktVjrnadlUEqMmt0Q8BrWyVIfUXU16eqtVV7j3YaGvBO8U54ZCZHRMmGZqzk6Am09g40KR1ykyeRQrkkvYf+luZK33CHMP8bYGMMI6CzcJqZKH2y8Goal8wSBghLwKeYOjK04596yKhmIkUkCW0NZjXIx6RxAao/jiMDyZx990mNDBmyLQt0t3acm0wczMn2qOd8RnmdZi5/Nv0wz1CvwgRrIvvLZp/3MOv3nOPRZsjoCk4zHDO7bdYFsIAKfP7m8873HfVAJf2ze9xZVigGxamRBgUhUKPcuMxGcNo8YUTwLhnz0pmTAaatJg3vwuFgfAVMtKpKXM1qRgJ+8U+peFr2RtL1YwFUlmjhGQXzotrg/CVpbxeDQoJqVof+6Ga8ljJTp823dRS+dJpdSLmqfiDtTzr14OGjHtID0nqV1mM4gmirHzufpT3jAthK0t4J1c6jH/J0wBULMP80+ZnziJFjXOQiTHqdyGFAVFZ8TcmIIQBP0laHAwLpokWiWYkE3ZYDUyBcwq4ChlymJOsfeEl1DZFnYe/QZuWqwFfKwyS8A1M62L8MEZFhYupKdhMfvUYWYzvCbpSGxGMGqYJw4WZYzLHoiFTLgxHlgQIZjs1RSnRQ920LPZVrhF+n/bmoN3axa7X53zfAoGB8TMfGBovLAOMRT41fcL44xx8mX81BnvFuKQCqhIcxUDySMxbICh5FcXCexwLEevkWvCCEeJe4PojMCBMEenuR84Yem4Ov36CAxCSkOmyqFmCl2KlkJDH/sXMm/fM9WaCTB0y7gESAVeDble2r7JU5ZhVtytDgi3sD/BQ7tHIg0FMA6W0vDkQCk3aWaR5AS8Zl/UMtWvBf2bFwljajx1WIRnzHO36J+MSnRy2MZPMIr7nQEYTktYsX7yAXZg7hhLQwmAAMFC0X8boxXK39hyswm2HAYcgMWLgCjCTBgrDDSPHea/UN/qUlh/uvf6ftXehL1fY5zBKCTSK8GdtfC/GLx+7fOP2lwj4Qz0gO2GKlvaS38zNGX778ePUKLf9UntexD6gQWuOpN6Ba649y2LeErBgWMou6AffiYDBPEgRU9ESpf6FKWu6jvRljNHyu5krAodSHdlD8+tTQtX6U2CkSHMHyjQPEVAxF7OzVg6Vv/0ytOhP5UDpg/uKmAoEiTDyXwVobhbI05uOcRPFjf+IqOinPvWp7hOf+ERu29/6rd/ygUrhi9/FZsc3vOENPpqbCPDnPve57m7sZNeAiJIedLuyfZWlKsesul0ZykojySIqcK2mvyKgltA3FpuhY7K86lZPzniotXGIctjAPNBmQlIVrgsXEkjKcE4yYYYpVbz4HYctDIgDMZyfRRRbBC6aIHOirZXLlG+biF1jmowPU7NiFsyl4bVFacGU2BQT4KXANLQexoZhcaCicaIh0pbP8CsrcE2R0iFGe8i8tfehZlgU8CtrQFj/Wr5aK+SR9Mf/WbvyvcWE7Von+5swNss/D10Hoe8/xC+/csV+tLw81xVANC/522M6c2a+G/Udr4nrKosLAsJ995nWSwR4FsmFpIBHSy1Mp//J/y4+xjMQmr8JYhQxb+bAI3zffWCdm0Yu64GuCX0CXWrgMyP+O+4F1q8gPt7D+B94wF4IjXIfFEOjUkLV3CB2PxoDRwjir1mmkrKeRX3dKHRTMe73v//97lWvepV74xvf6P7u7/7OPf7xj3fPf/7zuylQWURkH6lMepGWFdLb3/529wu/8AvuPe95j7vzzjt96hN9kvZUNVWRQ7zadlXfbFWOWXW7MiQm2g8BjdSg1fTXD6glaW/MK3tcg6EMicNR5mjlRMPMFOgF86aNtAcOSQ6kLJ8dc4I56rBUPrXSt2DaHL7y92ovGJd5wzQZk4OSg52/MqdKCJAfGHNxiACnsZgrZncFpQnQhMeNx5h18VtevFf9aOICGAdNTYyEtQg4RnnQ0jL1kh9baUxhVLnS0RSEFjJoacWQ8sHDvHPhhmsvxVhVnUv59GjdEOlsYeCcjhfmGDNj0NXs+sx1QVMUBBgTWrfN05h3+n7KLrYD81Zt+CIXEtdYyHzaE/mDEQIsuIzPDVhF8wkpXTUteVbYQwQT3WfsH/cvrg+7vxqeOXO/KfI9rL5W75jr+8Wk6PmEeQvnIGuP2u3+NvcbKTjtpooqf+c73+le8YpXuJe//OX+Pcz2T//0T9173/te9xM/8ROZv0HLJp8470K8+93vdv/u3/0798IXvtB/9ju/8zs+p/iP//iP3Ute8pJK5z+VFRlxjduV7assVTlm1e3KkPIs+1UWszzoNOHnjtPCivI2Fe3KIc3BQOQskKEcVqpXHY5r4B/KkzZzOb5DDjkVjtB9Sztpnbt319ypU2ADJFW0uOWzrArSBAnistzXJMdXcKUwSuaItiPmT842QgJMBuYZao0qJ2m5u+Z7lZYkZhgW3kDIUKIAbfkOoYG+Q/9pyEz5DX2iuWnfOOAt9c3mj/Chg1mmdSwTKkLC/Lm2kFLBGI/1wIgUD4A2qLnGRUPEPMXY0QJhcPLHMw+YjYBuLGJ+3E1MUMv5kBsfP9iFKVXxjzDGQLwBK8WmTVbPWxHWgqftzSO3cqAEnpEnrftb+65gPK4l80UgWlkBdyCdAgmFDNCurQWUkSqlqHj2Dq1Z86AGexhnAfPevLnuVlYMUEhCDO4nS3u0Nek+4xoxV0tnM5M76Yy6Tuwv/SvfH7Ic7FphVb74+aQcKJY0VRML146J/2aCPL1pGDda16c+9Sn32te+NrWRmLY/9rGP5f6O3F4qUJEg/6Vf+qXup3/6p91jH/tY/x3AIYCQ0IeIfGxM8PSZx7gB1hAQh/KMlQ+txPwsAvik6Ptr0a5MGwEIDHLMtbbLl3r1uUXD5hFBQKHWbUwkqfuMGZe/7XbSDiJVi1Sh3oIh6XYxmSZmZmZqZEsrBGksTF9h3IWFq67dNvVm3z5LC6Pv0BRrZClB9CPNDy2ItCcDsUjM1zEpWAomiXYbM46QOVGwBaKMqKLKddCFqV+mWRvARq0GIpgdtGYynvVwp2CUywfMC0FE5yDMRMxYACEwWDQyTJmsDUaBIIQwgakzqTRm80UQsTrhCcOlPcxOmhrz4f8KUNL+c/ATdCbLBpoe/fI51o0wmEzMld8rJY8XghLjK8cdpggjQpgwMBv8uwbS4txB/xv60BxDhqa9xWx+6tSca7etotjly1ahjesQChI2N6LhL3krxMzMtFtaqnsBQvsp3zt7Sf+HD8Oh64Gw2g4YeKI1Mx/Tmg2Mh3V28KC6RIAk6Vn0HQrBo6N1NzbW8u1VXhUK0+9Yx8yMVYGDSTMHgE+INJ+dxeff9J/LchI+AyHZM9J7v2c9nwgGCMkivu73HOf1db208puGcYMYRjg+2nBIvL/rrrsyf/OoRz3Ka+OPe9zjfEL8z//8z7unP/3p7h//8R/dvn37UshhcZ/6Love9ra3uTe96U09n2NqBz0sj0AyK4PPXWW7Mm1g2lY6s79UWdWYa223svKY3HZLS4spMJA8gIQ87HBkA8kRWe1arWk3P7+wBlAGa9NqbXDz872h7MoxRTgNGWmzOdNpP9lBLDMLkp0NYZQ3Hxjy08TElU6aVvY66Xv//kl37JiQrRKfrAWCgY7mP+3OtdXC4gHqlfUBA6D0ZBg8R3CPTOcctEJNg2gj/ySgJElVMgXsGZqVCGanwiEhw+TaMjcOfbQwpaQxDwVtqSqWfLLMQ/5y5bszP5gqjAZBQNXYZFrnOLDDPNG4dV/J1A0zhiHxd2ICKE+iookOb7sLFxJXAEyftSAUnD077mZmFt3c3CF34MAtntFjGlZePP2CXIf1hP0ARcwEN1yB2zxTJPdaAgL3gvLKmSfrP3685fbta7sjR7h+XFNQvuwa8AgReW+pgUIQtPgD9mnXLjODUBo0sQa1/RzRoCl6AoMWwlu3Rbvto8JvuQXkMot1gFFTNQ5h9+pVMUkDpLFiNwbxa/fZFddsTgWuFWnTtU4+9rKr1TDHF7uv5ud7k8ezn09QIxn/cqlzYTVtWq3BpIzdNIx7LfS0pz3Nv0Qw7cc85jHu137t19xb3vKWNfeL1o+vPdS4gRhFUy9iRMCI3oZa1IeqbFemjbRZ9qcfclpVY66n3Sc/mRWNYgcV2kYsjYeEpaRMdGhWuytX6m5qambV/akNh+WxY9Nea8pGfrP5ywSInw/mjdbHi4NZ5k1VEeOwR4OAcZr2MlNqXgTjYIYXc0w0N8OPtvKoM12NG8KkjBkUZoSGC/PjANZvMUVKY2WOYTQ3zBEGevgwgBsyUydpbSGDhqGGrgHbn8SsD7OUqVtBZBzEpq0lwpfykGHGfC/0NF4CToGsuIgJLpi6Owi93QIsAnMJ58DaWSvzBLpUPnAEGhg/pnCZ0yUU2S087i5cWOzUkE6+Y842H5ikWWPMwzfiTp9uunb7rNu5c7tvv3OnWUAsoMv21vaO35KDfdUtLs50TeWysOgaWu47Nb+T6lkIXTBH9h4hA3AT818Dn1vvmtLn5+1ei+9d5s0fTOqyZDA/g/rVvZD8jrUCPGM51zNe45bOAPOr1xud6m6gBI650dGmjxBPAhLb3XnY/Wr95D17MSGA6Fkp87yUbQNzHwTdNIwbjG8kp5PRE837PB92TGBYP/GJT+ziY+t39EFUedjnE57whNx+uHhZFxCmV8T4GK9MhZkq25Xti4ez3/yrHnOt7Wq13mC1xEJl2QNF90DR9/3a8cCHfu4y/aXb5M/PGGDyPdHlgFrgX+ZWtSpG5nfk4JXmBxOxSlv2uZnei+clzRFtXUUsQh8/Y4pgiFu3bnCnTs13NWIYnEznYsIwPzQ3zkMYRlibGiYlXzEvxtYhrHrTHObKORaFPl39DYPeBB+qcaDQd6wANAV3yWwcWg8w6zMf5i6tXeliCsZTSpNIddMFdKOxxSCR32UKVl/SlGGeCwuHXLN5sJOiZxjdihdAi1ZpUfYZk/nERNPdfbdVS6Nvga7IsqA9QUMFJ11WjTAVjbnxW8UTsIarV9GyLcEf8B/WESKRgYWv4MNQQOi9n4zxquIbZDCr4IaDtS5LTnJdk7a1XMQ1TOa4qeQ+UUxKrRbfr9ZPTHnPAQIWvn783UXtyvQV78Mg6MbxtvchwNyf9KQnuQ996EPdz5AGeR9q1UWE2eSzn/1sl0lTTAOmEPaJ9ozJu2yfqyHmO+h2ZfsqS1WOWXW7ctReczs95IMmgtQ4hFW/m9uXg1XR2/K7sk2YZY2K53rrrRRGQbsC3jKpdpZV5Ux+Ug5HDlelJYlhSFMSkIkiwXkpAlsoa/x248Z2N+JezFUgLvxVAJIOdygcJz4bQ2avCHaNrfb6DCYss7he+r208zC1jP2E0SKMhOPyXqZ3CQlxOpre0xaGxMsC4GyDG41DnfxwY5jy24uMaQu0BezuuW6dcSGmycwep5sRZZ5FMcqekW1UXD7WgsoMQxxSAB5uD8HZiuK68epDUebhfisXPJTZ87IwGBML1YkTFPVJ36cHo/s1q852GXCWzm5UeHZce7ppNG4I8/R3fMd3uCc/+cnuKU95io8Ip5KMosxf+tKXur1793ofNPTmN7/ZffmXf7mvSnX+/Hn3cz/3cz4d7Lu/+7u70tErX/lK99a3vtU94hGP8Iz89a9/vduzZ4970YteVPn8KUJCFa9BtivbV1mqcsyq25Uh+TrjqNLedvjG+j8euBn6+bjDNtToPnq04WsSlyFp3RyyaBswkdB/DCngzfzEdriglZdZZ6u13GUkWZTgSluZ0A0brHqYpXUlWM98J5APRYEL2xzatq3pRkcbXstjbvhLOXRhpGFQGYxF+bscwEoZm5padEtL490UIJGYCcKCmJhIKWqh5imoVPn1FV8AiXnyXkxU+67616Emr/XrvSqUiZkTcEiKHwIXsKbq20zI427DBoLV0gGCENdJEJ2MydpUDGdkZM6trMymIt7DuUBnz1LdDT8xzDttPg4tGIobEExoFjoZzwHV7eSKYEyCLLduNVM7vydugcyG+P5SDISizKmehzle1y1m9GLMur4KMBRMKs8Nz8/kJO+p1DWeQo1LfP9Jrjluhby0TyxnQkQs87yXPRMGQTfGLErSi1/8Yl/iEsAUgscwZ3/gAx/oBpcRYBUGV4FxTfoYbTn40dg/+tGPuttvv73b5tWvfrVn/t/zPd/jmTsVsugzBmoZ0s1PqtYVpuiHVYZuZJKfMkt+EaOAOLAw0V661H+daN333EN6jMutliYztzGXGddqzXei7tNgI6TvABfKHDngzd9r/lLnZv1zS/yjMRkrW6kIZWGga/4cwDB+1mWoZVTbOuQZGMIBffO9SpLSL2uAiYT+adrDXFTGM4RUFWgLbVifgGnkFw5zuBGWxBwt/cqYnawAoaYrQYH9PnjQBBT2JAQJUT65MW/yuw+mrpNw3RV5LRhXIvY3b252AXJCl4HGVwlSfNHEVCT+5aQ0qVwVgkegr2Zz2TUaoz3Xv14f8X58uYOZC78lCp++0bzznp8w5dL2puGFNlXIyxIkrbjPlZ4qfwi6Fy8mGm+rNerjCFQtjfkyL96z19K6iSUIi6FkMe+TJ1vdFMebhYZlPQdY1hPNq4xPt8p2ZduULetZ1ZjraZdV3rNfWc+8Epk6LJH0w4c7iWxNU1ziM69dPLewDZoDFGrdRfNH47b51zzjxs9O7m5IHJwcWjBKEKqIui6zTsA4YASQSi3G+eXC/EZbPnHCGDeMAYbE//ERw2A5LGVmFlQmB7QxLqpfpU9PmCXzFrO1fUhM3CClSaM6d+6Q27Jl3DOysD0UIrpJ45X5GwYDsS9o/2F0OOviwBYzUjqW0t20NtYe+ugRBhAcsHxorqG2D2PiGBBz5zchGKMJOtZvo2GlQOv1gynhSpH5SndToNvGjVa7+tZbZ1MIZ2HfMCrWSsyDuQmsihjXT+A8rEulRhEUsHjs2TPag2vAXMwXbmMxH+VsExvB7zdtspKYecyReRuyWtunYUmLzqJ+zzHMe9cuqyImkztrDe8JxX6wv+wN1pI8rRuS1t3PFVbmWR9UWc+bxsf9YCCQ2wbdrmxfZanKMatuV0RChMqSU8MqQ2VKiYYFR9ZS5hRz32oIc7lMsRxQHEohJg3fSYsRA8GPXGadlsqTmMTpX0hdYmQc2KQpcegvLc14BgZj4oBUTWwFdUlL51AN4UEx746P27p1DbKKR2idEL8TNCXjXLy46Bke5yF7APPkPcAjMBNiTik2ct99Fu0dmpC5hVR/Wihz/Ia+YSowLt6HEJ8wXj7n/3JHyCRP/wgDMITQPM57hCthuMv/HF4vPrNa1FZNjH7pSxjsulfZdyHOaZ/xdSunHo1c2aeheZjvmTvR6fzmkY+0PWLu/DUksqVUJS2uJ0JcLNgiJLJHXE8F3oVAlbIEFcGOKvKfWvBnziQR6mulY8canSBRuz+yytTCtM21YTgIRYQgXkZ/rbpE8kPGVH6zUwjaMqh2ZfsqS1WOWXW7IkoeXktdyf++0yrnQUYqTwJaygEuVGHUMh+ymaM5SFVsQdolhxSHNJqvQYrm1wkPae/elj8I2WKYdjxVY5hW1CE8IGGEMCiYh3yR6js0NYf+5NCkm+xN9lrNr53kKI+NHXT333+oW5GM8cWcVYlMJTxV1pQDnH0RkhmamfzxHPqqPy5NmXMZH/6uXRbhzb5ihg+Lgmh+9I1ggQABA9R30pRl5lfqmxDLwj0U4+G3R46AZ25aN9c29F3rOsjfL2CWhYVZX8qUPtVeSGNYEkB3Rhg4fnze3XZb2tcthDVR4itOoszDtcaGRNZCtLdSBcsEgglmVbW710JYqc77AjyAobS6rpmY+Jy9h8oICqOjK+7kyVH/jOXFhtxIxukh4x4glfWbV9mual99lWNW3a6Ikoc329TVW7GqXPBYmXa12kg3ylWHgcx+ZYPUDhxoufvvJ4/cELTI3YZkXpWWGq4DxiS4zPx1mtFNlbl6596bVkTA08aN8z5Vh3HxL0vjDJm29Z+kbRnCG0hY9W7feZ4S1jk2ZjCxIT3wwKK7enW8a/JWQJgCzJSGxXxgLhSWEyNXxTCIW0pmaEimcq4NAgHfoc1bfnMatlVBZlor40h7FW68KojJZQAJRU3QofLv12pjbmzMTCHSXtnT2CqrsSXHUgL09GksGUl9cVV4Yw3sIQLXmTPt3Ps29GkzJ/7PHsraIk0+q7Qne4JlR0VidH/nEYFpWAFgvHlk9einvCsmDE6Tud2i/msevCcUakJfvkj73K/Otq3HAkCPHav5dDpR6L4oeyYMgoaMe52VynipwDoQqqB8EZ1+9OhRj4RFxbEdO3b4aHbaEQCH5IY/HKLSGcFzFDUhN5xUNYLlyDXftm2bv6EI7IEOHDjgEeSuXLni0+NIZVNOOsF35Bmq4AqAMMyFvhib3wJkIghRmGGIHIdfie/pl/nzf+aJn4bCKzJXsxbGwK/P3Ci5ybpZG6hxtNf8md/Vq1e9vwciuv/QoUPeZ40Pi3lo/syB/SKgEKJfqoLxGXPir9Ddms2dHSQv2/fxcXCXl7x/CXMW+6DCJPKNk68MpjIagknOpnlbxTiCeJodhgPS1Ipvg49QcLC0Yx5o/+02tZZX/IMMmlOrtei/y2pLEM2JE6A/JRI7CFu7dq2406frbnFxxV93omRZD5G3IyNAPZpZbmyMgKFW5x4bd5s21b3Zms8sX5v8aUPu4kBjzhzeSj0Ki0kwbr2+4hYXm25kZLRjRmx5BvXAA2gwyb2dTi1SIRFD9BJD2bIFf6QBdSgHN7YmiiGYYIAzHQhPvrEgJV6CyjRUOCKx0Z7J3bX9oTY6+em7dh10R48eSmmtYY63NEMFiCk6mzFYI48Gt6K0V5UIxRxuzMosGmEhC/WtSHMBqug9ewGTRVjgpb3TmLII8KghODA3+dAVPCjUuHbbAtUYn+/pQ4JDGHluRT/gkE0/Jlo3/YdCGho3AYNCw/viFy+6gwen/B4b4FLbR4PzWBusKwzVBIuTJ5f9c4OVQLn6dlaZFYF2rI19Yy5YObg3gPblullNdt2ztc79zXMDWhsFThreGrBz56J/VmkL6IqeFYSyep37lngSy+M2AB3Lmmi3TSAhAJPnVhjmYT677tlduwzKudEY7yATGnAL93/6jGh49De5mHRGEEsCGM3u3ZwJgNGwd9pD1/PcDwrPfBicNsDgNJgUzKsfVdmuTJvVBKdVNeZ62q0lOC0JvkrMe9z5MA1MrPwkNIsVoSSFAWpF7RQQhz8unhPMiShbCK07b/6hpsHcYPbgP3P4cUagWYXR4xz2MKMTJ4yZQxzoedHzi4sIjBO+D3ycMQ61mNAtt3B4Ck1LVcXm3eXLlhrGIa/AMZ0oMATMlcovV/ER5075aGXNJ45k1nUgBe3kSSAzQ83qUCc/eLzr9w2jo9F22RPNgQA3aaDpfpL9glnBLPm/sMb1l8/l2w01N7kn2FfWHTJNMVq+5zgwU7h9p8AwPuPeEFnZSQLVDMccxss6Mbur6AlEn+wbfnxyrIkdAJGNOYbAM6xFVb2gEyfaKXM5AtGJEyNeW5YgI1Q4aHl51PvCz55t+2ssOFj+Iqiwn8xLlgEJH1kBkPG9BqF1h+Zy+kYut/vLkNMg9kFpgSG12ybcMfcLF2D66fx7xTMgVJdBSiRu5fBh+zEBdDyzISG01OvlkNMGEZw21LiH9JAhK+jAQz7SNa+GlY7KpoaZn7u3UlhRicSY+HznzqY7ezbftqgob/XBHO0AtQAfjDbKW5X2B6NBs8RPC+oVuNcEJBVF8kIyB8sXS3+KjJapkr1B85GZWgUitm0DIKPtf4sQIsbI+mE8tEPblAZKH2hK+ClDsA8xcKGRCRY0pJWVg14z3bhx0V24MJ6ZmyxGbmlRxsQUfa2qWdoz9pjP0CQRWhSIhjBnmN7G2OgH5ihhj8/RVmVO1x6J1Dd/TYO0/cRczjhxaVe5GhQwpwIqjBWmKtEn4yJscO/MzFDAZbabRYCWi8WG+YXuGeieexJfN1YMBd/pt2L4zG1kBCFu1N9DQsdT3ji/EToeAgtR5XJBMSZzjAv6Wb48Wq6EnlrK1x3W8w6Jsdk/7t+07NvyOOgUR2G+FniaxC/IfVQ2PCb20WOdCpl3b5GT60tDxj1AwvQ96HZl+ypLVY651nZPfepUptZdhmRQCDUwkTCokdQ5/PI0wXR/+Y9Qv4Cdou8T4JN0mtTiYstt3IjWnU6BMbO5/R9vgwKfOMhoB9PILvM51i26sXXrqDtzZtmnE3FQwsBhvIoUV+61cqnBhiY9DEaMSZOAOBigMMHlE4ZUi9sqTM26EycoGhRaONIlVhECMN+KaYjob3ERrfRQ1+8c+ncRXNgHg2k1TVclQTUfFSZRf0r1UlS8KpOxLs2P/WRfJJRIeJH1gf6k8apftWX/YfBcF+bIewXfhWZ/xjp1CjS1gx2TdTKOotoF8craDJ87fR8YlnmCzS4CCGZ8PGFEWHUsSyB9P8i6oPrsoZVB/m7mjDKZMDK79tK64/ta1xas/xCqlD7EvPOehTBXPbwPat1iO+x9w+3e3cx8Tsukm1q7pHMEgrB6GGQC3zrD4SukIeMeIJXFsa2yXdXYuVWOWXW7clTrqwlb5HGji8S0VrCWftGs+p4gtbionDQQMWz5V8WklpfT0fGKfFYRjeTAQwM2X6bqQyfgGqbVwpTQumAA0nxh2r2pQWbeVXurgUyO8LwPlkOo2LOHegLWVpqwUtXYzxAE5YEHmm737ka3FnM4ntqo+EeIVqazeHzc0NQs/c0YlgBIaIsfVZpsWMgkRDwLNXXawDBVKzrGIEeI4dpj6YCxIeCJ4XC9VE5V/WmuWDyYl4LXuA6W/5zsCTQzM+7On1/sAtzEAos+E3QoggT71mrN+fKp0Pbt5quOLRUy/UvrJmZA2PDhGForcxsfxx9tWnIoQMXxDyCpbdpUy7zv2Z/42mo+3L+mrec/K3kwtyJQ/IiQx/qw2jTLkIRyp3mKeaN1JwFuN05w2g2k/D/4icCyQbcr21dZqnLMqtuVIfz5/TRh5f3maeT6PX6xonriPOx5wA86DNKHDcFudqAqdzdMV4Ks8AU+7t4+QzSt0KxnJSLNHAz0JswXHykuAphcqCVeuULQWpJCleRCW81yNEAd4goI4nMO4J07l32gHwwN3zIMC/8r78NUKAsQmvX9Hz/ezDSTatxQGIFUJGT3btDU0DwXff/sJ2ArMFelcInRKXhO5t4QBlV52NozBADWyP4zL5UqlQleJmCV65RpWVqerBMSONhHtH76VCETWSRogzWAyHf+SkCRvzkm5iwoVN2D+/db/XP2gDgE5hMzbRG+6qRim4GzhOhvIWm/aBen8fVmA9iXaN0xZnl4beNwKgmmaN0qMhIT641x4tWXYFOLMjOKns8Y+jcLK31mhsA0WWwGU/mrDA0Z95AectRPE7b0oXahRl6m4Ehe4QRp7uE8Ll2CmU14pgpzDTHVQ80vJKKGQ9K5CIMgYjj8PEQk02cqWBFbEDZtGvWmWJmBxcSkhcZaqkGhtnxEdhjFLuJ3WRYOMe+s81B+Us2dOWj+MEpDZzvY8bPi7+4toCEtHQ0UbVyBWFoTa8Hsz37xF3N2CKYiEiCLfOSJpcSEEXzQMilrn4VUpqA1GDh7rdrgWCUQAkn0wKURpzNJSND6NR/GEXyrcIkssE3Wkv73pWnfVvUrDuoSScgJTdUiZQOE9zXrpI/4vu4nJJMVUfSssJfsWczUJycThhojEq6V4iImVDVTUZQbjYam8gHSLaggA25Xtq+yVOWYVbcrQ6RvSNPKYiYcEBxA1ArW4a+cVh1e4WFEf2UOAw6gPF85WveRI5jmSX9KH9zyOcYRsxs2tNz8fN2bS5kXa4E5CO6RHGuR+XHRutPMHCLdhf2QvzX0lRNQJthIaWch806Eibbbvn3Cl/1cWqr3uBgUdW6R6PRT7wavXbw46y5eJJQ92RD6ZlxV6xXDVhQ5n8v87ZwFq0mICC0Oes+1VvoXzFsMmOvAXIX6BiPIM8mqX609LCiCQYj1CQtcrg3aIQDJLwyDh9kaSE4yTyG3YUIXw2T9wpAPS43i15YPm7Uoyv/uu+fcrbdSr7tXmAvN7GCn33vvvNu/3+p1615WmppcG+wX+9Zur7iFBcpZ9rqL4tKfJkxQbS7bbJ7l8uL7MFAtr8iICtKEz9BIwL2EZa5CJGWfz7idAiUTMixzBPWyfQ2Chhr3AEk51oNsV7avslTlmFW3K0MrK8uFJQQtlcrMrZg40YA5aFXYIT6MypjPOPwYK4SY7G2TFHKAEBTQApmTNEBBR2JWhVmcOdPyvm60N5gFmoLwqDFxhn3LJK4MlcTfS25tMlYCuGHcW+hTIpWrjE31MAxSeIhijwUiCSNo+Lavbf83RBabnEwOW5nICc6TL1kwpWIqgvc08yt1rRe7wo3AVGBG7Cl7Il8rfSooi/dKtRJjZK7EG8iPLNhWBefJxK6gOH4HM2VfhfIGoUXjY9c9FO5BaA1QsBzXABM3Yylim7myRuRWri33rCBX2Tf2jzHIZ2Y9R45YLr/ua1kXVMqUviUI8hlCBYxRlh3dJ4wT1stmbATEuIymmNzEBBXmbJ9jUoWwzpXtuZdCbRbmnRQZaaf2M4FN1TPR+9xlmczLmrf7tSMFlLPjRqGhxj1AAJZjx455MJN+ACyAnwBc0g+Ahd/0A2ChL27KqgBYGJNxigBYNP9+ACx8zvf9AFjYq9nZ2S4AC//HF7waABY9mHbQ813bB1I1m6SGJWAm8kEac7LIVR12HM6YoAX6wONj/sp8ABaIvaGsp7CODXSCKFjDIB8bA1hl2Z06NdItZMB3AEIQrS1QEUF7cjsoDQyihCOXnc9Jj+FAB12NQxAGTmoLc7dQAUO64pIoMhffpMAnGI9xYAxnzpAOZHPWoc5cWJZpcQaSYQxt0e3YMeGOHuWeNyhKCJMs49F/GpzF8oNZwy23bHWHD5/xzJvIY/ad66FqTzJt88gI9hMNFc2bICuzhlgFMXuGbK/wu9se1FMBVlxb4avHfltVDGMvkzWYAIFQF/qPlce9cSP7wOLGvQCjKmkh8TuljYUBc+yn6pLL98449C3/M32xXmn9MFCuJZ9beVUDCyHmgkea/Gb+mvvAQEt0DyAMGNIZ2qkVrdH9xdzo8wtfSOaHwHr27LK79VaeFe577lvubwNVoe/w/m63x9yhQzW3bx/tmv4e27173N+PaObsj11PYiLMz76y0vLX/vJlExb5HX3TJy+BEY2O2vOx4oGK7Dnnuece5yyy71tegAEwxUBmAEGisEkxAAv3Oe0ge5Z5b8/i9PSYm5+3ORnAyhCA5SEFwALjgYH2oyrblWmzGgCWqsasol2YElYGgAUSElsWwQzxL0M8tMvLScqJDsy4jjAH5MxMsW+taMxw7KNH6500nbSZW0FMhg9tWpzM2ZbbXXfnzokJqroUaFSW703bsOoXh7ZM/yCtEYCjlCKYIXIoY3CoLy7aoaniFzBPGKMiq2VBQHjhkMVU2Wwa18I8D5OAAaMZKjJe5lkxUOZsYCOGqrZxI0wgma/BTSbpZKxTudGamyqIWbrYuGe+IRAIjOPw4WRf0Q4FiqIx2GNBmWosYZvLTM0+sXZZPmIfKHP+p3/qhd9kjgIuUbAbpnEYskzhFsy46A4cOOj70LriAEWQ0pDBtX/0RQEXYgZgerfdZsyFfsX8aUdAIu83bzZENWITmC9HltIL44Id/BYh7hGPGM2NDYnvb/PlW/YG4wkkyKxAJugRTxAjGQsKdfPmC+t6ji9ebHfN5WWevbLtYN5y3+TREIDlQUh7rTjxQNuV7assVTlm1e3KEBCieZQG0LAc4pBxc5DGgSp5h0s6B3ysL46zpRIBq5ieH93DIBMkqN7cWsGPnj9vkd9mVkygSSEOHEXKo9GiXcNAVFCDM0bgJARzCRGMghOkBbEWjCEyCTMfDmTaIggsL9fd9HTDpyOdPj3jmk2EKLR/tBDbC/mdpfmKMcIsOOwnJ2fdysqcO3u26S5ebPg5w9hUiENBWELTEoIYTJjrsmXLQc+8SRNbWEAjTIKaxsfNhCwzfqgYqXwn40iACS0M7BttJLjJFC8fbHzdheYWMlsVABEYjAGohMJUEj1vADpJHjn9Cc5T5npp6Fm3H5otn4coeCpIA3GfbN0KPKldY5myZSWQGV/7oOuUlyERP1P85p57TCA0608iPMinz9pihLV+OOYhjY6OZSILKspcvu7V+riLiGede75MYOq1pqGPe4CESXnQ7cr2VZaqHLPqdmUITPA8StfjNjO2mI18fjFt3ryQKvNpY5hfEy0TzZaDS7WM86JsMenu3AkWslUBE0k74dBUBbA4chtTo80F07B9trSUhoxSMBHarTRF+qvVFlP+d/qXWZjPmM+GDaO+PQc9n9MmKZCRjAFEqfCr0ZjMrG3m0DC9S4F6itQWE4Wpbt1qkeabNoEql+R9Q3wO84PJo63B4FgPTJz9RoO+fPmg93lPTGAiTebWbC6m4hpgRMyTa8p+MD/V2UbD5HPmg7Ai2FbmIp+1apGzDj6HQSlXWmAu8pFLUEEoQOtmXxUMqNgF2uneoB+1MdAd+z1Ck3LAmRPXgr8wZaWh6V6KhUStN7jD/TWUPx/SdYrzx1nb0aPLpar3sQYVb7HshkTA0PXWGvNQzc6cSVcyy6KlpUX/e7xnSnHUX/UL866yAiFocjcKDTXuIQ0pyrvOijYvW2VIQVAwGOF36yCTrzQPxKXdXnD794+448cbXW0khiq1YJ8sKNCWN5kXpa4owAdCIz98GJP5eBf1S2Mp6l1gLGhHS0ujrl5f9oyMwx6mEwJybNlSdxMTFokPU+V3HMDgmRMXgO9cGl0Yka7DXZ+Z63DW1WpzbmQETpbmQJZrbpowQhHMIKzyBbE3O3YcdGfPHvLMf3Z23APqyHyuYC00d/6PIGCWg3SAFtdRApN832jKCuwSYw+vBd/ze5gdeyDfuZUMNSGDfmBmIbBMyNQ0D1wVvLQ3EnYUZIbgonVzXyEUMK4EBQX+yVweguBACAJydfQDOimLgaS8a/aIfQlz5ONshCwhlnVwzfpRu93IxFpQmhymetwjVRMwx4oyv5401LgHSEX+72vVrmxfZanKMatuV4aKYAvT0ea1wrzruD9p3UJlUzBY6DPkAOXADUFcQsJPj7a0f3+zCzARj8l7NPA4r5X3BvghJKp8mZxDFaa3tGQCAP5SfKpoL1I8pAWilZmZG83Z0oKUY65D2Gp1N9wDD9S6lgbWZ0IEZnPDNA/nzG/ZVxiTxgjBQLZvh3mb5h2m/oQmXEjMUcTv2R9o8+aDHR/rort40QIJQwhb1gfjlaYcQnryOaZx/s96uGasDUYEg+SvEOJC4pqzJmUISANX5Dlj67pqTFkdQiAURcSHFh/z39tc5HoI7y0F7AneFSEBCwEvri2/4z5+5CMNcez8+fluehrrkRk+JEXpFzFuxcUoMp29Yy7ci9rTrHz9ItcR6ZFF1Gw2UnDAYe4914R9wmR++nSOfb9nDY1VtSHK/HrSUOMeIJXHza2uXdm+ylKVY6633dowy4sfOJmUpZXlYZSH/VnRkTROM7+JD/UYxCXPZyjKq9c9Okq+a6PHt9dotHxKDX5CcmOziPYwbQVFQWhmquzFISttPxsa1iLNlRucJaBAQgYzRj/j2u15Nz1Nvrf51JkvWi5BczJpKwca5sI8FxaoNT3nI45JeWJP1VYMTwxQnyPjCR/crAKkipHJQPAckcVJUQ1BpIpCpiX8cvqQ4MDcxPiFfw7Jh8+YqnIFchzao/zdjAMDpS8sMTBVq4AW+u8XU9Ydfhf6qRVxrr0OLUASEABg4S/ZEYI0DdPOsC4g2BA4qJxv7gfmxIvxZE1RfAXXgzZUj7v11qwYEeBuTTBA2OD3CqhjLbKIhEAycSpYSGNjV12zWWwub3UYtXLpw30I3Q5QnNudTWUYca2rdcfusUHTUOMeIA3zuK9tuzJUJq/T0LGWCvOus/rjYVbbfrka/RClig4aanbHea0aV9WW8nJO5S+N05Vg3kp9U+GJ9ByTBU1Pj3bzu2MBRZohDAktU1rh0pL5vOfn212mChORlo2JGYaCdict2ILADIN7wwbVS0+AToQQJz8yL1XfCtcHwhrjnDsHtnniy1T+dKj5hsAoytUOTdkC5IGxy68uJon/GY1XKXN8R1vaCbUXQUcFTGin3HmNPTl50DNQq99u/bEm03jbqfmFMRj6DNcCAYEIJ4JslXVEVc7oW9q88sd5xGjHNSBqHahaNPM9e6gXb0w9j3DTyGytzAFp2MQ4CEMgZNp5FqyQEELzqNa5JnH0vgRG7cnY2HJl50Lc5npq3UPGPaSHPHHQcOhwqKkU4mpJPi/5ybNMi6EptN+hJUJ7xpQpLaYMwbyVExtTnFoEqT65mLfGyZsj2heaHAxFh7HlxxoT5feKnFb6nJVlnPF7MDlp7TjQZdq2dDhjNPzWfNNi0rOegc/MNN3Gjc2uq0BFU8TAYpxw5SZDVBS7epXANQSKRd8339MXvm61jc3uEkT0EkMXgpz8yVwf+Y+FjR4yVUXssw9K02K9CF5YOMBcpy/2TK4LGL6C3wQAFOahx/cYQgEMOKzKBgnzXqZ2CR18LrcCnyMwMScFwplro/9Nt7KSmK0FowpJGKAfsjmxUhBMGIK45FFc9jSmegf4JotCgTELDrWK510lfa8X8x6aygdIgKIMul3ZvspSlWNW3W4taR9xOclEI1hbGgn507t31z1zU+RwfNjmBbqRNgYTVe3f5WW0TA6cts/tVpWtcqkrZjInxSY9RvpQE8QmoBrkdKv2M3MIg/XCtDfLHR91zeZyN/3JiEj8RpdhYQbHT6zgM2N0M+7YsXmveWPOZz0c8DBbQW5qfJgIWmkS4kDd6Tk3N9d0CwsNvx87d1Jz3OYIc5OGKVJQlHzCFCc5fJigtcVOPvi4XyMmaHzWabx3Y4Lsg8zxYbBXXLqU/2N+R/BgTpjLac+ewqzDADTtvdLPzBR/MMVIZMmAuXM/3XKLpdaprnlsAYBhnTuX7SYJ60nrUsovjk+ev+ovFNjiey2r1K2BvCT3BnsXWjw6WFNeyzcQo8QNJUFRQlDMcMOa3el51LountDiwz3B54nLaszDvOJ2Cp939ou5yGI0MTHu51AkUMd7cT1N5kPGPUDkNL575CMf2Rc57dOf/rQPyOqHnAbghZDH8pDTHnjgAY9KVhVyGmPyvgg57bOf/ayffz/kNFIwhCZWhJzGeh/96EdHyGmkC531GsqZM7tLI6fBgOzwJXCGyNQRd+WKHj47gCjlB9MwHPBsNDT2gVzvq1cX/Bgwsq1bmx5trFZbclu3jnmfLmUzE5Sztj9cYVb0SxlMIactLFB6c8SbfmG2Si8aG2u68XGitUHG4tCp+ypc5E43GvUIWYrKXlQsa7pt21bc2bMb3NmzLTc9veSvFfME9a3V4homZR0piyi4Tw5XNBErlNHsgIZwQLFfdvhRfIHUtVqt5dG3mO/Zs7a/IyMgWBn4BmuFiYXF3TjUN2+ecRcvzndR4kIUuBBrm8PZ/O4GhsMcDxzY7k6enHMTE4ayBjY2mne7Xe+igonCFDQ0PBgojwfatwkfh7xf+fLlcc9c5K8X3juMXLnVdp2SNcg3HWu27CG/ZRwYLo8Ywgt/Yb6hxcVwAsDAXvI1uNP474nrgv4QwiwNkGtuKHS0ZR4wIYQPBC/6A5WMrAGlm2kf7NrZHMUg6R/BSN9T3U3IgOH9bWh/ZCK0OteLexLEMwtONDO+XaetW20RYt70yX1mkfqGnGbIfDZ/E/wseHHPHgRXQykDF2B+fsTNzwMmZDWzbY3LnnWdPt1wGza03bZtFjdhOeKkubXdgQOGrGgIbCNuw4Zxd+RI3QvDlnWQAMQomM9S/NodNLre554zBAYfIqcR88G13bIFQXCInPagRE4DnhTm1Y+qbFemzWqQ06oas8p2BKiVRU7jAURAipHSYuJQuvXWet8AsrC/0HQmU5oFWVGGs54b6EYbTJcgM8GI0Rx5D1nQkv1AiGoHDrTczEzxAbGwsOgPGTSWWOvm4OZQh0mh+XIgS1MyQYjD2w5U3c5Fa7jnnmWPmQ7z5v4B4IXbCE0OASg2zWMupY8HHpj3/bBn0vQUuGWHPkzNvmPOVlxFUJ4mzC4s1N2tt9r6uJaYWAX6EZ5sjIemivypQC2IwDWZw3fsMNAWDnQ0UGnPoRZM/7y0rvj0VE44/m3GQ+Y25pZcT5i6YHXBWKcPGLcqbklDDfvkMwRJy1W3zxmHOVp99Kbbu3fW3X8/WAD1znVMB4YhuLB/XHcrJgITnfH9CxceIShdltPub92jdt3TbhggdylWwz7RB9dPGOXaH5j56dMGe8o8dI20J3I54EaZnV10U1MEEhogi1k27CaS1andXnEnTiB09977tFGApZ4DiL7MdG+ZClwbBCCB2+j+p09iC2Jzfvysi9C65SobIqc9CAmpddDtyvZVlqocs+p2ZShk6sW+LTNZr6Y/KIwwhyztaKXQvB1Hb8fm1IUFtO6GPyDBwQ5Nk3mEtsT8t21ruVOn6l4DE8MN87blb5X2ZlHeNa8l0V7zkok/y5SIgHPhwrwbGZnopg7BXLOYtkzNFjg146an5z2KF3T2LIe7MTzL1zamHQoV0t6B94QmJk75Pdm8uZEqzhJizIfFUWSSl5m2XjdN17lDneA1rodFn2P84QAPU9V4MUbWumAY8umGKHEwbdUtF3iKCU9mvdm69aCfF21DGNawb3iAKpnZ+o2BWXZAs7vnWDvoi7YwUfZM0ff85frq97IiYBnBKoC1JX7UdH/rHg0L4YgQ9DDjI5AoQp3rxP4pYBBS0BifhdcmrJ0O02y1Rrp4CFevcm3bPXnau3ebm4X/x6byMPANq5riQ7BUoFFzL7EPMtnrWmpOCDeWC96/upmIZ36Qud1Dxj1AKoPLXXW7sn2VpSrHrKodaWEf/3gUJp1DIQMt8mfxkJYJIMtjyJcuYWaThlrsk44FBJ0PZsZW3nTTa97mq+3PuCn2gGeBw9Z8vBRcsMNOQCuMizk49AmL8LWPjVmlL2OWZn3IA49ZWLjspqcvu6tXt/tDGE01Nv2G69N3U1OkinGoGwMHc505cXgqgEzMUppiSPX6DmpxeY2TiGq+R5MSXKkYrqpghfWnQ0Q3NF60zVOnDDIVBkXb++4b74G9JeI6BH4Jc9LlYuElqFJp+Vo7eOTS8kB5Y70wT2mdIUkYoP8QmER+aeZgQYKznnHjq1c7GCd7BtNnL5ibUtdU0IQx5SIR4luWj7tYiK37tajMJ22xqogpyhwuK3Is9ECxyyE7FdHoimfEFuMQl/sMAYv4/NixkVS+N/fUhQsNL+ywtDiYnP1EsDKcg3TKZt5zfD183cOo8gGSfMyDbFe2r7JU5ZhVt1sttKGCr7JIGudq+rP3HDhmksOHjfkWhLIiRMUseEoYV3K4G2FK37MHsyQVmfL7s8CupLymzIzj4/jR+0eNQ5ajbX5le2++zTzwGNEtt1AoJUlhEoOGcag0p4KQpCXChEZGZrqY64I2xf3Bgc81UkpTTBMTLfewh812otWxTODDN4aFaRdmBSNjHKWYaX0hcIew2rduvcXNzh70yGv2ftFt27bYjYZGg+Vz5glzhtmzLuaoMpwwFtN+26l5YxYnot2YxUGv7ds+2P3BPOlfDE6BVrIShEwdRh9qrVoPcxAWAS9SuyRsCUJVbglezJ2+mBPCFhp/eK8mMR1F94qlQQptDyGAPhlPaWf4nVVqNssNHK5NPnxlQCDMETsR0krnns5Li1RZ2CR+xcY4dszuaaG4ZZHu+fg+L4JFNUS1wUWYDzXuIT1kSUhpWVHlmL04QFZDiXmPdxYgBInhxaa3WIDANwgh7SvVKTycOZgUTIb2HKOnxcJDaNrjoOPwg3nrtxzmMLJYQNBYhvJFII7lXpcFj7nlllEP1oF2E1YTQwOUtisEOMyrih5H+z53bt67BJgPhz0Rwf2vE1rwrF/3vfcaYMviom20NG8OcbaDfhT4Jw2cNggaZl6u+bZWke2gXzf9X7qUBEOg0W/fPuqOH7d86XDPFOkMmbaPv9beM/6uXRY5Lr89/RMToDQ25mFlNxPhBp+wVSmzkpS6VgZp2nSbNs36OdKf1Wm3vZG5XGl3wja3PH4T/u6+264zbbhe3Iv8LgSYCe/RLBM+14h7SNad0C2gFDdVVBN6XRh5HqZJmuCTFKbR9zHV+6ic9gz0fi5XQNHvwxS+tdB3fuer3LWmIeMeIBHNPeh2ZfsqS1WOWWW7Jz953H34wx3OV0AEf4Uk7SROcVEE72r6yzLv7djR8pJ4EcMTYyJCW6lTHHAcbvLXKu3n4sWmGx2lvnQiGMRkMKC934l5w9T5rY1rpRYt4MvaheNakkLNMzQECvUfEsFQZE4sLBApbyU5t2wZ9XWchZ+ttDFpuOwHhzmXFlMx/+c1PT3jtTUCDUOfev51MgaUmGhnfXnQ7ds1ScO0lkYKo2Wvaa8ccDOpJsVP0BJpK/hSM0kfDEzdh9zc3LJnUmEsqvpXXW9ZJ5rNg35MBBcsMGJIYvQSKsRU0B5jYq1KL2Te7I3cKBpbqWL0p/+zBl70zfqUM2/3q1k5QkCeEO3N9r+RukdVqc2gbm1vWJcCC3kvrR6SewUSVrriECCZ0WnPded+IPKcgEoxd90zek15eN9ioVr3aJzGyNwAqLGIdUqhpoVW1X/PStmMz454PIPXHXXPeMabfGrktaQh4x4gZUUkXut2ZfsqS1WOWXW7MpTlH5aJL6QyAWBxfzFDw6QNg0QrPHWqONgNf/KuXaR5WQQvxCEpLTykpSUKihRjrudBOMK8SS8TYc7F5AvzUvAQL5goh3BSz9lqa3Po9p5faFwNL6CgzTN/y2kedZcvk56XVMJSPjGfIZxwgOvwDLXj0VHAWi65CxfavqhK3nUinxtmEPoxJyZmvcXi/vvnfEoQ6W8QB77qKYtZJvuSBEehIYrC0pvJmAe9IAETjAvXyU3AdVPlLe2XTNIiMUvWzH6HiF9Z11RWB8Y2xLOmu3p1tnuPsD5Ss1ijmafT8KWCbmXcdKWwZD6CjE1qpiebxLj0YVHdxoQ1Z6XShSU8IeaGRYXvuddCOGFLt0wEMeYILjzfhcxdGQIInQjUGzeyD0m8Rt5+da5I9zMhrSFocI1kAQijyhlTJWPj+zwvtkS54aQnzs623Hd+Z8194ze6a0pDxj1AOnnypM91HmS7sn2VpSrHrLpdGQIulAjta9EuSyAX8877PqRmE5jVMa8lqBhFlrnPcm3zscw5zGgjRLSQ6HPfvpY7dszSxIBGZf4yjeMXVjS5NJEEd9wifGNNxFwEljcsX6eCpNC8DSEMoUS5zKblC93sYQ9L3AIwVAV77d495SYnG+6ee4wzUTs8Hpf+VAKTPVHZT6s1br5v8r4Ze9u2hu+fWyk0wypQKwQm0RxgRmEQW3JdEyzu+Bopal/BY8IdZ47sC4e8GLiYpV3XfibgJbd/v6WsnThhvvxQ+FCwHMyRfQjdLIrmz0ph0/ciuTLMklHvapPKFNWaBZ9ruei1rgAh4UzAKlhzQk02z8WDZSa85xRYZxXSLIebvTx9mr6BWQWcJfu5kk+foDHLOU/6VvU6iobs3m0R+WHcBVp/Vp9ZZ0LoHhtkYvWQcQ8QgOXYsWNu+/btfQFYaAf1A2ABIIAc5yIAFvri86oAWJgnYxQBsGj+/QBY2B/NvwiAhb1i/lkALBDzpe3Fi3/nxsefXQjAYiARSx0AFiKnR7s1uq36j4Er0Ia94P9FACy0g+iHVBNKWwppTHWzKUG4axf5tS23uNjsgFmAipYAsDBf1sR49A1DRWMF/ATmL0II2LGDfO8lNzc36Zm38JgFwEI/O3c23NzcSKe0oTFK0/4BaFl2e/aMuKNHR7o1hrn3AIHhsDcUsjSIBocewsDu3Zj+TfPZsMGAXZaXR93Fi7a/7IkBstRSTGlxcdSNjy/7g5gDXqAqXJa5ObQomx+HKgc04DXOYXrHEjHljh277I4ds2pTW7cayA7avcaxfTYGIXMxBTf4zfT0dreyMueZnVEjMxVJfYhRhybUuI3dU9zjFvCXXHMC6iytLZwXpNx5NHoe6dACg5YstDbuTbTTVkvXZswzDSvuMubOnDFAEqBgQ4Yrk7d8zCGuuZiv1Ve3/G39JmY4eg8jxY2hdSgw7+pVQ/RL7u/sgEUhyykVjX3at6/pgVT03NizwD3JXEY6PnjAUSg5a1kGiglDQzcgmrZ/sZ/gCzQay/4+HB0FwEj9jrhduwyUhzrxYBDY82Na9dwcQDHcy7bYffvYc9vvep39Xg7OCHvu+Sw8I6Bmc7z7fCJcGL76tY8wHwKwDBCABaYHw+xHVbYr02Y1ACxVjVl1O61hYuLLcvMtOVxgBJjB+1X94lBCUu9Hcbs8CFUCeJiWgFl6+8kGkMmCmLQSm7UUBnOoeds6k/QeMaK4tvfhw6aJwGgUZYtsJCYlSEg78M1yIG3p0KF29zCnfCPBaNDIyKhHpVI/HJLsRYKiZZo377mkMFqhlikdis/NrAxTtPiAhOa9hkyVMcyqCpqzPbS/9AkTZL0CM0EzszlR59v2iIpj2moxNoGgQPTBGDHDljuBAC7aS6PWNeIvWONqr+9FMCDBq9I30KgqrynNLyv1Di36+HEYH2eO5bGH1gD9xYKBxQHrSQj5CuGHNnQ1K7caEmtB2xSaGAIWcxJxtCGk0QZBQ24C2oegQZB86lo/lgbmcPBgb1BlYmq2tMUEDMVywxXvQbogjDokrmuRMQ5gIAmI8pEL/hbhEXcDn5E1kGcJEAnpLyT2SOsWPsGrX73iLlxoXlMAlmE62ADpfBiCOqB2ZfsqS1WOWXU7BallEQ8UDxjBQeHfvAwPfKNlKG7HIckhhdbHocKhvGdP02tm0GrzPcMUGwVrhVWK4ipirAcGdd997W4dZrQ7mbDDdmj8/AU9jf3AAKOoYw4zmCK/hfmdOpWW7zmAIb6LhZ9Qa8WAQp/M3wLs0LCAoLSDEmYAI+NwhpnxYs4YU0jd6XUVzHiGQMQ5Fo4s4jCNzc5o3xZZjZY666/T5s1WtIQAOq4ZfnGYpVKZlGakNYWmdRgJ6xa4R3iNTBgyhqXAq3CPZLblMiL4oBXCIMPo6jj1zkyyViENi1NYhS5UvdgTxoTh48cVnKf6RRhi7awrnBPrYQ+YM+4H5pJl/hWwjdwE9pmlHspvrnWouIzcGBIK8kzNCtCjLWNwbykqnn0KBY22L2pjzxvXIL8Qz0pXuOLeuvdeG0/lZLnmCB0jI801VRBLA7T0j3avioam8gESGtWg25XtqyxVOWbV7cqmaSlgpShNC/NdGQrb5WnbFMLIQlVbK8Vzg3mjeYM/bqbB9DqFNqV1KsfVDuYVb1bEd41ZkrMJRSHUZLWOOBAI5o3mTf9o4/TH4azALUVscwDDKITiRV+Tk6Pu8OFl/x2/D/GzIeV4Z4GSwLxhMkCmkk/NvGNSFL6IdbMuBSadOzfrhYWRkTl36ZKBcRDopgqyQorjYOfAN/N+wlzoC42TS4EPNoQKlWk6ZCKKC5C5WtHRMCUEpqyKX2EmAkxb5nEFvoVBcxK2lDeN0KZiGyFEKt/t3UvgH9H4li8tRks7ZRf0s8Nyn8Dw2BvmqWh9pXEpghshiLkqIC1OEInTtmx/7d5Vepz82mHFsHq97Z+rMDAxRE7TtUA4ZU4SgiSchFYFs8D0fy6zzoR0IR67hmD5l6mqth4aMu4BUj8z9LVoV7avslTlmFW3C5HUwC/PTtNKP6B5aVoF6IaZ7dLCQbp/Co2gVXJQGPOm8MfaRfM8VwAmayAiO61S34k5qshEuB9KE4MJ4jPmsA0Zd5gvHZOY98aNDa894zu04CE7HGEk5lNMmDZaLoUfbr11tFNikgC5JA0JjS8ER8larsGXznQw0U2wY+6QELSUT82YMAPcFTADBY1BKyuzHXz0OR93IHPpxYuNLiIXJnEOePlq+S0Bd6HvWoKR3jNmGBymMqAwfL4Del+51xJO9ILRMX9p5UePmjmX6mj0gfCBJQfhSzItv2OvMWELUIZ1SmAyNDKr523BZ0g1YNnn3WPp9/IAWhUzu04KHoMZS1iBaQr5DAqrtclnrmdB94hSD8X08WNzX0rAYYy4fvy2bTW/D9w/IcVCKs+KSpkqx159cV15Dkw4q+UGuiV7kp2REmIMYH6fnOTG61OXdJ00ZNwDJALRBt2ubF9lqcoxq26XR6Hmk/XwZQXWEDBWhtSuCKKR6O5YOMBkvlbmnQW9iNb9wAMNt327lQAtWmfWfoh540fE543PLw0jmX+iHTgAQMtld+rUjGceQvrirwKypHVhlpYgpvzp+flRH2An36MKchQJT2KClu5EoJUBt9j6Ep9m4iuu+/EUKZ6s3z6bm7MIdNLynDvtNmywTbp8mUBBc6twsMeMTsxWAiCEFUFrUBocDArBhM9xBbCnVqLV9kSauABQDDyl2TWh798/6+t0C7aTfUPAQtuX9shYaNqq8AazFUwsxTsg8uQttsPSDrOi4hWYp1zx5P6xHC+upcUZ2JpJdWQ9ihzn2jAPKahouQQSyqzNddB82atQSKQPYiQ0b91/7D3tlKbVblubLAqFVAL7lHmAVUCY8Fon9/XJkwhsvc9oTHmQpyHGAPv17/7d3Tjt3LWkm87HTRQ3BznBSk996lPdJz7xidy2v/7rv+6+8iu/0kdc83ruc5/b0/5lL3tZp9Rj8vqar/maazL3IeTptW0Xa92itH+x19yVxZOK4A2z2hXlaFukd/JexQjWim+cNzeBjsC8i9aZtx+CkSSgJ4aRXFyMVJuIyD8HgAWCmYgJKTCLvmA0BHtRGhINKDRz4vs2/7cdvPw+L1hIcJ8CC7EcdDDPMQFb1StAZpD3OFA5WNkz5RbLZCwTca9WP+tN0tDMTNMHGs3Okg1g/cE4efF/tDUYL3OReVx46TMz7W4b/iq9j7ULvQ1NDWFA8Ky8p2QpTBtqNkGEm/UmfO4b4YvDmAXrKp8tTJDP+V6WBdPwE6YtxmflLo0RxsxKQWowZsUhQOyXTN+4CRibdXC/qXob3wuRLiSzHkiwMqsKa1Let4j5UzRGlgmZvLEmWUS8UP2Knx09b6xTeebM1YrIJOZ8CV3sWb+iQkVngmJRqDz2oz/6HHet6abSuN///ve7V73qVe4973mPZ9rvfve73fOf/3z3hS98wadcxfThD3/Yfcu3fIt7+tOf7hn9z/7sz7rnPe957h//8R/dXp6mDsGo3/e+910z0BJR2QD+KttVnTRQ5ZhVt8uj0A8VUxZC0lqoX452/L383evRvGMyTcpSdVShSSSIU0gBQ1klEcfGLEoec2VcDrSIBFgjzVF+chgIl0/wnpCqkHHIxznSMG+0b5gbfcQ+UK4XTFJR2bSBwSjPmKh89oHcb8BbkvxvywHO8rrEmn3i/jAgF+a9sDDnzp41k7UC/YgrSIqlGLMLI9ANzjTRxFWIQ8KNym4KJhQhgfb8HxhTmJs0Xq4VkfQwx1DQUM51qBnrGihegX727ZOmnV5rHiKdVbUzfPsLF0hBHPX9wfyUa695QYyT4Non/YfHcuj6kE+dv1p/WKWL+1fpZ6L9+xNmXe/zyITBe5bCll3chPgGxpfV4mahm0rjfuc73+le8YpXuJe//OXu9ttv9wx8amrKvfe9781s/7u/+7vuB37gB9wTnvAE9+hHP9r9xm/8hg8w+NCHPpRqB6Mm51gvtPNrQaSMDbpd2b7KUpVjVt0uT+tOaxbJKS0zatYDW2Qazmon4UAgF2GAD8hfWcLBWjXvvLlpnTDvkOJSh+F+kN8aAo7wOfW+IfM1Fo8JkUM7Pj7iGg07TjBj8grLSsr8antiYC3y7cYBaDDvrVtHOylopvHBXNCg8eGiqcPYFYmO5gcz4XNpTbfdNuNf0IkT+O7H/LphfHFpSlXHUoqQNHKOAfyoMBK0Xipw8eL/MJotWywqHbM6e44GCRNQZLntQxL5zdjyyVrUOxXfmh5bnX7oe2mJoLlZ/33IAE0AaHuNU8Km4gLkFkiuh/01wJu2e8QjZnpKsnI92SsYmgQjCxpM3ycwajFxNHu0+FiOFsiMYgCkfYuSuIskwjxk8kpbVBS+/hbpT2MdCNgsCoVUrVP7EZPWwnWT1s1+oPmHddjV141CN43GDUDFpz71Kffa1742dWBg/v7Yxz5Wqg+AS0iij3Ot0czR2GHYz372s91b3/pWD35SZDIJzSbkcStdICtlQCRAj35UZbsybQQyMsgxq26XtwZp6zyUBKPIjGeHkQVcZSNJGZhGP1K7lRVjVFZnWd+ZdkiENUwoq7sdO4BDTcvP/cYtmputE38dkdIIDKrDHY9v2huMQBpacpi2PVDGkSMNz7yt0lfxfpgWX+tW9QoRtDAlc7hboYwkelrdiXmLwcnvTI1tnvFDh1b8IYpJdn4+wXNP71niYwzn+bCHGQe59955d+KELfDAAaLvTXiQ5scc8YECWMO1ws8PMw/RvOhfcKNAjRJkhS9XEeGt1pwXJMS8DTM+SU1TPjV7zj61WiYAaM1hxao4sltaqqwWCjKM25kA0vYAOgDqQJioY54DRG0ckS3Bzfz8WGyM0x08OJKKqu+9DWpBNDhMr9aFK+1cHf8v6wYbn31hT4RdzyuMwk+EgPRA4ftGAzCgJDsiXoOeNxVlUbqaYg7C+8ai++0ZzsoK0Z6UORMGhYpy0zBuEMMMEWpn6nPe33XXXaX6eM1rXuP27NnjmX1oJv+Gb/iGLlrY6173OveCF7zACwN5Etbb3vY296Y3ASSfpjvvvLMQmhOkryKAlmvRrkwbGJ5QyTgoBzFm1e2K1rCy8pju/5tNkMlGUgUaskjt+hHtxsam3IkTREjD5Go+4jVkhsvLC25pKX8wDo75+WkvnJZJfeM56Cf9G5rf5o4JNO0jABmK+eIHTvt3ga3ELGrIVFu3gs425rUscMP7RffPzo55tDZjiKZ1hwzJosvt4CaQDCaoMpZokxCCBgRq1dWrCMfNLhOggAOoZyoPGR+WWkvWHm7bZtcT5m2R8zXP3EyIg1mDVMfhbeZvKsOZ+d8EH8O5NvAe20Pr1w502y/ntvrAqrNnDaxmfJyALIOvhRmgkWuuHGMy94fr0D0TQ5Dit4fRsW/8xRog0BJjTm3PtMXMRkeJq5jxzJSYAvOPX+2sbcwdP57UqaZ/7lvmi6BikLogh4262dm6u3gRdLK627TJzOaxD9vqnQNmctnV6yDkjXqmv2ULsLrJupg3go75xe29KooJrMXAZ0A6W3bz8/bh0tKk27ZtPoXbv7KCiXvEm/DBBZCZv15fcUtLBDq2g3Yb3Px83bsamEvoIrI9A6QFRMgEBU1kWOvgv9Pn1b7PXdk00ocM414v/czP/Iz7vd/7Pa9dhyhcL3nJS7r/v+OOO9zjHvc4d9ttt/l2z3lOdpABWj++9lDjBmIUv3sRg0EwoO9+VGW7Mm2kpRIL0O9wrmrMqtsVreGTn0ysI1hKysQwrKZdu2340cLijml6etxNTxcjv1GgYGxso5uZGalkbrThoD12DEY07TZsqKVMl8qbFvKb8m7ROkFhg8mAE461AOZ0+DBzy0N9a7nTp8+4dvuK279/u1teBgbVDmwOZhiUQaZaxDCMkHHQHuX/3b/fSmQS1GUaOcx1wu3Zk7gZOGTvvXely3BlthWTkxl2cnImd8/QtmF4ly/Pd1HV8Bju3j3mmdzly5fd+PiMR2xTlLSED7TwZDwrsWmlX5O95VyXGX/jRkz05vtWnq8YtbDoQ9+usL0tmC7xz+I/hrECqiOMd9rgP1YeNgIF/QOHyjybzQ0dF4Rda/YVP7fND23cmDG/UTyEctgRajZvtqhzMhUQhIWwh8AhuNbEv6/yq7bvxAUgHNGGtuyhLBpivuwL9xZ/2RtZvrDuUFFvchLMfrvHDet8puQzMNZ5Je2AKUVzFp4AQoPAewychT3iGa73AOWI0bda425ykme0+PkU/PW1ppuGcYPxbaH7J1Of8x6/dBH9/M//vGfcf/EXf+EZcxGBkc1YYGjnMW5umKybBoZRxPhg7mVykqtsV7YvHs5+8696zKrb5a3hy798pJvXjdk9Lw86pNW0ywryCskO1eK+OKxOnKh7FDMYaREka5m5qc3evS0fRIWWI1jUdDpYPVU5yebrIoAaclzpx9rGQWvmpxRWuWmbjEeUNIekcpphZmjYWCY4MAXxyWEN41B0r5YGg4GB8ngL83rvXiqOwWRweVk7Dn9hUJs5t3dvDNs/MYXW6wkjaLXmvXABQ9y0yeps04ZHXGZtpXSZVmdxCyHAC8Q6TXvVtTMmqzQ0wX7SF30KjCQ0x4fgNSp2wXptfkndbuWYMz+ERZi7YXiT/2wMOgZzAfpTcK+qdKWqXuE9zGfsN/jyMptrXEWqy2QvUzKWhbiKGAApsmwxH7Rgs8TYerEMKAgt8W9jaVnwKX60xVVjQWm1NT+f9U6JU669BGtj3sQWmLB09Wrsrko/E+zf6dOjmRXJQljier0xrMcdX4AnPelJPrDsRS96kf9MgWY/9EM/lPu7t7/97e6nfuqn3Ac/+EH35Cf3z62jWAWY4xT7qJouXbrkg+kG2a5sX2WpyjGrbleGANno5w5YbTsFZeVRmZiWpSWYBVoQjVve752FVx3PLQvLnL9hGyGrqZpYWqswLS42fcY5saOjTbd/f90XUciKOOcQHR+f8lqLfLtZICCsBXhV/grKmf9L2ws1aNqjpaFsdersdHzfvEb9QU/Nb4GOoM2FyFzpA9UAPWLADluvMfF2e96dP4+2puISFhjH2ALtkAYMWE2IgqdrZRjoSbQ4h7/KlZIKxz5Y9Tb7i58chkbuuTT7UGBT3XJp+TBmCV6MQX+YtpU/7dx0ZsS1GJF+q31mrJBpq6znlSujPddDzJv180pqoiO0dXLtOnns/E6Ic/zGLBYJEp3AV1TLW7jvoesgDI7M0mzrJZ5PtVP0vAQeXjDzJBAvQRqMmbbWjhkdoS4EaonREvntZz/7cnet6aaKKsc8TW72b//2b7vPf/7z7vu///u9aYsoc+ilL31pKniN9K/Xv/71PupcVbh4yQfG3x//8R93H//4x33VKoSAF77whb6KFWlmVRMMaNDtyvZVlqocs+p2RfSoR035h+vKlUZPtGgW9csTDdspojyLLEq3eDChrnEwyMeGBh7jVcdzK8Jfj+cvTHOYt9LBjNJBa3wea5J20LVSKTnxoTo1tdmdOTPlDh2qdTHSBQKCliPMc5mA0X5hQuo/y18tc3s4P/ZEh73Vgx7xhS9kiu8UjMvYm3ZXw8tT1HbvJs952u3cOeMraBEDQLAajJfDGuYC44FpTU21PEiNot35nHlyvcREQ9xxtE7aKpfcyqQqaG3J96mIcTEF1TWHOTNvCxJMz1kBaAgf27b1ugi0l8pdZi+4HmjIYTAg1wlzNvPDb8z7rEA0ja/73cp7JpHbcj8g+Mg9IAOYcs8RxJS+p/7yhNswBWytz6eIMUw4QItH4O5+5Z/RBHs9zbTDZyIE2slDS/zbvy3GPHhIadzQi1/8Yl/i8g1veINnwKR5feADH+gGrBGcFEphv/qrv+oDfr7pm74p1c8b3/hG95M/+ZNeu/nMZz7jBQEKWRC4Rp73W97ylmuSy11GQqy6Xdm+ylKVY1bdLo8oLPDmNzv3uc9NuR/8wcvdqOUsbXYtFMMeijRGrdYsfNRi1DXV8IZ5o3lnIToVQazy+e7djR7NE1CPc+canhnt3i1fr2l6kMzNMSJVbHblMJXmDYGRfuoUQWkWOFVkWpXmleQ7409NgtNgKmICmJoV2CTiPW0IzkLTFiLZ1BRBUQCDLHvtvRfJzhiJNEb6ob0Yl9De0KiY68gIZWcRBuZ9mVFFI7NfMpUr5QkhAbcADEnrVPQ862We7ANMTYc+bXkh5MAoY5ImZ6lorrvPmNsRvkQwa0GPmgBpkf1hBLjuH9Zn1cFsTgK6QfMUNCmpeGZhsc9Vj1z9i5FrPWL+SjmEiGPAuiBSkRMVDxGiGyShN34OY6z8mGrB/ahqeGmkv952dn9la/E8o7t3j3T3XJSUAU2P1w8t8VrTsKznAMt63qi0mrKeN9saMLW+8pUwbXv/Qz+U2AU5MPrhE6+G8szW/UilAeNocZg3BFxnnHvMgYE2mUdogawvq/DJxETDH8wzMxa4xHxhXBz4MMVY24j3KDwoz541rnrkCLWirb5y6HvkM8zBCrRiXJhcOB80SfPzqq5xgm0eRkoLA5vP0X5DASO8lrY3y6n95TcyzwrnWwAgYkw8ukSdK0hJgWkq9XnxYhLWLOZAoBzXjjHRjJUehmAhhiRTuwqfCMCF+4NxVW1MPnRuX+2RGC9zxzwvxLcrVwxeVgyVveCa7NnT9shjihfg9/QnH2/IlNDw2UfmZyUpTYAwV0sCdqP+lT6nuYcY9BAC2G23WZod+6B7RddMOefMhfXJjx8WaIEdAXTDc5CnbYck2NyiQiMx444r6oUU5rbregk7P362wpKeIub/b/7N59yFC48clvV8sNC9qH4Dble2r7JU5ZhVt8siJH8xbegd70g4UGj2Wi/kaV4JzjJ95TF3mc3j7+mvn6kf2NI8rZwaxDBqNO9GY9Ef4DCOOM0nBG7RGoSAxqFlmlmrU495JNMELa1PUJUc3jt3JmZJiIPRqjZx6FsnaIZohTBATOLSksPc7xCyNTZhms/VXqyNF33AXAW1qb214CTTIgmo4sAPNVbaMl9gVVdW7LV9u6nbJ08yIQpL4Gc2xgpjoj+5LVRxy9KmVCDF+mVejCsIUYQx+cjtGpBi2PZM264dY2PKT6BIFdjHOk6danUZMoZIzPnsbcy0WRfj4HYwt8Rod39VEEV1sEOmLeYrDHq7Li3/Fz+9cMhD0jVTPjl7yfx4MbcsBguOQBEtdp6BmGmHhUZMwFzplv0s8pnr/tb82CuEEqHf2TraKaTF64nHcnOqVzcpraVU5HrbVZ1XWOWYVbfLol6TW9v90i9NdTXvAWVvFJLMhVkp3BwWtv76KiFWYSRZ9awT5k2azNWrYIQnWhmHcpbJ0X6T+K7TzL3l7r0X0A87TuLy6fFcyRHevz+pvqXvhQVukdEwFcPA1mGpAzTLDw8lJvb05+025l98+yspDHUYEetVDjFjEXQHM+HQ1l6EgVX8hs/Yiz17ZjopUxrHCp0IFU447SEQ46VLFmkNCZCFNLSQeam6mLkIrP64XBCK2GafVDaT2yNBqDPXiop00E6lPUOXB5p0guo22rUCyHUQuihUmEPod+E8GZdUtfAayEIR33v0qVRA+cIf+cje55WcbdZdRO0OQlv+/W3fnzjR6FQGkxDc9J/nuchkIUAo4jrRvzTuGGmxCEr5WtOQcQ+QisBZrlW7sn2VpSrHrLpdFsVoqaRrlGGA/SLFV9OuTNQ5BwJAD6FyroPCEL0STHP6EzpXHv46zCDPmgCZX7Lpzp+nT6soJg07K4C/Vhvx5s1YuODw5mVmdwL1Gt3a3jrsxOy011wDBQqFB3ZctcswrNGAzXoAA2T8tM8xnUOddaCK4eO/hSmTSiYrpny4Ig5ry2VP5izmLjO4lbK06O70/TPjhR9q4ojBofXC2MUMN26cT1kNVCBFbhXuVyw2oY9Y0ekCZuGvGLGiwsOofLRkLBYyY8d44IrwR8sWEIx+q3mGleTkn9acQ0qi1JOSqmHMh/zoKvHKeNyXCEGKoBcVacQxNRqGFZBHXCPGh2nPzlq/oAlCYdnPsD+5lZizggfZSywD7F3si8+KbQHo5su+rBizoQoaMu51VirjpaT7++67z6N8gcJ29OhRHxg3OTnp4VTvv//+LmgAWhT+cEjR7gsLC/470tBAiSOSGthVArMIyIMOHDjgvwO6lfQ4/Onkm0PAteJfPNV5gsl9Zi686JvfqsrW5s2bPbgF40IE9xFhz/f0KxQ55omfZnp62h3n7uz8ljHw6zM38t5ZN3sAg6W95k9+/dWrV92FjtpLtD7R+/ijAVRgvZo/c2C/zmG76+TTk5rHZ4zDXyGjzc7O+vFYG8R8act79p0gQ/bb2u5xj33smPu7vzM1B/83fb3jHXX3b/+twYLKTIYGavWpV7z5zxDWDErV0oPGApMawBTW1iLLKcLQ7LS1cp9qy2/5HLhdiOtEuha/U1vnFn0wWq020TGrtt3ICGAadbdtW9PnkM7Pg21Nmcmmq9VabtcuENAwLyvXtuZNthwerdayn2MYwhJCNqKNWz7xipuYGOkyQ5gzud9jYxxkTbeyAvgGAWwNf8iG0KgiDn7wcbj1+I0FnNW7MJ9EdKMZ7trF33pnT60mN3NaWWGuaRVIJlv2gcdLwB8IBcqrtpeZqqemGG/Fm0ZZN4FGx44RGZ7kqmN2Bh0MBm4RzcueiYkZiYlbmpWlB+nAhtlI61fxD8qZco1YExHoWEUEbKMwCy45DEtm6lbLNGgIJi0zPcRcuKWlYYvpyecsX7wCELlm7AXtxJghjQUADNHj7CECUKj5489WzAGvMHFD8zctO8k6EGM35m6wpviouf5WhxpXAc8AMQLcnzB27td610wObrwqgtE/ewlyme5hKxHb7FQvQ8BreCQ0e25GOvDCzc7zOdq9/ga7yj1tNyf3L3uZMG3Q2NqpnHZAaMyiQopko3u/8J0EEo4uKwDT9gLdvn2tzn7aZnMm7NnT6vzGAj3vuIOCVW9015KGwWkDDE6DScG8+lGV7cq0WU1wWlVjVt2uaA24yN/yFvIrrTzl+PiEu+MO517/ephBNnpKlQhrZdrwGCI8IdDkAUsob3hkZLnbX15AnCFGjXcDp0JSKg6GDLQcE0pM/UDzJnhLEcAqACEGAbOBacTM28pWmklf4128aDndnDDnzyNUWLuVFbsGIcGs8POmNXo7kGF8zElgJvffj2BiLS5caHmmnQeMYRptWPFLAkZiIue78fHlbuAX36Nl8Ze9hKHCnEINj99g5hVmOHvDXilP3YLCYFbG4OQnZg1cLzQ5tHPWrNxwvhdTQyNVgRNpvCq4gkwqM7iCwMRsFYCnICr6Zd5xABp7CzSvTPnMLw5cM4x5BKW0JUjxBjA82pCSxv9ZU95tzjhHjsCgE8hYXRNqwHPfUtymzHMALSxQptXu7ywAJCwrFuyI5Sm5WYUUB0MO18Q60GMUDBgTe33qVNvdeqvdx1lkxYLa7oUvnPbKyrUMTiutcYPnXZb+6I/+aK3zGdKQKqeHPcy5d73LDjwePh5qgnbQFh72sKkuqtqNTioFSu5yUv0oP4e8KE0NBhymuFhRkkanlrel88S+bA54tA6VpAwJLWR5+bIbG9vQNdsCJAONjIBZLW0H6wMoZumoe/m3OYjDcWHaYf4z6yZy2fyXVkxDQV8xwUiZi5V0ZH7mDoBh8jlrkQY5MjLqGSzBXVg4DE/d9gqhRahnSoHCFUEuemhe5XPuKawLEhb4DKYII4RhCHlN6hKfw0T4ja4HewMTYT7MQUU5IIxmMb45JMFNQpki1NE65acHe1xMSRH7BHdZ7XATCqRVK/ecgDtgZ1WQRLzIYFFNcFDgXVE0t/mLTZAJifuC32/fvvo4lkZkkhcxD4QmS2+jtrmB6Siy3XLPe+enlMAsCve5iLZv7180aaCMu+rykA9FKovGVmW7qhHgqhyz6nZFZGhb8hX3b485uwyVaVe2rzJUto63xsyqt8zBLK0w1Go45MAnR8ui2EhIHHSYU2Fg8neiEcp3OTFBcQeYY2LWVdcrK2hSxsA5vOfmapm59LxHs45zcuPtk3/8YQ8zF4RVEAtzm3vz3GWaZX4wUjRqBBD5cNkfaaW4PVjnsWPLfo0iGDiasAK/YIYaQ+tlj2DQZvJOI3EpgEyHP205VgU0I5J5XBHfzI35qi8ozzCm/mHCBu1qMLEIYoqq13VXcB5tmEuYlw3xnJCTLaFK4DbUNxcMq6wBQq7D1RIKWiLb516mrftjtUGiY2Pp+zu8ZzDL2x4Z02ZdYuwKNBQ+u9YqF1DevgpKN4+xr7Y078AY9/veh91+SOshfNP4iwfZrmxfZanKMatuV4ay+qJud6x1iyH0oxj8ISt3u2xfZWnbtmV35sxoIfMOx4y1cmlwlidspnIRIC0cvnv22CIofykTLBoqGiIHNuuEiXFYw3wJTDt9esozQ2l1qgiV4Gu3fE43Pk9MipjP40Ah+VwhTJzLyxYVHEOBptfZcAcP2hpg4BzUKuUoCv2zVlLTGBOapGpAw9TQCGEGhoA22jU9T0wsd7VvYYjHY2gcBADBgmr/FYSlvZE2boUv8A8nxVS0X8p917zjimEqVSkTtyLF9f3Bg+bH1vUOMyy47jDlsG9ppTK1s+eElfBb1Stnn5UKZvufhgdlfexPbDa365bAioZMO/m+PDWb6ftbgY4CWCFX29wqvaZ03rPvITAQQie4CRJkspDT8OGrel0W00aoHlSWyjCPe4CkIK1BtivbV1mqcsyq262nL5j3aqv8cDAcOVLLhBxdbV+rIStv2y6U9IvGlIlRzFw1kZX+ZbjMpItxUJtrAfOyymtysHHAoaXBvEk9O3Wq4QOMWLsOUR3qYgY6AJeXqVvf8oc3EeNZae6GRmYme8yxZfcWxiImHjJP1YROfmfrNR+tMXDM55iOw7xxCQww8YUFhKVRX4hkcdEKqQgBLSR+Z7nbrW5OP/sV3iOsS1jjFy5g4UgYZ8gImaeKfsCow7HQyNGsuT5cG0uRosjOqNu/35h21vXWftA+hlFVfW/hhiOoqVobxWY0pzxMb4i5xlC97LXG57pz/cW0GWu1CIbNjPs7ZNpaC9q3UvMUXGn3dzqineulgjisV3K2kNNYJ89cLGCETHuQtKaocqJ4iwIHqgb9eLBQmWo2Vbcr21dZqnLMqtutt68szTuPElNsWmNNKmpde4CGsmbzLOKg5DAHwlNMTAycaGCLABaSmx3y+KdhFMr3heEpkllRwRye+J0VjBUyghhOVQc3lZlMU0sYQ5hKtJa9xZwLcyTa2Zh3LVVyVZooTDoEDVHOMwc1miMHeljCUtRqNXy0OozPyk725iaFfcZxBmlIWJsb5luVOw1haA0S1jS+rVtrXReFXROYt4HMWGlZ8+3GBp7YZUL/aNJKm2Kv9R19wbAN1AUt1PAAJJRpXTGmt4ixEUToU4KgqsAR2R8Gi4UgP6uhWg6UaYiKJt4eQ+ZKo45jBZizBU+mYVqF8Eb2g3O9EsagmfaaGfcrwZAMiDSXv//7v/e44RTtGFI2lak9XXW7sn2VpSrHrLpdFX2JefeLAhdOsVKNQhKKlzSMqnHvw/7ymHe/McVMLM81Obw5QFU/GsaC5m35x41uRSwOYIGs2OGYhjgVohaMWtCZaKd8bv7v9J6RBscBDv45ZCZogs6y9zbU6uPo9GT9yuXW3Go+PSr0d8dY0xzQmPr5jnUJ/S6E9UxMy3W/d7Q3I07aqWvoW8td4YV+Ys088XPbnsqlEea3M65MvTBQGMojHmF44iEkKpYC82+j5VvqXkxpl0liZsZqwjwkhClTgb0T4pwEmHBvkyDC5Pozd+GrM2/uE6wKly5Z5PmttwIpmw/yQ0qVc1M+xiLr+6z7+2IG09baYkFd6G9hpoHN28rHhq6a3v1LP1M8c9eDaa+Zcf/Ij/xI5ufkNH/yk59c75wetEQOM3nbg2xXtq+yVOWYVbcrQ2X6gnn/zd+c6+RXZ1NiLkxr3PH30NLSYmFfq6W4vyzmTa46Oab5fdjBD5wmKWBiDmJYMCulGHHIkUO+YUPD+2Ot2wRwIzZDqxyniAj+EE413LPQTCps6kOHkih0/OAxqSwjDACtnVx3BWWFvvAwol4+f9bG+MwvLtuogCUYjyKQ5cvH7y9wE8PeJufeMM1jIJykuAz58OOZmNaiDRuWfQ12RVubeTebczDm3r02pjTfWJM3DbY3ejvcO3NX2L6xPhg2cQswaO0JKWAQgYqkjUGx6Z09MZ+9+a6Vrsbn2lP2knvMYgKa3XZFuONKiUvW02tKX1xccuPjY7lMG2JtlDzVWkTMS0A3WB0s0n/FjY8Xs8PwmRp0MNo1BWB5wQte4MtqPlQC2VYLwHLs2DEPYNIPgAWgEfKS+wGw0H8/ABb64pCsCoCFefYDYNH8+wGwsI4yACzsFaAqawFg2d5xzLJvEGPTVvtNW+YPkYOPaZj9brePuXb7SbkALPW6PcAG+ECQTz0FBsHBqbbMFaCOIgAWmHGzie/XAESy2kLMGUAKM/nVu23Rks+fn/AMdnR0xX+me4R50VbgM7YPY10ISuUKS6tUVLjMivK9Xrpk2ve2baSNtTpake0NB6xSogy0wzojx5roXjDAAYtBi9OeSctZWVnqwGqO+T3Yvr3h7r+faOaEgdttYQAyjMetQP6wWQsAzqC9wFhqHca55HbtqrtWi9KfaHxcB/zqI/7arKywHxzqrAMBRoFeNoZ8oXZvGPDL9u1UQgOfHWZEn1RaA6gGsJu6Z9aMw75xzcANqNXYazEPuz94b2sZ8cA0VP7iHnCu4ddj48pCUPfWAlKwWId9xpkD2A4gQYqI5rtlfy8SCMhfoq/tPmS8UV/9DPeO5g/jQiOWOyERsBJhjLa04x5eXOS+rHWenaYH2jGzss1ZxWC4HxAoVMFt8+aWO3my5RYWEJ4wl4f3t0WVHztGIKLKjdo9Qt730aOkpHFf17oALMvLyz4nnXazswtueTl9fxuWA/uDUKJ+24FgxfUgFzwBZQI8COI+xCzOusPnnjFBWNMct2xZdO32WOqMqLoa40AAWN7+9re7X/mVX/GH80OJygKwwChhZv2oynZl2qwGgKWqMatuV3YNqxnz/vvzARSEUY1vN9a446pjPPD9UsLKAk/0608gLWNjzcIxVV2MQ1sakggNk4Aw+SaVLqWTgrVdumQVxvhuwwaYQdNrf9RmFsXaUqIlm9k0zwyqvUXbYy9IIxNxaKIpwWStqpgxbVXjgpSeJH983p5pHPYihohFrg6jrLEaoDkLt5rrhYBQ5G/XmOE4Mm1Ls0WDZ+78lZCk4hkICtIMkcfDcp0wUqGW6bqw/tnZFTc11ei5h8I5QBKAVJ2MYDvBqaYDzto+2C8PXMW05Ja7fNniFDQP8ACskIp1ZFXI2u7AgZr3n8cE0xfbQFCNoYkxkoWR4+1226OY9SOuQb1u+etFmR9lnlHaLC3ZD/NM5AieX/EVm24cAJaQnvjEJ0al+9r+oENTgXEPKZvQdAfdrmxfZanKMatuV/WYu3ZN5AarJabYdCGPuBBBAqVaHRX1J7M5B0zROSQ87ywFgcOYg1raZpgqpOpnW7YQuAbTb3jmiXa0axfaXiPXfykTL5oMcLF5pL0lFc2AQBIYS8yuxiAsfYrviXiH0Ya446oIpsM+LJfaO475OEH2ShhP0hcGI5hQmJBQJqhK1yk022OilxtFTBvBif5gaKyNtspNRliIS5CybuZC2zClib3g2pNyF8uusT8fvzrz2rLF8rTZPwSD0MKioEJFlmeRgt6ErGeBX2mmDVmQV34edL/EC32fNo3Xin/U0byFGdCvXT/qx7QHSWti3C960YtS7zEPYL581rOe5R796EdXNbcHHWGiLQPxWWW7sn2VpSrHrLpdGVrtmEWR5haZjZlspFCaxyxYZYBav/44WDDtYTqHsiLOxUxIRYJpiUkrIpgUI+FmG5k5N/Y3yrd49ChFH8Bvp8Rjve/8JyaK9yNrb8fHW575YCbHRAtjYT4wHeUyi9nyHk8QmjK/XV5eygxk47xGEOG6iTmZxmkaKD5StF5D90oC1swnWqyhYX7VmEo9U91t5WHDtHnPnltqmDF4rB5o2cxNJTXNOGpZACo0IrAZMXUVyIj5kNLKRKyFut3sJXNRIQ1+q0hq1rpzJ77wYsGz2cQlOOEtJEr/C5m20gDZS7Dis6h/tbs4R3up1DNl12A8Fxo4bpdHrGfzZnOv3QhUmnG/6lWvcm95y1u8v/Orv/qr3dOe9rRKEaGGNKQblYrTxEAbuzFr9fRLF+MMAnBlZmakC2kpxgcjQXODQRhjMB9r1gELQ5mYOOV9+cvLu7sHLGlO66Omm5pK760sBFeutDrpbOb/tbSmWmEEehbxvczu7Edcs5ooZ5itwbEmOeftdmPVuceG150u6RmSzNOatwBWYgqLfnBtQsx14gKIPhcJmjYJDLQI9jNnrNiHvkNIEFwrx7osJpYC1d9ihNWF3+FzVnCZfN0ICXxWFDTHWIqTiInPhYaWFYTWjwRLK4sD+4TAo1x1E8Sy1xjmaWfhDVwvKn3i/OIv/qJ7zWte02XcBCsRdDWk8kQA1qDble2rLFU5ZtXtruWYecybikX9qEyb1dBqxsTXeOoUYCEETVkUcch8Cfjh0FTkd55GYubt7LFg3AoKwlyOWRoTeh4DL7sfWe2YK8wF0yyvmZmW1xpnZig0YuMhrEhrlIkVUJJiE2wCMBMy79D1IWAX/S4rUhyGkMw/PabmhHkcpkEffAazIvgu3F/NK+2qrvV8Fv5fgCjsSygUEBioGuIQ1zbBk0/GRdvnxb2QCDvFyhlFagjQYh633GJBZkr3ClHZrDJZvok5dFuE+4/gIjxz3DMw37Gx8vcQQaRYNUKmrewBxlKcBsJ3bE2KwVWqfo7XQ6VnQvTzL/zCL7jnPe95/kH92Mc+5iOZs+iZz3xmlXN80BDmyzK1patsV7avslTlmFW3K0PrGTMbGjWfoa2mzWqo7Jgc4gKJwRwO8MXJk+lKWuqrqFjJWtaQmNB7GXjZeNisdsxTNZ1hdmiJVsnMDlksC0RpcxiTSpaAhfB9tp/bvrfDnPKNpD/JnJ0XlEV/Bw+m+xPcakK2YQJQoT+sGDBN4YJDMEk+DzU6zYvfKTda89TcBFsrgcN+ZyZ+hAzlUiPYhKU/ua2JC4BxQWJeWf7mvH0La2fv2bPc9RHHddZDktacR/KXi8k2m3YdiSLnM+WNT04iINa7hVeKiGcg9O1jmRE4kK1PLoa2t9go2DALEa3q53ggjPvnfu7n3Pd93/e5t73tbT7I4Ou//usz2/Fd1RCPDxY6f/58Nz1pUO3K9lWWqhyz6nZlaL1jxszbUr2KH6MybVZD5cZMmwc5mGDeGza0Ooe45QOXndta15DFwIl4L9MXB3ZWO9Wl1uXB9KtUNIQToXVt3Wr1sQ0Mh75GC4BalH9ugXPy8eboJp39GE3tN9jfYS45aW4gbakMqfDNFTgXkup4sy6VUYXhWyqV+brDvHP6krldIDPT0wgd5tJgHlgRiFMQ0w9LtPJ/lbeMkxjSgCi9+yamrbz7hYX1Xc/02PR9xbXb0/46LC8b0w7pagdBD2Gnn2/chIVkgVyXLHO8+rWI/2wY06qf4/XQyGoC0niRskKY+xe+8IWhqXxID0laDTTqakimXbSEomC3MkQhj/jAEyypBaO13IYNg1MfQgYOnjZFHdbqA4fZcsBaWU7yeAmMS6KaOVst6rvl24LIBsY4lgfBqvYrDZmVHVDWh6rf79wJroLQwEwIQPtVac9QkyPNDUakoi3MW4AkqiJG/rPlFRtTRlPGNC9tnr+UrZW2LQAUUViZDMYdRs6H887z3ccM+1pRsznp17N1a7ObItYPnTCP4uvXz9hDBgNj3wiR40W0avGBPNO/+qu/8qAXN4r0cbPQEPL02rYb5JhloVGhspGotdqkL1qSlV4WdlGmP8BA8ggGTpUjC1orN7eqomkTBp5ECecx8LzI8zSzrXchVtEjOJJ4haloCaNpdWFVITHxsDQkACcwxLwc83g/snDIE62wljLnhoVDeClHXq4Klc/UuAwB5ACmbYHiwKxYL3PlM5i9zPMwJcZTmlwWjrjmkRVXnCWssM7QLJ7FtPtlCJRtZ/eDjQXgCjj6eVTzlt0yYxpaXujjjmMO1B/Qu1Ae075RIsqhNXHeZz/72ZnBaQCQ8NlDxVS+WuQ0EMduv/32vshpn/jEJ/xv+iGnMa7GzkNOY0579+6tDDmN8RirCDkN3Hrm3w857dKlS108gCLkNH5/xx13VIKcxnjscz/kNPbxyU9+cmq/9+3b1y2gwx7cfvuY+9u/vepNiSBUYQo0BCVDQxNyGoASpAUVI6c1PbLY4qL59eTfxQyK1sVBRpQ1c758+bIXmkPktKRfAoSanUe7kUJzY+3ms7Q8XkzWc3MG1gG+dR7KWgxgMT5OKtZSB1kK5K66Gx/f7up1lTUESWwlQKFKo89dvjzvtm8f7eQ5U9rTjiG+x6Rt+5L2h9OWz2CsjIdggqkXYigzHfM7qk5RqYwANczmwI6CdrXgI4c5sthDbg9uH/okN5zoaTvIQdOa7FSvMix0EM1sXxqd/QbpbcU/22jK0mox7bInthaECsNHFwJXtn/Uro/tOWNyLW0/2LelpXEfiR4CoqhcqoBSEkAYw30XgEkeilcC2AIgil0z9g8T+8rKYqfoCvn5je51g2FzXy0s2D3LHnJ/m8+96c8ToaHxLHANaR+25T7l//QdI6exV9DevS3/vC8uTrh6HUaZIJ7Z/Wv/5y8oZnn3LP8npY/naetWwxbA4mIxA7VuqVgV0Nmxw567rVtBJWxEyGmgzy35z1gnc8q7v29o5DQmx6EfM24gPdFUOKwfSlQWOQ14zzI5xFW2K9NmNchpVY1Zdbuya7gWcztzZk9hGw6tftI6j8w996gaVy+BHCWzYJn+FhdXPEPMehRDZLcw3xuK08ZkBg4R4hIsbjPlMkazuew2baq76emGPxCF8521nLz5Y0YXoYUvLCymtDSZjoVABuPiHLWcZNO4kfsUPR0CpQBvGWrJYaGL06eBKrX2jUY59DqgTGHqmLGV4hUTwVX4vaURG9JbYr5mOpY2Zel6MQob80PmTczFib9WzFspe7bfoMjZ/GFaCCahj1vaPS+qdO3ZY2b3cMxYu46vQRaVaZPXTlYXWWNCBEHWEKK9hTQxYShseVaR+L6VNUKgieg39Cstm2vAfhQ9VmWeuxsSOY2ocoiN+I3f+A2/uSKkq4985CNDAJYCQnMddLuyfZWlKsesut31GJN2D394sc+7jBReFjmqbH9ov2GRjTxzKH3JNBjnfBebgS3ASrnfu3aNeAal4iQwEsYiejmO/s2bfxzIBra2fisYUM1F2iZ+aYwpMG3gP2HaYW0Vvmeuu3eP9DB/0dSU5YTD+O6/f7KDRNbrD89aQ5E5nX0JZcgw5SzMmc9DYWOurId9Dn3V2g+OXwQB5qvrKqIv+Aafhzn6EnYQIrA68H9+hzaaZQ5Hq82iUPgh5UpCUBHFfRUVCNEa8u7hXZ3Ss3lzyyqjynxh2AhIvIjzgEg3M8tM8QIGpU1Xzrjf9a53+b9IRe95z3tS2gEmA8y+fD6kbCrSxq9Vu7J9laUqx6y63fUYU+2KAtbKxIKUQY5aTX+CegxrMGcFu4V9hYAtOqgS32CaicFIJLfDIEDhUlUtjaVSkGiFoaLSb/4hGpsOd7DNE5zttP9W5UjjFJ/w+2bTzKUx09b3HPT79tXctm2Xuhq3/OGC/yTnXRHjWkMYlR6Tosf1vVLOBP6hSHPrr/f3igyP88sFRYqwopQuCVOzs6MpjRPGj4+cPlTKUn5yY1jG/LdtI8+/NzCt0eidWK/wU8ut4pXVV6xlF1FcR1z7VS/goSG8a3zfhpHjYRBaUUyI6EaK6VqVCIEvkNdXfdVXuU9/+tPd97yIMv/gBz/onvrUp1672d7k9EBebb9r2K5sX2WpyjGrakd08ec+V3fz80/0f8OSkoOcG8w7i+R3KyKKXOB7yyIO/lCLLNOf2ig/GyYruM6iuXGQ6TDDzytTYpZHTR+pwliW040oaWOc6THN52jfm6m997fbt1/1BzsvotABk+GVNU4697j3e9YiNLIsYo7xtqJ5sn6r8sVcKQdKFbF2p1pZohXG0c2CCxXjAdQEWFGEGGQ9GDoM1YBYsq8nfYvZ05bf0gf98TvBi4ZrEESsNE6+h/kbnCk+XkzHwLhS6MXS586dI2jPTPL8DfPJ4/sjS/ghbkLCT5HliL7EtIkYB7Uv79rH+xDfw0sFz8D/396ZgNs1nf9/3Zv5ZiASY0JqaoQMIgip6SE/c2npoLRK1VCUirSlpIa0UmO1OtAB1VItpUX9TaXUUCJEBiFBKUU0kRIZbm7uPf/ns3bec/bZdw9r37PO3nffu77Pc557zznvWe+7hr3fvdZ63++qHgvtB4Nw3/uD0NJcU50BHXqEIKrcwaEzgFixiy9Wau7cBvXRR41qwIAGNXasUtOmEeBWnFQxj1ikTf33v9VpXGnSkmyCmxp2wLqG85JTx/yQyUxyik116k5bW692e5dh0fN+DB3qpQaxnLnhhp7z5hASgb99wranvWM24+0Mfl/tpILR1I1q2TIvoAmw9Cx53BW6UByaN9OXFLU0/eifzbN6QbAUlKW0JyQ0lMXsXVLBcNDM4mXGKUei+vuI1RTK5bvlyxtCiEg8Bxx16lnSw08Uxay39dFTbbihd4qcP80rqe/TokdMGydFjhcFHXLc7GffeOON6m9/+5uOvpWEdcHDDz9sy74uBdO8d5tytnPtbeqsVY6ZNU573rzqz+fOVWr6dLZ2PPKKrG0LOm/TJbZSaZUaPnyAammpOICwAC/TpXITxMl5p0R5DtxjgIU+1VukY8YoziDoKOVYTYGfepS/sLdF7Zv7HYbfNjnNjONDvXPEe6jNNvOUwJBGuTgMOfrSDz7HKVQOTGkPyf9eu7Zp3TnjlX3RMDBTZf/e76RkaV3oRVtaiPz3nDvZAZQfxsQWRcca3OOVM8JpI2b87F3TFjDFYT/L4UTc49jFacvskln+uqQM/RsO+yAmQBDcWhAHHKT5DHv44dzvqO/9y+I8mP7nP9XpjsG+N91G7tnTW1kI2waqjBVtXZXTpnwiyMPKM9HZWdAhS84880ztuA855BA1evToxAhMBw+SQpClnGlZprCps1Y5gmuCTtvvvPk+6Lizsq3aeZtSfFYYsRIkTUoz0mnCH81NlZma5Lp6UcleJDZL5Nw8cUgse1ccYPXJUBLrx/9hZ5iHk2qUIp0Y6XM4YtLYuBF7s9AG/TBXHXgmEfDMNBtDD7Lw9jo9RygHZACJWGepun1ONGlU1dzl7BP7+cf5S7Ce59g8G6lfcI+WWTTL11FgNk8cgTcr9qhsqYc8NEhMAkvJm2++Vs9sg8QlQp0qJ4356+PPZ/bXx7OtZDCbrUS7y/dh+9jo9udTUyc5QlQC5/wPNXEnerW0kNkUvWpTyfP3bPM77eg6JKFUbMd96623qj/+8Y/q4IMPtm9RFwZ5xiZBTzblTMsyhU2dtcr5z0cOQ9j3WfaBOG/yu8OCfDoKk/JMdSbJVc5b9pZpoaTkBrpkiXfn33xzaEW907m8wLZqp43jw6HLHn2a5eqgbcFAJc/hrNUOlJvx++/30EvHXtCX95kQqTAD5nO6Clt5L0vL/CaYRuZ/kEA+GDfh5RE3hAZreSsCni5Pb7XX52GSvWpxIGnSqdra+ihoBySqXkBb8Bkng+HAgg8oEuRGehj9I45aMgCC4HuC1ghQRA+/kTYnf54MgsqJY96DmJziha6wwDOhH/Uf9OG3kQeUYcPot946r5u282hsPfslAr5nz0rgXdTM3T9uJbc+2mnbvaayQIesIILc5hnPDg4dARd6Ld9nAZz3Y491ovMAOwBvFaD6DPBKCpl39/7oo/fUJpsMUc3NkG9Uopdx2v7UnTTR81Hf+wPBIMbo169Pu3QynAnOhoA/z7H2qLrR4wS4sXtkN97MOAi+4zdBynovJ7ixXMf2wVree1KPoDmtPngkfi84CdK2MlOV2ao4Ym92SiogxDYVxyjndxMkhwztQ7pb0Pl50fOeo+RhhWhr3vMQIisPXuCdl34nzhunKA8AceldYQd9CGgz2rJ//97adimbdsLR00d8vtFG1el0Uas2XnCfxz1f9D1tK4777LPPVj/60Y/UT37yk269TJ6WOY0nbw6vSGJOQw5ijyTmtBEjRmi5OOY0ykK3LeY0ZNERx5wm9icxp1FXsT+OOY124W+QOa2pqVWNHAlvfm/NZgX5AX9ph+22a1FNTR+q//2vVxVzGuxnlJvEnEYd0BnHnMZnUle+o29hm3r//QGqubmf6tNnpdpggxVq5Mg29dprvRKY05q1/bRNezY0TxaI84QMIp45zbtRxTFLCauaEEvEyaJHmOCoNzqR3WAD2KKwYbBqaSEXFtYu9llL63ivIb9Yq5qbK8xp3v6j3Ei5f1Q41Hv0aNXkMSIrOtHvHTpRzVjFb7HTL7vxxh5rGM76gw/4jeyrtpUZ5NCP4xk+nIhxHH1l+upnmyMFzPuM7xvb5UHjRDwHRP1gdKssG+MooxZtvGVgj32OevE7HkK8fqRvvG0YxkePHjCP0a+99IwX3R9+2KZl+G3FZs9pe+MB5w17GCxgnhPs2RNdyNF35Nj3Ue+8UyovJ9Mu4qT/+19vhYWihF514EAvqt5jOWtQm2zi2YCNm27KyseacvBhZRx6Y5bxDfsd7Gwe3W51e8hyPQ8MAwdW6Ez9D2q0tcz+Bf6lfGFWI3qenHzvoYQHFMZFb83WJo6cuIK48R3GnOZtj6wtLnMaJ4MRWc6Nbocddmh37uwdd9yhuhNMmdNwPDjQJNiUM5FJw5xmS6ctOfwogWhz5lQYl8aObYiMKq+nbRLh7t93HzNGqalTV6oddoiPNvczRiU9DIujitsDFJkk2JDjprV4sfcQ2NgIi5ycVhV+E4M28913w6Pn/XucJrYlyaDDi0ZvLNu1ZElDFSMdkD1hoQv1g1mqlOXpZBvGc2zMxpklBk3gruqdJe3N2rmfS3Q4lxh6ZcYdV4dqtjjPkfB7/wzY34YbbdSsmpo4H1tWGqL2/CtjyH9+No5TthCogz84z2Npq7gLTN5889ZUY4izu1mFkUA5/3K9LIsPG+Zx2fM5tgr7m3fimtcfzCOifCRtK9sTgwc3W7sOTGQ6JXOagBlb1LGeDrXnAdqUs517aFOnDTmcM9HjnIX81lttavhw73SkYFBavW2Li3C/5JIe6qc/tXeqmDc7i2ZFw/nVcuZ1LXKkbTF78RO5BB14Q0OL2nxzZmHxp6CZ6EySkZlbc3PrutlTD50LLg5cotElIh2qVWbUcua1UGUK65Zfr/CPsxzO770ZcCUAjYUynKBEs+PIiUTHCfkfUPzOMGh7teP1ZsaUhzPz772LQ/Y4wHsnks1wnnXw/Gz2tP17+f6mJYLfo2f1AgOBl2mQbgxRbzlgRSBL/VKMOGTa1U/ZKli1ynP0/r16AasA2ChL48RfmNpmQyYrdMhx33DDDfYt6QZg2TxrOdOyTGFTpy05nPTo0WxDPK9Gj2bVoDFz2+Ii3OfNayhHuNtw3hzsEXZTrk6rMdvCsiGHAxs4cJBeDhW5II2q34F7S/zJRzKaUbtWywRXIYJFiNMBQ4d6OdY4BvaEcWqkqomDlmVpHGT7qHLPaeNAcPj+fWzIUvzpZ+JcKB9HxIzR/5ASVc/2OdMN5fJwWtiFLgnCYxkcgpXw37ZnkgvCb5MQ3Yiv+uijVj2u/LEAftpcE4gctgpdbhA8ROBsGUc8MAVjDyhi+XIv7oCHIv+KAE6b/vDvZ9u8DkzLygIdDpFjefXvf/+73gs9+uij9f4mh4ywPODnMHeowOVx11cuD50iFxfhzvaD/3thWOuoA+emG3VTluCcvn3Dc4Pb21a7HI6bOAj//q4gzIE3NdWeYx4mE7YKwQ0eB8ApXkGwn84MGAdEQBtOfPBg7/QxmQXKw5DMLnEcXhqTt2SNY8FBy2wU8Bs+J1qdfhcGNHG4wcj6YK60ICgX3EKhTBy3/33Ub4PgAI8giEuQfWQgKXe8l7S+MDa/tJwBURzksrXw9ts4YOIOKt/Js4EsqS9Z4i3dC0cAMQhh52jbGN9py8oCHdpJJ9iJYxYPP/xwddppp5WDpy699FI1depUVU8QDEZAF8FV0KtyBGYcbrvtNn3wCfLYfO+991Z9z83mu9/9rg6UYvY0efJktWjRorrYLkdNZilnWpYpbOq0LZeHTpGLi2BnOT3s+yiK1CQQBBYHCXwygW25KAiVKi8cuDjxWnWKTNSBKMyEWXLlNCk/gox0UHCuXs2rRUdIw84mFKselajnWGSmxx4tDwo4ZpybnOEtZ2zzG3FsciqXP/LbDwlKC6L91kGbcQR+UnQ+wY7kWvtfRINzfCwPNDhtOSSFeZjUO6ztOjKGJF2L/Wj/X8omgM0LyvPazL+VIm3YpgPHPHly9MOcdkdtq0WmUztuCFg4q5gIYP+SIvvesKnVC3/4wx/UlClT1AUXXKCee+45NW7cOHXAAQeUI6mDePLJJ9UXvvAFdcIJJ+gzoj/1qU/p1zzfmuZll12mTz3jcJSnn35azxwok6hjBwdTsPxJIFoYCJaTACcbzrvWlCrb4OGXiPrVq1cZ7QMSMOR34CZOPAn+gyWqbfOIVUhJCzqJINlHpSzPccmyOs6bSGXYyKifnyZU/soBHnJspv/7jvYP9kWljAkbnMlv5QFEXnL2tPDAyyvcobaVA+nIPQ9ru44ijIOcaPBNNmkp62QGLoQ1/gWHfv28ADTGjp9fv7ugQ1HlpCnhFEeOHKmXyDlwhPQdUn223357nT5TDzDD3mWXXcfLL40AAGeBSURBVHQaGqDjSHv6+te/rs4555x28p///OfVihUr1D333FP+bLfddlM77rijdtRUnXQg0ttkpYBoQFKTYIY76qijUkWVM/sKiypniYgZPzc3AvuIHo7bB+LJDjkQJ8t3pFwJaPcg2QO2sX1BuU2+u4Bflm0PVi523XVXbWucrJTnh39rBFnqGRVR6ZclHS1uW0W+ozzaL47NjGhPxiSR8chFyWI/bSb7bTyghclKPWkHkaVfgk/d/vZYvLi/+t73GnRAGrMjxhfO/NvfXqO22KJ6mkV9ZOmQMmm3mTNX6wfH4JKoR3VZSfthxv322+1pQ0G/fg36xqqUl5YVd3lTJv1KMBlycbJEW8tSYVCW72TVbejQDdedTFaxN1guaUJysiByQqPq2R8uF2wHT29bWWbFisbIc7EBQYv9+4fXjzJZSieynPJot2AfjBjh/ZYzzqU6zLL9w4FgNwlow5Hj6IJzCuqHLV5aVqWepEr594ol/Y9Utnfe8dLZJCBOjksV54msBHDxvxyf6dlBH1f29r3ZMvq9tMMg/Db42zcMIosM+d5ebEGFpMdfR2SlrOA9yg/vwWhlObsibPtjk3V72XSR32F711x1eX77/XULygbrGSYb1RZ+WVLx9txz/c4ZVe7P0fOD/FgceT3AzW3WrFnq3HPPrWowlrafeuqp0N/wOTN0P5hN//nPf9b/k7+L86AMAQ6YBwR+G+W4yfeTnD+5eYOf//zn+oYcBDf/s846S3cqTuLKK6+MrCc5j6wQiDOJk+UG+e1vf7v8/oorroi8+TLg/A83V111Vbs+lHbkgvnOd75T/vzqq68u5wqH4bzzziv/z+qFv23iZH/zm9/ErmyILLqvv/56tZyolAicfvrpelzSbr/4xS/K+eBhOOmkk3QuOKBccTph+NKXvlRO9+JhTvLZw/DZz35WXXHFSPXmmw3q1VeXqB49Vqjm5oXqzjufbfdwcOihh+oVIwALoeTYjxt3TLtyCfyS88G9me1qnUe+dm2PKhILIapobITHuaRvHqtWRT9ENzX1L+9NM4ZXrgyE7/rQt28/38Pk8sgHyiVL/lsly4Pz8uXr1lgjxvtGG3kPu8yMP/zQG5OrVi0NfTjbYIMh+v/m5tVVfdyv3xBNgynw0qa82xv1I8d48eL2ZQIeSAYP3lA7NaoV7CsvYIrVhL5q5cq1Wp4yyTsnH5w+GDjQO/xEIDPvoUMrDwDQlZJLTR2bmz1egYo856Kv20jX/SzXGxkSjYGzwckqgLHOeyfXZp8+iwPBeF7sQd++g8rBeqtXf6jef59+DrtPNFRNBBZrMnO/HAFjPFj20Xvk/fr1XhfdzUNTq84H95xYY5mkpaXlI7V69QpdrjzEsUIaXPb3Y8CAgb6VnPfUeusxnjydPOAohSNdvK68yiHk773n8S/4beThgTMAsIF4FHGy//3vknU52eGQB1AAl0PcpIExKWliy5a9r7JAhxz3/vvvr2/m3CDlIuFCZgm7XjSoNB6Oxj+4Ae9feuml0N/glMPkhXjET0ASJROGGTNmqIsuusjYdi4s8qRNaDQZIP/4xz+M6DZpD8oVxM+Y2qpkk556/bJhD2l++GWTOL79snEPA35Z2i3uYQDMnDmz3GeQvsSBbRZxhDiWOMydO7dM/BL34ABefPHF8rbNm2/OLhPPhGHhwoXl8vwO6IUXbm7nvInWlr6FGIKb6cqVy9SQIf3V0KH+G1Sz3qNta+u9jowmvi9of5wwNI5JfeHJfmS014eNIgshTrxsa1m2T58W7ZBbWzfSjjjowBmHIhscZ9S9qamfbxkbZ12ZZfJ9FFgxWL36I7Xhhn3LUeYC79zrVj1OYGgjd1vKJXVqww09MpLly1v1LLOtbe06qtdW1dbWonr2bCo7zZaWFdqBeM5QVn0qDvmDD7gmS2UnHI2GqpUq+jAckJCs0GeZe22G445ewfPKivoeStPB6r//7bmOsIWHjVa10UYcp+txgpP/DuWqnFDGbH/w4P46r5z2E3KhpEVexpfYgYPHZtreO7Bl3QHkofaW1tnof6DF7n5qww2hUaXtGxLvf4BZvzj5JFnuN5VYi9q3fermuJkFMnNlWZynf6LKCegaOnSo+v3vf6+6Opj1+2fyzFZYsj/55JPLM42wpXJmVVtvvbVeqo8Cg4VZHXIgThaHQh8Idtppp3aDjOV7GNaCy99+WW5EOD62ITyGqmhZKc8P/00EWVYygjJhsmy50G5REFnajYfFpKXyf/7zn2rSpEl6yT/6YJI31KhRo8oXJbEaYbJST/9SOeUGnZa/PaJkw9rMv1SOLBe/vw/Aiy/KEp/nHAAPHDg32Nbao2dZlgedQYN6x94kkSUwilnvgAH9I/OJxQH36SOrSdWyjA1m2mDIkKFVS+XcTIPl8iAi9lcvf1fLsoQ+aJD3UM1CVvul8kFVpBj9+nkHT3gHaTRUBVLB5iVlBeEvd5NNmvVsTZwt56TTp/37b6QfkAYN8rYohFVr6VLYBTnEwls65yFIfkNqFtSoFXv7q0GDvAdGnNq773qsZcJWJ7ayz9vW1t5Wf139y7NyrcoyM+/97RQmG0UmEpQVOerOyV4MaW/YegFkiLPgCLsY3/lvP/QDKXfLljWq4cPZ6vLYCv33liC80+hWla/9xkbai715mTx47SJjyG9vv34D9HGxFRu98tasaVDLlvVUw4f3KccXVLIg2o/JYDuIbFAmTNaEcz43xw29I/vaHDYyZ84c/dTD8u4xxxxjPW9YwEOBR+xQ/STKe//yjh98Hicvf/mMqHK/TJzDZPD5eZsF7GmEOW4BzpibWpwMYGDLTT1OlsHtT8UI21MJyoTJ4rhwJGwTJMlGleeXhU3PJEUEB2oiJ+0WB+ogVJ1h2xVR9kftsYfVk/fBm05Ue/hlk9qM71at6qP69Zuo5s1bXw0e3KBTXSZNap8yxg2ZpegkdjXGpwkdcUXOO5QjCo2NfX3lVcv69ciDn+/bduVG218ty3GVIibngffvX62Lvpay+K3/EBJ/NDLL3EGdwZxvL9WIGXOl7diz9ctwm/AeDmQP3/ue1C/vllB5yPLbJvaKXjnZyks7awjk4dMO7TsjWF6wXNkD531U34tsVFlBWZGT4L/gTyTX28+bHvyeI009qlivXaP2zKUOSrEl4TnVyh52j8Qx1NLS/lhXf9vyvVyGwd/GXVPyucl1lxXlaYe0MMvmZvPFL35RR2X/7Gc/U1/96lfr5rQBT30TJkyoilrnSZ/3u+++e+hv+DwY5f7ggw+W5eHkxnn7ZZg9E10eVWYtiFt+r5ecaVmmsKnTtlweOm20B1SpU6Y0qpNO6qlYyPnKV5Q66yzvc6LOg5Hn3nJ5PExk6iFXL53BVDKJRA+WFRapHCbHzguzMwLS5C80mz16NOlANW/5t72M5HQTrBYVoZ5Uz/YR8CWdLkh8Ajs4OJ+w3Sl/eXxPGdjJ36S8bVPbouSiyhfGs6iFHX8KXJJO+lQcNsdwxkWKrw0pK8xG/6w6ro1sXlOddsbNJj+pXzju/fbbL7OnDJanv/zlL+vlTZYX2Wdn7+T444/X3x977LFq2LBheg9a0tb23ntvvbTP2eGsEDz77LNVe/Pf+MY31Pe+9z217bbbakc+bdo0HWlO2phtmKaY2ZSzndZmU6dtuTx01toecVSpcLBD5yqMa4AZeNyStsBEph5y9dYZJHQplXqEHksZV1ZYzjcOBmf/5psckOK1OSEIdJ1EMAN+w3PYpptyyE387TOOytSvlwA28s1lX5ZVAhx4kLtdyoumu63eSuiIbVFyUZNkOWs9bGfKT9rC78N0+lMB6VuPs5+96HgSr7aQsvw2VlYBvCV9yQdPU15HZLJChzwu0cDspUDAgqPE+eEQ6w3Su4ichjCFpezZs2er++67rxxcxp6vP+qX/c5bbrlFO2oieG+//XYdUT569OiyzLe+9S2dTkakMfuLLPtTZtxya0cRtrxebznTskxhU6dtuTx01toecVSpOO8gDwwO3OTmbHoDtyHHd2wHsCpmQgtpQ6c/d9ckF9xfVljOtxw1yQxWztMW4hWWWP0zSn5LRH9H7fc7EPT6nbZfB7eyaiffEEk0I/KNjWbsXmn7gPYIW1Blf5v9fZy0f/4mZ7HzvbCsSVnBVZOO5GE3hNgvNsphJf4XD19x80ub11SnzeMWEBWLMyQg7eGHH9a53MzCcazdCaang7EPa7Kna1POVMb0dDBbOm3Lmdahs/XBY4+xkhR9OthVVym1117h5SWdNmZyo7Ell+Z0s6iy2u85kz5lVpbsgVfnDVf2ub2IZE8G58zStx8sU/OZOG3mAv68cCEBEbA8nsTsHFdPdOFsRW/F7uoZPkvx4jApT/LNo8BpXXDCd7QP4uTiDrZheLM6wUscJ06b+Q/fr11bPbOudRyVIuxHP+3pDzaXhwg+I7c+bOZt0h4mMlmdDlbTGjc52yxTP/DAAzpIjei7NGlS3Q0Q1GQtZ1qWKWzqtC2Xh85a2yOOKjXqeykvbP9b0NkoT5PKCttz5m9CFmC5LJm1wW7GRytXcs52W7mc1asrziL8xl35X5ZW4wDNZtp6yr60OGyciX9/OOi05Tf+8pL2sk33umulKcUB8j+zbUmBEq5x/g4c2KaGDWtTG24It4K3X73++qutMZytibBfjlPFNs/GkuZzh6qBVY2oaheN8rTDh4zI3t1dd92ll6Nlyfqb3/ymPescHLoJVeqcOe2/4/MoqlQ/bB0X2lEwE2GGQa53GPNbEqKXf0uaNYybsAlNaKUczzlAkEKQk1dWJcJcllSDe9xAqDW5RzNTCyOB5Lc9e7almvdEzVZxfMxKJSI72HTBeudNdysnrrG8L3VhH3nTTStsZiCKO7zeaG2tPvAnOEtOG8TXWdEhx33//fdrZ81+MUt2n/nMZ/Sse6/gmp5DFchbzlrOtCxT2NRpWy4PnbW2BzOUadO8ADU/ASBOm8/DsgHDygueOOYxTCXDhpyXe+vpNdl5C5YVxTNOWpWcdhaVsOIvK1iORCiLE2cWLsvdm24KX0JFnt+y9E3uNTd6lnmZEYOKg6qce520hO/Z5t1e4/al4eth9s3+ehCyNyxbCGvXekxlwYcOvzxUppzHbWpbWjl/XeShCBALwKpG3EOW6Vgzs6tH6OftdVf3Uy222bQ/F8dNRDmUjTfddJNmSutlEtbZBcFJZbyESQniEVi+iE7/z3/+o5dWSJEjCh8SDiLguSlwc2M/HHDSGelCrF4QxEQ+OUF2fM8Nmr06oeSEehMGOQIDCQRiq+KVV17R30GBST8IcxfEJtjCe/bg+a3QapIbTvCdnzmOfSW+p1zs53/sZJ+GWZQE/bH3JGViG3EN1Js2wB7kxX5S7SBTEAaxbbbZRi/zskdLOfxe7McG2ktYxCgXCl0+I+0PG4TBDLpS9FE/gL3I8p52JytATu0i/x/QbtJOyEp7I4v9gPgEckxpb/oKnf72hr/gNXK01pXDZ1JXvoOOlHbkZke/St0ok76R9iagk/YjRoTvttpqS3XeeUvWBaL1UUOG9FADBryvSqUP1YoVm2hbkGfskNNOHdFJGxJbwXtAe2+99Wptx1tvra/69eup208IPrBLlvv43+M9X6MZsGiLOFmWQ9eu9datqTeza08W1rbKDU2uBSG1QZY0GiEs8Yg6PJ3ejZAcX64JZmvCrV45HhTmLO/0LZwuslJuL826RtmcZgUxBhzu3nODF0ksDxHoJRCKU8JgZ/vf//qqlpbWdUvV6GlUjY18Dy84+8jesZeceY5TxVEzY+am7xHuNas1a9pUnz4Qk1QY2oJt6HFtN6u1a3uplSt5Yqg8SMh9wCMp8Tjmqx80vBkrXcAZ4RWiFvrZYyfzL/97FKP00Zp17Snt3bvMOij53fSNRw3at/x/mKycm8Bf6uBRjHL2unec6IoVMn2ttDdt5x0A0lpeQvfapVm3k0dO06PM1Md14afQxgb+xw5sYIysWSOyPcvt3dbWGjq+GVd9+1aOvZXxxNjihDi+Z5xAWxs3voNjFqIidHqcNV4bVmQrbZhVhlWHgtO44dSLk7wrB6dxI8d5JcGmnIlMmuA0Wzpty5nWobv1QdISOjdIk8j4ODluWosXew8lG220cSzBRlhZ3GTDwgBw2txw/QFacWVFlSNlbbmlx07mRzCojZs19nuHelSCrOQuye+ZVXKTT2o3sS0sGM6PYcM4AKWa6EVIzSSIzauD50hwkiy68BCBA/cH4JkGCCb1u0R8+5eaeZAIq4v0U1LQnslYMw1Oa/aVFQxqxHcKg57YJkF0UepNbDORySo4LdWMm8MQyG8Wp83MhRmLPGUwM+HkLlKsHBwc8kdwCb0zImzPObhc7If/Rt3Q0EtHNEftXVfKYebU/vPq06VKmoYUtjSA3+A+zclXAlm6TzOxSt6X5mHBO/XLDwlkCwMT1gQKbWNEpdLRNkFnlfceu2nswCabVE5pY0FY4hvqAf94ZDXhK1+pPtgqd8fN2dYsmbL0C+DJJpeaZU2ZicPj7Rx3OOSUqSzlTMsyhU2dtuXy0FmUPogKYOvRo3eZectPERpEGK91RxEsS6hEgzfgpqZG/bnfnvY36urZVFg58r3JjXvAALazBlbNKv17uYBVUvKlkxYqpJ5JDyZ9+oTPLIOBVDLblj1xdpXkAcKEgKXaQfcyStEK9lV4YF9j5ENWXFm1oLdeUo+OHWDGzey/b1+vzUzK64hMcDzysDB3rkcIVk+kWpAPrqrXkALeLSH7rFnKmZZlCps6bcvlobNIfRBMH0uTgpV06lsahJXlTzWSv5ttxt57RSbsRu0FxlXISsLK4a8XtGWGoINnf9n/IuVsgw3WVBGJRL2kPB4cgjNqeaCAH93EDi/mxHPawWh30q8IviuVOCGtmho2jOwE++X/eGrRatvC6oJdJg9HtsfQmsigxsrKiKlOE7mgTNSDw8yZdtkqraeDOaQDWwlZy5mWZQqbOm3L5aGziH2A84Zq9f77V+rUKf8srXLQRfVNOOlowzSIKkt4xgXNzdwoo6PGPZTaRZ8HywH+2WUSCKBiaT1u6V5yk+PAMjOBcAJmyv6qM1vGF7AUT7BbGFjy9f+G87xZ+iWUJrhc/9FHRHWvVf37Rx8ykrY/w+Tk4aiyXcEpaPGH1KTRaWpXq0FOO0GHpuWllYl7cKg3nOPOEKZLRTblbC5P2dZpWy4PnUXtAyLYL7+cYxub1ZQp1XfAsBSseKazRjVgwEb6zOt6Up6G36jNcnTT5JZHLd37Z5WyDx4HdJrkMkcdsRlcimVmy/nfJJl4FKLt5U3sEttqkfM/HK1ZwznbvTOlDG3Qp4zFy/C9Tepfs/HYSR03OdxEUPtP55q3jmyZNBSHaJAylLWcaVmmsKnTtlweOovaB0JSgcP4yU+8G9Lpp6+MvCkFHUv7veaeqk+fxnWv+jwUhd2oq4/NrF1n1KwyuP+f1QNstR0V3vAoP0OgW60608rl9QDe2poc1GjyQGFqm8l4zAqpk844nYvIcl7k6J588snl98cdd1x9rOwikBzgLOVMyzKFTZ225fLQWdQ+ECpVydsFP/lJk36F3ZT8clF7e+ytBg/GCIO/rDRyYQddkO5jEhhlqtPkiFDT8jpazyg7evVq1n8DR8IHCFjMpoG2bLNdlimadZ53fOwA39eznlEHr0iwXqeZcdvco+iOBCxvv/22JgRJImBBDiQRsNAfSQQslMXntghYsDOJgEXsTyJgoX1MCFhoK+y3QcCCbhMCFuoQbO8wAhapaxwBC/Xn8ygCFuyXsUPb0j9SLuMhSMAi5YQRsFAv7OD3yNJmQkVK/d5cFy698cbD1fbb91TPPNPSjjzkb3/rpU45ZY0mnagQVHikGKCtrc86shBvfxm72trWqvXXb9D0neT6QgnKq3fvHu3ILCB98crtEUtmQfnVhCAsPTeod99t0IdteIQg7EeXdEQ1udUUJeQhfpIP8mvlvktdK+U2lglBpDyxodIuzTpa2CP58AhBqIOfECTYhsjyV/R4ufmULbK9dTmij7r7CUH8skII4pHlQC/KuCMor0JUk5aAhbKC7R1GwCJ95bVh6zpZbKq0IW3b1tYrQKrSnoCFsvztXQsBS0vLGl1uQ8MazYLHaW3eEZ7euIPZrrXVIxgKtjdtGCRgwV6pTxQBC2UFx6zXF6qqLyZP3lR16tPBHNIRsAgbWhJsypnIpCH/sKXTtpxpHVwfVMAzyIUXrlUvvtizHdXqugzPcgqZ/4SzIAkHwUlDhvAwCvtXg17eJXAqivSi1pPXqvNmSzqdKmnZ0kRnmhPO8j4lL0g6kpaAJesTCG33wdpOdMqfvy/WW6+Xevvt19U++2zceQhYBLfddps+ynPhwoX6/cc//nF19NFHa85yh2iYUsPalLNNR2tTp225PHQWuQ9wzpddtlYtXtxT73mzfM6hJn5+dEkfe/LJ5eXPgk5y0KCG2DOlgxHqNgOjmNUlMbWl0WmKvM9ED4ucTzMFk7O9o/bw86ynzbIa6hicFuwLrhVWzR5+eIl23PVEqsV4ljQ+//nP69eLL76olz55zZ8/X3921FFHudzuGMgydpZypmWZwqZO23J56Cx6H3z00Vtq3DjvzG/+hh1qAjbaaHHZiQf39ngvTltO1wpGqPtRz9zaWuVMkYdtNuuwZk2DUQ5/Z+2DtTm0bZyMnx+hd+/654ilctw/+tGP1EMPPaSP8nzppZf06WC8Xn75ZXXnnXeqBx98UMs4ODh0TXCDmjSpqSooKO486a50lGJXAf1BnEAY45hJcKFDtNMGc+f+VXUqx33DDTeoyy+/XJ8MFsRhhx2mLrvsMnX99dfbtK9LgYCxrOVMyzKFTZ225fLQ2V37YK+9mtQBBzSVDwBh1k1QEPvdQQSXX4ueilR0PgNWQAjuC0NwhaSz9kHvTsDHEGQiFPz+9z9XncpxL1q0SE2ePDnye75DxiEcEgWdpZxpWaawqdO2XB46u3MfsKy+zz5NavToHqpv30okth9haVpuqby+cklgRh23pemfcXfWPlib81J5mMMWkEnTqRw3qU1xJCtEV5Nm5BAO0nqyljMtyxQ2ddqWy0On6wOOl2xRgwe/o/7yl2qO5ij+6lroNjsik0bOFHnYZqsOXn9EO+60VLd59EFbDm2LTNQsO2ukcty77767+vnPo5cByGlGxiEcLqK5vnJ56HR9ILm3i9Rpp72mdtyxr9pqK28JndX1MBa1LCJ9OypXlKjyWuAFF4aXFVwh6ax90JBx2+KsR4z4UHUWpEoHO++889Q+++yjc0SnTp2qtttuO73ksmDBAnXllVeqv/zlL+qRRx6pn7UFR3c9UjIruTx0uj7wHPdBBx2k89DHjCnp87GVij4HPOs9bpZ+W1t76/zzuKNLu8seN3XfbLMGHYhGNoCsmss51v62cXvcqjzDtn0dZ+a4J02apP7whz+ok046Sf3pT3+q+g4mKXK7P/GJT6jugo4wp40dOzaROe3JJ5/UjF5JzGn8hu/jmNOIORgxYoRV5jTqE8ecNnPmTG1/EnMa9skFk8ScNn78eGvMaeg2YU7bbbfdEpnTnn/+eV1GEnPayJEjrTGnPfHEE7ptk5jT9txzz0jmNPqQz0nlxH5/ezc1Nel287c39aZ/ZcwyHmCaYhyhl/GC/eim/tLeO+00Qtfp1VebyixUtFGvXr0TmdM8OtOGKua0lhaR5Txp2K5W69/5mbz8zGkwei1e3KBWrIAFi28bVFMTDGxtqrGxpSbmNNopjjmNz2Uml8ScRllJzGnUj3pynVdkK23o7VvDFtaSyJxGeZtuOkCxA+KdL17SKWLvvusd89mrF+3TqFauXKH7Kpw5jXJgLWNFBhIc7G2NZE7DZsaLLea0AQMGVrU37RZsQ+4/2J/EnIZ92OVv7002WaJtb23to69PrqnRo0eX7xFh92T6MQt0iDkNIzlsRALRIGDZf//9MzO6qMxp3MhxXkmwKWcik4a1y5ZO23KmdXB9YF8O2x999FH9UACXQ1ycCzNwbsrcEJNgIhcngy/gOcU7WQuHU9kZZHYZJIZJw5xWq231kEtn/1r1zjs9Iw/okLaJ0ll9apnXtlFMeWnqaVqH5jq2bdQetsm1wsMrEy4e1jsdcxoO+tOf/rR9a7o4mEllLWdalils6rQtl4dO1wfeKgwPHfJ/nOPmprhkyUo1dGhT6DK6HyaMaHEy1eclNyQeXZoGtdpWTzkTtLb2iDxLmuVz+a6x0TuFy6+6/SEzDbFMefWwv4dhWY2NvbRdcexwUl5S0Jnt6ziz4LSnnnpK3XPPPVWf3XTTTXqpj6U3ltBtngDT1WAacW9TznaUv02dtuXy0On6ID2krKQIXdkGikOcjD+tKfzs6sTiO6Q3jUw95EwQVXfWX+WBh9UKdpyCjGrVD0TJTHn1sL/RoCxs/s9/GhLZ4cC4ccnldaaMqVQtefHFF+s9McHcuXPVCSecoPO3zznnHHX33XerGTNm1MPOLgHZ48xSzrQsU9jUaVsuD52uD9IjWFaUA5d90DiIDI4Ip0EAmn+GJQjbEaxlApjGtqzlTMCedpTT5q84Y9otyKgWdPrBto16KLBpf0tCWbIq4J1iV0GwLjL28riOM1sqnz17tpo+fXr5/a233qomTpyofvnLX5YDoi644AJ14YUX2rfUwcGhS8PvvJOW0aP2WwWy38rfqH1cy2RqhQIsd/36NVa1Df6XF6FKwVmzf2sh6YHH4op4h7EmsCoQVhfIg4qKVDNuIn6J/hUQkEIaiGCXXXYpR606tAfRxFnLmZZlCps6bcvlodP1QXqYlIUT32WX5KXJHj16t3PagPdMkEhvwtkEA9PCiGGKnEufFr16NVbxzQtw2iRifLguZdnfbjJLDR4yE2zbqAeiLPkYWlvb2yb4yU+aVGNjU+7XcWaOG6ctqTOE0j/33HM6ZUZAeottsomuBEmLylLOtCxT2NRpWy4Pna4P0iONTlnKjNoLj5tZSY4ywVIjRpT03zhimDSQ9KVaZeohZ1oWbeBvE450JeOPTCdZ/fYvg8uDDn+rD5kpGT0Q+e0P29pIa38cxIbgMj5OG3CEbd7XcWZL5QcffLDey7700kv1qWBEl5MrKpgzZ47OM3UIB2kOWcuZlmUKmzpty+Wh0/VBenRUZ9hS+tq13Jij04Zkr7tHD/Kua/TWXYTy1F9W9dnm8B0Ez/X22jc4kxanz4NTSwt53+SMx69iiM64rQ3T7Yu2hLaQVQFvj7uhymmPGeM9pOR9HWc242Z/mxzTvffeW+9r/+IXv6hinOFkMPK5HTpPeojNFIyukAbj+sC+HPeAUaNGadIWE6YqGzplFj5wYHxZUoRlxlOj8kx12pbraFnBmfQ6yciZtDj9vn1bjPa+0dk+laxjR4o2JLRFpS7tnfa0ae3Pnc/jOs5sxs2F+dhjj+klAxLkgxW57bbbyuxK3QFpmdMAzFJJzGmUR7J/EnMajGjCzhXFnEZZ6LbFnAbLFjrimNPE/iTmNOoq9scxpwmrlA3mNNoM2STmNIDOJOY0qWsccxrkPHxuizkNUHYccxqg/5KY08T+JOY0bJL6hDGnjRs3Tmec0FbSbtLe1IlxQHtRd9GJPbRVcMxSX8ayvw3RT38tXrxYv5f2HjKkh3roofXUSy/BEOYdcjJlCoxlOBSip2H7gh2tsZyqSjkVNrGOMacB2MHimNNg7BI9ccxpMHnxN4k5zYv6XmOFOU3Gh5+pznvoalabbtpD53lTLaLPe/aEDQ0muiBzWqUN+byaDa09cxrmNTcTpS6seB5HuDfDL+mtjZaWRiPmNEEcc1rPniW12WbY2lNdeWVJ9evXrAYPXq769FmjWloq92TGOtezjDXGLOMs7B7B+Cwcc9oRRxxhJHfHHXeo7gRT5jScoclWgk05E5k0rF22dNqWM62D64P6yNm231TOe5DaSpHsMndu5XOZWW21lff+sceWWWPt6hrMadmw1wXlWlr66FzqKLD8Xiott8acNnToO5lex52SOQ3n5NBxmD4j2ZTrAKNtZjpty+Wh0/WB57SffvppPWPn/yTHbdM2ZnqwUP7whx5ZCIs73KbYw/Qvh2622VJD2tmZauedTWhn3zagnU2W8culSYErKkxSyWwd2z1xYpN65ZXOex1n5rhvuOGG+lnSDWD6BGZTzvZTn02dtuXy0On6wKM5feihh8r/JzFM1aNtcdJxjJRRZbGrIA5/0KBGNWDAcOu2mcpFRc6LQ2dJ2hZMyzKRS1MWz0NxufW9erF8X7tdE9e1ZWe+jjPnKnfoGNhnzFrOtCxT2NRpWy4Pna4P0iOPtg2TY5X94ouVmjfPe18qEUOwiZoxo1Elra5m2QfihNj7798/+rjUNJCTsGzIpSkLplKCxqKiyk3jvxpjdPofgDrzdVwL7D3C1RnsHRxzzDH6qYdAFahW48Lzkf/617+uj1MkQIwggjPOOKNdLh77KMEXjHD1gAR4ZSlnWpYpbOq0LZeHTtcH6ZFH2wblmGn7nbbghRdKavr0Bv19XraZyPnz25MOx6g3zWrasoL54x3JrW+J0Blsi858HXcLx43TJmr1wQcf1AedEN3OoSZRICqX1xVXXKHmzZunbrzxRnXfffdphx+2BUCnyOtTn/pUnWvj4OCQJ1geDzptAUFu6wKOCwMc1s4791E9ey7Qfzs7JJVswAAzGlWHAi6VL1iwQDvdmTMJHNlZf3bNNddoQhgcM+H6QXDg+Z/+9Kfye6IBv//976svfvGL7QJomMGT0lJvhNlZbznTskxhU6dtuTx0uj5IjzzaNiiXRIKV9H1n74Mk3nebNKv5ULb2avdZ2MpDZ76Ou7zj5jhRnKs4bcCJZOTuEc1qeja4hOgHI0ZPO+009dWvflXnsp5yyinq+OOPT0xF8B9fSjoY4IFAciSj9JsQVNiUM5GRXM44223rtC1nWgfXB/bl/DaTY5tlH3S0LALR2NOuRqn8GjSIIKm2wvRB3BiaMKH6N88+26z7KSllDJjI1VoWadktLQ3r8saZfeOUyak3L2vnnfuEjrs8ruMsUAjHDXEFhBF+4HzJmTY9ao0keZjfgsvrHFW677776sT5Bx54QJ166ql675z98ChwdOlFF13U7nMeIuIIaNh3j8vzroeciQwXu5BtJJ1za0unbTnTOrg+sC8npBeAVbGkqPKs2zZMjuhxAtHY067AI0HZeuuVau3ad9Xjj7+Vi20dlevIGFq7dlSsXGvrWtWjR8+aZcLkcLw9ejRpQhaoazEZgpbm5p5q443bVKm0KtJ5t/rKYnvg8cfj65kEW/0JqVKXd9zCe560TF4rmBEfcsghavvtt2935Og0WBrWYfz48Tpy8/LLL4913Oeee66aMmVKVfmwPnHEaVzHwr4DS1YSbMqZyMhT4qRJkxLzV23ptC1nWgfXB/blcHYwTcF6h/1J7FFZt22UHNHjBKJViFtK2mlfckk/zdqm1Mdys81E7oMPGtSbbzbov+utV1LDhrWqxsYn9MFPHR1DzMb9gPEMlrQ4mMiEyTU3NyieM1asqMg0NTWoIUMa1JIlDWrYMNg548vaWe/n79FpxpqwOXYq5jTbgDZOqD+jwPL17373O3X22WeX6TDlRseTPTSrcUvlPAEdcMAB+mZCUFvSbOCvf/2rOvTQQ8t0dzaZ0zor0rBedVYUvQ7O/nxQncfNMvNrascdP9bp6xBMZQOjR5fUqacuVTvttL4V++tJCMOyuLR7EE1NJTVwYIMaPLj9saN+TOxANH29kRVzWq5R5XAgb7fddrEv9hR23313zUs8a9as8m8ffvhhvTTELDfOoXLoCWXcddddiU4bzJ49W/PWmjrtNBC+2yzlTMsyhU2dtuXy0On6ID3yaNsoOUhbxo1Taq+9cHxt6qOP3uo0tkXJRaWysXJwxRV99Qzchk4c40YbLU5MOWP2awK/XNxxrMzA2UqOO3Bko408zvoiX8e1oHM/Vq4DJw8deOCB6sQTT1TXXnutzuE7/fTT1VFHHVWO9OOghf3220/ddNNNatdddy07bcjfmbHzXoLIeGCAKP7uu+/WS3wsLeHUSTW75JJL1NSpU+tSjzzO57V5hm9XOYc4a7mu3gfMtl944QWdSmlCeZpH23alPohLZWPPnuXzIUPs2xZ03jIjN12z9cslqUc2bJlcbHjlleJfx13ecYObb75ZO2ucM4EXRx55pPrxj39c/h5n/vLLL2tHDZ577jkdLAaCfME8ObGHRUoBp3udddZZmocWuauuuko/INQDpien2ZSzfVqbTZ225fLQ6frAozllG0r+T1rZyqNtu1IfJKeyNWRimzjRxYuXq403Hpy4tO6nKcUpExTOK8zx870/gDv40DCwC1zH3cJxs3d8yy23RH6PI/Zv1++zzz6JpPDM4nllBceTXV+5PHS6PkiPPNq2K/VB0llPBKpx1GdWtplyrftpSnHKEsPIsrn/Vg0py7bb9lXbbNPYpa/jbsGc1hUg5yZnKWdalils6rQtl4dO1wfpkUfbdqU+4OQzji0Nw7hxDWrzzUudqg9kj3zDDd8t/z9pUpPaZZcm9dhjTeraa5vUNdc0qauvblKPPtpP7bBDSX3sY22Z2Z/XddwtZtwODg4ODl5AHVmsYWeQn3rqKrXeeslBuJ0BnJUePI51+PA2NX/+cyRG5p5lsP76Sm2xRfypc3nBOe4MYUqralPONpWrTZ225fLQ6fogPfJo267WB9FOb4GR0+ssfRA8jhUiFrKF6qkzSi4sxY6HIR6SaO8saLFN4Rx3DSCwjZdEGxL0Rh4fSfosq0BMwclksL698cYbOrcPznT23iV/nb152N8kb3zTTTfVQXbkhZMPSCAe+e6AE85ggCMAjxQ35IU5jhQ2gu3ee+89/R5CGGzhoJWhQ4fq37766qv6O+hjCSCS32688caaLY7vKRf7+R872dfhODs5GYfv0U+EPraRZ0+9aQOCN5AX+xnoq1atKp/IRvDf66+/riOPBwwYoP/6baC9JFefct966y39GfSy2267bZkViqwA9AnZAfYiy3vanUwD2htQd0C7AepCaqG0N7KS5kEcBdkGtDc2jxkzpqq9hw8frl7j6l7X3ny2cOFCXVe+o1zakahq+vWVV17Rstg6bNiwcl35n/aDYwB92C9jh/6jf+g3wHiAFAh5mKYYP4sWLdJtTRuiW5bwaG/qhR3Yv9NOO+k2I3CTelO/N998U8syJvmc/qAMf3vDeUC7+dtbxqCMWcYD/cI4wg6BZG5Ie48YMULXiXFAe1F3GR/YQ1sFxyz1ZSxz7Uib0Sb0F1kgQNqbunOdYJOMWcqmDjJm6WPqhi4Zs/QjDoJ25EV700/Un7pig7S3f8xiB2XRvh//+Md1G2AHQNbf3pQv9tPelOEfs3KPoM84xdA/ZsPuEdSdchgT2OTdEyr3iCVLWrVu6kK5YWNW7hHYz3kO/vYOu0dIX9Fv/IbxLWNW2pt2Q4+/vZEL3iPQhRz3CP+Y9d8jqCvtSdnolfYO3iM++OADTZgVN2axC72Ug720i4xZyvK395Ila9UFF/RSc+aUdLu1tKxVpVKbmjWrUV18cS913nlL1Icf/luXI/eIsHtyEvmQNUDA4lAbPvjgA01wvHTp0li5RYsWGZVnU85EpqWlpfTII4/ov1nptC1nWgfXB/blli9fXrrwwgv1a9myZZnalscYysM2Eznb9pvKdYXr+KmnVpQmTChFvmbPNisLH4AvwCfUEy44zcHBoSYw02DmwczG5EAHB4fOhg8/jI/CT0rByxq5Up52FTjK0/xR9Do4+/NH0etQdPvzrMMLLyh1wgnR3//61x7DXhK6BeVpd4PsS2UpZ1qWKWzqtC2Xh07XB+mRR9u6PqifzqLbDwYOfD8yxY7PScGzPYZqgXPcGcL0rFabcrbPh7Wp07ZcHjpdH3jHSRLERJCOSURwHm3b1fsgLYreB2st6yyVluno8aDzlqhyIt+zOmvbBMVcTyko/NG3WcmZlmUKmzpty+Wh0/WB0hG1t99+u/5/8uTJifvcebRtV++DtCh6HwywrJMsALK9gil2zLQlXc32GKoFznFnCFIbspYzLcsUNnXalstDp+uD9MijbV0f1E9n0e33ywXzyus5hmqBWyrPEJKPmKWcaVmmsKnTtlweOl0fpEcebev6oH46i25/XtdxLXCO28HBwcHBoUBwS+UZMqfBEAQrUBJzGnKwbiUxp8EOJOxcUcxplIVuW8xpvEdHHHOa2J/EnIYdYn8ccxp28NcGcxrpesgmMadhNzqTmNOkrnHMabznc1vMabA/UXYccxp20W5JzGlifxJzGm0l9amVOU10JjGnYaPojGJOoyzGVBJzGmOVsmwxp6GX/otjTmOMiP1xzGnYwW+SmNPQSf/5mdP89wjs52XCnEZZ2J7EnCZ9FcecxnvKSGJOoyz6zwZz2qpVq3S7JTGnif1JzGl8Jn3FmMXO4D2Csmgvx5zWzZjTlixZYlSeTTkTmTSMS7Z02pYzrYPrA/tyaZnTsm7b7tAH9bDfVM5dxxU45rQuCJlNZilnWpYpbOq0LZeHTtcH6ZFH27o+qJ/Ootuf13VcC9xSuYODQ01giZDlW5aOHeVpfsdQDhrUqAYMGJ63SQ4ZwFGeZkh5yp4Mez5JsClnIpOGZtCWTttypnVwfVAfOdv2m8rlMYZqsS3svOdBgzpWh+AxlNzKt956hZoxo0ltvXV27eau4woc5WkXRFdIYXBpMOnlXB/UT2eR0sFwtN/4hseJPWWKUl/5ilJnnaXUggWrU+vkASB4djR44YWSmj69QX9fq/2mckUfQ8ClgzlEgujHrOVMyzKFTZ225fLQ6frAm4kQzcssw4TyNI+2zbsPohzt3LlKXXJJj0RHGyyPWXuwLH+Z6wKmjcqqVc5dx9nD7XFnCNNUAZtyttMTbOq0LZeHTtcHHuXpb3/7W/3/3nvvnbjPnUfb5t0HcY523rwG/X0SMZe/vKRjJpO+L3ofNAXKCtuC4G9nvo5rgXPcGUJyirOUMy3LFDZ12pbLQ6frg/TIo23z7oM4R8persl5z/7y4NGOQ9L3Re+Dob6ygnv9/sNBhg/vvNdxLXCOO0MCFggAxo4dm0jA8s9//lMn/ScRsPAbCZaIImBZtGiRJhSwRcCCTuoTR8Ayc+ZMbX8SAQv2yewsjoCFtho/frwVAhbRnUTAQl/ttttuiQQszz//vC4jjoCF+o8cOdIaAcszzzyj2zaOgIXf77nnnokELPPnz9f2JxGwUG/61wYBi4zvJAIWbKHOcQQsCxcu1GM7iYCFetJntghYkGdMxhGw8Dn1AP37f0xxuFRrq3fCFHURciHKHjSoSb3yymuxBCzYzFhhTDQ1LVMjRw5SL77YozyuvTDjkho1qkU1NX2g/v3vVZEELNg/ceLERAKW5557TrdfHAEL9Rw1alQiAQtjj+vABgHL22+/rQPY3njjAzVtWpNetWAsyXL27Nk91YUXKnXmmW+q9dYrJRKw0McyjqMIWPj96NGjHQFLdyNgWbRokVF5NuVMZNIQN9jSaVvOtA6uD+zLpSVgybptO0Mf0CzHHVcqTZjQ/nX00av192l1vvpqdZk77dRW+uxnl5deeaXViv2mcnlex7Nnh7epvJ56aoU120xksiJgcTPuDMHMJWs507JMYVOnbbk8dLo+SI882jbvPmC/laXb6dO94DH/ku655zYm7m+H6dxqq+pjKAcNYvb+rhox4mOJccdF74MN15WVtMWwalXvTnsd1wLnuDOELKlnKWdalils6rQtl4dO1wfpkUfbdoY+CDpaOe+5rW05GzUd0uk/hnLt2jb1+OMsK3/Miv2mcnmOofUS9vIHDix12uu4Frh0sAwhe7JZypmWZQqbOm3L5aHT9UF65NG2naUPcLLjxim1117eX967PkiP99eVxYMPqxZh4PP11vtfp72Oa4Fz3A4ODjWBoCCWEQnMSWIcc3CwifXXbUEEnbdElTc2esGSXQ2O8jRDylOWWohITIJNOROZNFSPtnTaljOtg+uD+sjZtt9ULo8xlIdtJnLdqQ9aA2X587hlCwKnnnU9HeVpF4Sk+GQpZ1qWKWzqtC2Xh07XB+mRR9u6Pqifzs5g//ohWxC2bbM9hmqBc9wZghzCrOVMyzKFTZ225fLQ6frAozyV3GcTytM82rar90FaFL0PmrvAdVwLnOPOECT+Zy1nWpYpbOq0LZeHTtcHHuXpL37xC/Xss8/q/7O0LY8xZFped7kOim5/XtdxLXCRJBkypyEHK1AScxozF1i3kpjTYP8Rdq4o5jTKQrct5jSCkNARx5wm9icxpxEXIPbHMadhE39tMKfxHbJJzGnCopXEnCZ1jWNOo/343BZzGm1K2XHMadhP/yUxp4n9Scxp1FXqUytzmuhMYk6j7qIzijkNWcZUEnMabUdZtpjTkKX/4pjTkBX7aW/K8I9ZuUdQV37jH7Nh9whsov9oF2wC/nsENvGiLnLdRDGnIYftScxp0ldxzGm0NXJJzGnYRP/ZYE5rbW3V/Rc3ZpER+5OY0xhb0ldRzGlyz3HMaV0EjjktvYxtOcecll7GlpxjTqu/bSZytu03lXPXcfbMaW6p3MHBwcHBoUBwjjtDuJOp6iuXh07XB+lR9JOpTMtzfdAxue5yHdcC57gdHBwcHBwKBOe4M4QE7GQpZ1qWKWzqtC2Xh07XB+mRR9u6PqifzqLbn9d13C0cN9F8xxxzjI5IJNLwhBNO0JGLcdhnn310ZKj/dcopp1TJEIV4yCGH6GhAIj+/+c1v6uhPBwcHMxAZTsQy0bqO8tTBof4ozFWG0ybd4MEHH9SpE8cff7w66aST1C233BL7uxNPPFFdfPHF5ff+cH3C+3HapCQ8+eSTuvxjjz1Wp0Zccskl1utAmkHWcqZlmcKmTttyeeh0feDlt5566qmaqtIk1zWPtu3qfZAWRe+DEV3gOu7yjnvBggXqvvvuUzNnzlQ777yz/uyaa65RBx98sLriiit0nl0UcNQ45jA88MAD6sUXX1QPPfSQzuvbcccd1fTp09W3v/1tdeGFF+q8vDCQC+hn0ZHcVWbqcbN18gjJh0yCTTkTGWwmJ9JkpcGWTttypnVwfVAfOdv2m8rlMYbysM1Erjv1wdud+DrOAoU4ZOT6669XZ599dpmcQxqIp/vbbrtNffrTn45cKp8/f74mCcB5f/KTn1TTpk0rz7q/+93vqrvuukvNnj27/BsS7SEMeO6559T48eNDy8WpX3TRRe0+pyzIHOKW++MOIamHnIkMFwpbBpAJQJiQhU7bcqZ1cH1gXw7bIcGADAMCjaTl8qzbtjv0QT3sD8pRZlPTZmrFiiFq+fJGNXBgm+rff6n6z3/ma7KUWu3vCtfx8uXL1WGHHVb3Q0YKMeOGNYj9Zz+4OdCIwkYVhqOPPlovbzAjnzNnjp5Jv/zyy+qOO+4ol8tM2w95H1fuueeeq6ZMmVI144b1aeLEibEdy40NJqMk2JQzkZGnxEmTJiXedG3ptC1nWgfXB/bliDX50Y9+pP/nITrpJp5123aHPqiH/UG5N95oVBdf3KDmzat8P2bM+mrq1E3Vdtv1qdn+rnAdv5/Rmd25Ou5zzjlHXXrppYnL5B0Fe+CCMWPGaKrA/fbbT9PpQU/YUUCDxysIBlrcYMO5mxwxZ1POtCyebpPst63TtpxJHVwf2Jfz24uMDftN5fIYQ3nZZiJn036/HKyu06crNX++Ug0Nle9x4ldc0aSuvrqhfCJXd76Oe2YUnJlrVDnL3zjmuBfL1ixzC5+xQHh/o/avw8CMGAgnLb8V7mOBvE9TrimEEztLOdOyTGFTp225PHS6PkiPPNrW9UFtOqH19s+0/Zg1q1l/b0tnGruKfB0XdsYNETyvJOy+++6aeH7WrFlqwoQJ+rOHH35Y74eIMzaB7GUz85Zyv//97+uHAlmKJ2qdvYntt9++g7VycHBw6FpYdwZIh7936IZ53KNGjVIHHnigTu165pln1BNPPKFOP/10ddRRR5UjyjltZrvtttPfA5bDiRDH2XOSDoFjpHrttddeauzYsVpm//331w76S1/6knrhhRfU/fffr84//3x12mmnhS6F1wqTIAnbcqZlmcKmTttyeeh0fZAeebSt64PadMaFLfTo0TP2+7Q609hV5Ou4yztucPPNN2vHzB41aWB77LGHPgNYQG43gWdyHjCpXKR54Zz5HcvyRx55pLr77rvLv2G/4p577tF/mX1/8Ytf1M7dn/dtEyZ7LbblTMsyhU2dtuXy0On6ID3yaFvXB7XpJIV5zJhwmXHjvO9t6UxjV5Gv427huHnagWyFcHtC7UkR858DTBoKaV+kgEkgwaOPPqrPtOVc1UWLFqnLLrusXYg+Uef33nuvdvicsUpeeL0CDOQM1yzlTMsyhU2dtuXy0On6ID3yaFvXB7XpJPBs2rT2zpv3U6euTgxMS6MzjV1Fvo67fDqYg4ND5wUPuvAXQErkKE+7LrbaSqkf/tALVGNPm+VxZtpLlxLQW5lEOdQf7irLEJAKZC1nWpYpbOq0LZeHTtcHHuXpGWecYUx5mkfbdvU+SIuO6mRmHZxdNzVtblVnd7mOa4Fz3DXgpz/9qX7BeS7pAqSobbnlljpYDjapfv366Yj1N954Qy/bjxw5Ui/p878s8UP2wnI+AXFEvD///PNqyJAh+kVOoyzRMHA4oYZlffbw+Y7fAQ55gGNd0ubYKsAWWIhIbeO3BOwBDmnhBiskM5DOQKLB95SL/fyPnWwt9O/fX/O4A3SyRQHpDP+Trke9aQNmXchDdoPt6F21apXe2gDbbLONDhQklY8ysF0IF7CB9hJ2PMqF8IDPVqxYoQMUqQsgEwF9QnaAvcjynnYnYJH29p+hKyf7+NuN9kZW0jzYjmEfi/amf2DO87c35AuvvfZaub35DMpc6sp3ZD7Qjsw66VdJO2QmypaMtDe0ibQf2z7ow34ZO/Qf/QO9ImA8UH/kOSQH/gECKdFPG0J2Qp0B7U29sAP7d911V91mxH/Qh9TvzTff1LKMST7HRuz3tzfMgrSbv73pf5lNUzfeUy/GEXoZL9iPbuSkvak3dWIc0F7UXcY39iAbHLPUl7HMd/xOxiz9Jema0t70M2MHm2TM0ibUQcYsfUy/YauMWd6TlcKY5UV7M6aoP/2PDdLe/jGLHbQT7UtgK22AHQBZf3vznYxv2ltSWGXMyj2CMbPDDjtUjdmwe4SMbcYENgH/PQL7eVEXyg0bs3KPoGwonv3tHXaPgHkSHfQb1zG2ypiV9mbM8d7f3sgF7xHopS7cI/xj1n+P4Hvak7LpR2nv4D1i6dKlapdddokds7QFW6TYj320i4xZyvK3N+Oqsp8/Qr8P3iPQue2225bvEWH3ZP9ZGHUFlKcOteGDDz6ANra0dOnSWLlFixYZlWdTzkSmpaWl9Mgjj+i/Wem0LWdaB9cH9ZGzbb+pXB5jKA/bTOS6Ux8s6qTXMT4AX4BPqCfcjDtDRB1aUk8507JMYVOnbbk8dLo+8ChPr7zySv0/szhmSVnZlscYMi2vu1wHRbc/r+u4W0SVdwWYcObaljMtyxQ2ddqWy0On64P0yKNtXR/UT2fR7c/rOq4FznFnCNlrylLOtCxT2NRpWy4Pna4P0iOPtnV9UD+dRbc/r+u4FjjH7eDg4ODgUCA4x50hiOrMWs60LFPY1GlbLg+drg/SI4+2dX1QP51Ftz+v67gWOMedIbpCQIULykkv5/qgfjpdcFr37IPeXeA6rgXOcWeI4BGiWciZlmUKmzpty+Wh0/VBeuTRtq4P6qez6PbndR3XAue4HRwcagJEKRBPCGmKg4NDfeGusgyZ03gPK1AScxpywmgVx5wGo4+wc0Uxp1EWum0xp8F2hY445jSxP4k5DXYjsT+OOQ2mIv7aYE5DD7JJzGno45XEnCZ1jWNOo6353BZzGnWn7DjmNOyi/5KY08T+JOY0dEl9wpjTDjvsMM2yJWx4ccxpojOJOQ1Z0RnFnEZZjKkk5jR+S1m2mNOQo//imNP84zuOOQ2b+E0Scxqy9J8N5jS+x/Yk5jTpqzjmNNqWMpKY0yiL/rPBnLZmzRrdf0nMaWJ/EnMaY0D6Koo5jbJoL8ec1s2Y09555x2j8mzKmcikYVyypdO2nGkdXB/UR862/aZyeYyhPGwzketOffBOJ72Os2JOc0vlGYKn0KzlTMsyhU2dtuXy0On6ID3yaFvXB/XTWXT787qOa4FbKs8Qpvt/NuVs7zna1GlbLg+drg/SU57m0bZdvQ/Souh90LMLXMe1wM24MwT7N1nLmZZlCps6bcvlodP1QXrk0bauD+qns+j253Ud1wLnuDOEBD9kKWdalils6rQtl4dO1wfpkUfbuj6on86i25/XdVwLnON2cHBwcHAoEJzjzhCkN2QtZ1qWKWzqtC2Xh07XB+mRR9u6PqifzqLbn9d1XAuc484Q5HRnLWdalils6rQtl4dO1wfpkUfbuj6on86i25/XdVwLOk+YXDcgYIEAYOzYsYkELM8995xO+k8iYOE3fB9HwLJo0SJNKGCLgAWd1CeOgEXsTyJgwT7h/40jYKGtxo8fb4WARXQnEbDQV7vttlsiAcvzzz+vy4gjYKH+I0eOtEbAMnv2bN22cQQs/H7PPfdMJGCBNAX7kwhYqDf9G0bAgh0C7PS3dxgBi4yPJAIWbKHOcQQsCxcu1GM7iYBlwYIFus9sEbAgz5iMI2Dhc+qRRMBCW02YMCGRgAWb+Z0NAhZkJk6cmEjAIn0VR8BCPUeNGpVIwMLY4zqwQcDy9ttvqz322CORgGXevHnaniQCFtpTxnEUAQu/Hz16tCNg6W4ELIsWLTIqz6aciUwa4gZbOm3LmdbB9YF9uRUrVpRmzJhRmj59uhHxRNZt2x36oB72m8q567gCR8DSBcFTa9ZypmWZwqZO23J56HR9oPQsY+rUqXoGZDLjyKNtu3ofpEXR+2BYF7iOa4Fz3BlClhGzlDMtyxQ2ddqWy0On64P0yKNtXR/UT2fR7c/rOq4FznFnCPY0s5YzLcsUNnXalstDp+uD9MijbV0f1E9n0e3P6zquBS44LUMQ1JC1nGlZprCp07ZcHjpdH6SnPM2jbbt6H6RF0fugRxe4jmuBm3FnCCIbs5YzLcsUNnXalstDp+uD9MijbV0f1E9n0e3P6zquBc5xZwhJy8hSzrQsU9jUaVsuD52uD9Ijj7Z1fVA/nUW3P6/ruBY4x50hyBvNWs60LFPY1GlbLg+drg/SI4+2dX1QP51Ftz+v67gWOMedISAeyFrOtCxT2NRpWy4Pna4P0iOPtnV9UD+dRbc/r+u4FrjgtAyZ02DtgV0niTkNVh5SD5KY02CeEnauKOY0XjAT2WJO47foiGNOE/uTmNOor9gfx5yGHvTaYE7jt8gmMafRV+hMYk6TusYxp6GHz20xpzF2eB/HnIb91DWJOU3sT2JOo8+kPrUyp4nOJOY0bBCdUcxptAX9k8Schn7KssWcRt25tuOY0/he7I9jTkMP4ymJOY3rCH02mNMoh3ZKYk6TvopjTkMPZSQxp9FPck+plTmtublZt1MSc5rYn8Schqz0VRRzGr9lvDrmtC4Cx5yWXsa2XNEZl4rcB8uXLy9deOGF+rVs2bJMbXPMaRU45rT2cMxpDg4ODiFgVsWMjb/Cne/g4FA/uKXyDMESV9ZypmWZwqZO23J56HR94FGefutb31KPP/640VJhHm3b1fsgLYreB5t2geu4FrjH4wzBPlDWcqZlmcKmTttyeeh0fZAeebSt64P66Sy6/Xldx7XAOe4M0RX4dR1Hc3o51wf10+m4yrtnH3zYBa7jbuG4iX485phjdEQikYYnnHCCjlyMAlGXRGuGvW677bayXNj3t956a13qIGcLZylnWpYpbOq0LZeHTtcHSkfUXnLJJerRRx/V/2dpWx5jyLS87nIdFN3+vK7jWtBAhJoqAA466CCdbnDdddfpdIvjjz9e7bLLLuqWW24JlZdUAD9+8YtfqMsvv1yXIyksdMYNN9ygDjzwwLKcpEKYgicxUhdI3yDFpWggTYX9SY5lJN2hiCh6HYpsv5+r/Mwzz0zkKu+sKHIfdAX7u0Id3n//fZ2iR3pbPfO+CzHjXrBggbrvvvvUr371KzVx4kTdqddcc42eGUu+axDk2pEj6H/deeed6nOf+1xV3ingRuOXS+O000DyhbOUMy3LFDZ12pbLQ6frg/TIo21dH9RPZ9Htz+s6rgWFeKR56qmntHPdeeedy59NnjxZp548/fTT6tOf/nRiGbNmzVKzZ8/WhClBnHbaaeqrX/2qJgo45ZRT9Gw+blmERHxewb0PnhZ5RQGigLjv6yFnIsP3kFFkqdO2nGkdXB/Yl/N/zkpXlrblMYbysM1Ezrb9pnLuOq7ARFe3cdywBsE85AfLKCxLCxtVEn7961+rUaNGqUmTJlV9fvHFF6t9991Xp7E88MAD6tRTT9VLf2eccUZkWTNmzFAXXXRRu895iICFKQqUK+xCcbApZyLDhSKMQ0l5uLZ02pYzrYPrA/ty3NAEM2fOTFyxyrptu0Mf1MN+Uzl3HWd/Zneujvucc85Rl156aeIyea2AUo+98GnTprX7zv/Z+PHjdcg/++Bxjvvcc89VU6ZMqZpxQ9fIMn7cHjd2QINoYq8tORMZeUrkoSZpX8mWTttypnVwfWBfjhsaq2KAuBPiPbKyLY8xlIdtJnK27TeVc9dxBUJp26Ud99lnn62OO+64WBmWr9l3Fj5jgfD+8l0Sbr/9dh3teuyxxybK4nynT5+ul8LhqQ0Dn4d9x0CLG2zw38LXnQSbcqZl8XSbZL9tnbblTOrg+sC+nN9eYkts2G8ql8cYyss2Ezmb9pvKueu4gqwC6nJ13BDB80rC7rvvronn2aeeMGGC/uzhhx/Wyyo4WpNl8sMOO8xIF/vgkPFHOW0HB4f2N1ocNtejozx1cKg/CrHHzd406Vonnniiuvbaa3U62Omnn66OOuoofXIL4LSZ/fbbT910001q1113Lf+WE18ee+wxde+997Yr9+6779ZPUbvttpvel3vwwQd1PurUqVPrUg+T1QHbcqZlmcKmTttyeeh0feBRnrLtZUp5mkfbdvU+SIui98EmXeA6rgWFeTy++eab1Xbbbaed88EHH6xTwsjLFuDMX3755XYEENdff70+2m7//fdvVyYHIxBlzox+xx131DniV111lbrgggvqUgeOictazrQsU9jUaVsuD52uD9Ijj7Z1fVA/nUW3P6/ruFs4boK+CDAjao/kdhyyPx+bc1zhktlnn32qfscMmijFsCU8ZvHPP/+8LpMAG5bJTz755Lot98m5vVnKmZZlCps6bcvlodP1QXrk0bauD+qns+j253Udd/mlcgcHh84LVrlYqWKPe6eddqorY5SDg4Nz3Jli6623zlzOtCxT2NRpWy4Pna4PvNxbiFfk/yxty2MMmZbXXa6Dotuf13VcC5zjrgHsj/OSmxaUeKSobbnlljpYDmIK8v4gj3njjTd0Stv222+vl/ThNZclfkhk2D8hkp0zX5955hn9GzhvWbYXzvUttthCLVmyRM9wevfurfWKbiLh2bOXtDnyyrEFm4YNG6Z/++qrr1ZxsQt5zcYbb6y3CviecrGf/7GT2VP//v3LxAPoQxe569hGuh46+BzyGeTZfsB+gjnIfWRrA5BKweEvpPKxzcEWhTDUYQPttWzZMv2ect966y39Gb8fM2ZMmZiB7AD0Sc4k9iLLe9qdgEXaGwwdOlT/pd0A+mhnaW9khcqQ7Riio2lv2hGmPn97Eyvx2muvldubz+bOnavryncspdGOpITQrwRGArgBuOilvekP2o/6ow/7Zeygl/4RKl/GA79HHtspB6If6kUbkjNNnQHtTb2wg3KI3aDNiP+gD6nfm2++qWWxWeJC+N/f3gSYUb6/vfmdEKtQN8YDKZN85t+yEhZBae8RI0ZoWxgHtBd1x350Yg9tFRyz1JexzPiTvF7ahP4imBRIe9Mf9CE2yZilTaiDjFm+X7hwobZTxiy/4yGDMcuL9mZMUX/6Hxukvf1jFjtoJ2xmTNIGsoSKrL+9/cdAUl9JYZUxK/cIfj9u3LiqMRt2j6Bs2ogxgU3Af4+Q+wF1odywMSv3CF5k6PjbO+weMWfOHG07/cZ1yPiWMSvtzWfbbrttVXvzWfAewfe0EfcI/5j13yOoK+1B2fSjtHfwHvHee+/poOK4MUtbvPTSS9p+7KVdZMxSlr+9KUfGN2OWcRa8R6CTOCu5R4Tdk02CM62AQ0YcasMHH3zAQS2lpUuXxsotWrTIqDybciYyLS0tpUceeUT/zUqnbTnTOrg+sC+3fPny0oUXXqhfy5Yty9S2PMZQHraZyNm231TOXccV4APwBfiEeqIwwWldATyFZy1nWpYpbOq0LZeHTtcH6ZFH27o+qJ/Ootuf13VcC5zjzhCmR37alLN9zKhNnbbl8tDp+iA98mhb1wf101l0+/O6jmuBc9wZQvYXs5QzLcsUNnXalstDp+uD9MijbV0f1E9n0e3P6zquBc5xOzg41AQCkCTI0FGeOjjUHy6qPEMEjybNQs60LFPY1GlbLg+drg88ytPvfOc7xpSnebRtV++DtCh6H2zUBa7jWuAejzMEaSJZy5mWZQqbOm3L5aHT9UF65NG2rg/qp7Po9ud1HdcC57gzhOQoZylnWpYpbOq0LZeHTtcH6ZFH27o+qJ/Ootuf13VcC9xSuYODQ02AfOLqq6/WhBeO8tTBof5wjjtD5jRYhmAFSmJO43tYt5KY0/zsXFHMaZSFblvMaTAdoSOOOU3sT2JOg5FI7I9jTsNW/tpgTqNdkE1iTqMO6ExiTpO6xjGn0S58bos5jb+UHcechl28kpjTxP4k5jTqK/UJY06TZUR0w0gWx5wmOpOY02gX0RnFnEZZjKkk5jTqT1m2mNPQQ//FMadRV7E/jjmNevGbJOY0PqP/bDCnURa2JzGnSV/FMafRLpSRxJyGLP1ngzmtVCrp/ktiThP7k5jTGIvSV1HMaZRFeznmtG7GnPbGG28YlWdTzkQmDeOSLZ225Uzr4PrAvlxa5rSs27Y79EE97DeVc9dxBY45rQuCJ8Os5UzLMoVNnbbl8tDp+iA98mhb1wf101l0+/O6jmuBc9wZwnQZxaac7aUbmzpty+Wh0/VBeuTRtq4P6qez6PbndR3XAue4M4Tst2YpZ1qWKWzqtC2Xh07XB+mRR9u6PqifzqLbn9d1XAuc484QEjiRpZxpWaawqdO2XB46XR+kRx5t6/qgfjqLbn9e13EtcI7bwcHBwcGhQHDpYBmCFIWs5UzLMoVNnbbl8tDp+kDpdKDzzjtPU57yf5a25TGGTMvrLtdB0e3P6zquBW7GnSHIO8xazrQsU9jUaVsuD52uD9Ijj7Z1fVA/nUW3P6/ruBa4GXeGBCwQAIwdOzaRgGXu3Lk66T+JgIXfSDlRBCyLFi3ShAK2CFjQCVlEHAGL2J9EwIJ9Yn8cAQsy48ePt0LAgm7IIpIIWOir3XbbLZGAReoaR8BC/UeOHGmNgGX+/Pm6beMIWPj9nnvumUjAQlnYn0TAQr/5x6yfgAW9jBfsRzf1jyNgkTZLImCRvo8jYFm4cKEe20kELPQFfWaLgAV5xmQcAQufi/1xBCy01YQJExIJWLCZ39kgYEFm4sSJiQQs0ldxBCzUc9SoUYkELIw9rgMbBCxvv/222mOPPRIJWObNm6ftSSJg4X9p7ygCFn4/evRoR8DS3QhYFi1aZFSeTTkTmTTEDbZ02pYzrYPrA/tyK1asKM2YMaM0ffp0I+KJrNu2O/RBPew3lXPXcfYELG7GnSF4msxazrQsU9jUaVsuD52uD7wlRGYy8n+WtuUxhkzL6y7XQdHtz+s6rgVujztDyFJSlnKmZZnCpk7bcnnodH2QHnm0reuD+uksuv15Xce1wDnuDCGzkizlTMsyhU2dtuXy0On6ID3yaFvXB/XTWXT787qOa4Fz3BmCoIis5UzLMoVNnbbl8tDp+iA98mhb1wf101l0+/O6jmuBc9wZggjKrOVMyzKFTZ225fLQ6fogPfJoW9cH9dNZdPvzuo5rgXPcGUJSOLKUMy3LFDZ12pbLQ6frg/TIo21dH9RPZ9Htz+s6rgXOcTs4ODg4OBQILh0sQ0CWkLWcaVmmsKnTtlweOl0fpKc8zaNtu3ofpEXR+2BIF7iOa4Fz3BkypwkbUxJzGuw/fJ/EnMZNUti5opjTeA+DkS3mNP6XMqOY08T+JOY02kLsj2NOo2z02mBOEyamJOY0+gqdScxpUtc45jTaib6xxZyG/eiMY07jN5STxJwmZSUxp6HfFnOatFkScxpjSdowijmN94yvJOY09PGyxZzG99gex5xG34r9ccxpUnYScxrvGV82mNOwnzZKYk6TvopjTqOd0JXEnMZ77LLBnLZixQr9fRJzGuMd+5OY07gWk5jT0El9HXNaF4FjTksvY1uu6IxLRe+D7sTalYdtJnLdqQ8WddLr2DGnOTg4FALMTK655ho909x5552NlssdHBw6Due4MwRLMFnLmZZlCps6bcvlodP1gXdwC0uF8n+WtuUxhkzL6y7XQdHtz+s6rgUuqjxDyF5elnKmZZnCpk7bcnnodH2QHnm0reuD+uksuv15Xce1wDnuDEEQRtZypmWZwqZO23J56HR9kB55tK3rg/rpLLr9eV3H3cJxf//731eTJk3SUXtEGpqACMDvfve7OgqT6O7Jkyfr86n9IKrymGOO0ZGOlHvCCSfoiMh6gKjDrOVMyzKFTZ225fLQ6fogPfJoW9cH9dNZdPvzuo67heMm5P+zn/2s+trXvmb8m8suu0z9+Mc/Vtdee616+umndYrGAQccoINpBDjt+fPnqwcffFDdc8896rHHHlMnnXRSXepASkXWcqZlmcKmTttyeeh0fZAeebSt64P66Sy6/Xldx93CcV900UXqrLPOUmPGjDGebV999dXq/PPPV4cffrgaO3asuummm3Qu35///Gcts2DBAnXfffepX/3qV2rixIlqjz320NGxt956azmP1iYkXzhLOdOyTGFTp225PHS6PkiPPNrW9UH9dBbd/ryu41rQZaPKaWRIC1geF5Cwj4N+6qmn1FFHHaX/sjxOCosAeUgDmKF/+tOfDi2bJH7/EW9CHiDkIVFATggYspIzkSESGAIC5CCVyEKnbTnTOrg+sC/H1pKsYkGeAbFJVrblMYbysM1Ezrb9pnLuOq5AfAATx3qiyzpuPyuYH7yX7/gLo5EfDBYYnUQmDDNmzNArAEHADObg0J3xgx/8IG8THBxyByxsTBS7pOM+55xz1KWXXhorw3L2dtttpzoTzj33XDVlypTye2YZ5PhBtxfXWbvssouaOXNmYvk25UxkoCKEbhJ6QIL0stBpW860Dq4P6iNn235TuTzGUB62mch1pz7YpZNex8zKoUFl8ldP5Oq4zz77bHXcccfFysBz2xHI2alwzhJVLuD9jjvuWJYJ5uYJn3Dc2avw1/IKAqcdN9jguE26oGzLmZYFkLNRXh71NK2D64P6ytmy31QujzGUl22uD4pzHTc2NnZdxw0RPK96AFJ5nO/f/va3sqPmaY69a4lM33333fVsedasWWrChAn6s4cffljv0bEXbhunnXZa5nKmZZnCpk7bcnnodH2QHnm0reuD+uksuv15Xcc1oVQQvPHGG6Xnn3++dNFFF5UGDBig/+e1fPnysszIkSNLd9xxR/n9D37wg9L6669f+stf/lKaM2dO6fDDDy9tueWWpVWrVpVlDjzwwNL48eNLTz/9dOnxxx8vbbvttqUvfOELHTpkpN7E8vVC0e3vCnVw9uePoteh6PZ3hTp8kJH9hQlOg0jlN7/5Tfn9+PHj9d9HHnlE7bPPPvr/l19+uRzhDb71rW/po9jIy2ZmTboX6V8c6ya4+eab1emnn672228/vbxx5JFH6tzvNGDZ/IILLghdPi8Cim5/V6iDsz9/FL0ORbe/K9ShT0b2N+C966rBwcHBwcHBofsRsDg4ODg4ODg4x+3g4ODg4FAoOMft4ODg4OBQIDjH7eDg4ODgUCA4x90NjhRNq+f1119XDQ0Noa/bbrutLBf2PQe01AMdaSuyDYL2nXLKKVUysN0dcsghum+hv/3mN7+pSXjyth/5r3/962rkyJF6/MDGdMYZZ1RlTdS7D37605+qj33sYzoLA16DZ555JlaesQHLIfIcBnTvvfemviZsIo39v/zlL9Wee+6pBg8erF/YFpSHLCrY1gceeGDd7E9bhxtvvLGdff4Mms7eB2HXKy+uzzz64LHHHlOf/OQn1Wabbab1yOFUcfj73/+udtppJx1VDgU2fVLrdRWKuiabdRF897vfLV111VWlKVOmlNZbbz2j35BDjuyf//zn0gsvvFA67LDDQnPIx40bV/rnP/9Z+sc//lHaZpttUueQmyCtnrVr15beeeedqpfkz/vz5hk+N9xwQ5Wcv3551gHsvffepRNPPLHKPn9+JfUcPXp0afLkyZoT4N577y0NHTq0dO655+Zu/9y5c0tHHHFE6a677iq98sorpb/97W+aY+DII4+skqtXH9x6662l3r17l66//vrS/PnzdTvCibB48eJQ+SeeeKLUo0eP0mWXXVZ68cUXS+eff36pV69euh5prglbSGv/0UcfXfrpT3+qx8GCBQtKxx13nLb1rbfeKst8+ctf1v3ob+v333/fuu0drQPjYNCgQVX2vfvuu1UynbkPli5dWmX7vHnz9JiiXnn0wb333ls677zzNDcI19mdd94ZK//aa6+VmpqatJ/gGrjmmmu0/ffdd1+H2yQKznGnAAPIxHG3tbWVNtlkk9Lll19e/ux///tfqU+fPqXf//73+j0dy2CYOXNmWeb//b//V2poaCj95z//sWazLT077rhj6Stf+UrVZyaDOc864LjPPPPM2AuzsbGx6ub285//XN/8mpubc7c/iD/+8Y/6om9paal7H+y6666l0047rfy+tbW1tNlmm5VmzJgRKv+5z32udMghh1R9NnHixNLJJ59sfE3kaX8QPNQNHDiw9Jvf/KbKaUDilBXS1iHp/lS0PvjhD3+o++Cjjz7KrQ/SXGff+ta3SjvssEPVZ5///OdLBxxwgLU2Ebil8hyOFAVJR4ragg09UMLOnj1bL++G0QAOHTpU7brrrur666+vy3F2tdQBgh3sGz16tD4cZuXKlVXlsqTrP0HugAMO0NS48+fP7xT2+8EyOUvtweMObffBmjVrdJ/7xy+28l7GbxB87peXthR5k2vCFjpifxCMk5aWlnaHRbAUypYKWxhQJ3MKVD3Q0Tqw/cKBRxzUcfjhh1eN46L1wa9//Wt9/HL//v1z6YO0SLoGbLSJoDDMaUVCPY8U7YgtterhAho1apTe5/fj4osvVvvuu6/eH37ggQfUqaeeqm8c7MXaREfrcPTRR+ubGHtUc+bMUd/+9rc1u94dd9xRLjesj+S7vO33Y8mSJWr69OmaBbDefYCu1tbW0LZ56aWXQn8T1Zb+8S6fRcnYQkfsD4Kxwrjx32TZSz3iiCP0OQivvvqq+s53vqMOOuggfdPlAIq864Aj48Ft7Nix+iHviiuu0Ncsznv48OGF6gP2fefNm6fvPX5k2QdpEXUNMBFYtWqVPqu71nGpurvjLuqRomntrxUMuFtuuUVNmzat3Xf+z6CghV728ssvN3Ya9a6D38kxsyYgB2pbLvitt95aFaUPuPAJ0Nl+++3VhRdeaLUPHMLPFCfAj5mdP7iL2Z9/POEgGUfIMa7yBocm8RLgtHngvu666/RDX5GAw6aNWUXyo7P3QVboto67qEeKprW/Vj233367XjY89thjE2VZcuMG0dzcbMTVm1Ud/PaBV155RV/s/DYY0Ukfgc7SB8uXL9ezjIEDB6o777xT9erVy2ofhIFld2Yv0hYC3kfZy+dx8ibXhC10xH4Bs1Qc90MPPaSdQlLfoovxZNtp1FIHAWOFhznsK1If8PDJgxOrSUnYqo59kBZR1wDbW0Tw0x619mkZqXbEuznSBqddccUV5c+IZg4LTnv22WfLMvfff3/dgtM6qocAr2AkcxS+973vlQYPHlyyDVttxelvlEM0rT84zR/Red111+ngtNWrV+duP2Nmt912032wYsWKTPuAIJrTTz+9Kohm2LBhscFphx56aNVnu+++e7vgtLhrwibS2g8uvfRS3fdPPfWUkY4333xT9yGnD9YDHalDMMCOExPPOuuswvSB3GexacmSJbn3QdrgNLJU/CBzJBicVkuflu1JJd1N0ZmPFDVBkh5SXrCf7/1YtGiRviiIgA6CNKVf/vKXOt0HuZ/97Gc6FYLUuXogbR1Iobr44ou1s/zXv/6l+2GrrbYq7bXXXu3Swfbff//S7NmzddrGhhtuWLd0sDT2c0MlKnvMmDG6Lv70F+yudx+QtsLN88Ybb9QPHieddJIezxKB/6Uvfal0zjnnVKWD9ezZUzsF0qkuuOCC0HSwpGvCFtLaj21E7N9+++1VbS3XOH+nTp2qnTrj6aGHHirttNNOuh9tPuTVUgfuTzwQvvrqq6VZs2aVjjrqqFLfvn112lER+kCwxx576GjsILLug+XLl5fv9ThuUoL5H38AsJ06BNPBvvnNb+prgPTCsHSwuDYxhXPcBiAFgY4Lvh555JF2+bQCnm6nTZtW2njjjXVH7bfffqWXX365Xd4iN28eBnjSP/7446seBmwhSQ8XQbA+AAe2+eab66fCIHDmpIhRZv/+/XWO8rXXXhsqm0cd/v3vf2snvcEGG+j2J2+aCyp4Tu7rr79eOuigg0r9+vXTOdxnn312VbpVXvbzN2zM8UI2iz4gD3WLLbbQDo2ZAjnoAlYBuC6C6Wof//jHtTxpMX/961+rvje5Jmwijf0jRowIbWseQMDKlSv1Ax4PdjyQIE8Obtobbj3r8I1vfKMsSxsffPDBpeeee64wfQBeeukl3e4PPPBAu7Ky7oNHIq5BsZm/1CH4G65J6stEwe8TTNrEFO5YTwcHBwcHhwLB5XE7ODg4ODgUCM5xOzg4ODg4FAjOcTs4ODg4OBQIznE7ODg4ODgUCM5xOzg4ODg4FAjOcTs4ODg4OBQIznE7ODg4ODgUCM5xOzg4ODg4FAjOcTs4OCRin332Ud/4xjfyNsPBwcE5bgeH4oCTyD71qU/p///73/+qr33ta2qLLbbQp4BxutABBxygnnjiiarfPPnkk+rggw9WgwcP1kdUchTiVVddpc8F9qOhoaH8Wm+99dQnPvEJ9fDDD6sig6Meqc///ve/vE1xcLAK57gdHAqII488Uj3//PPqN7/5jVq4cKG666679Kx46dKlZRmOAd17773V8OHD1SOPPKJeeukldeaZZ6rvfe97+lzjINvxDTfcoN555x3t/Dkq8dBDD1WvvfZaDrVzcHCIRccp2B0cHLIEhxpwmtOyZcv0YQd///vfI2U/+uij0pAhQ0pHHHFEu+84VYzfc1JR1LGFHDfKZxxaAjhM4cwzzyx/z2lMHMiy2Wab6ROROCzBf0gNRzJyOhXfc4ALp7DdcsstVXbcdttt+nNOsOIwGA68wG7wzDPPlCZPnqzrwKEsHBjDiVd+YB+no33qU5/SOjhIRo53lENbwg6HcHAoOtyM28GhYBgwYIB+/fnPf1bNzc2hMg888ICefU+dOrXdd5/85CfVxz/+cfX73/8+Uke/fv303zVr1oR+f/rpp6unnnpK3XrrrWrOnDnqs5/9rDrwwAPVokWL9PerV69WEyZMUH/961/VvHnz1EknnaS+9KUvqWeeeUZ/z8z+C1/4gvrKV76iFixYoJe1jzjiiPIqwPLly9WXv/xl9fjjj6t//vOfatttt9VL/nzux0UXXaQ+97nPaRv4/phjjlHvv/++2nzzzdWf/vQnLfPyyy9rfT/60Y8MW9jBoZMj7ycHBweHdDNuwLnRgwcP1rPVSZMm6SNYX3jhhapzl7m8mZ2H4bDDDiuNGjUqdMa9YsWK0qmnnqrPEpYy/TNuziPmO2blfjBjjjvL/JBDDtGzdMDsGZ0cq2oCjiodOHBg6e67766y+fzzzy+/Z7bOZ3J+vBzLGNUGDg5FhZtxOzgUdI/77bff1nvbzHSZse60007qxhtvrJJLc2ovM2Bm8gMHDtSz1V//+tdq7Nix7eTmzp2rg9uYtcvsn9ejjz6qXn31VS3D99OnT9fBcBtssIH+/v7771f//ve/9ffjxo1T++23n/6e2fovf/lLtWzZsrKOxYsXqxNPPFHPtAmWGzRokProo4/Kvxf47evfv7+We++991K0pIND8dAzbwMcHBw6BqLE/+///k+/pk2bpr761a+qCy64QEef41QBy9CTJk1q91s+33777as+++EPf6gmT56sHeWGG24YqRcH2qNHDzVr1iz91w8cNLj88sv10vTVV1+tnTNOlXQyWXrndw8++KCOemdZ/5prrlHnnXeeevrpp9WWW26pl8lZ6qeMESNG6Mj53Xffvd3Sfa9evareE0Xe1taWui0dHIoEN+N2cOgiwBGvWLFC/7///vvrme6VV17ZTo5ZOnvRzLD9IKVsm222iXXaYPz48XpGzcwWef+LMgCR6Ycffrj64he/qGfXW221lY5+DzpZ0s7YpyZCvnfv3joSXn5/xhln6H3rHXbYQTvuJUuWpGoPygPB1DcHh6LDOW4Hh4KBmei+++6rfve73+mgrH/961/qtttuU5dddpl2loAZ7nXXXaf+8pe/6MAw5F5//XW9/M2M/DOf+YwO6uoImM0TBHbssceqO+64Q+sn6GzGjBk6GA2wxC0zamb3J598sl7+FjCzvuSSS9Szzz6rl78ph9z0UaNGlX//29/+Vv8WWfRJwJwpmKnzcHDPPffoslkpcHDoCnCO28GhYGA5euLEiXppe6+99lKjR4/WS+XsCf/kJz8py+Gcyd/GMe65555q5MiR+jcsSRMNjlPrKMj5xnGfffbZulyIYWbOnKkJYcD555+v99whhSG/nJm4kMcA9qIfe+wxPaPmQQB5VgcOOugg/T0PGOx5UwbR6My+N9poo1Q2Dhs2TM/mzznnHLXxxhvrSHgHh66ABiLU8jbCwcHBwcHBwQxuxu3g4ODg4FAgOMft4ODg4OBQIDjH7eDg4ODgUCA4x+3g4ODg4FAgOMft4ODg4OBQIDjH7eDg4ODgUCA4x+3g4ODg4FAgOMft4ODg4OBQIDjH7eDg4ODgUCA4x+3g4ODg4FAgOMft4ODg4OCgioP/DygsEOWmKf1cAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ct = data.query(\"LocationID == 'SanMarco'\")\n", + "x = ct[\"ISOPleasant\"].values\n", + "y = ct[\"ISOEventful\"].values\n", + "\n", + "msn = MultiSkewNorm()\n", + "msn.fit(data=ct[[\"ISOPleasant\", \"ISOEventful\"]])\n", + "msn.summary()\n", + "msn.sspy_plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "254fdcb6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fitted from direct parameters.\n", + "Direct Parameters:\n", + "xi: [0.065 0.629]\n", + "omega: [[ 0.149 -0.064]\n", + " [-0.064 0.101]]\n", + "alpha: [ 0.791 -0.767]\n", + "\n", + "\n", + "Centred Parameters:\n", + "mean: [0.283 0.451]\n", + "sigma: [[ 0.102 -0.026]\n", + " [-0.026 0.07 ]]\n", + "skew: [ 0.136 -0.131]\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAekAAAHRCAYAAABO50FXAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsvQe8pFlZ53+qbqibO0yH23kSQdwZkguCCYEl6O6CoguoS1LUXQPIX0kKSBJFBRYWxRUx7BowsLq7rsiKYFhHhqAE1wGGnumeDvd293QON1b9P9/z1K/e855633rfe2/durdn6vl8qm9X1amT3/OcJ/2eSqPRaLg+9alPfepTn/q06ai60R3oU5/61Kc+9alP2dRn0n3qU5/61Kc+bVLqM+k+9alPfepTnzYp9Zl0n/rUpz71qU+blPpMuk996lOf+tSnTUp9Jt2nPvWpT33q0yalPpPuU5/61Kc+9WmTUp9J96lPfepTn/q0SanPpPvUpz71qU992qTUZ9J96tM604033uhe9KIXbXQ3+tSkj3/8465Sqfi/m4X6e6RPedRn0n26Lujzn/+8+47v+A536NAhNzIy4vbt2+f+1b/6V+4973nPRnetT02CycD89JqYmHA333yzX7c/+qM/cvV63W1W+p3f+R33rne9q+v1hvNRrVbd3r173dOe9rSuXRBOnDjhfvqnf9r94z/+Y1fq69Pmo8GN7kCf+lREf/d3f+e++Zu/2R08eNC99KUvddPT0+6+++5zf//3f+/+03/6T+5HfuRHNrqLfWpSrVZz73//+/3/r1275o4cOeL+5//8n55RP+lJT3J/8id/4qampja0j9/4jd/o+zY8PJxi0l/4whfcy1/+8q63x2XyBS94gSNNwj333ON+6Zd+yT35yU92f/qnf+qe+cxnrplJv/GNb/SS+KMe9aiu9blPm4f6TLpPm57e+ta3ui1btrhPfvKTbuvWranvTp06tWH96lM7DQ4Ouu/5nu9JffaWt7zF/ezP/qx7zWte4y9ZH/zgB91GEhIt2phe0UMf+tDUnHzbt32bu/32273kvlYm3acHPvXV3X3a9PSVr3zFffVXf3Ubg4Z27dqVer+0tOTe/OY3u1tuucVLdUgYr33ta938/HyqHOpH1IRFtsHf+I3f8GX/7//9v+4Vr3iF27lzpxsfH/cH7enTp1O/RVKCIe3fv9+NjY156f+f/umf2tpYXFz00s9DHvIQzyxuuOEG9/Vf//Xu//yf/5Mqd9ddd7l/9+/+nW9zdHTUPexhD3M/+ZM/2foeKfU//sf/6D/ne+r5zu/8Tnfvvfem6tEY/vqv/9r9wA/8gC+HNIt0d+7cuVa5F77whW7Hjh2+fzGhoqWd1dKrX/1qX8cf/MEfuC996Uup7/7sz/7MfcM3fIOf18nJSfet3/qtbfPGmqA+P378uHv2s5/t/8+8/PiP/7hbXl5Olf293/s999jHPtbXxThvu+02r3HJs0kj4SPVMp9STbMPLl++7Pv0spe9rG08x44dcwMDA+5tb3vbiueC/jDPSNWd6PDhw349t2/f7vfT137t1/p+huP4l//yX/r/v/jFL271nfXu0wOH+ky6T5uesEN/+tOf9urIIvq+7/s+9/rXv9495jGPce985zvdN33TN/mD9HnPe96a+oBK/bOf/ax7wxve4P7Df/gPXoX7wz/8w6kytPu6173OPfKRj3Q///M/7+2xMKYrV66kynE5gEnDxP/zf/7PnvGiyv/MZz7TKvO5z33OPf7xj3d/+Zd/6aVPmAzMiXZFaBYwBTC2d7/73e4Hf/AH3Uc/+lHPdK5evdo2Bvr7z//8z759GPRv//Zv+zqVrfbf//t/7+6//37353/+56nfzczM+H7EEvJKifppK7yM/Nf/+l89U4bp/tzP/Zyfv//3//6fv7TElw2Y8dOf/nR/yfiFX/gFv7a/+Iu/6P7Lf/kvrTLU/fznP99t27bN14cEz3xwycoj5h9VMYyT/vBCyqVPXMaQ/OOLwO/+7u/6sXz3d3/3iueBixEvxpFHs7Oz7olPfKJfCy5iaJPm5ubcv/23/9b99//+332Zr/qqr3JvetOb/P+///u/v9V31Pl9egAR+aT71KfNTB/5yEcaAwMD/vWEJzyh8cpXvrLx53/+542FhYVUuX/8x3+E2zS+7/u+L/X5j//4j/vP//Iv/7L1Ge/f8IY3tLV16NChxgtf+MLW+1//9V/3ZZ/61Kc26vV66/Mf+7Ef8/05f/68f3/q1KnG8PBw41u/9VtT5V772tf634d1PvKRj/TlOtE3fuM3NiYnJxtHjhxJfR7WffXq1bbf3XHHHb693/qt32obw2Mf+9jUnL397W/3n//Jn/yJf7+8vNzYv39/47nPfW6qzne84x2NSqXSOHz4cMc+M8bx8fHc7//hH/7Bt8fcQZcuXWps3bq18dKXvjRVbmZmprFly5bU59TNb9/0pjelyj760Y/24xK97GUva0xNTTWWlpZy+/Gxj33M18VfEevB2sfEPqPsn/3Zn6U+v/322xvf9E3f1Cgifvu93/u9jdOnT/s98olPfKLxlKc8xX/+i7/4i7n77uUvf7kv8zd/8zetz5ivm266qXHjjTf6tYI++clP+nKscZ8emNSXpPu06QnHmzvuuMNLEUizb3/7271EhYf3//gf/6NV7n//7//t/6KWDun/+//+P/83VBWulJBUUCWKUM8iXaEihf7iL/7CLSwseIk7LJfliITaHnXul7/85cy2UKOjmn7JS17iJeyQwrpRcYtQUSMF33rrrb7+UCoPxzA0NNR6j0YAG7LmDVstkiFzeunSpVY5JG6kuptuusmthZBMIdWN1Hv+/Hkv+Z45c6b1Qo2MFuFjH/tYWx1oC0JiHVALixg7movYdLBaeupTn+o9spkDERodNB1lNQu/9mu/5lXzmGYYl0wnnZzUWJPHPe5xXqMQzh9riIYBbUOfHhzUZ9J9ui4I29uHPvQhrya88847vRMShz1ewzqwYJgwGhhVSHiDc3iLoa6GYmaJOhWSTVd1Y2cOicNZZUWoKGFOOBRhn/yJn/gJf+iLxHT+xb/4Fx37hIcyKvYDBw54+zvqWtqj7gsXLrSVj/vGob9nz56UWhk1OPVKpfrFL37RmxpQVa+VsPFC2IohXVLwdKbf4esjH/lIm1Mg9nu+C4m5De3qqIaZVxyy8A3govPhD3941X3WxeWP//iPWyYEGDZ9wV5chp71rGf5SwMXuU984hP+IoKanrrziP2U5QOAilvf9+nBQX0m3afrigibgWH/zM/8jPvlX/5lL0HijJQnba6UYtujCOkui2TPXQlhM8QZ7gMf+IBnxIQsYUNX6FJZQmrHVolz2e///u97xgYzwNa52pjkRzziEd7p6r/9t//m3/OXOaeNtZJ8CnSJUh+xo9Lv+EW4Vpk1CAlplZhhtAFoXpDGYdg4xa2WuLhwwYBRs96Ea/3rf/2vfcRBGeKygET+lKc8xUvHOKP1qU9lqR+C1afrlr7ma77G/z158mTLwYyDHwlNEoeccJAu+T6UwPgsJNTVqmulpLppG4exUHUdSnoiPHbxyOUFA4Bx49CF45t+X+Qo94d/+Iee+SCViXAuisclom84q4lol/F+y7d8SxtTQh3LdzAkHLtibcBqCGbMBQrzBYQHvhgrTKxbxKXi3/ybf+Nf7Aek61/5lV/xTmmxlqXMxY6L1KMf/WgvQcNwjx49uu4gOuwntBgx4fGv79d6Ie3T9UF9SbpPm56QhrIkVtlSpRYUs4mRo97xjnf4vzAbEQwCu29IeAnnSdJFBJPB3svhHfY1C8UK23GsdoZ5KEwMlS5MG0kbhhBSWDeSZTwvtJ83BsYXhlehiSBkLY7VxUbM4U/oEar3tXp1Q3hZI+k/97nPband8SsgRAqtSFbYVxziVobiuUWlTEwyFIfhhYR0m2UiEKHup/+sJ5qK9Y5vZi9j1sEXQ4StnTUkPAyNh/oN5V3M+nT9U1+S7tOmJ9S62AMJh3n4wx/uJV5CjwiN4cBCGoUIfUKy5CDj0CJEh4PuN3/zN32oUShFIrHihPSc5zzHS3Y4pBHugl13NaSYXcK9UIVyyP7DP/yDjwGO6+SAJSwItTIS9ac+9SkvFYchXYRU4TSEGhxnIZy2sB3j/CYISNpBOkXtSp0c6Ng980J7mDdUrqiukdJAvqIN1MLxWJ7xjGd4MwK2/PByU0QwfanKkeqxnaJ6xubO/IfhUjBoLgowQMZJKBltczFhnF/3dV/nQ9RWQqzr2bNnvZ0bqZf2ubgQYhVqV2JiLdhPaBAwp3BxQhIXfdd3fZd75Stf6W31ONyFDnjrQcSVE+bFZeBHf/RH/T5hHxNbDcSq7NlcNlmj973vfd7WD9PGOW2tTn592kS00e7lfepTERH+8pKXvKTx8Ic/vDExMeFDnW699dbGj/zIjzRmZ2dTZRcXFxtvfOMbfajK0NBQ48CBA43XvOY1jbm5uVQ5Qlhe9apXNXbs2NEYGxtrPP3pT2/cfffduSFYhLoUhfFQJ23v2bOnMTo62njSk57U+MIXvtBW51ve8pbG4x73OB9+RDnG9da3vrUtpIzfftu3fZsvNzIy0njYwx7WeN3rXtf6/ty5c40Xv/jFfgzMC2O46667csfwV3/1V43v//7vb2zbts2X/+7v/u7G/fffnznnv//7v+9/Q/mypDApvZhXwoWe85znNP7wD/+wFTYUE3NI3wm7Ypy33HJL40UvelHjU5/6VGF4F2F04TFGO0972tMau3bt8vvk4MGDjR/4gR9onDx5suPaXb58ufFd3/Vdfq75Lisc61u+5Vv8d3/3d39Xek4o/0M/9EOF5eI1g77yla80vuM7vqO1/uyZ//W//lfbbwmhe8QjHtEYHBzsh2M9AKnCPxt9UehTn/q0fgQCFdoGwE9kxy8inLbQPmASIMypTwbnSaKXu+++e6O70qcHEfVt0n3qU5/a6Fd/9Ve9A1sYp/tgJpzoUMF3IxStT31aCfVt0n3qU59SuNfYj2FIQJE+2L2HsQEDPkJ4HHZosM/71KdeUp9J96lPfUp5duM09b3f+70+dOnBTn/1V3/lTQWA2eC4BTBOn/rUS7qu1N3Yx/C4BKaPGz7gAkVEphg8R0FkIswlK0PMe9/7Xu8lDIoQnpF4BPepTw8UIoMUridl7NGUA8kNyRHI0Ac7ae7wEgfdrk996jVdV0yaOEHCbGCqZVVVhI8Q+kHYCli5hGiEWX4UdkF2I/COqZ/4zX6e4j71qU996tNG03Xr3Y0kTcwiHqh59KpXvcrb1kLkJmIxiaEVni+SM3GRiscEoQgsZGJziVXsU5/61Kc+9Wmj6AGtzwLcIYYbREpW9hnAHUgeQLIGESAB/CZE+gkJJg6oBE4koVMN6nRefepTn/rUpwc+NZqmIcyvnZKlrJUe0EyaZPW7d+9Ofcb7ixcv+kw/YCoDoZhVRhi5MZ04caKFOdyntdNLXvIK9/nPv8R98pPX2r77l/9yxN1226+7D3zAYD03I733vR/f6C48aGnHjml37doN7sqVdmXg+HjFjY7e786cmVm3uqGy7XOI79t3szt7tpYqT7nt2+fd8eOH25Ki7N//Ve7YsXzv+v37G+7YsX8uHMsP/dCT3APnrHix++Qn5zbVWXHfffd5dLv1ogc0k14PUpo9IB+B48uTpLGHl4Hm63U5YBtJl4eav8gxqJt9yytz+PCk+77vq7hHPtLeLyzMu+Fhm8eFBXIe/6z7+Z//yVWNYb3n9q672rMyoZ0hwUMRXb58xU1MFGdDKlNf2Ta7WY4ywH7yPHQK0+pGm2fOJBCc9XrDVavWXr1uf4O02k2CCVbc8PC427//QG6bIyPnO67B0tKAI9/K1auN1hhpa88e5wYHp93CwoAjy+fYmElV4TxgRNy1a5/bv9+8wRuNqme49H9sLF3u6lUYzKNcpVJPzYdzo77uLEJwm5xsuEc84rEOqHYShA0N0Yf27Gcf+9gpP7cPf/jyuj3Hqym3mrPo1KkJ9zM/U3VBZlcHNPtP/mTdXbny3W1nxXqOASEPZ2TxhPWiBzSTJlyCDEgh8R7M4NHRUZ+ggFdWmbxQCz2IhGSAp5tHHGCdvt+ocjwYbCrKFD0Y3exbXhncBcJuNBr1VL9I4Rv/ruwY1nNu77zzSqrfSf8bhfNKmYGBqt97RXHIZesr44ndrXJIfEroAG54pxSSneqamUlUhI1G2nwU0/i4lSURx9CQ1Ud66iz/TjHMAwf4XX6/Ll+ecnNzAymGyYtu8JqerjvSiM/N8XnFM0LuETbcQf952OW4/wjGGvu1a1aP1Z0ux+dLS0je4XzYZQQmzW9D4uco/zi25ueTukZHK27PnqqLrW5ag7vvHnSPe9z4ujzHqym3mrNo+3Zw7cmn7Rz5UMgWSkIw5KUTJ6Y35MxdbyyBBzSTfsITntDKlCQiTy2fQ9wuAdb/6Ec/2nJA4wDifZjsYDVUNlHDRpXrdZt5ZeKUvPHDWjJl76r7tZpyMOg86nbYUpn6yrbZ7XJ5DDdNtULGC8WSaJm+5d0NVE9x+uk5NzY24RYWKl5iDpkhErMYdkww77j+rL6H3ysxWd4Y48RljJOiSO1x38ifwh0JCX9gIKmPMpTlchK2Hc4ZezePUa/lOV5tubK0I6gPhhwoMVfc5kaN4UERgkX+W0KplAUIdQT/Vzo/HMDIhSsiyxGp9sheg42ZrD+///u/737sx36sVYbwKyAQASr453/+Z5/hhlAvZVZaLcUpBjdbuV63mVeGW/Btt7lIzWfE50EK6BXTesxtJwYd978bVKa+TmU4/Dm8kTo51IsyccJsZ2Yqzb/Zr1OnBtzy8m7/On160H8Gw816DQ4u5n630nHG5ZBq21XdxvD5vISW3S0vtzNoiPdnzlTdyEh6DMkcVd2FC8lnqLFDitsX04zLxd/H40QqhuneeGPyF+0A0ncW0e94KuO5zdvDa3mOV1uuLB3dgL51ewwPCkmalH5hukEYLER6QkBKwNcNJxZ7AiFYMGUgDjHuA9KAh7eI/LbkrX3961/vHc1IaUd4VuxM1qf1IW7Er3udc29+s3Of/7xLMWg+z7oxbxSdPLm9TZW4mYn0ySEDwobLAV+rVTMlRAimMz+/6Gq1zuruixfNcWpycrdX3feSuGjAd/i7c6eZREgjrWBS2Y2LJWnU5+0MOmZ4oeNufLnAPMNn8vm6dq2e2b4uFFcy+GPRhYJ6wssIF65OVCYluhh1J/V3nzYHXVdMmhy8ncK6s9DE+A1OXp0I1fZa1dsxkRd3M5frdZudytx8s3PvfKfZmc6eHfR2J9mZNkv/zQbdPdVzWVptmxzU5vSEY1M1pbqFMU1NYRNffZtlqVod8sxOzk2JTXd1bTYaw+6++9KMFbstEubSkuzAlUx/gdUwNL4fHs6vbGKi6svMz6OlaLQuQOfOJWpxiDHDuE+cSKRgyqG6lt05nJ9O81F0+Yi/71RXqP5e63O8mnJlaecG9K3bY3hQMOnriQjt2szlet1mURnZmc6evVjKWaNb/SpTTlJHGdyfbmMDrbbNU6dM4kMCg3EdO2aM2ryA8WzOVhUbwzHJsBNTLSvJnzhRSalmJWW2aySKx6mLR1gfjG5igjAYu3zwnvnAgzq7ndUwvEZhmVpt2Q0MJMfplSv1lJ0ehk1fCJvCSYyxIKHj/HXmTFJXIoXntympPEuizpLKi/aQGHX4HGDzlnMWzyUOdPzdiHOoW2fMepVbb7qubNLXE509e3ZTl+t1m9dj/zm8QvvdWg+KxD6MSDvm7aFrqS+rDIzh5EljsvV61V29WnUnTiSPOZ/DzJA6s5gqzI6wovAvnxf3IbF985c2Zmaybb0w2nhYS0vF40T1HNc3NWVMjnZDXpTXTkxcWLIuKyHDK9O3uExse5cdu15f9PVK9U30DtI0QhtOklxAivotqTwOz8pT85fZQ+xzPQeHDzsH3tP3fi8mReKTncONh88JO+r1c7xRZ0y3x7Ba6kvSqySSFQBQ8LKXvcw961nP8s4ZhHXt2rXLg/EDeoJ3ILfY+zGYOVRyN3q7N679xFTv2bPHl4NuuOEGXx/2cYV4nTlzxl29etV7oWMHVLL5bdu2ecQz4YsDY8qGwuGN8DHAVr7yla/474jlJnEI7ULY2nHA43vqxW7P/+knoWnj4+Petg/RT9oA/IW+kV8YZz0eekInKH/8+HE/BkLWAIi5wNXbOR8/CDIbYRZkVWJ+1H/6wHs98NR77Ngx/xlzxdjlW4DKifb0wNBfyvKetkH7Yb5Db0zmDaJtymq+KXv48Fl3//3j7urVIbd1a8VNTp5zs7MzbfM9M3ODm29yJwuVqvrwH2hoaNgtLy/5NcFbl/IqS1/5PCk75D8j5haHrLk5i5WFoYyPN9zevZRaaJXltzpU6bPqYf7pR1ivyvLZuXOjfg35+uTJig8Z4sDHBi01q0KMYNRInPSZelGHLiwsuxMnBjxzCyWva9cq7sQJ+rnsqlXC44b8OtEXxkEf8I7W2PTbkZGKZzpIlMQIG3EpaXgVPKFDQ0OLrTmEwvkm/Ghx0W4SzC9rubg40FTbJ+0MD1dadl51W9/RDn2rVlVv1VWrNoeUsdei2707WRv91hhexS0tzbfCvhhvuDbMg4VKoaVotPpv6mUkZis7Pm5lFxaq7v77a7781BR7Oum7lWMPO3f6dMP3u1JZ8Ossxy+prZkL2jO3mZpXs+PlXashoc/7S5XC+2zeFltzmLVnbb6r7ujRSXfy5AX3/vdPun/8R/Yx/afemvvUp+bc615Xca9+9bA/Z3RG8ExxnsRnBGdCeEZA8RnBeUgoH+cPoXycVTyvWWdEvV73ZwKfjY2N+Wc9PiN0lnJG8H/Gx9lHXeEZQX91Fh06dMifmeEZQf8h0MToa96ZTD96QdctdvdGEZuRDQUz6aSWZdN0ih/dqHI8qH/7t3/rvv7rv77QDtjNvnWzrrJjiOtCEnjTmyw2O3RQ+6mfqrtbbjFG0sl7Oy8kp6gM53psR11ernumATOIQ2ZW2qbUqqdPVwMnscTLl5B/1Nu8py76AwMNzxh+h+ScR9h8Q4nT4owvu9HRSce5Gku4Zhs2SbF5JqeIMaOmXgll9RGm1jzXvdQbozPmtaP+c4GEaYXOaGtV8xcRbVy4wAULtXIafARGzZzplUdx/ztRmT2kclzsmOP3vCfboez972+4Rz2q0tPneCPOmDLlEBQQrmDkXEbWi/rq7nUi3eo2a7let7nR/cfGFjNoCI/yN7xhyd1xR1q1nUWLi8VhQmEZqYCR6JCYsq7DWSEzZduUCnVoaMlLMTGjhNHs22djh5HxQqihPzGFGtGse3uexrSTdzTt5Hkt54UcdSLqGhlJ903MB+as/4f9L8to5UENQ+dv+LtyYXDLKXW/5is2A1CO6jBJ4Cewdau9ROwTu2wkY4jrWKmptMy+VTnV/SM/kv0snDmz0PPneKPOmG6PYbXUV3evE0mVtFnL9brNje4/2q6YQeswQo2JejHPNrkaJ64w/AmbI+9hJNhAY2mv06Gb16akZwuZSsc/w6y2bTOpDE0d9mc+4/CnOiF1hRL8Sj2GpTqVbVuq9LAPjDOCo161c5P6MD3dcLOzldbFAIYHY5XTWFE7q6GivpmDXLXNQQ51NPMcXoqIu2Y/aH5g1Pv311uMWpL14CALOtgWRheq4ssiXZVVlhoSXvrZiCXqiYnlnj/HG3XGdHsMq6U+k14nwhaymcv1us2N7n+scg0lBamAi6jMoSjVaXiwiinLaStmHJ0YZFabIYO2+g2yUmPBCQmzPBpEjZvveXFec1GQBK+LiTyGs6TimNnBnM+cMTvdli1T3v7NuEKGzJhpn7bKODcJj7uIYF4HDlRTqmmsTrGT2kpipYuoU9/CtQ6XCsaM5iKGJQ0lYeaGeYNRQzDrbdtsbEjS8T4K6+DzXbuiyc2hsszcbNXpfaDnBGaNaWh6Gkl6rKfP8UadMd0ew2qpz6TXicqCoWxUuV63udH9F7xolhqPw6nMYY4TT5kySFThwcqhy2GNKhNGFgo2RdJe2GbMnEU4c8EUVRcMGiYRxpmLgfK9zuzwYiKPYWMKldLMTow4Fjpoj7YYN/bs4jjp7LmNbcU4cMXgHhBagaRcpas25by+hR7nMSOUxgKNRkiUozznP3PG3Gk/EDbHfI2O1r2D2ZYt9Y5AK8vL5Y7vMvtW5fKgSF/5yivua75m3O3bVw78pNtgULs34IzZLIBWfZv0OpG8CTdruV63udH9v3r1ij9osgg7Zxm1aFmIzlgqv3jRVN6SqlaCjKU28xi0yojJUqfUq+IbShgRXxDidgVBefBgvQVBiU2b3+XZQ1F3I8nGjq685/PFxXqurTdrnKsNCQttynhzd9Ppqwh2tZNKOf6YcuwHnPnor9TevOSJzxqDZHbuHI6F+Ud0lilhpf0vC0XK3zNnrri///tLPX+ON+qM6fYYVkt9SbpPD2iSM1haUoxth+lEBWulmEFwUCPd4gAKsza7tOFLl0HG6sSgQ+Jw5ZyVtEzdQuLiPRK+ADDyJHhDuwIWtOaZIY5mWfZQhVPBpLB7a2zyJKYffG4hZiunIlXv0FCnzFsjPmSuMxE+NukmJtYW3FJ0GcjSNGuO0tJ/WsswNEQIFKFxXHJwCmznyLFvQzcpS1tRJklHn7pPfSa9TtTPgrV+dZVlzMvLu1KHqCSE+GAsg3a1UojO2L7LwYx9mM/JD3/t2lU3MEBsUOfLwblzI4UMOuwX/2Vc2KVhkoxVcJlIart2mdodSa4TLGhnJkkM9Libm7vcsnNnhVlZXeUuP2fOwBGSsvTXIDnjklwMcPKzNI5ZJJV3J1Kfjx8fTEm7MePbt2+547rLhht7zAsJLUuI1QWpEyNUm7OzVTc52c6oTSuB195wT7OfqUwR9vd6ZsG6Hs/StVCfSffpAUNFIVRQ1sHYbfS/TlJ7EeRjuwTdKJSgQ+LwR6KVXVr94WzFSQlVKxeVIjNlFrqXiM+3bgVv87IbGkIjkHhbhzQyMuDOnm24c+eKGfX4uMWNiy5dSsNlpqnYE78MMQenTqX7DuMXnOjFiw13/Dg3GfpVaTHtMmtNPfLuDkn1l1HHJ3VbeB0XLKO6/9yAbbrgvr4G6ifqWH/qM+l1RBy7/fbbCxHHPve5z3mUmyLEMX4jJK0ixLEnPvGJXUMcY1xlEMce85jHdEQco3/qfxHi2KMf/egVIY7JThYiMkGMid8qVzFIYQsLIYpYgsjEuDshMqE+ZXzMexHimHmFLro9e0jpiFQKKlXDDQ6C30xYzYJHo6K9PBSxs2drTRzqa25+fqgQcUyoboZbXWshjNl8KPyr4VXeYEc3GglaliGOJahWjGVpacR7bYfoXhDvLQvWiKvXd7vZ2QEvtTJksmxJK0F7k5N4fZOYZMgNDtp8hyhizCGgLvZ5o/UdZRmr2mXu7f+G1Ia3daVSd3Nzi74czH1hweZleBjHvTmPo838mdp+volbbnNoZYkxH3Bzc1av2rlyBcZsntYTExVflvkdGzNEt2PHKkGf6m7nzjk/h2gmlpdZb9MeMNdAgE5Pg8w26MfH5axaXfJ9ZA9k7VlDBlv03/N/yu7Zw16ptubX1uRaE2HO0NOy92z5/W1x9trfhpJHWUhzGO7veB/+zd+cdfv2nW+dEffdd5972MMe1lXEsYsXL3ZEHPvCF77gz4MixDHOJZ6XMohjjKePOPYARRwDdg5GVUS9LrcSlJ9u9q2bdWkMo6Nf0zG8hIeUB6+IulmuTJkitKh0DPTK28TuzPkVxy3rgM9C4Qq9qGGAMDQctfJsv7Xaort27azbuROTgjJBJWYEusPfuTkOyuK5jcsJpS1WIys1ZCeUtjJtUu/hw0CdZleCLV9ncF59SNphv0ie0Yu9BtQqND5+oRTiWK/3N1L1Sp7jbp1Fd/f4LO0V4lhfkl4n4oa2mcv1us1u9395+eGFZbjtlqFulitbV1knsdW0CfOSR3ea7IOYuYWAGcPDfIlkZUwKgenMmUqG5/awq1bHWzHE1JklWNRq5eYjLheqkUNGTdrNInVxXpvhRQICu50LTZaXdGgCyatvaiqZl4sXK+748URdH6vFu7nXzPu74S5dmnBXrgykUmKuV5srKYMKvNGYdiX4YGk6tAFnTDfP0rVQPwRrnQgVymYu1+s2u1UXB8CnPx1AO3UgqeR6Wa5sXZ0otEGj+iyiuIycmWLiYI+9umFGJNZAVWsM2hzOsNWeP9+OapXYVBvrPrdZoUAk+igSCvGIhrFj1+avmDPaBYVxof28fLnqsb+zvKTDS0CZMYyMLHmmLcaNLZtXe3awcnFTZdbdOXsOwpSYm2V/Uy7OIrcWmu3hGbPScutNfUl6nQgbx2Yu1+s2y9Z17VrNffaz7XlsoZU+8CuBQ+xWubVYjzhsYycxIEuLKC5T7Lhm72EiltIylpZhJJWWs1IIRgKDNLhPsltZCFboLS8190r636mcJHQxWrJn8f+4HZGFjaUhOvF0h2GHnzEG3is0DnxzEe2FF4GVroEY9YULDXf0qM0xmN2CBSUsreiiUXbeyJt99WrD7508iXoj9ndYrhvOZXNdPGM24ixdC/WZ9DpRGRvQRpbrdZtlypCl6o1v3OLuuiv5DCjC173OgBRWSiuBQ+xWubJ1lWHQa2kzK9xsYKDupWXzWDZmku1FnQ6FirMx4QR15QoZgibc0aOWEjPLO3olcJ+dyhk2tl04zJmtvZ1EK9AO0cn/8WqPNbWDgzjSVbw0Hfc/vACUGUNchr6QilKXpB07rMJr15b85amTTX2l+4h9g51aEnXMrDdif2eVWwuzrnXpjFmPcutNfSa9ToSX4GYu1+s2i8ooS9Vddw21Zan61KeuFB5qWYQXdq/Lla0rpE7qyrXYCeNws+PHh9tsqVlzGh6wWfbrEycq3gP7yhWkuCRxCD/jvZgQtLSEF3G2lF1m3iyto10UiPOGGXLxgOnGzI52siA6pS1oT9lolxb6rnqy+riadVdfRNJU7NhhMehI1p3CyFbq26ALnph1yKg3Yn93Kidm/ZjHlGeCe7twxqxXufWmvk16nUhu/Ju1XK/bLCqjLFVx5hmwtovSOeaRwq16Wa5sXWXRxLqR/Uf2UfOMrqTstTjVxg5flMtS+4aSaq2G9JZOHCKiXspgA77nnnohpGenMfAx0XfURXQOf2HQSL9I8OHPEohO6z+XB0wljIFx5mlqYdJcAhhvtgp9PtVGbO/O6n9e7P3p0w3PsM+cSTQaK5mPItI+Ci9+G7G/y5TDt6SMA2i3zpj1Krfe1Jek+7QpKAuxKkyG0W3Akc1AxBhDKwErWSmFqm0SZ8DkQgkP6RTQDXxk0l7U7WrfUDqMGV4sqcZJRqBQyi6jFRHqWRyGxXvL8lX1/Q6zjNmlwj6gLzBTLhN8Li/uBMvcpNmyWk2p3WlfY2WeELhi7W7R+IaHiWEWWEq+N/hqKFR/F3l+bwaCWaP96AOiZFOfSa8TmAmB8ATqF4GZUI54vCIwEwL9KVcEZgJYAG12A8yE8dBGEZgJY6DOTmAm1Kv+Z4GZ1GoL/hCkH0hCP/zDV/2hmiSJaLj5+UU3PFzznqOAgfCXeQiBOEJgCAOuWCgEMxHgSRGYCeX4jHqWlupucREnJuqteJAK5+ygpQ7zgs0GMzGAmAn/GY4/aicEKIHYI7zn+05gJthpzXPYADMA0hgaqjZDggDlQLKpNB3J6ilwkMuXDYebw5xpk813cNDG02gM+zZxZEJ1TVnWJ8vmaIyb9atEn6l8xTMP+sq4QzAT1OfEI8/MwDGtDyahZ6OLiUmiFSZBCmOHKSFlM32JpA34SsMzUrYmDJbfcTEZHma/VNyRI+wPU6VrXvbtM6AUwExg5nhlM5/UH4ZssRz8ZP9+A4FpNNBYGNQozJ+xcolQvfZcuSaoybLP6oXZAJAUgFEEZkKbqMTxuqdtQdiyZ9incszKAzOp1Wx/s+aNBmdN5/3N/kr2dz6YSXp/Z+9ZygpkJ2/PJoA21ibl/v7vL7vFxQW3Z8/ZtjNi69atHrykE5iJztIiMBPOCZ1FncBM6BPnWR/M5AEKZsLiUq6Iel1uJQAC3exbURls0j/2Y8794z8uuZe/PK0q41ALpa8iMBARDy4PWhGttFwYVxz2EcmTfMdFZchuxCE8MTFY6IhT1Ddrp5FKLwkcJyrcLVuSz2BO997Lo14pBO/Ia9PqsP9PTi67K1eq7urVpD6YEsMBfYyXeUyn2wT4xJhNXHtSjvjlEJQlzFVtlzj7Hs9/vmOslIUpc5rBsEkCgnpdea15sf1omzKjow0/Hu7PupiwPjBzypw/Hx6LoIdV3IUL9cyYatp4yEOsDepEuqcvttb2f9nsaXfv3kqm9C5wFKRqkNBmZiqZ+4ffln0GIJz8dBFb63PQzWeqaAyhZH2hC2dMt8v1Csykb5NeJ9Lta7OW63WbRWWwHeLF/epXp8MeyqRzzCNJ1N0sV5SdCRVmpzIkTDCaW3PfstTBOELBSMDLDk0E/D/rPi67Ld/LzgqTyyKYgxj5+fN1t307moCkHql/kdQvXULyrzY1AMmLfoH1DSMOX6OjC63/x6AsljXMPtMQeE9/6LO8tyVdo0mADFY1+S2SNMonmCaMFBs3/5c0zPqgzmYu4r6ZZGypI+P0kcw3y0T9MhvwGWsjxo/kDjYGEJ956vUwxrrTHlup6ce0IsXx1GWeg24/U50ojLM+3YUzZr3KrTf11d192jR0880cUJaiLyt932agosQThrGcXYbDne9gXGVzAa+kLwIjkScxUraYaNYcwrxQC2PfReKTMIMkTh7pmJko/hpGBnOcncWmO9iKY5et19JimhQoVWwyB9kpMrNIoCzUA6OGUWre+Bxbs2zhsU1cl4aQTMq2cYVx0XJ8ow05KcYq9lrN7Pmi3bsThocJQTmhwwsT9TYtXb7Nm27if8sdj10YNXXMzQEugyd7miOrfx4efgVUFKa1menOO6/43OQPVuoz6XUi7BebuVyv2yxThocRRrPKUOOewILKozmP6nXsve2fS/o6dcpwp8tSp75JojIbY5pBh99DqGLpg2yyvNDQwaCRJsNmYLB5Dl4c8pY8o9ZkwKZWj8txOTBP8DSDRqrMujDkjRMwFdwpdBlBQKPubduS+O54vzAfMDLKhUxTzDz0+hZjZw60tpo3AbUsLdV8n/EA53ICzc4mje7ZU/V9sQuEMWz6ByOVJE89fD86WrzXaPfMGdDQGn5dY0a9Uklac9spTCssV6aubpUrS/ffP+3Onu2cx3ozn6Vrob66e51IGZ82a7let1m2rjw1WRpeEaYwvCHq7iKpHseesIzUozBnXtBKNAOd+qZ6hoet3hhjO4wfhunClGHWvIdpIDnCnOLwJJgvzE2+QkeOVFsvCBUwks3wsEk42UzXmPzBg40UtGfe2R2Pk/5hU+YFsyPLFAwePGj+D8E0xQBDHx7DtTYtAU5zIRPnvaRw6kblrUQeIJNRH+OhTkK/KCdPdVTWsYmSdkdGLAsYxFpPTtpliLpJAEX97FvqLrPXLEe3QrXsAqZLWLiuZSluMytMK6tcmbrWWq4sLTXr6wQ1upnP0rVQX5JeJ8IDcDOX63WbRWX04MmjNKQsJ6xabcjt25ftgCPKqmut5UIVbEx8juMYDnmmLk2k57AMzLHsGdapb+bZjA3aDvSQFOMcxjZfvVp3W7ZUPTOSAxXqa5gY32NTNQbARWPA21eTfsMsVsYgLI/1gqvVRlY0zrDPEAyXsWBPZhtRL2UYI4wYlTIMFBW2fkN1MEaEIf4v6RO1NnuJPcVvWQ9J03xHLm4uLfwWJh2HWzFflIGJy+bN540GHtdD/jODGq02JXkbl+zgOLQV5fJmrPRLSG6sraTqgYHl0uaCrLntpP4u8xx0+5kqS/WoPs6LWKrezGfpWqjPpNeJNiID00rK9brNsnXFXp55TlhlYm7XAxa0CBfb0jyakxGHfiitJE5w5QMqOvWNfMgc+MBPhhTGOAtYBMJDGQcr2ZUlUYsBwejwPOdyYYyp4lXNYf+RZrmIKPkAeZbJ3bya/ueVi23tSMwwPjFMJFKBivA50jz9pxy/lX05vlTwG75DOmYcMPdQWCJmHKZOOV0IrG/Zey70mcDZDimbOWXOCKuSKtzm3kLPlpeLlZe2xxpepa4+wKh37SKMzxj1SmJy8tYgVn9v376+sKAroRgTvlpt32Mxo97MZ+laqM+k14n279+/qcv1us2ydcUPRpGjVpaTT15dZdssKpeFi50whGF/4JnNt+rVoHGZlRyweX0TCAb2YJhL2BckQSR1JNBQAOHgROWN0yqSohiNlal6SZT3fI/TUtxPeT/v31/+AF7NGsQ2V76KGaaFcVlIlABL0pQtyW3fDjMyBi37fDg+GCzq9DwhSp7bkuaZd/pCDDAEk5aWAsKbnTZo1/eqpIBJIg72WDpHt10SlRhlamrM+w8UaTiK1kBS9dmzIy07dcwk1UYvGFyoOWs0FDJHSs522NYQD3wzn6Vrob5Nep3oMNkiNnG5XrfZqUxoYyoLr1jm+7LQiqspJ1xs2Th1cDRxYPzBl1dmJZTVtxBFDFpamm9JknaIJzCcSI4ctnbYmbpeDEgHMDmarV4DHKGvMDI5SbVfjMoz6ZXO7b331t2pUzCK5HXuHCAw9hoeRhWPc5YxE/rIRQgVtF4TE/Op9+HrzBnAYQDUaLhazV4jI3X/on6TdtN9Cy8rXIBQQ2t+9RcPb32P0xgMBc0Ef3FSu3Ch0pKIZdsvmg9lAAshS5mm++83x7jTp4cK4VbLrgH7dXBw0V8wT56spsYWttHtZyqmUHNm2cOcb5cLJH3ABJFVNWfIZj5L10J9SXqdEMdAvAHdpghxjHJQEeIY6rIyiGOoIW+55ZauII7RzzKIY4xhenq6I+IY85OHOObcnhYyGOXpl1DEqtXh1m3a3ieoUPzlvQ6EGHGM78sgjoGIZEhPnRHHEuQkkJLSZU+eNMkTYIzBQfpkiF2GsgRyFGWRMGq+T8yhOaLlIzKxR/SdEMfI/WwoYvSdOgzZaWQEpKclz8CVZAKmDOIWjk1yEEtAQRJ1Jypf6xtIZcZcjh4lFSXfalyJVzhoayL6u7RkxtOsOUz6P+QWF6s+jhz8alvHZF0rFSRRcjIvukpl0KN7kYLR9kvVS/BWD7ZZG5v9zqTL+fmF1hyGTkshWhZzuHXroGeYpuZOA61YSBqXhCT8jP4xh2ant/njooNdX3PCVkU1vWePocyh6sYaoLGh6eCCcPkyKvFFNz8/7Bnf9DToXoZOpj7XajZnvJgjxqOxMYckN2FeuGhhU8dp8OrVZT8/Bw+yJ1a3v5kbQ/AbcFevmsmDfatn7sqVhm9jerpSCnHM+p+PkpeFOIbZBBAX5o8+hiA2ENoS5vHECXNGXF5OkAZ5xo8eJV1bMeIY518ZxDEQzPqIYw9gxDG+h/EWUa/LrQRxrJt961QmlKTpX9gvnmeTVNK/gelNTJhKME9CjevKo7WWQ/KQ/VkXghAdKqaVoEWFbcYSdFwmRATLioWGDyDZIWnLIzwMJYLwwoZZk4JSccXhQUl9t97K5eeSq1QITRr2l5IslStSsRDCwnq4x3EZ4PGhX7QDtGeIxBWqPLH1IjErXjpEK4vR6OI5yyJU/ZzRlrPZxh9m8sIuzaVGDmfi+Xy3axeALGktgzmwNXzyEu7CBgiTvhAhDVs/ubhZZ4VqduhQuw48awzx+sJEq9UB3za0d292Zi3VlafCDsstLg76NnbtSvokLYH11aBsu/FMZT0HrA3PO3OXJTHv32+mCvZpPFa1WYQB3q1zrVeIY31Jep2o7zi2urqQWEOSoxYOT8Jz5kDFCYrDu3NO3nLWnLWUgzGHDmI6bIQOtZoUm1l5ivMYdNhmluqf+TJvYzlgERJmn586la4LtbypVRvemxtmGttQ+Z7Pzp4d93CTnMOC1ERKiuE+t2xpeFxqeSqL8RtOu/WLduL5kt2fcrSHyhjp9Nq1hLFmodEZIzJtAn3LujzwO/aO7NjSENAffsNegxHTT6nSEabsUoEkaP2bn7fByoFNWN1CSQvBV/ic9kBiE/QooWwwaqm/Q2addXnLM+3I+5vMWgcOtBcybVO+s2OY+1ttKCIBZk1YmRh12Rjtss9UTAn0b/b31Wa1Wf1Qm1me39ez41jfJr1OJO/XzVqu123mlYljHkkukEXcsKkCVSR/eV9EeXV1oxxM2STntAd3qJhabYrNkBYXTYWdx6BVBsq7DMAosOlxmBvQBoe2hQJxDhnDMKZoYVsLHkQkRrWSB3gIUkLiE9TvHOIwNeoJbcAwBzFom5+EccG0dA4yb5oveW7LsQtmQttIuMRdI00Bsan+imgHrebhw4YOhtYyy17LPMEoR0cTpDBI0j1Mms/oG33hPWhhXGKQlIUWKTu5xmLQq2kUNI2Z73ixb9FkKLVnCIUa2qrxFo9TYna67CkELyv9JXWVgRllH8VtiFkLjAdzw2qfqTTWAe9NrR2SQhyzFEzjTQdJKGsuwjbzYqk36ixdC/Ul6T5tapIjCQdwKKVhl8MOvFZJdTUU5oAuuiysNcXm6dMjTU/xYhWjsLWzPJPJigVDFUPI9043gsEgecNoQ9srzAWnKwuLsR+Eql+YdGiqiyXxrBSXImlKYLAhY+fQRk0vxpcVXgUj5nfhelj2qWyNBir96eklV6mQqSzB/5b6Xf0xNDLU9oJ0tT6EsM47d9ogzpwhEYdpVtJ+FCahh3PIPMF0dEkJpWo8rPE9COeAOWUO+E0W6p05mJntGEYdpr4EqrZshEQWDoAYNe1fvEg0wcotpHmS/M6dI7nQs6HJYLxp12d9ykLLFknU1wv1mfQ6UT8Ea3V14ZTVrRCsuC4oyy6XVS6vvpBBQ/EFIZYM8i4Q2HIJnynCKLewqc4MenjYwn/CGO6QUcOgYWzC2FZZ2tRcKJRIYTYcju05vuue2UB8h/MWjDoccnwpwV6dHk/6e128UFWi+kZ44UAXbCl9oi8wJj6DYVhITqKqpU2+jy9MwuQWelrs51OvL7iJiWE/x1w0snKaq2+0wdiZx9hOj42Uz9E+kI6UfqIK11jlbCamD2HnhhGFlwcYNe0cPSps96QRxoAUz5j5a6p8mwv+DxOjnjBJBwSz5uLQibRmrDv9y8MBIAoAp0GycxVhf4fPVFHCGRzeQvM1ayq/gGvN37A/YdBod/IS7mQ9x1mMuh+Ctc703ve+13tJ47X3+Mc/3t155525ZZ/0pCf5BzB+feu3fmurzIte9KK275/xjGesuZ/kkt7M5XrdZtm68JpOvy8qX74uDmqgGpU+UAdzWZW0MliJQYeSR5a6O+/GTwjTzEwSPpMXRsNBOzpaLIrLWxaSLVcwnKgp6QMq7rCcYDfjPhjTNlQrHM7wYobxDA0ZSAuHKUwpz98tPjz5bTg/lre5XX2J9zQHsDQmHMQwJhgZTIm+zc6SV5uczfPu6tV5d8898+4rX7G/Z87Mu+Hh9tfgoJU/fpwcx1anOaEZtKw+g6SmbifTHDC3kn75f/iXMdJ/ObahBpc0zW8IywrrFspbvPd4T4gVZckaFpLU3pgcBJqC+Ye/QlEThcy6CDxHa5Z4mKf3kP7aWJdT5p48VXYYAdDpos2Ysp4/+qR5Gx9HS9DINHF0et7zVN8bcZY+aCTpD37wg+4Vr3iFe9/73ucZ9Lve9S739Kc/3X3xi1/0oU8xfehDH2qF8shb75GPfKT7zu/8zlQ5mPKv//qvt97jir9WwmtxM5frdZtl64rh/4pU2Z2+j6EmOcx44GEyocqQcCZu7p0yC9mBVHfj4+kGixDI4v4l6SXTGN+x45QkIbzYi2Ak4zKKr0V1askk5FiGo9dQG+xm2Ac+F1To2bPmJAWh3mXu+A6J3OzFxkyYZqGWxY8OcJl79iSpF+VUFXp3Q5XKQitjFH3Gbqt6IS4HhE6RhhK1rqReDm76KKbVbDUVWgUTxab8pS/BsJO+USf2dz7burXm32d5kON5LiCRvLUGDYzQIS6BQkAbHrb9x0WJ+ePSI6atYym+ZOr9uXMNjwAGo2Z9k/lMzD+g28nkkIXAB6MmR/XZs1z2LAY8pvAiGT4vivGPiTLj40MppLJt2+ptcwIYC3Z/aTo6UafvB5r9IAyt6FzuBEUaStQbcZY+aJj0O97xDvfSl77UvfjFL/bvYdZ/+qd/6j7wgQ+4V7/61W3l4xCp3/u93/OxbTGTZvGJ8+0mlQnp2chyvW6zbF15jiRFB0xRXUooETNoiGcR5sSBn8X0JTHo0I0pRCDLg6RcifqeQ1WHrMXodqYsT+AwGUZcrqgPXBAWFxM0LxgD40HQYv6ArDx+vOHtk2EsMMybMvG4Q09tJFqpfYVwZu9hvoZ6xvwpLjpkqkL5UlSMhm3q5Xx7vD6/fLmS8jrHBm0ZwPgcqTtRr5sdvub3GJKrxhSOJUECS6NjUS+gG9SD7LCwYCpw1MVcBmmDPahkHiGF70Er27atkWLUynudRVlqfcLCLl0iLj2xU9M2l5xYdVwGyjMsI6QyNExhmNZKoxvK+JRUVti3Tox6I87StdDm6EUJQiL+9Kc/7V7zmte0PiNY/qlPfaq74447StXxa7/2a+55z3ueB+wI6eMf/7iXxAEJefKTn+ze8pa3FMbRJUAcCaMPb3rYM8pkgul1OQEa9LpveWXiMH0AD8LPTPVpqRPDw0n41Hib5oVrhHUtLVksb16qSR1wo6PpymZn7QSx2NB030Kinxx6ggEN7d4wHakc8WjtRITRMCakRdrC3lwEZRCXOXrU+mw26EYUUtIokGxsPkNNX2hLFSIXns7YjNOAHmJiiZYAiRgGKOJzVOgxMS94ITN/QkgLz1w5lRkgi6XIRDJFeuY3rKvu5Maobdz0k/s3TmXhNIbIaxava2soOziXrUrFVOTsvZtuSm6DlIulTEICYfj8PoRjRYJm/TlOTp40cBkAXdi/ZsNO5lTmBLuUGoCKjplt20wrUpS4wjzMGy2zCvN/8SLOaGCBAye67Ntlb2hfal7iZy+L2p9P7OcWpgWJWcMwGZdgUvMu2qZxyH+GRavpWxZ94hOX3WMf272ztBd03TBpkF5Q14EeExLv77rrrsLfY7v+whe+4Bl1rOr+9m//9hby1mtf+1r3zGc+0zN+UHDyCEStkF74whd6+3YY6N4J7GSjyvFAHW1mr+cB61Xf8sosLz889R4ELZIIhMQDv2sXatrBpoctD/W8l1A62ZPTdY01HWiymKQ92EiPy8tXfXugO9EeCRGYpmvXFt3CwrWOe8Ju8jV36hSoTclhAYPfvRsbJe66o62DNLQRQyMjqBFhQFdaTlBZ8xG02Ownh6+pY2HyhnR2rQ3WE5sdqE6VCn2odEh92ZkZLC2BfCZ1tDHMG26oND3BG+6eexb850IFm5hIn86xFrFaBRHPtAYkpNC2DL2s9V7Me2ICNa4xPC4GXOSwz8KU6YfASZSMg/KofjUs5f1WfDR9tfhv+/7AAebW1P3XrlW97Zsy09N5N5xRd/nygMc15+zWZVCSNswXhrlzJ1KtMWEYMP2fnU32CvLD7t34LFgIloj/oxXBXl2vJ4h6QgVLz+9Vj9Q3OzvstR1cakAq27vXJOpTp5a9hD4xAfJd8gCxHzvt76wy9fpYCwqXyxDodsTPy+TAxatev+a9uJG4wzHZc0F5C4UbGECAWMxktEs5z0H6WeVyWOlYD/Q//sdHunKugUjWC7pumPRaCeZ82223ucc97nGpz5GsRXx/++23e1hNpOunPOUpufUBK4fknSdJw/Cpp4h6XU63vyc+8YmF6pxu9i2vzKc/PdeG+Vtke+LZ48EeHwepKL9cWJdJatnlAHGwQx3bHRCmFc+E8OpN4kQH3O7dg97Wlke0cd99Dc9s4vCg06erbv/+Cf8eRnL5cvuByAGDfXxgYKLUfKifpEOkr6hFGQdAHbVaUodobg6YxFpLEk5LNrqomLOXbJ15JGbH3GF7xHELNW8i9fL5gL9MZfVF2gYIadhsrIl9mf7Jo9v3LkgXKS9mpgWmoNhlHkdL32nvqYe5gFljUya1KUwCVS/zFKrLQ/ANxTXLL4DDHobCeE6fTuYllK6BZAUARupmLgrMERIkzFiMDA0F/QQz/NIl89xn3ZM1sr7v22fhXLH3P3WRptXMEYY4JqItw4if8JcAJQPRxRRGDTEO1h5tDd7tK3n24jJEKGgb02/ATyYmBj2jNlhT+mSaS7y4pWFi/m0tLAWnaUkG3Ph4zUvW6b1S8XjrzEcyF43UM2AIcQY3y7O6Z097PaKhoW9yT3xilBh8FecaTLwXdN0waTBXLcdtOsCc90X2ZDCtsUe/6U1vKmwHfGraggl3YtIw6E63LL4rY9PYiHJI0JQpKtfNNvPKxHYkyhTZlkwAHfOOOnmoUnFdClMxBpmUkcQlpyeYCwdqjJikcBEkrDxhAwYn56iY+Jzv5VBGMovQG5e80PTDpiipwCTfbPQpOX+F3yNpcfhl2QJNErH+w1jt90miCqWiRApmPrLUk/EZPjKCZ7UxA9qmK5RhXpnHvXuH2vrPuHWwovqGcaEdGRqyQ5x2ebRC+6tJz9RnKRxh4EphqdzQlhmL91YXqmf5ICjFJXVQFgmPMdrloJJSdUP8ZS5M7S2PfIOlZP5OnTKNAXTzzTid2WWFtuQAB+3bl0jnEO0w1/QdJzjir2MyL+5KJvwmDB6nMgJz5ueX28w/2j+dTBow6xtvbEcoK/PsxWViVTaXWhi1OVgC9pLsT/pmznoWZcHziKQfStesD8xcTpzzzfjqq1dtb0H2DNmzLyc6P+pKbA/PflYZw2c+M18YQ110rvXKZn3dhGBhT3vsYx/rPvrRj7Y+QyXH+yc84Qkdf/sHf/AH/gb4Pd/zPYXtHDt2zHuBk/xiLUSyjc1crtdtlq2rSAXPQ0s4zpEj5knbKQtQXJfiL7HHheEzYf7l0KlKUrQI6aSTet1AMSol4lEB0lhshbZMTAz4fpB2sux8MN6EiZoXMI5G1s+8sDKToLikmMrdDlGkzTC8RV7M8ZLJiQqGPDa24EZGFjzTOX3a1LDW3/RhCZBGPAdi0FCifjaGiDQOg4Zxsy70CcbIXLF2MGVJ3WIO9BXGxz6gbvYEY6COEPpTeNCy82KbZezCN6cOfg9Dpy4Qy2DMWIf4LvTK3rULE4wN9PDheXfkyLKfO5hvuL9CJhHODX1D8rxwQdm+4r2Sv48Ip+NCNz4+2BYi1VrppkNeHnEZjRHKip69rDJZe4XnBvhTcqqjQYopdOKMHf3kxCmEtpMtX5RkMGLC6WegvUzes6oxdEIl6/ZZ+qCQpCHCr7D9koEKtTUhWEjJ8vZ+wQte4Pbt2+fe9ra3tam6n/3sZ7c5g+Fi/8Y3vtE95znP8dI46o1XvvKV3t5MaNdaiKxTsd16M5UrQ91ss2xdZMzJU7mF6GNlPEmz6uKGDkNKJ5RfcrWaPQp8JieYmMyOnH/yWa5oS7AREx8lTkJ8j+rcDjfUr/mQn4tuYKB9PoRnDTO4cMH6q0QUSgwREuPFJprMndkBeSTi6cYkUqsNZHoxE5NMXbznQA0sPqkkFUld6XCoGCY0PPPpt2GNk3XNyuqg5T2HenjwwmyZP0nyAgyhn8yDAFpi0yTt79kz6arVBTc1RdYpYwxYgoRqJUhSpGthnWeRGPXsbN0za6RqpGcxF+oOPajRGCCVw6Rh+sk60UAyGZaKM9/kwLwTIkckQFaCDqHPxcArEJ9b0o80Qpmp0NtV7CFlPVNZ+dX1TClMKwQ/oUyRE6fW+VqTCcfPleY2pLhMnjah0xmzXmfpg4ZJP/e5z/Vpw17/+tf7CXzUox7lPvzhD7ecyXCIim96xFCT9ekjH/lIW32ozz/3uc+53/zN3/SB66Qpe9rTnube/OY3dyVWuk/dpbWgj4UUx4Ca2tAeBR1KsRQd/jaPpPqLLxGcG1hkkMrUf9mFUUFnSdBFxDZH6pS6FBWwZWMyJhMnnUhU43xiB6aAXPLCZBRvLcJ5ioMR9S0HInG8jFeMOfQCz5sv+iK1Mozi7NmLLWai8leumIOR8LOlhqevpNCUKQJVOQwIqVfzTF8WFsD3tnSRQlWDuCQZSAawm2TxwoY57+tYWppqtScGLVW1YrnFHPk8Ph62bAHZa8h9+csWYnboUM0zfeqmH6yV7MOKN6cce0JZssSoGS9+ASGTjiMGZObhs3vuIbTLwGbCeWfOtM5i1MJfD237ONQdO0bYXaMNjlUqdLVNSlHNY5lnSmFaIaNWys88km9CEVWraw/tuh6gQ/upKtcpVSW5lcuoS3pdbiWpKrvZt7wyscrJMillP31KYwcRNxojMpnquFxdeW1ymNhlwJC6Ytxq2STzCHsomOLhZQLGSd/DAxAv2dHR4VbCC9Upx6kkfMvsqyEZdKQdvqgUzU3DynD4si15qU76YikOJdXWU57faBZCZsxhvbhodk22yIkTxng4WFGpm2raPKWRPLEXwthkRwydvPbvB2ilGuBrX/RzqUP6/Pmq1wBs3dpo1SMGHUrlwu++555EMpdTWDNYwZc1BtDwTmLsFTEyxXnLQ5wwMdYEhstYyW4FiSFNTk61MldZEhFrE+bMHrPfJVnZ8NDGiQoC25rPbrqp1lLtq91wbmCiYWghjJrf3Xgj0izza/uCz5QCU6r5s2eRyM1Rir7dcEPDM+r48sAa0j5jF4Z4LCFTBjs3Y1lYiCMObLzhRaVTGta8Zw9GDcGoaY+9G6dVhXRRAnwGurdZJg7Jg1DzW2a09jJZKUzz+gZlMeqic62fqvIBwMzLMLiNKleGutlm2bpgXnmMdaWgCJ3q6lQOD24OpdBHkYOqiEFbH5bcgQNDKamHwyM85EIiXSQHNi85VMVoVkJuEskWRz+zVLmSkpLxGUPj8Dt3zvC3E0k8LbWETl0mGRuDxutcjDpMGwoTw7aL+lbAJyG4CQzw3nvN7dscwbBXpiUpwwFXAofEASwkxitvac0P9VisfFIfzIjf03/6YN7qaQbN59QhlC7apV+Szgntm5+/2MwjnRy+0hRYyknbH1m5rnFohI4cmfd1AIqi3yqHNXXQN5g9ZXjRTy4O5OGWfwO/YR6R4FlX2lDfydMtFb/5BFTdzTe3S9TDw4stlLAsYs3Zhzt21L2ntRi1gFlMyg+zlVUKgUrM8zxZxFiiLnLilAPfaGu902YTARmlEeCsTB7aX6dzIUui7uZZ+qBwHLveqGwM3UaV63WbZevqFJ8b42QXoY8VAT/klePw5QBVGkeBWxC7WgxxaIck/eEQkj2Tg5QDHSaAJDQ5OdSSkuQkkwXVadm+0oxU/zfULjuo9VIe45DkUYy0xzjkCEU/JF1JauTARgUK4x0YMDAPYpgPH05+xxhgGqhNkaQZB5I7lxiYDod3o3HRHT9+0d1zz6WmpFb1lwrleQ7PSPorZ7YwdWSyPvaCkcFQedF3XrqAcIkR0puYC/1T4omQQdNXeVwrTEn7yjQmJCTBEQ/np4tuePiSGx42OzlzzhzSF9qmTeZNjm9p2FFDZCMMTePg0iJbO23jJ0C9MEL6zjxov0hq5gL0la8YM0I7wKVGfq1hezBqknNk7cnOe9b+KrMXjFp15+GZF6VhzWpTmPcw6sSJs5Fy4pQpgDULndIaQR9CJhzijO/f38h0ooup7LnQqzjoIupL0qsknNe4jb3sZS9zz3rWszwiGrcukMuOHDnibeaEcnHzRDUOkRiEz+fm5rzNGw9y3kOoTagPmzt08OBBD+By9erVZnaaig8LU/gX6DqnQG/wat4DXvWCEx2/IbYPJzho69atPhmJ2sF+j8Mc31OvQFzoJyob0NhONoM6cbCgDW6U9I3wtHvuucffRCcnJ33548eP+7pxvEM9hOoHwuHi3nvv9er1iYkJ/xv1nz4wX6C2zc9v93PBe/pAefolzHVU8iHwB/GPJ06Ak9xoOYqArbx7N0+xMdEQCUj1Uo4sOQsL8y1/BD6jLOOkTUNjG/aM0RxQkpOB98Qjo9IkNEbIbXzOb4kegOgnn1MnxDqhktyxAw9oSUB2KMNgjVEarrJJczYua9MkZSQQ2gUNbWFh0dsFCc1C5Wl9RO1Zbf1OKRptDglbNKAHw+M2Jkl3OfxhrFxKAPsgJzFMwqQvk6BRd+LspHONfuKwxWEqFC+81WFWjOXUqUupCwTfcxhj95TEqAM4tLYYc9bYE6lddl0xSJiUMUNDqQpzXHOmCpwElTNtALICgIjs8UjYhoKWMEOc27h4IE0St6vLC6QxAP4xNHSx6ZRnoqW0JVKzIiWrr5oD5hNJGI3E9u0mUWsOUaMriciePcvNlJLY4w0IhgsQjFcOVpovveeiZNqLBOVrcLDhVcT795OGE2yAJZ9nmX0NmA3Mk89rNbKw2Z4F+IQ5Z045fnbuZE8b1GcC+GJ7S88SfzF5WNhivfV82rM833R4JG5/IPUskHiENTANRsPt2jXnNQ2m2jcgEsB4lpeHmuNa9tjvzI1pplgHXoSgJWcEny0vz3lgk2q1Fj33aLaSsvRXzyvPLv2j7N/+7YJ7whOm/BkHcfZynuWdyUBM94L6Nul1sklvVlqJTboXVBQGkUUc3NeucSgMdIyTXg2dOFF1992XViOHDlGx3bsMcUZxcEq1R8iVtQWYhEkBCi3LI9qV2pHD6vDhaivsKaTYFpfYo+tNhKtENczcwaQ5NOmb4olrtfkWg5KXMBcMqVaxhSIxyxYM9jVjHBkxaRNv8wTJy6Qe7pMwQtmJ6QNMRnZsfgfjD22Moe0a6VlqZtkt/aia+aWpO2SY9Ie+cxFhjjW3SW5nY/L8Biczxof2QFI7Y6YvoQeyaUgMh9tQw6b8/IWOa+FehDnTB854gy21TYW3vmF6J/CrShRCWKHqYsyMFwalPWiXL0o0/Dob/7B9wPfYctkD5KbO8vrOItplDGE41I4dmh8u5NlpPNm3q9UGy0Y9Pn7BX+LLYHP3iso6kvXKJt1Xd68THUY/uInL9brNsnXphtuJDG3oqpf+DGFp9XWF5VDDIZFywHMQ6iU1JvfZostAVpswRuoI1bgwaN5LdZpXr+7Q4ffHjlU9RKQOSElSsl/H9mh5c4dMTC8lBBEzgkFThguAwqBgNtQL0zCp0MpKTVmtohI2KROVdjh3SPCKF6YuGDG/VypKmBIMAqQ2JFykWPojZgnBMM2OPOMGB2dctTrj6nV7OTfjFhZm3KlTM252Nvl74cJMU2tl/YBRi8lJvcu8M2e6DMghD8mWPsmOj8QomzYhSpZXmX5e9BcKC2mS9iOZey4DMGFdOpSCcnl5vhWrLRIuucwhzA+fcbkZGDD4W7NvJ/tIY9Fch9m72B9KsgIamlJ0Kt1lSIlaObmZolXBkQ/0LiHJhfuxKLlN0bMn1felS+VuvPMlnuWVPu9FgkM3z9K10MaLUg9QWq09tFflet1mN/u1niQv5VCqkL0xRKRaCZkTUGIvNolLnrn2vZzH2rM4GSRlbGOjH2UybyVq2+ywl9DpSqR80VI5W2astFeyXTCwJ5p6gCQOjANmCrNB0hazVTgTKk7D9zZ1r1T/km4Th7KZAH0tSc946VJyXIXOZZZq0hiavW+0krKMjMy0MmuZalY17PaMRiFrqgcSw+ZzpEhhgUuTgIR9+XLVTUzUvWbNbKmT/hISehczV9QhzQEvNBbUiaqZ9JiQGC8WKSRWzTvrS79NBY2K1i6QfMZfAZbwlz2LYi9ERYNR33svCTAGUpEFAu8J95Thn2OqwTSS2HtJTAM2/NxcwtmLHLPKEpEJ165ZNATpLrupEesGbZYzq8+k14nKqj82qlyv2yxbVxHA/0qobF0qZxKkSTMCtBBxCJIMoNi7u71AkgqQg0nfJ3GwoZOMQSAmv4VBhweipCP9zuKUST6Qjex0/LipZ8UkQtW9oD8lkWOHVj9F8W8UQ4w6VAyaMCpdXmCsN98spppIrzAmmLvqg6EmF54ZX0a28m3b7FiCGapdeZDHfVN/GQPqVzmf6WI0P49Pg/UbyZh1nJtbctXqrC8DA6Tc3r3T/ndcJhRyB0OVgx3lZFNnPSyJhknUW7YQZnbJ3XrrlG9LDE7rKCYsUwOJQVB9nz3LfNf8JYCxwkh1sVGSDupRdADe1zBqiMeJssqkxf9h0JKsmQ8LdcJnwyA6RVm5p7Wf4pzq5jtAussBt20bXtHtSGqrefYE98kcM59A7tLHvNCugRLP8kqf9yJp+sYb10+FvRLqM+l1Iuwsm7lcr9ssW1eZkKmyVLYulZMNlQOZQ1BSnw7trIt1e1rK9gMgxjdGlSgSZnhermIOX14hhTmi88Ypb3H+yq4sb1zGxHJwGVGYGRjcYiAG8mHjpVyMEEY9Z85c9vMC9rKIsrSDqpjDN2SovIeJSM3daJgjo+jatcFmyJMxU6miaVtSLAyEeiRJiuFLA8Ga8X8cuBSzHPYbhmsXh8GWDZixwLSPH0d9Tr3TLS9wmLONNWGgEMwFezr7wy5AVTc3R17li+6WW5KDPeQF0qSo38wzjlM4lI2O1lrSfDhnXBi5NNBnJGwxakwKXHrwb5L0z7hlO5Y6W+A/vEh1iVlE2iBpSEJzkXAH4lh99g977sIFCuKPUF/Ts5eG+0zwvrG+ks4zK7SrugrI0rWWO3wYR2C34dS3Sa8TnVDW+k1artdtlq1LnqDdIOqyVIN2uCtvb16bOiA5+DjsYNYcjIaxzPfpw0nOXsIPz8MRz8bCJiTJJKG4rA5c/oapBDuNMyY7oM07lzGgCkXSVIgU9Qv3WjG6ZO/yPWuqn+kfjAqJLgyTqVbNg/vSJVJiJuFfMAC6onjpELwDosyRIzPu7Flj0BcvDroLF+wV2uXNM9xs4MwBTJs+0FfU2qxhaK+GWbJGgvbEA14XI+ZBIU+y6/I5TB2TI/Vevjzozp8fdFev4iWMqt3s3MwbzFoMWoxWDFTe7eb9jRNg1X35yxfdV75iRlzmK1xzxQEb00swuE+eTEK0wnhu3mNmYHysGWtnkm7dM7nw0sh4pJkIgXGk/keixiMa+zL9zsK9Zx8Z4Ez2niY0j7pCvO+V7Mn03kx/liD8mU/DSupb6dnR7XLrTX1Juk8PWAKmkQMmBgfJU6mFSRtiSqAaq5nSQBGOuKRkC7exFI/K8gSjgMEVIdGGqu4i0gGtUE/Uy6ioOaxpU5CXfI6zmJy5JEEL7ARmJIxwQxy76PuNindy0vC/zaPbpHLsoVkE4xMgydCQ2T0152JIXAzEQAXPqUsE60j9XDSklOG3ijU2tCzTDoiR0m+YS2g+gOnC9KmLdoX9DZmK2Y7EiQlCl3BCo1+7Uw53mgsbl83j6dMK26t62NG7777o1d9psA2o4cbHzYRhTNxyRxNLvbhYS82/TBSMS9I7Y6efY2OYGwwDXH3ReGJmDXEpw3FtctLUyuH8ab82GgOZezosU6stuYWF4Rbe92ooD2tAEjXx6XjRbwa6cxPAhvaZ9DpR2SxaG1Wu122WrYuYxm6QgXCghixmomGb7YdqwtgFb9kJR5yDuhOOOAcpB3/a7pyk8YvVfMPDQx1V3XH/RXE9kqi5DCCh8n/mSJCl2B3pszlhmVMbDNpide0igTcztLwM2Aex4td8PmJ+D5OWbb29fzNNezxSszF/LglhKkrFPFv8eKJqpiwvzRfrido8BNmQpM5nfE8Mq6AwWTeFLYnBoiYOndJEuihYiJ9J99u3Ew8/65aWzGYtiVoaU2lqQP+CDFjF1N9I1TffPBWZMMwJMJwn4pKJ/R8cxORg+b5pJ1T5Mw6hujEWA3JJHOTU99C7PzGzGOCNQa5W3O7dxBibrwLE5yZNW5a0UAMSlxkZGfI5vIl778SoOz3HnUzCMOoDB9qTcgyVOBfKnh0rLbfRjLqv7l4nAlhkM5frdZtl6+qWR6Ux0WwIgBgtKWwzRDCK0wCG5fKkAYWoZH1/8iTADu0e3LyHcWep+bKQm8qo8GdmTMoVyhnMFkAVG6+ppVG169BH/YmaE6kQVCwOUpg5TJXDWcyJFISGvgajskMsVFPTlxBGcnh4tvk78pcnDADpFwaC5M0LG7EkeVS6SM5qkzEqY5dsqHnoDoZehlez1QGmj5JY0KaYEFI27UrytNhpq0OhcczT1BRS/2Ar9CvUgNJP7SUYqtJdHjlizIbPv/QlgxfVBWRoaCkzOQXJSmgXlDeIdijPZUbQo8IAh3TJAtCGdhkPUnYYfaAkG/QznC9SiwpSVpcd6mLdQ/OA/uq3hoxn+1FZ2/JU32tBDlQWOhj1ekSQrKbcavAcukV9SXqdEMewwXKjL0Icu+uuu3wYRxHiGL+hXBHi2OzsrNu5c2dXEMdoEypCHGOsoO90Qhyjf+p/EeIY3pd5iGPDwzVvK1paIrXeYhuaEGQoYiAW2VolUgHJIexDg3WstxDHGPcNN8y7M2eG3PBw3Se/UDwlakAQkZgP2mPehOoFCYHJypraE/v1/Lz1Dxxj2mo0xtyxY5KE01jEMGqLpwYVarkpedbc/PycV9vj7ISDEhKMmDT/t9hkg0R0zubQHGMGvURMewk+csNjcCNNT02xLwEhmW9CZFr/uUuZgxcoaDgL2S/xXmaIIDtR1uJtYWB4/Da8BzHOWqdONdzevTau+XnzSsPWSx9lj5XdlcdCIUqCXoWxSNLmM8YqrQemAkK3qEvhTqGjlbzVQw9lQY6a3ThhXNTD54rLFvZ2yKRRjTNO0yYMejQsi9Hmu93+8sNjpkQjMEzdRc2jHPVywx05ctFNT0/6+H72BahgQr4D7QvmSB2CiR0asvAs+oTdnP4yJzItSBV+5gxIaWhu6n79DYVrvvUs2L5cbOYAr6USlhi4jKGKQcI4j/mXLnSsOw6FPGs4mMnuv7Aw5OP2d+68lkIc4/uRkVob4pjQyXbvHvIe3QaDamh4RDLs3s0zuehGRwdbWq/t24ndZ6/XW/ub8cVnRPJ8ckbkI47x/+Q8SRDHYlRCew4NXRBirH3EsQcQ4hiMD3jOIup1uZUgjnWzb3ll4hsqh0xRmlC2LBeNTkhFdsAlscid0JLCNrm9C2ghprCcUJpilTd947DJ8lA9enTA3XNPPrIS04PUFLdJmA4MamGBjFd2oIdpKbkAALARtnnPPZbpKlRfGnwoUpQxFRgMYVd4GocExCpOZEhmQsBC1Q2SmDERLlOLXhLdunXAX6rC5CDWB1NxT04OtjyrYTT0WepznTxIgUiN/J42kaiResPkC/ocCZm143vLUJV4eDMmPofBc9eV172I+RE0qhi3wq74jDupHgkLuUsSjYgxmkc4zJr+TLsvfcnK79vXcPfdl8wj39MeYx0aku1+yo2M1N3evYZdHSYzkbTMZ/gOwBfm5myvKcZcYCWJY5mtJ5cF5pzsWdn7ds4NDo60UMXCHNOsNRcHZeBS2FlMrBHajqWl9ueTy6L1c3lFz7GN0ZAD8/JXX2kik23bdq2wvjJtrrVcqPbuZ8G6zqkMc9vIcr1us2xd3crjbQAT6ZSRohgtqWybYbkwrjlsI45rDik87GOSR3VMg4M1r4aGQUs9CXHYCmwD5sl79UPqYDEYEXZTfZ5HFgdb8ZKmHLS4i8I8kOiEUkbKSjDHdceXmcBU56a12bVrsJUpCslNQCC8h6FqLPwWiZH3MIOYQUNiHMztiRPkjU/U6nI+gzGzrkqeIUCU+fmDzfEnUnLosCYPaknVzB/tATWvPiJVU4Y5n54edJcuLbkTJ2bc2Nh0KyNVSLLlm2kDhLi6q9cvurm5Kb9nYLxi0FB4gSFEj9STExOG9y2veq1l8he78oBfJ4N/rWcy6lrNVAusH2MNow8uXaKtqrtwwVDNeEy5iMQ+E1xYzOeg/VlRTurQRl3mmUKz0Ghc7XjZHm9mzzp3bjRlo86i1TzHKy23Efbpvk16nUgg7Zu1XK/bLFtXWWi/IuJAQXUW276y0JLKtik1WCf79Z49S7le2rIRx2Ga+jwGkoBQR4r5xhKvvKCRqGA8lFPYjDyD5R0c2xmt//MthqIMVoZWJUnOkmNcuXLRqyYF4QkD3bt3oJXdSWS5p7HdMg+Dngmh2qUP1IONG6mP9wJFkWTIRUCOWWLQIXxprXbULSwcdefPH/V9Gx8fcQsLI+78+RF3+fKIm5sbcdXqiNu2jf+j7rTvoZGRo/518eJRd/bsUT8Hir2WPRqnLNT87AsuFDDLcJ2Q2vkMRsr6oiEwMwrJZSw5SLie7LPQDePqVasMRs060YfwsiTHPoG+wHgt13iCRpdFtBN6nKNBydu3lIHZsldZZ4XiGQQsyGTWJ4RC1osy/OW9eGjesxLbqONnZS00Pm4x3qGNej1hQYvK9do+3Zek14lk89is5Xrd5mr7FYOFZOUazqNqddEdOFBr+/1qQc2yDENC/RLNzy93fKw4gCGyeCnbk6A2sxPUF/dJ2OKxMKJ44iw7ozyICeOBOTTdJjwhTek3WXCiMBmLYx51164N+LqU6ATat2/QMyjKkYUK5idva4iDH0ldGbmkvr/11gQpDBoePhpIfMZwcXaD+dNfmAfMXSk6efGd1MAWYjTS8hyH4RmWd1LvwsLBpq09QQRT0gsxcl1qOLNhnozH8l6bBzihWvx++/bplE06lnxBKCM8C0a9vNyuHlV8vs1hw01PV31O6ptvruVobZDQzVFNe5PL3uHDdXfzzclDElo06YtSUoaEZmTrVhJ51FvjT9pJwgM7GUdDiZrLTDdpaMiyhMVe36sj82LvxpnQC+oz6XWiPuLY6uoK0YAEHdgeDlUplTWHumImWtTmWssVlZH0bQ47ZovrBLNIyE4ndbnUtiGiGCTGFyZHEAmgg7pglKFqU5Itv0Wylwo5NA9YBihMCYOtTFiQlph1Y82U1UpJLmDIUhnLdi0sasv2JMzvoy3GBnMOl1rTS1kYtfJD00/lmpbtXPHGlgwjkWzr9ZHmmFEpJwMYHj7YukiEpgVBc8IQYdAh3KfVO+jGx5c8itoNN0y31lTe7EKss35X3JYtDXfqFAuTb8fUfti1i1zexqjDUC7GxUUPH4WQccLkYdRI1DfdVG2DwYzR70QGoWpzF0KIwmzZC8ydhQl2Zhli1KdPj7r9+7sX61ytVluq7zxGXeb5ND+ANJZ5HnZCp/qQprlU9oL66u51IpzLNnO5XrdZti4dKEVgIQr/KVNXL8uVK5Nk8eIA7PQTDlWpOsUUdHbwOYeOnMgSh5+6P6yROmM1qWyrMG8Od5iSbJ76a/HBaclcYUYiZe8KaW4OwJLB1gGoECUR5akfdXaoWBFDtFC0o02b8EiLQYvRCchDWcqoC3UtDJrvUFNTD2Njj8BQ+QuD4SCmLNsQlS/zAF25MuLm51GVM69H3f33J0xboUmKsw7xuJXfWmFnIJZB584ZAAprEYKepL2lLfWkcxk3qJwMUzDqEI3O8M+TPN8hsfb0GRt1vCfl3Z5lBuJzUNAIgSLWm0sBdeEch88AFiuk5CJtMYya/pVBJitLA80xyKkzS/Vd9OwlZ0ol80xpzw7Wub677uqN+N1n0utEhCZt5nK9brNsXQrXyAMLgUxVNbhu8H/yKF1NfVllFDIlRrS8zCFRLn9uvb7Qih+WVy/CDMzGvKthUI0Wahafm+rV3mMDln1RNkjKhkAgclpDHQxjV2YlmFKtdtGraaUSFFMI1dIhyZ7M+mU5yeWpS2V3pvzu3SNtObthqvLOZj74P4wF9TZMmDExF9jMYdIWYmcMHUaDPZ0xMibZXB/yEFPt40uwfTtt2qVgbOxoKlwp7KNI4Vq6YFh8t5DUZvzFgfmkjyHkKUyWtUG1bHWkGbWkOtZdhDQtRi0qsh7JvwFGrfCjOINajAXgnNUPHjvjIre6Ln4Kxbp6laxVxe0PD9tz0C1GvRg8V3mMuuj51JmSFdAUYyeUqa9X1Fd392lTUtEhsF5Z5FCjFTmorIRClT2Ho0A8du8e98y6QHvoD0vFGAsiU+pT8zRODpckJMukS+Fvw7zMVps4Z8H0JAlbVqskNAqiDAxe6mJIv7U44fxLFO3DKIX2Jaafxdh1+ZD9GYcwGCqXBcamrFIQEjpMw5JoJNKg0l5CoVd4EnqWgLYoycfIiIXxTU5OeMmKdaEOnM8sLvmon7+5OfMMZ7zMpTznwxh8eY2bTXnQbdmy5MFPjh+f9nMIs2be5F/BGqHWJxSLhBLYqKenp1Kmj1hahVEjOUv1XZyFKvEwP3580Gcmi7+PpWnDPbd1JZ4e2z5StWKpDfiEPNOJBqcTZXl9d4vGC1TfqzlTuujO01XqS9LrRNOcMJu4XK/bLFuXoPiKDqEyZuQs+L8stK5uwgmGZUKVvQ5zDjccc4ipxR5cpDqkPkmEMJgw4QeHpTEdUh+m02tKHSunOSRHmA8vyqLyFVY5kmXIoNV3OXYhncsuLdAJGIBBhSYvSeUwGpabQ53YWpiUVOkwWhie1pf6xbwbjZFWDC+2bkKg6D9/mUd+Lw92eZpT3003GdOjHjQGXDgEs6q+qR36HEqC2Ih5LzQvPjepe8T/bnLyqK8fSVMaByXrYO1k7xaQCgSjtrFZLmvqo5zWTcAl0J49Zj/HRh2aPgYH2/daKFGTj7wTapfsq5apCwS2YkamvZbA11p7QgBbCUPTc1CETFaWhjKevViiLno+E2S8bC1WfOZ0C6J4rdSXpNcJcQzQExC6ihDHvvSlL/lA+CLEsRA1rBPiGOVvu+22riCO0UfqK0Ic4/uHPvShHRHH+E3YhzzEMV68r1aXPYpViO0MGRhHww0M1D2iE0hgxOuS/AL8aUgIQfSZ/wtNqNEY9hjJCv+BTKrlwDO0M0M1GkmFYGCbot25OeyCA37eVJZ6eZ+Ea4D8ZChGS0tD7to1i2/mUA+fedSGwHRawg36u+xxugEmaUccG/F42dQtVZ1QzmA6YDhzYLEdwmxNMDykRiRNeYBrzKZObvixI0k2l6ZZt9nNlf1p506lrgRf2uyNMJ4waQTwn5ThMqCLiRycYBgwC9qgvzBwmJf9nt/e58uBkMb2B4FL6SLVBnUI8zzZC4kzF32VBMrvuITQx7AsDJZ5EDY48zc/zxwaDGoIk2khWCNu+/Y5H5dt83LQt6c0j5AkesYkYBNIErXlyt7dvISYRz97jbnkosjFY+fOul871jpB1Fv0jNrQsrQPB5uOd1V3773zbs+eAQ8/yl7Sfq/V6n5NDaXP0LLGxpbd1atj7p57lt3evYtte9b2N3jjc65SYe+bzZzuMreMLZSobS7qbm5uMWfPzvv39N2eF7JrDbrjx6tu9+402hf/pzzlQhQxkrAwR8vNsnyvZzlEHDOs9UE3MwOS3lyzvWzEMdDPOFPSe8ieI9bQ4v5BO0xuzrSj8yR+7ruZVrcT9RHH1glx7O677/aMqoh6XW4liGPd7FtemU6IY3ne3RzAYFPPzVU7emiWQQiDYPoHD9pB3Ql1LAuBKA4Rq1aJk7Z55RCmTcUqQxMTA+7ECbshIPUh/WITzFMdcjEgOxL2QcJksoj8vhwYSp0I2YFjfVCqSSWZgMyPb95NT1e8KjhMu2iHY6WFNGahOUkCEDvkGoH0zaE/4yYmSPeYoFqpL/yGSwGMkbnALq6Li6nmiWOu+ThwzWeYgpLPYBRS2cvUwfdqT6F5tEt5fs+eUGgZjFtZsdAQcJkYHbW5NbCYNH63CE0An507N9f0ej+YikumTUvjmEZBoy6Ll15yo6PTXptgGbUaHqebdReDR7vBtkL1rXzUMGyBkGQRqm8Y2E03jbqFBZu3vEgB7VvNRT4yGalduVhaHfyVMx6MHwLiFpCgLES9eN+ypp2QycogB5ZFCUP1TSx1UR4f8+5O5xvP8+4uapOz9Ou+bmsfcaxPD14KUax0CBlghh2G4SGRlyJS1MkRjUM+L2tVJ8q6RHBT16FbVmVfpDostj/a4RfCPcJEsKE2lS2ZY9Z4hRkdXiZEkqiIxZXHMYwGpqPwJ75HOmXMMNXQYUz/x0atFJEwPuYMZqly9JW1C7235QwnhzXmSRjf/J/fqIw8rZH8DTc7cZ6TRzuSNWpxyFTb4HEnl4nQW51+s46Mx8wVI65Wm3NLS0fd3r0HW4knQumbuplvpHp5vyMREkd98eK0vxjwO11UBNvKfjbHLaBaL7YYdSfC1i3wFUGM6oKTF/crG3UWMhm/IzRJphn5HdAvLpok5di1C63HgMd1z6qf9pMQMVOdh+XW20Z9+XKxjZq52rt32dXr5uTXj5N+EFMfFnR1dcU319jBhUMkziIVfhcy27CuTowQlVcZp5G4vqwQMW7ouiwoJjUrAZjCpzTGPEIakdp427ZGmzRt4TpVzyRhWmEMrxigmFyY5SnUn1le43YoTn43NDTlEcdC9xX6DYPh0Ge+YUrXruFzgM5cSFzpumTvpTx9RAWPZH3qlIVckf6w8xql+xVm3lL9koZ1mWP+YYz0DwYtZgrzsfpMQ4CUrzhtEZcQ5jSEVgUYZetWU38DgiInPgGlUJ6LCBK7PLqVOGVhYcbNz0+34FDlCMje4AJD37FPI00bow5SieXQxMQ1d/gwqmpLxqFLQywZLi/X/PiYE/pGP0NGzbwb7joJV5J9o1AuvOANsAaTT8MzwkOH0owwxG5vrpifl1hCDRn13r1RDN8aoTwnJgDRKba9S9PVjTZ7QX3HsXWioyHKwyYs1+s2y9YVh4usxUMzrKsTI0Ttlgfsn9e3PMmcunRZEL53whSMTJXbaHkqdzoL5ufJOgSzsSQNIUlVurDQ8FKpgCdCj2NJl4r3tXSIaUQs9TMrphrmE8dDc4hzgHOgIzkyViGpQVkMOv4c5mXJJzi4R7yq+MtfNkhTJDfmBmlRv6ENxTYbylci5etvKBGH+Ny0gZQrbG7GioSMdoC20FTKg16EpCupV3XDYIEfNWfDZD8rx7XQz2BWaAwYk3lZD/o1BkLUgGDs0oDbCEzackNbXTBq6O67s2OoQ0JatUsPNuDEmQ0mzOPGHDNGgE1oT3Or8DYYtRwp6YfGGxIXCHlys9/Gx+0ZOHIk2YwJkw/X3Hw/ssK15EyG5D4wQIa/Sse0q8ve4a/esYyeTyTqouiMojMmr1zsdNpoVL0f0npTX5JeJ1ptjG6vyvW6zbwygNWHdukiF4kyoSdZdeUhLWWBR2SFYZmHNc4qaSaX110dJFLZw3A4zHnAYQCnTpG5CtVh8QVC9dBH7Nexmo56+QymgKQEY4WJCuxDDmBWnx3ESHwHD9Y87CRew+qnpEA5WnGwSx0eeotD1CMPZ33PXArNLDzwOeT5XH8hvYdhSgMguFIxG+ZHW4f+cejr7LRQoQRoJbwMMG6ZQESyi4uhkM2LkCJhZvOdNBACKREqmvaJ+XWOeCcl4rqXlg62EnIolljzwcWFv/QBxylQyY4fn3XDw9P+0qD81rSDCUAEoz5xoljKRGVLu1xeKpV5n45SxNwyhzDZoaFKplmIi93dd9fd9u2G2611jVOAKjSL8bMft26tuPPnScFpErUQ5qKd630V8sK1YNTs0ZmZUTc/n2wUyikyQF7/V30ymWIbsp4VY9T2HGdRWTessFy2aavi3vrW33HrTX1Jep0IL+nNXK7XbZatq8hjsihhfBq+MqlL0mKZhBsx8YAigRw5UmklsOAQCCW9mML6+L8cl+iSwDQ4KIs0auTujfsLs+Gv2gA6VHZai4s1ZkE/YcYKeRLj5bMsrHDFTyNVwYyEZQ0xr/wO5kK/6T/SJ3XSLsyCuaBeJa2IATyYR6RRVN0Q5W+44aCrVhOMRs0ndXKIwxyQSrncWOKOhm+ffjAPHOi0J2mXF/2Uo1ZYL+2zbpKakxzgxswoL1u+8MDFKMILmdVlKGVI1NI0yBGMNZV0zDwyn5RVaBaqbxikbOYw1FiCJV0lau88ou7lZfKcJ+AjSpgCKb5dsc0hKcGH5XKmX+nGYx6mC4vVa/sRRg3dcw+5pbMuq5Wor8Zs2SfyAcF0Q/vYudUOa4oWAHQzheGdPcuzZlEF6n+WhB6fHXkS9UphgDuhH/7u764/Nmhfkl4n6uT5vRnK9brNsnUVeZyL2SrlHg+u1JWKYZWEGdeV5YhWlLAjfEBDD1RJr7StpAgQZbJgHdV32gIh7PLlq25gAL1jZ69Wwm6KyGy67bZ6JYuAoUoyyfIADh1+mDLFHAuvmrHHYCfK3CWJUGphxUYruYfqh2FwAMvRSxTPPf0ScxFj1DoxPi4kvGcNlZKSNYD5C/jFIDGtbkn4ssMzFvodQ5NCWl76yXdcWEIFUJgwwy4moJQRNnXULS4a6AnzJ+ATxWWHpNAsxkKfhYJmaF7JBWdwcCDXkUz2X/qpOSaTFSYUGPX8vN38OjlM0x57mBdrPDwMMlnyA2mKhAmvO7aeKT47f97CIe+/v+qlYYHd2KUmjTkPw01LobZus7MNz4Bh1KOjy61Yfz0/9SZKn9ZZz1rsfxL2zXw4CL+su5Mnq27XrrSjW9EZE5fr5HT6yU92yPvaJepL0utE9yFybeJyvW6zbF1l7EUAOUxPL7bgDGUThFnI7mYpGdvrkiNaKI12ajN8QONsQhx0sWTOoVUkma+EyszH4iIx1+0qRfpBuJouM0NDxMzGJgE7QLFdym557JjFRYfZubAJhgya84sDk1d4/zp1asmvBd+jQqZNLgrYrvkrG7HqD0kSayhV0jYaCyQwW9uGZ1AwWT6H6AOSMMyOdmiDfcGLCwOXFBgRdcX1x5RoJ2z+YOihBMeYBIxCO8rORSpMytEv0NI0t1kEox4amvH1UJ75D23G2ruyT4cSdWj/JcY7nEMYtSTqEFzFVM/5lyPmzy4X6XLChGefi2nSr7APSMPmjGcqc11qCIeCeD7CXOci5UPfsgVzg03w4qLFMGfRlWYYYUjxJYu+Setl+5jLA+eC/dVv6Av7SYBAnWzcWe30mvqS9DqBmZw4ccLt2LGjEMyEclARmAkB9MQaF4GZzM7Oek/qboCZ0E/aKAIzYQwginUCM2F+1P8QzMRoTwvMhPL0Sw8It1kDZhAAgoE9zM9f8zbdmZlBd+1ao+2BNpAQVHEhqAH1JgAOfEZ71BcDFWB3JE5ycXHIqwsNPKTe9Ag2AASrh3VF3VxvxUkTdkPMKcQ6EVsK4IMd9mPeGWxpadktLgIfCXiClc0ChgDUwoA6OFUTAAqrV2UH/UE3OdnwqSFNogTqEpWpMa9KpeE/27PHQGIAe6jXTVXJ/IUgKRxix4/XW6Equ3dPuuPHL/nLERSGatE3sjpxoLOux47N+uxMFlbE/M653bvHvOoxxE3GJgkzk6QcYpOLzNYKwEkllQLS7MQNf8gKz9suTcyBSfwcwjBYA6kw5srFjK1tOa6l6k7GDqNh/Ug3ahnUGLviu02CJ280SFwwV0mboJOhsicJBXODlIiGQY566ndop7cEGTPu8uXdKe97m3u7XLF3p6eH3MmTy+7uu8+7G2/kOYKRWX/xkt63r+JOn7b839TB2G1eTaI2TYLZ3g0Mhzkx1TEgNbJb8yiSXGPLlrqbnLSxcglgHoEvXV6ed0tL9hwCnqM+sF/uv9+AeQTVaWNkTwCWU3FHjljMfqUSAvIAwmKpTHmuWGMuReAIXLqUcMVGsB9MW5O0C6AKz5lJvPTNJHMc0VSOtSLGm3zoO3cu++9OnUr2k8xPFla2lDojbKw8XwJNsnqzohfWk/pgJusEZkK5MgHuvS63EjCTbvatU5nQcYyHoij7jEAQBgYmvQSYR4cOcVAUP01ZbQrQhEOTW7m12/5wIs2H0nRR/wk9gWGUBXEI68NRR7bAkFDrITVIWoE5cbjC8Dm0Q5stzBTGhjQr718OdFC3RLrUSG1NvbOzF93588Zx7MSwY8O8pe1Qh2GTBWp0dDCV4zmRwhLPbMoqE9fAwNEm3vdIy2QhJy8oDAyIzROaf6mLsSvTX2zVYRpOMWoYGHVYdqxkbmNnpNBRSPOhMrKvmj1V88PczvnQLD7jaKBNtAfqg6FjJXZzmILZs6dbFw2Njb1LmzJRyG68a9dUE0mNi0bdZ6vau9fK0Wep9A3JjTYJz7K6FPolCFnWgDEpfSi/QbKlD7t3W5pX2bZlcqhUQC8b8BcJ5lN7y/ptDGzHDtD/7FJIX/ICOyzsDCHG9h5rw0XAHBSNUS82Q8KoF42IcmHT7xgTgYsDfiNZBBMGjY3LSGyCkOmG+tNmINsfeUBIrMEv/dK97i/+4mAfzOR6pL539+rqWsmdsVuA+Z3aTHuFm8eqKMv23O07b5n6kCiQdqSClP0WhsOhHIbW8H9Uv0ipZeZPBzW0dWvdM+o4vzPtURbpEkZ07Roqb2PUUmvD3HgfQnsmqR7xkD7q43Hpp5g4B3J4D7by6TVIMLjtr2yscZ5s2TapEzU5l4CdOw2yVTHVOqA7OQrxOVKumIVARbRM2KeBD+UCwncwHhReMAYeAdqibT6/995Bj0gGcxUuuq15gvYlsvSRML/03OMgB8OUjVchd8PDSPsw8nl36BCauMQOrSxolKMdLnL8lguFYFVPn667bduw6yaoavweyZh5UjKXcI4N051wQNJcEjo42NHfQ5c4EXvz8uWKh4et1dAwAS1q7TI+aW/ynD077Wdj/Cbpx6S9Edu4Q20Z7WWhHz7/+WgHzR9hvahvk14nSlS5m7Ncr9vsVIYwLJHU2nnJMEIqRvQqxzDDNmNaqVd4p7pWQ6jFi8tgIkjSDkp6ld08tsFy8HLg5M1feDjB6Hk7PDzV5jylxBJSVVvWrOmUSjeMl+UQREVtNtfkrxgI7cEsYBLyGNYhn6CJpdc0ZKzyxJYdNItgllQBg4EJyr4chpYJcEXOdqEaXqAh2g/h3JKb2vpqKTdh5DAXpcUkExVrZBcZhZlxmZlpzatgT8PLkdqdn6+648cvttrGZACD5flgPGh8YNhcGmgbFTT1zsxYLmouSAIoCS9Y7AekYvaOcpCzHgCrILUjCaOZ4XIhlLP4EgQZSI2w3e02wf7Jg7y12Ovllm8JlyvmCkx+CGeySsWk3AMHMMclaTWzoiI6Pe8waAiI2ywS2E7esyyn0zC95/79DfeTP/ldbr2pL0n3aVNSHm63JLIysc8k3VjtPdRipc1OqQcUtCVse5sVSjBEZ9MBn+UkJUCNTgxNoURinIJjhZBmYZh8z+EuWyzlOFBZO3lV65CVRI8jmjkcJQwJhjA0dMCdOnWfO3RoJOWFL9WwkL80ntCTW1jTks6L0piqXVT8tA2TQ/KlPsVlh2FFrLk8l2VXFpZ4HJq1uGjwoc7BqE3CUvYvGF6YVpTLiJK9NBozbmHBMsXJ6YsySsyiC4vFpcOoJ12thn01WdMwB7YuAcTAY6fNuztKxc5rZsZU1MwFlxkYNtI7lwM5eikunTmKY+GFnU4djFdx1JJC01K3zfXCwpybmBhPmX1Y+/n5igfpGR4GmWzZLS0tuLGxzvGKPO+WCCTrO/rGQ5G9OZROtRPF6IeYmfDDWW/qM+l1IpysNnO5XrdZti6cpYrUjfv3V1qY1dhD85h5N+H/LNa5mOGbDbAdcGS1VKZvcZlOlxdJqRB9tAOU3yd2aRIohFoCqueQ45Devbvucw0jfV29WvcZmURKrLFlCzbWGQ/+gjSH1CWJikOdg17JMZSkAqevsL+aO/qKehkp0EBSzIsYhgTzVmIQxgtTU+x2TKFEbihgCZMR8AhMLQz/EYkRS+2qSw6qeIWsIdHqkgCjHhqa8+NVSBXzHs49c8LvDAhl0E1M4LQ044FOUGGzp5kn6ggDI7ZsAeUKh8FLbmhosqUxiQFItL6QMWqcobL3koVCmaTO/8VMuQDAqOmj7PA4ekljoctDOE8KT0NVjvpcjNqQ8Uxql3kBMBkYcUy0x7hRn1+8aGiAAwNlnoPB1oWAvcJYND/SGOBAZhCuCbOWSSZe9z4s6Crpve99r/eSxmP58Y9/vLvzzjtzy/7Gb/xG0wswefG7kFCfvf71r/ee1nhnP/WpT3VfBp9wjXSMa/MmLtfrNsvWhUdrp7hExUd2UkNJHbZa+L/VluOQP3q00QqlCUNqVkt4Ya+0XxxqHIojI3i0Jp8r0QSSEqQ0ksIZJysXzmJ4gPN5CEBBEzffPNUKvzp2bNGdPSuP+8ShK0zcwSEJM4KZ8jkMFemTwxK1rKAzmSMkt/37D7pjxyzuNAyl4a+kXBgbY4OBodIN1dB2iUjwuUXqs8aLFK926R/MBvAMDnf+0i95jUMhI6J+yrGdKfvFL5okLlQ5mBrjHhoCPvRoy4ZsiR3sO10YqAeJnHmfmjIbvjzQaZt1ii94UntDp08TdZFoLWKSKh+HKi5eAwPzqZAyEXNrgCryzE7ITALJh/Kuj6XwsA8Ka5STo4VBJeuvvxaX3dlxcmqKKAXs0+WeA+1nYbazlrxok7VWSKHU3nIaywL3KXsurDddV0z6gx/8oHvFK17h3vCGN7jPfOYz7pGPfKR7+tOf3gpFyiK87ggp0ovwqJDe/va3u3e/+93ufe97n/vEJz7hQ5Cok/CjtVCvGcRKy/W6zaIysktbuFXnurJs01lIXKuB/1ttuSLpfzWmarOVGvQjTG7/fhy3GqX6JaarvxxagKjIaUjqbEj2T1TXZj80Z6Y4fhqmlmW7g3RQh1mlCDGjjdB+ySWB90poobbZHnLAuu++udRc8plBSMKIDRs9tK1Kpc768xnjUGy04FRlf6WcQFBgztTD8UFZMRkuDEiBMGQR/SRHDAxepgJJhDBTmBBjkBQHs6bM4cOJa7N5JDea9kx7IZ3zW9blypVBd/68hUpKbR8ureaY9o0BWh7kLGIdBSJiMcPKpT3fgj8Vg1YImNT6MTFm1N6aByVkySI5U2pPEs519Gi7Cpr9AKMGf7wMnTpVLNWGz4Hl505fHuQ0h4TOuFnPhz7U9kqW0LxZAp+uKyb9jne8w730pS91L37xi90jHvEIz1jHxsbcBz7wgdzfID0Tw6sXsZzhIrzrXe9yP/VTP+VjnW+//Xb3W7/1Wz7u94//+I/X1Ff6tZnL9brNsnVZfGrnMmXVxyuF/1tLuZVI/2UoYZIJFKmYZFG/5HCnQ0own2JseU5v5CiGiLvmAI3vqdTJQX/pUv7hBeOzes2+KiS0kCkoBEYOUgIZodyePQd9Hy3+2qRTGDt/zTYNOlXSnsX7GtNnfowpEVttTJCDWMAmlrM5UXFL3S3pP5xbGK/CfDjEqcegLG1eFe4kZDXGJE9k/k9fdu82rV3IqAcHl7w0zYWC/ugSEUq3g4MzqfHZGic2cebBENEwO1xKMV2IfiOh68IgNTQY3lwszp6d93PDZYELjWzwSLbMhcWnt68tanZin+kHkmeWM6UkUu1JOQbG8LbaT8ThF9GUT8hhEnUnCtvs9Cwqq9y1a5apK+88KXsurDddNzZpJLFPf/rT7jWveU1qElFP33HHHbm/Ix710KFDXpXzmMc8xv3Mz/yM++qv/mr/HcAcgHxQh4gYaNTo1Pm85z2vtLcy9ovQhgGICF63RdTrcgLsWM82L1yAsQCgDzgCWMuNUnVZNirz6OycDMOkqTBeN4uoq8xtuFO58PNO5ZaX7VTLi3s2L+FGK5bUXvl1KZwqrI8DlEMW9aXsbMbkOGUamQ53wkOGkSUQm2bnC5u/6aZhd889ApQhr3A2StXU1KS7cuWS27vXUlKKaUvqk/MSd2Ek1rk51nww5fCVnl/7a85jiTf1yMicR/OKnY0EgCLwDxhWGAqk6ZJHOZcRHlWYEYxXqm+1LXuz6gvPZQ57bM2MBUmUtrLWNc7CBaECZy327au548fnW99pf8veLgc4XYhIxFGrLblqddZNTOxuoX3Jlq7QJx0/MNQzZy66Q4cmW5cP1S17vkGpAuhh88JckFgF/wmYKn0VZrtUwpAwx5VkhN+dPNlwN91k+8fimpM5TS5oyZ5kfgwoxRg1sfvx/Jn2DEex2JcjmVDDFkiAZ7JIz6eexTwCmIRngueoTH0bTdcNkwZ9C5f4UBKGeH/XXXdl/uZhD3uYl7KRkEHC+oVf+AX3xCc+0f3TP/2T279/fwqFK65T3+URiFohvfCFL3QvetGLWu9BACuDV93rcjBopY0suimutE3qGxl5hPuFXxhxn/1ssrkf+ciK+9Efvejq9bt9+3m0vPxwH3IEYtfOnSPeySM+pHfuNI9KU59HCZAjoi5hIK+mXL0+4S5fnisFVFKpjBWqtMHrrtcn3eXL1n8Ox2yePpZC2LIyxKxaeItlFkLVaYdvtQpaksWhz8wMpebMQm/weuVQBdJxKVfKIGEDfVteHvUHWUhslRtuqHqmxcHMYU5/BIYB6hWSGge3wr6mp3mOgiwXUQhXghRmn1l6SZyM9rsjR4555LLgly2UL3PyMuhS2qQc/9++nf7YJYhwHDmCXbqEBFj1zmnm5ZvkdE6Yp/3FmSnsH0yFNadtGE0nivN0A8yBkLC8XHV3332v27t3j59j1MowEpgEFxyc65hLc6JqNCW8up9LwqC0D3DYA1lOGbxATWOf8Fzcey+MGqfLxaaEOur27h3wlz1kB6WjZJzKxAay1okTNY88xmNOG8wX6ncudbxMKgaBbdk1Govu6tUx95WvGDiKaUisz+wDnAobjbmmhmHJx0lXKqN+rFwscDoDtSu8rLN3yWPNcRteDNnbRFjU6zx/jdYzeuzYqNu69VLO/rXns/hZZF3sITh5csRNTl5e1fkRXzjcg51Jr4ae8IQn+JcIBv1VX/VV7ld+5Vfcm9/85jXVDcQl8Jx5kjRQm8BzFlGvy+kBYS6KEMdW2iaS8yteUfVQlMpZa98798u/POje/e6v85J1J/q7v7vQmseDB7Nu10DzUfllNz4OYld+XUBohmuSd1uPy4muXBlwY2PJQPLKqW7GbAwj3SkOMLP9TbiLF6s+5ISDMg49EQHokMBJAidq8J2637BsoDRhd6MtDpLxcS42Jr2l7xH0Z6CJ311z4+OWDxnVZjIP8qBd8ChoAD7EF7gQIKRWm3Tj4xf9WJeWql5CPXTIxsFhq+QVstlu27bkzp0zyRtmEYYehXc2myPz3ga5a2LiqI89juOU+R2qXpiOSY8Vz7iR2IRfreQQFj4GDKr9XyklbW6NsdKuwri0HrJxA6EqaVl9jxGrIHluSxVvNmugMif8+p84Me/TVG7bdtDXw1wSqkRyCS4+hrDGWtpFAuZ15sysGxvb3bpUsfVgdiZZmwqYdTLkMKBYF9ytt04296r5E7AWrJ0Sm4h5Ke55cXHeDQ3VWmFlVMsRYRECyrPOPzhvsfcbzUsOf43pclGivdOnATGZaKLPzbuREYvUoB3GwD7hMrBjh2XvMukbz3PD1g63HO9PnVJ9yTPKpebixS0+LCsmldGzmK+JAwbYnmsuSleubHG7d+fXl0fdxkS47pk0ONjcksCmDon32JrLEHjHj370o1sY0voddeDdHdb5qEc9qmNdMOhOUiZ1l8m2shHleLApU1RupW2iEvynf8qWDv/f/+P7AS91Fa2RDkrLBhSXMIcZ/78o206nuvLjriupcm2tBZ93Kkc/81CJ+NzGYQdrIkVaxEFMIZM1nPA05rNJYfZenyONcFi3Q5eaatz/r8ElxVTpKissZx4F/o8tFwxnnGvCcXBoS/1p9dF3q/vSpYo/fGHQKqN1u3p1txsfn/WMgu+UgEIxtbLVK5mD1N2ScsfH59zlyyMpuyxMRDmH+YzLCQw6hN+kX8Y4EoQw1oHfisnSltJaJjHgxnBDiFBpPGgjVAWLybPGqkPzz4XAIgxsvi9cGPGwodiqtSfYKzBPoX3hM4CdmLlgjZnfpaVZd/DgdAtRLYmFTmsZWH8yWWk/UZ5+27onKG3ycodgmlya5f8gYvwGupJGdmMcOIJxBFvUgEGNylQRZqZCG8b3ijRQAhSYImF7CwuGdc/eY/xZ6TpV39hY8oxinyYsK+u5UZniZ9Hw922sXPbqHevbaNoclvESRAKExz72se6jH/1o6zNUp7wPpeVOxM3n85//fIshk1wCBhPWCcY0Xt5l68yjTmrdzVBuPdoMUzZmUdH360VFntexane1xCG0b18jMxxspfWEfnaxh28syYlZyM4ssA77XdWrNiEBkuhAFOoXDALmMTVVazIZA6AIHYPEEEPADA64EFs5dAiDSUhSvnJl2mfDok6FhMHUmRt5ntNHGJbGYfbhgy37rYBVhFLGPVsWKaQmPpddFBLgBmOjD1wOKSNnMLVLfyjLxYEXDD1OfY6mgc8YI/OnOrgooGhSXLPWie91fxeIhwGRAHRCWsske1gcly3hzDCwB5t5wWd8u/yfV56ZlNhkZczS/EP6SzvhulPX2bMVd+7cfJAi0/ogkBfmj7+6UOlI4NIjj+8wTamctuJnSnN3/LhdOoeGjPUQm6+LShYtB/MRh2V1ok6hmWHd1tdsRMPNQteNJA0RfoXtlwxUj3vc47xnNpmf8PaGXvCCF7h9+/a5t73tbf79m970Jve1X/u13n58/vx59/M///M+BOv7vu/7/Pfckl7+8pe7t7zlLe4hD3mIZ9qve93r3N69e92zn/3sNfWVBByhOnyzlStDK20zy7EmVLNv2VK83XbuPOXOnk20Gmsh2kT7UsbzOsQQLqovjwQvmThorQ7IJMQKRqWs2zyMm0M0dpeAASCFMhZzLEs+F1OFcdA/SdAxXCgMFkbG59hSUQEb8lPiHBV6IVcqOCpdatURuzeEn+sQv3iR0K5BP0fCmoYJSNqD6YthMVZTiR/0jO3q1ZGWCpo+KEWpvKtDaHiNW9/BsGiHORUwiHmFW3IHzVeYvIP5UAKHRmPBHThQa8VzK4RLACW0gbQoKZp1oCz4Pdp36ovN4VF3+fLBVoiXvKV1qTGmbhLrtWvmSAZp3hh3vF9DHHkY9e7dxvXkPc9jrIQm0uTIac1Uz/Pu8uWa3z9cYGzvJfUz7/IFCGObYdTY3HV54C/MGFW+wFqU+1omDmWmEpY5nyuZStazoGcvtg/DqPftW859PmOEsLAczpHJxd3MSaCRhRqUoue9V3RdMennPve5PpUj4CM4dqGS/vCHP9xy/MIhKrSl4YFNyBZlYSJI4n/3d3/nw7dEr3zlKz2j//7v/37PyMkORZ0x6EmfigkgjNtuc+7zn2//7vbbLavSRtBaEnGgCiuDNCZ1OrjDYmRxZqWQqtVhr6KUXViSYiwJyItdyRF080/qSTyc+Z6DERKal1Cy6IcO2FB9HpIkcJybQuQnzZHgKs0DfLHZttkmw4PfnLisvJjCyMi0O358ppVrGYaChCYmnjYLJJcUHexTU3NeEoXRiCFTRmApIfCIxhZChWo+NAbhm4de4aFmM1Tdai1vusnaFsM1tDCTKkPJVaR+oiUQM6xWR7wzoi5J8sKWhzp10jfgLbnEiMnX6zPu0qXplnd3GChhTltWFkQyMmbNzFjWOerA5m0QnMZ4E02IEnRg7uBiNu+mp2t+3hlXePFirSz9a2KXV7ISZfYKEdXoC+VZb9nyYyKDF85XIbyoTDDyKajlaKGk9l4dhQy6XbMWZ9eCQnx3XcB7Rf1UleuUqtJufsV3oF6XW0mqytW0CY4zPnkho4Zx/9RP1d0tt1RL1fWZz8yXSlVZlOpRKQbDdJNZxOUhL4xbKSvD+mLKS2WXlVKP2z+Yy8eOGSZyjGUcH0pqU5cAHLVQUUqFyoG+a1fDh7wJUUtwiDrsLl6s+8NUqSl14MSkVIBIVdjq8y8iMGaYNFmOLrvJSZNEYKBcFMQ8jQE2vG2TOTh5csb3CWkagnHBXCSlx1uNw18q1vvus3SWhGWJqFOeyDARZdcSCXQDxqbLirybFdLE54xL9tuQqN8AUtJrkDBO63Nojw5JsdXMaehxv2XLnP/d9u3mRAbDC5mnbPq8pG2QND04OO2Z/tQUoUZkxSICwqBXxegMqKbu9u2b8rZmQpeA0qUtxYPLJwBpnfdc7sCmR2MgwMVYO0I5EoWYmjxx4sPfYHjY4Da5QCifNBdC6tUahfCm1MXFAPxr2mHP0FeNwfKTJ4w669kTk5Y0nfd8xpR1HuzaZQ1fu1ZvpT/NW3fNMf1/xjN2eSGwn6ryOiTQzQ7wlG/ScmVoNW2i4nvnO40ZYHdDyoEJXr6MZ87+UnU97nEHUjmmV0swEnwZihJxDAzwkA+Uri+mUJ0eHxSxRAZZDLSFDYkU1xvf4tWmYCU58GCmISiD4nvlbBXb/tVl8heT67mTdzKUd85JuufQghkDFynnJ0gJITjAxXR4cZeFGSwu4mCY6OqVjhL1ar2O2JnW4YpRGQM2tXfYN6nR+QxmEKbBZG6Ycxga8J18BhPh4A8ZOv1l7DARvOVDdbDWgTUYGBhuO6il2lbdUoOHiF9Z2aJwIoNRS93MHMTj0p6QGljx03zH+tJetbrorlypteZYCTgkBR87dtEtL0/5deN92D/ti1D7wffDw4bxnbUHzGnR/s/lhWc7iflPJ7PRc6A9bg5gicMdxHySYpV9xFzDHNUXjf/iRdsfhJbFz14sTec9nzEtLqZV9hCe5GLUGmPeuodS9/Of/x/celOfSa8T4b6/mcutZ5vcrHmFdObMXM/7XyYfrPkQLpVi0vngI51/F34vJ5UsgmnJpp3kcrYTi2nBFm1SpZJN2O9g3vJozqLk82W3Z4+hiYU2aWF6w2yZE2zSeZjKppoHnvSMP4QHBsY9EtWVK9UWdjaHPU5VkuZhEqhcFZYFs5E0rTy+hBzlkVTL8/MWliVpmvHDnPG6Nq/0JIxIZ7UYtD5j7gTAQRgRtl+YNm0wD/RTmb8gk7oH/W/idTMUryTZhkie6p2yRc3Njbhz5466rVuzcxHLwz1ERrO5mHHj49N+LPPzw22XALWNB/XEBHHLF12lMuFNElnhYwqTC8FvUHvPz9da+arZE3IcZC25DIWSP3uTSw7rz6UovCRSP7/n4kC/QgfDBErWvL2Z+6yxzPvwrHyFr2zTZZXCBpKSvb8xMwwMKDa9UejPcttt3+rWm/pMepWE8xoH6Mte9jIPKQo4BQk6du3a5Z3TAF8hbIyFRjUOkRgE1Qi44MTf4WVOOeiGG27w9WFzhw4ePOi/u3r1alMaHG6FjmFfJzxAmOVIsQCKYFvHDk+bxC5DIH1hXw+BW1AV8z114izH//kNKhuwy5FmRbSBip++kckKlDa85CcnJ33548eP+37iJQ/ICKAxEM569957r1dfo5bm9+o/fWC+hNpGvSTg4LNLly75v2LWqNENRUjJHGr+hgvQAH+ZB+GCS+WuWHCYCN/pVr9//7Cbn8fRxcJLDBBisZmPebiFxkZZ3tOHRqPWVMklIAxxWW7bOiDUlt6bNEGYy0LzUBtrfmfxwFYGZgGoRbXpfW1fEB9Nkow9ewwtymJK5UyWOHQhlXPQmg00aZd2JMHTX7C0OfhRaeI0BcQmZCrZuj+Ap6dRxXMwzqXmW/Ha4XxT/8GDI+7o0Wvu4EEYgoXkYIdky+swJu9uAh6y21Uqs55Rz83ZxYhpZZzE1to6AkiBM5IZdUHGCh2lJictLIsDPjzYm49Z6uIhBs1aDw8bQ5b6VCFCAvFQeKCkRDMN2Fpw+bD1svfsZ+qwEDDDgkYSpD7GA8Y42adC6FNRIn2iyib26WDmd7L12gWKPTToRkeXfEwve3JmZrCpEUjC7DTnphLHc9pQtZA4JZWrjC4ujI2pZq/ooiIVNJcA4b0zBubFQqYsGQfrJBAX+oTam71iew9TR8M7ic3MENdvFwUuNLTNOgA0g8qeNrLwvc+cMbxznj/2nV2s2IeD/pkydLUhD56i55OzVc+94QikzwiYMI5tQvTTs4pDG89atcol0pg+F9bECdLKhmGOCws5IOZdpL5NeoXUt0mvrtxq6spTea/UJl1Encp12yZ9330D7t577fAWKdYXaUOMNyQYAC8OpC98oV3iHxkxhiCbcNg2UvaJE/Umw0r3n7bkvR06rx0+bMySOrNocRF7Ooc0BzKH76XmoV71DADbYiKxYec0/HFBVppkONOSpmUDzFoCzl15DCfSngX1bts20kpeEp5iUlXDZLjUSKXLHHMBkVmAzzm7pYWQZUfZk5gPmJKlVbRxKTxM7UiCVBpGMcgwL7XgRUNnL+pDjXvhAmrvgy31u5ihpFbqYo1C2/3s7JLbu3e39/8w3lPJtaefOmXMtFqdasVvK+pAnv+SFhV/jDOgOcfVUpEC0ozwu1BiloOiYawbw4VB792beOsr3El2bNoEmAQmyMXQ0OGShyLcCw99KJeH/GeUfkuSVpksR68w8Q7x61matR078PBOJGkusnn+LHx//vyd7iUv+Zp1tUlfN3HS1xshRW7mcr1ucyP636sMY2IIUgvqFSeykFdwHDggKVPJEEJCKlEsLeWQSmKiXqma47jQ0ESXldISlWKS/Up20KGml3c6XaWSdoCIBjANDkYobK5cmWrF3gpVLCR598Ye5UjTyuObZeZQikPahxmi2rb4ZIufxktazEOMVH9hiGpPjNLmsx3CUxcBvQRsguTJuGHOOG/BzOXIxotzWRIgmhD1A2mdy4A8ovlrqUOtb7xgoNRnDPRoyxNeceIwaNrnt/xOJhCN49ix2ZTdPvZGlip+ehrgDtq52AyDs7IosZSQRIk4lEwEzYBySCtGPQlPMyjScF9pDagbJ0X23p49aKfSe40X86VkLxaPnc1443Wan18onc4yTnMap4wNU1rGz0v4/FFO/ixZxOef//yfuvWmvrq7T30qoDJhWEKq4vCV/S5mSpK2FEYT2t8MRjK/btXZ6XvFQ6+GOMBCoBPq4gDHt0AewLxQHsnxSlKLYpH5PdKUskFp/KhMYbCCnISqVdD+ZlphV3HuFd5joRFQCMxSByhzfOjQQXfixFE3PDznFhYMOjQJA0uclkLwD8ZomNXqQ9KeQrPkXU+7UtkLbUzJOgQhSr8VziXSuPmdOXclua5pG2bPwc+lw3C3R7xpgfmRRMpf1oH25SvAHKq/OP+RTUv+C2LumkdBnYqBT08vu/vvH/D2esXZh3ciobPNzjY8vCt1GNrYvFtctAlR/ZrXeG8rZEoIbKiLY0fA2IFTfQ+Tm8QapqEgaUc3gItCP9isOGqec+YH3PAifxbm83d/95fdO9/5Bree1GfS60TYmDdzuV63uZq6yDG9Fi/vMur1onI8rKi8O5UTZKKcoELVKxKD8tXKgYxDF6cebNBivvkMOoFzDJ3FRPLuDsEs8igPmEH9l6ocfHTVqYQWYsS0BwwozjeWEMN+s7CAF7H5LmSNR45d8gg2FeS0R9Patg3HrOEW2IckVpixGDSkeeU9kuqBAwfdvfcedUNDcx7j2+bLbPe0I3uwPmd8HKxaK6mPYQRcPpDaFWfO2EMbt+YI5kpZMWYxRmA8BaASxmzr8sVc0SdpLOTtLHAZ+goD5xIgRoVmRPHvtMse0nd8VqvNuvl5bPyJpM14uCyG2hoYLmty6tRFt2fPVFsKUhGqXeX1Fkod3t44kcmkIEk/Xl9pjbjA3Xtv3R040L6hBREami6EcGbM3RzIIO3z8XH5EKTri0010Jkzo277dhJ2JPMfPot6PoeHV3YuSOqO1eeNxrL3w1lv6jPpdaKymK8bVa7XbW5E/3tFPLwcAJKCwiFI6gxVknYgp0OwkFh1aMfE5xxWMCfqERqXJA3eK49vFh04UHVHjuAYNtBSCYdlFVIF0XcSPeCTSDy0ElbQhjIldZJsJFGFoCs6KIWYpQNeEJ8nTybe3ooXlye4GHRMzCvM8YYbDrr77z/qMb6VjMPUvFbGUjUmzmEwdzkvqSx9xklMnvdKDpPlrYNES99g5nLWUzkxZb2X7VWMjTmmfVSrjI055hIHwEmjcdTt2HHQM0d5ksMcuNywtrSrtAXUPzEx6MbG8N9IpH2tk7z9Q8JfgIxaRZEItG0XANan4mq1ZBJ06dH+IbQy9meQJ38e8VsxPJkXWBccudCACABGID17muaisN/S+vBXNnbs8qFGJgtVT5qV1QCRZEndJbL4doX6THqdCI9nPKs3a7ky1M02N6L/ZWH91lpOUkYe1LlAOULYyZg4PDhckWJCNbjQwjj8cDRCBc0hJ7uoMWq8arP7T7s60E6dMm/XGAktPACFUIWac2BgvnXpUJ+UpCIPRvX8eVJNVr1Uio1SkiXMg2UV85V63DlTe0tFrXhxJMcQ6jMmqc4pC3O7etVU31u3mte3Zf2yftO2EkiECSZUB/Nhsbj2PeaI+LLE5yh55DwlYBR+K6ev0M6tiwqkfaH1Z96ZHzFw1p3LCBcFhSVRBs0DbWbFWnMBYQ0GB2fdgQPTHVG6QmdMkMicy3ZyYg2EuU1/uODwV2stxDbWkXkX5noWMRYwE7JI80afcX7T3LG3WAe9p+5aUwMFOp8+51LC3MVmI+YxhCeNn0f5WeCsVvS4oz3btm2+Dwvapz4V0VpV3r2goudY4VIJ5GM6XpWDD8kvBAtJ4qSXXa1mj6k8i5HAQi2bcJJjCUEq2gQZCskosZlm2fyU7YozPZYklRoSz90w8YHsibUah/9FXx/SKIds6EUtj2lJUhoD9umRkcTbW1qCTjZ6zQf9MLStg2509KiPx794caRlF5cGQIwLxsMlhDlUPUhszD+MmblXnDdMUjmYCSsTQ5D3OGOEqQhERfMAA6dN5l4SLp9JopbEDgPmUoS0TJlQ9SukMhhRMyqzjS5fHnRbtiylvkdKzyNJ052AfaRZoG/MEWFrtdq827LFcL0pgzZA9nNpd0LtjOUXd4VkDpN4eFu0A/CkIOoROaDnZL4pNWt+pcmBIQs2NiRpILKcMHVxIkysU9Ib7NL4oWwW6jPpdaJDJYGqN6pcr9vciP6XQR8qW46Hdmwsu5wcYjhYsw4GoVpBhg6VxKzGuN0hXjbUaCQcVNmJaM/gKuX0g3erSSIh0xUjFHM4f97irEVIRDB3wWfGuODqb/h/hdqE4V6SeoS6NTtbdxcuZKsMhMCWpXbF23txcdAzLiRc+kwflZ86jE8VQpoObtoGlaxeP+omJsyZTP2F2UiFHTLaGPmMOhkXUjwMgLZlJxe0ZzoNZ+IsJ4lSuanlIc53tC0JGfxv6mTeuSww96i8KTMwAKLawVaMNOMWAEi8r0KpXZTnl0Cse0hLSxfd6OhUZtpWaQzkNKc4bfYXbdJPXswHcytmHGpnbG2rvv+dEs3YZw3vQxCaCDQG2jvRumQqDDKxLcfajhMnwEIYcLt2EUeeDYxCf6emql09P9ab+iFY60QCGtms5Xrd5kb0X+AGay0nT8+8crrZi3HGB0NoByOeE/AS5666iYmGL9NJEgeARGTISya9EFLCC7Ui72EwHIqodXV4iRHKzhmjTUm1DCmELFTHm0SV5gR8BqND0rz1VsOnhvGo/ZtvTqtSs2AYQmefpF7LvAFYB4yScTA+pFxJeOob8wyTU1gU9cEcGQ+oZFCtNtcCFqFf/EaqaetX2vEORiB7OKT4aBgU/1fOZEm5YqTMPUwXZsLWlVqdeWF+pJKHyYF+Rjn6LOhUXYoAZ1GdoSNYCJMpkuOW1peYc8XEZ2kfSIYSStOKCec+jCaAv7wHZlSXxFjSRppWQhFJuWFEn7ynlZvawqDqrfCnrOhFXW6lgUhylSee4tcCuF3bJxpTdly9oF5Zb6Uk5S/PgDQW9vx17/xYb+pL0uuEOHbixAmP3lWEOAbaFx6CRYhj/KYM4tjs7KxPtdkNxDHapJ0ixDHGCsBLJ8QxPpcnZCfEMeZq586dPqMZxP9vvXXZ/dM/uRUjjhk6UKNl7xwaGnYLCxZ7gq2JzwyJa9H/NgtxTGUJJ+E932ehk1Uq8+7AgWGf7EB2WLJOEVNMbPPS0kLT+7bWQlCDAYOutbBgh+jwMMhJOPfAyIWcZG0CndhoDDWZrSFMidGYxNNohQhRBnsheaEBG0FlGSY9CAkGZmFElu+5Xh/wTAlPX+zSJNvQocbBqUQVly/X3ezscnPODdls3z7asbEAFWp2RH5riG1y8pFK12yKhuBk+3q3O3dutpWKcXm50cw1XPFlBfKhUB0+Y1sLmUuqUFTfSKWjo6b6lsNTfF9I8M5ZR/+JlyCVPtNsvnYhkX1cYV5h2JvaVlgTDPnhDze1cOj4JgZPn5kvZQELKXZCg2A48gIP60LS37FjwJ09u+z27iUueaCF2latDrnFxaq3v/LssoaNhmF/s8Y4BRLbPTTEGtpnc3NVn2CCPY02gUeZ8njzT06yh9OXt9OnqdM6yTqTG5oXoCS837qV56bh22JOLPxpuYUeyP5G6j19GvSvBKkPBDLCxsy+3GibG/ZSFoM2Jg8ym63FhQtp9D3tYTDsUanbM8ezvOiRzMLnnjk5fXqoie6WjTQYZlxcT+oz6VXSpz71qVzEMRgUCyoHqDAn836udgHBuGHIIpidCGYrgmmF5aAQ5QaGDxnDqPg+xH2CxDRvueWWFlPj/1llaZNLBy8RTD1rrNTJC6Yajk0EI437H4Zb6TtBoMb9r9USMRDGDHPjL2PlYQ9Jzh4GRpBWWWWVpQ5j4kMdytoDrPryymJDJtUj55DyIxPSRNKCVk2VSrP/g/7/IyPJd4I8FIVtcvBwaMI0gFlMiIMwSRcpj/KJiYEWSAZlAKmQelJ2coGVXL062JKm9+5teKkoOR8NzpHl4kLA99euXXGHDuFcRjiW2bpt2ofdzTcPuy99CQclm1cuGHLygdloScwpzlTwXE64TNDm5KS8ve09zEKJOIjlpU62JP1mLmwdrT5zDGIeUX1Tl132FhfNTh1mB5MEB3PmsZNtWZjVzF0YchMmIIkZfgLxavPExUcMWuUVxqZ+044c20IVfOh8Rh085nyHhkHEWNHwELZkWbKUWYy4a7so2J3Y5nBiAsY71DR31N3Ro6DETbmxMRJLJA6LqJWV3Ut2eF1GtG7Mo+2/hFmyJkmYHnXIbmJ7gHU36FESaujZYaDz7uBBLtpK2wr6GGkzrd+7d1u9uuBoDhgj86P3unDRXuKEadCf6p9U8jDy0ATA5T3rWV5cXPbnQ+g8Fj73vZK0+0x6nWifAi43ablet9nr/tthjeTc2S6W9ZDmx0sPFoZv5HlZt/evHOpI2DcOKyQA8vDGxCEVnhn8nxeHFYyVtJgkxoDk+JQAi9iBzkGNYxUYxjABmANqWe5LkmLtvll3Z8+S2/Gy2717lxsYSB8jcpAbH697TQKSoJC5wvnj4IbBoG6V89DAwLRX34ZJOGweVHcikcuLV45xlNGBnmRfOuhGRo66K1fQXo2kckjjtCTNAgc4fZGTGONWKBESYNpPIK2WDVXDfEddUkXHEp8YnvqpMDG1JQbNpYiLjXJMG+Rmuv0Qrevuu2fcrbdOt7zm5YuAmpfLjy4EjP/SpWQvMGdKOsJcyM4ve7NC/+xSM+8vlHFK0HDsmvcsKTPLD0HajzC8SZIw+/FCU5rXXOuyx2fsbe1dzTOX1W3bBtzIyHJmbLMukp1gQ9PhoBuPmt23Sa8ToRbezOV63WYv+y9YwHvuaWTCAsYkFXgRWTq+oraL69q7tyBYNac+pJus0Cc5M4VOX5KU5WSGUkfSUMjQQwcsDnRJdpIcwW8mppsDu9NFJ6Zbb51qxSsL5UtxqiF0qlI9Dg3NB2Mw8Vg5lDUeLg5I0zB3rakw02HctIWkxD0PyViOTrt2HWxqDGDUc600klLjW7pS+4zfyV7M72mP94xBHuMyZ/AZTAMTQ+xgF/7NItmUDekrmXMuLLxgQFoDmBXjxYLFOvA7HpOjRw1XGme7MBZdDDqENUWypqyyUYkEYxqGLSkUjXlmzvmcy5aFSdVToXHSEPAb2ZHtElJvSrmGHd4JMS+LdNEYCnwRlHCDCwvrTHvMFco9gzQFRCXt/c/ayh9BexeVfifY0GRsm8PDuy9J9+kBFYpVBhZwE4Q+diRhTZuHs6UMFIYyTJXDhGw9Z84ksJxKmZjlUc53MC4Odpi1fPOkwkaK4+CGGYXqQyQ8JakQw5DEMzIy7kE4rl4daMFPSnrXew5zVJeo/UO7cYgEJuZQrZqKEaYAg1xennZDQyZRz80N+jHQvoGsJJIzRN1idIxBqm/WmvoN1tTwvpeWjnpGvW/fSAsljHmVKluSNmUFDWrmAAurYr4038xfaBYQNKxwrJU/OSbmUKA0cvKjHZg+31nokzFPXRZkU4fJMl/0Tep0+mYpRtPOgmEMsYHUJPjywg8PbeDhJY82pBHhwqCx4Fw2PV3z47Z1sosN41Z4maGKmYpbjlz6vcK1OlGYzrXSvFiqb7oMyBIVRkPMzeH41iG2qvl71OCxZ/hmPh/6THqdqFOGrM1Qrtdt9qr/YfxtLMko/CdGDioLWLBz55LP09sJxxtbc1kiCxBOUXEfudVzQIvkqSqbGrGuqK9lpxOOMAcrkp+YTngY8v+bb2546Ysy+q05ZhmTkCTBvClNIQeXMeiGu3CBVH5+xtz584TwJA5sgtZUWkNL7Whx07GUKRWnvJfpN9IM7chtwSRsAzrB43tqatAf1GI6sn9SD7/n0IVxSZUMk5RzmdTEtswH3czMUXf69FwTlGXEj0mexiFKGoy+mUnWf87/pU2gHVwuJLUrb3SIPqdUmWGML2vJ51yYmCMBcKCOh5CUGQtMVpcp9UmJQ5RWk/0jdT1llNGL8dJP5cbWevBe4yFWeHHxojt4cCpXwpXPAr9LUp4Ot+qTBoZ+IqnrggDDJrOVcuWEamoYdRlGiB+D0oRClk86Getqnz2dD1lajvh84C+gJoru2CjqM+l1om7gRq9nuV632av+p+1e2Wkl1xeyNJ0KMs/utXXrJXfxYuIkCFE2ZtAQ7/kcpiAwEFSYqPrkAIVKVHGtvGC8vMLDUHZleXTHACZK4QizlcoXQuVdrc575sBhDKPBfouXrVTAyoBFm9gRlVnKJHxizC1uVqSDlvFIWuTgZxwwR/kqDg5OuzNnZtzFizg8tuM3h2p+MW6p9KW6pt/KQGbgJgd9H4eGjrqlpTk/pkOHjFmHsbWhhBzaYGUTpk4Ocuo8eRJnOlt7voN5w3D5XhCk9Ms845NUlqh1+Y55IGmIrD1x2/S93Q6c9taX1kBoYbo4wLTD/N7hGlgyiSSHNBcdXVaEziYHO/bnkSPkRa/57xkXdYYObRCfUR/7VQ5esiPnXZRDGh3FNp726m40EsfIPAZf5vm057+y6vNjI6jPpNeJCI8qk2N0o8qVoW622Y26yqi8w4QKU1MNV6ulvUOzHvCVwIISitUpK5bq4nDKypwjqS3MhStpmv7FDFpjQrriRRc40M2u3HCTkxXPiJgygWZYrl5jAKHUQqhZpTLcUoOHsJVytlIWI0lcIRGGQziOAhDEHMODXYevwqQajUm3vHypxahipDX6DWMJLwa6bGieSMRRrc64a9eWWkdWluNSGA4lYBWpgGVHFhM2h6+DfiwXLhx1x49b2svdu0daACqhSl5jCpM26HvWA/MDjlwJUpwxStl7xejuuiv5vTziSRCiC0Y4lk7/z4qdhuEqi5n6TX+4KIXSfEjMNXuS3zEnkrbl58AaMA5+j116bCzRnihuOsbWVr3SMCjvtKgIPzz0m1iM4D0N9CX7d4Q0Fj3HAlDJY8Dhz/W8bzT1mXSfHlDEA66EFCSHCBG2eMDXag1QVqxOUnKRXXz//krLgezEieQRzIrokIORpBAOUQPdsH4IxjILXYnDNpZabryx2sxSlMCPhna/rDPOVMgmTZtUZ7HNqCB37ar4dsTUZeNFOmO+zdY75S5dAuWq6uef+iztoal9BTeZdTHQgX/woEnUwGBeuNB+bLHeYfYjqaoFgyopTOpn+iFbPCpwGNHp00fdzMxcsz8WykS9YnCyu8shSZcIg65M2zmpl7lA46CLF9J1DKIyOKgfWciYCElybMywzMN1EOxo7OTE5wpzMqxr67suPHEd4Yvyyg4mTYRgP/kOx7hY8pVZIW/PQMl4+E9yqyhWdSdanflgnIJ45TKCySavHuWXBjgoJpk1shLZlMkktxHU9+5eJwJgZDOX63Wbveo/Dy4HTqiuheRZzAMe3+RXCh/K4UFy+yzvUMqEdvGY+DxOHIE0DcWafkkoOuxCL1c5LGGXjPGL5QWLXTMcazhOmAeHnpyV5PEsJKdQtaiQHGjLFmXvarjt2yttalTK8l5q1CRO22KhWReIOTPvcZcrGYfzYNLotJ8jGHWo9mQMHN4CIVF9oeQbO0apPX1GXcTqDw0ddNPTBz3zRBU+Njbn1bbYjhkPzJfLR5idibUA/CUk5kVOV4IkDaVgi9meazpWGRSonMkgNBa0pfdi6oxVubnDMLBQLQ4xz8wJjDqWxIXPbSF98t42tbUYt+Bq5UgnJ73Qcz3Plp2EYbVnMSvLCGW/P3jQGDJ/0brIrp0VRFHmOTZ0QIuZjvulNV1Jfb2gviS9TohjIIA99KEPLUQc++xnP+udpYoQx1DjCLGrE+IY6GCPfvSju4I4Rpu8L0Ico+1HPOIRHRHHCCVC3VqEOMZ4H/7wh6cQx2iPNqDHPvYmd8cdFwsQxwA0AGDDABHsYLEMUKB1zc2hBjfUIPrHa2xsrCPiGCAPc3PX3OBgzXsb8z6BKkTiaXhJZM8e6htKfReiJvF+cZGwFEMyQk164gRoT0tNUJhKS/KS3TMMlVJYyNWrVR9ycuwY/UjCiTReOReBOmZqZ8ZNmNOAu/HGmjt8eNnNzy/6dWXedu+uu5MncSYyAAh5HstjF1pYqLnR0flWnyStqm0xEn7DQQrD37PHwF2uXp10585dcoODIJXZSS9pL8lL3cg86KnPGCKq1p1uYOC027HDnMmkzkVjEkp1QguLmX+oetYcj4ywN5bdwgIJK8zuvrh40Nty2XZnzoCpnfRn715sshV/uRLDNdzzcM1NzUuMMkxG0jxq+OVlY84Q6GiSjsnRDbjLiROgdxH2Rky4AbeE/aUtyt90kyGJVSqgdtn8hZcPxqHkIlyuQmx2IceNjxtCl2LMmeNYzW9AJWTIMjUxc0SMufJ2h1KpzYch0Nm+EyIZfgkVNz1t6HtmtiFP9VJrX/Kc2bNMBwZbl9bFxUbLEVCqajzzha4G+A/7m+eVmH3qpT3e88wDnpI+I3juBnwyj0bD5nBwcNlVqzSQPPf23A75S9j27fMbhjhWaWSB6/Ypl2BYoILBeDt5IgPhGaNmbYZybLK//du/dV//9V9f6KTVzb51s65PfOKyv2jA/LOcReRoxaHCQxoTwrqkG4iHMkYiyyKDBa15KRDVLXTqVPpBhXHy8MqzNYuQzBoN4DZrTXXigD/cOJBD726WRzjEIYiDCCltdtYQosT40n2xkCvI8veSXajqJRlwlXnyhaqlsCkBSVCGe52yHMneDKAFxKFJ3eb4ZE49CruSPRPHNsKdBgdHWvHM9fpFd+WKzRntwsC4q5G32GAlE0lTZ6BBjirEq+5hI48fn2kCmQw25y7JVY0aHtATcM2F0hY6XnExYE5pi0cYRiMHNvrI77jgwHxiLUGtZgw79E7X2LkMS3Mgmy2vkycTpiwPc+VSljOb0NzEkGGWzKtASGhDUqRIEj19P3NmyWcTkzYhbcdteOQuZIVwX23bVne7d0/5etlfOL9xwQ05gpzHeGYs5SrMutZaJ/YgbcrTmxfjAU4TYB/mnzZJX9oeZ4+Pw2U3OTnhma4w0Q02FQ2RDSIGMdEzFALMmIqfPV1tAvU0MtXdK33eKYcPSpaHN2fp133dVi+YdMsPKIv6kvQ6UQwbudnK9brNXvY/dB7r9P1qvLslRXIgiFGHxGEDs8hLByjV5MmTYIG3fuVGRuxA43ccQLJ3K2Y6ZtCJvdouBeCDh4xa0lmYrlIMhTZ27666o0frnhFJJSmVHzje9fqgZ4xibpLg7r8fKXLeMwjFBMOsZccUAhRSpNmdgQVV6Bjfoa256K5dq7ZAJgxK0phlGAetPgnYgxeIa8vLhMWY6vu++0xDtHWrocHZRWPRLS4O+4M9DC1LvLsVCmSOhbqrMk75BWTZyO0SYrZjLkAKkbLQp6NeQwZDCtfJ1Kv2G5kT4A/0SeNWOkapnw38peLHrcxnXIpi72w5bbFeQhtTQhGpv1kjpHfGwgUAlb1MGoq51n4Rrrr8BTRn2kfaQ2EZ5pd1o4wysFkmLIPUFCJbnjc3dcjJ0tDQkrhvOR9ebZpv4IMy04Q8NklnmcCUMlY+z+LF3Y3mWH/qM+l1ohinerOV63Wb3azrsY8dcX/1Vxlu0JFziEJiimxiK7FJx/bsXbvqKWkaFWCaKaXb5pC0PLjpxPOWyAC1m+F968BEAo1tzhxepspWqFLdnT9f9QelGIQYaILhnBw61Efyh8nJqrtwwZJLCGqTQ3JszMBDBEAiL1/6T73Ly8NuZgZVIurZaouxijhEZTfV3Ia5sunT8HDd7dyJRBzmlTYGxaGvywS/ExO1w3ggmlOY04w7d27J7dtnBWs1Wycd7MxVCAhianj+XnEDA4lKJUTFyvMeTzJPJZ/B9AcHD3pGGjqvQbTJ/NAHmQ54sQfomzJ8ycNatmT+Kg1mCGUa943PmS8BhvD48JkYMXNmeOs2NswCMHHK8zvqEgKZ9qwuELrUCBiF73XpEf9SPm/eg/wl6XZgoNwzRSIQPSdc2sIsZSMjmJjMXMPnstGHtuN0zvTkedccZ8VkdzOFbS+o7zi2TiSb8GYt1+s2e9l/MUlsjUXOIWWhPFVOzErSdFx/tbqUYkpIxeHfEE0ppIWF5RajDseBrThU6ym0B2aKWpZDF4bLYbVzJ0zeGCmHMgetICI5kLGxctDB2OkfL9lJqQ+GyYHHEuASwEHO+8OHrS31m3mt10kpWfWq6thTlvd8bt65ydwKHeohDzEAjdOnyZRl32F1Y/xKpgFjkxoZEiY1l5uQ+D2x1NDx40v+ha1SMclIXtSFClXjg0mZwiZdl9T1sbNVFoXmSEFOStINmaikY/4qVSXzwxrSd9aOvrGOzDe5bug7/aDvcvASxZeHsTHbb9IACHMdhsf6SupkXllD1an+K6aZNs32anuHF6YIxiYmGK+zpGqp7WUqWckzhcYmvthAaIUEklNrOrDxf56hWIpOfp9+HhWTHdNKnvfNQH1Juk/XJQ0M3IX7Xu73PMiEOHEIdALRX3m7aSkZRg3TI2mBoXEttx6rOHFAUYwojDpO0AEM44EDtVa4FIxE8bCJty62RCTiqmfUW7aYNK9UiCHak+yGMAnmA0l4Zoa4b5NWEicxO/iR/GEutKeQJBxupqcHPfOTzVJOWiHiF7/Nm+9bbplyX/4yYVkw6mxZIZyrIo/5G2+c9gyJRBOgwlUqOAcNNrUWiZ3Y0LBkThhLJf2QjR4mR9uyW9s6JIxNsKsi1Z3H2BUDLSx19qb6JY2F2XvtL4wSZiQP/lAdHxJe7tC2bYmqW74FEIzfVNQWKicpFaa6ZYtJqJDCtNgvXNi4oAmGVSQtQFasdRh9wOVMOPFlKFzjrN80Wkk7zJ8i3k9FMdf6PgyZrFTIorf54D/zqM+k14mUpnKzlut1mxvRf9nHisslT2un+GeVk5SsckihIyOWnWppqQyYQj7t2IEn+0AL4IQDRQxav40v+DrcYNQcZEjjW7caow4xnOXwZFmnEtsfakWcY2DmSLE6dBkX6laWRBjWvEeywumNy8l99y14z+GzZ9MnrOJvyZGdR/v2Tbl77jFGjT2x01wlh3H26a/vsc0CanHkyBnvTMVhfOCAQYoKsxrp1RDSzDGLOWCMXH4012gexCi5oOhyo1j70F4dStUh2InWJitcSZ7zofMZpDmmfqmjlSSC9WiPs97dSs8px7n2S03aGSwJOzONRgh6wivMpksdCpvj/6G/R4j7TR9h0Lr0TE6WYy3hGktlH8eGQ3iGZz3L4e+zbMh8HwMLse5Sm3c6H8rCBa839Zn0OlEZ78GNLNfrNjei/0iWKynXCSXMVMNpQAZJyRMTBiwCZXmThyR1eRayWGgvJ7wHp6YTJ6qp/nAQt3t6J23u2EEscjXVlgBdOMOwWcKM1L7UiDt2VN2ZM9iJk/IQh3+oDRCzg6nu2EH4D+Eu8277dmPUknyEqU1fUJ1mLaslDJlyV65c9EhWoQ9B7DuwEmdA1kCOVF/5yow7dQpHOOob9AwABimnNghVsLQEwnUOY701z5K++DxLBc3BH6+rhXgljJq/wtiWzThm6gJcYS0kbbNuUtVbW1xAdrdwyWV/z5NQ43nTOulz+iObs1TiGpMgTmHWlIltx8wbF7a777b3aFhwqizjA4p5aHTU/AyUoStODDI2lq+VkUrfylcyE5mEfg9Wp0VRhDbrrMt50bPcK+rbpNeJZsOTbhOW63Wb69F/HMg60dJShBrSoVwRSphJK/n1EaKBNIoUV0ZdjmSQZy9X6IgBlbTb2eIDObTFmaSMI5k5pgmiEpIEGDMS2SWzoE5j9WboWEV8qaUnrPlyN9zQaEE6KmsWNmTNXxbBZAgD4jDlsiPJFsYeRgiGCTD0ymPoisfnD0k6FhbMZk187OCgMWzlT04uHclFQiFnMCvmBubDvEkzA9MKfQ1QEcO4GIsgVzVXwlCHZAeX13IWHrfeSy2ucCnGwv7Ytm3Jvw4d2u3f63utfbgPkjobLSkVQtUdMm0uEVCYolP9YF74HKmeS5nMAIydeeAv38kHQb/JQs/Lonp90Y9DaysQHuHU39ic50ploeB5Sj8HllYz8ctIz3EjZbNWatt2cKLNwaT7kvQ6gZmcOHHC7dixoxDMhHJQEZgJAfTEEBeBmcDkbrnllq6AmdBP2igCM2EMhMR0AjNhftT/TmAmzBVjzwMzob+U5T1tNxo3ZICZLAWH04L/y+15aAg0sBCgxMBMONSXl2s+qxMHaoj3TapFbvjm/GOHP/UAwhADnzQaNbewMOCGhmDWKjvkgUsIlTJvZzjBvPcKr1RGPCgGIAqAfKCqRmXNeLduHXIzM0NNyYLDp9JKQg+6lXnkEktrL/OGNlAOwB+cq7lz57Bx0wcDPOEgu3ChkSlRSc168SLgGQ0vDUmy5ACVxBQ66TAH2KbNvgqXXPD43syVVLb0jcsLfcYkkFx0aoEjGBL4pO/fwMClZmYsQF9sXACCEDe8Y0fFH+DmuWztwDTsMJ5vZrVibZb93l1crHmJnz4sLu720h6q7qkpuwhauFhyBCq2OcT6DhmPwGrwOxgbw0/A9haxuZg5ONRBs4J0ieB3PHrmr2BrRTn5AMSkVKRIjtxXDRDFmMnWrQDemIobJzC1Q4w5Y+M7AXwA4IE0y/zSJhc+1kRUqUz6/WvheGavNgQ4cz6Ud7eF3tmesC1dayXjkPZBUJ2h2t5gRUkdOdB0KltoPgu2vwEswV9kaWnUA+4wb4QvsuaAuvAM8txVq4Ag2dzxLBv2AVL6oJufBxCIvV3xPhaGzmdx9PYM8Fv2ZRpQKPw/ceFnzhjQi82LlRWq34EDxF4D9MOa4PuRPPeVyqB75zs/6tab+mAm6wRmwiEBcyyiXpdbCZhJN/vWzbriMeQl3cgDM8kqx2GOGjSGuVTOZbqESraoPqm9O6Wz5JHrBMYSArLIkUy5o+335vilOF0oy8Z25Ij14fx5U+8hCZFLV5KXvKeVzADplTo54GD4fIc0qbhepSc0r1sQp+bd2BjiIYhixsDPnzd8b2AtIZKcwGSpS166EHcwxd1KKocx29KDKEdbVT/3wto2z2i7cGnMMCf+hluZQ5SDHAYXBgsg+TGnzVlsmjBmWwyVvoELHsK2Yp+1kLHEXCB1aNgmfcwDsGF8rE3znuznm77I7BCiyjFOJVNhfpgHmLMYIMlG6Pf99xu6GH0hbI/f0rewT4of5vIJo6Ld8XHTVkxPT7U0A7RjYXkGAhOqwcUd6O/ly/g61JroaFa/JO0EnczG95CHJFoFy4iVPA+KGqBfXF5JS0nmK8XDi9AGHTpUT61p+9iS8tLAMCbMRWik+L4TsBBSerIn2glJnnpjQBNrv+Ge97y73IULD+2DmVyPdP78eS9dbtZyZaibbW5E/5H0iMMsU25gYCgznZ/ecwCWqe+GG+Y94EenTFllKHESW3K12mCmnVMhXYODDTcyYo5iIXHAwahx7DJJPI1pLc9imJKYKFNvqSurHp+cw5kDCZUmkp1sgIZMVfN16NA2hZHZqLnQoNLXnCpzFweebK0K3ZGkCLOCaR04MOVOnbroY6nx1IYRcyAL1UqOTniQ038c4ULmhKRm0pYxWUmEtI+qNoRdrVR2e22GxZjPeFtv6JwFBKzU01KdoiGIL0VoQkjHmScd0y4HPkhYMFV9LtAN2qRuLgTsNcwmoeq8XicTmNltQ99KSfwwddm6RUlseqMJY2oMGs/6kELPdTmyocJnjJo76rD9iERuly7WVmkqQ9hUgw01aRnS2MLohCyzUpy1LaTl5pqqjzGDhsy0UvF1KMnG9PRyG7CQNGsCTMkjyiHdt/cl2zS2XtRn0utESEqbuVyv21zP/uelsOT2XYYop4MkiyTxlamPMsqUtRZGLRusqSGX3Y4dA16aFnPlcBQEJAwGZpp1uIlRb92K5Njw2ODCcJZKUwc6U0+dkkh27TJUMksokmSOMsnJDm1SZcJU+L0OQ5zJLP80qu60g5KFrSWJQwQwEtqsLb5akh7oZDCmqmdCTWtQSyWbZf/kQK/Xh1rqYn0P02OedMCrfYQgmNLJk3Y5FBRrpTLjGg1swFpb+x1jvXrV1PxcAiz8DNtqrRVaJRIzF0DN3NyCGxmptUKumFdT4VvSEGkOoKUlQ1ULL1aan/R4hbudfKb6xcQvX85m0Fp71knmDNZeKSsh5oh+gfk+MJBcGgVYoxA3OXnxe+YNJs16hTC5XNAsl3e7M1unXNPLy5iD2mOj02QaMeWwVt/zgIX4vMwRoSNJTmWdwgHXg/pMep2oSJW80eV63WYv+y9PTexRktQ6RVNYAgE7OOL8tcKRNom1WHUuVWynlJZlcPl1uAj8Qowa1XUcAhR7quYxauqBqVCnYmPpLoezvGpNejTcaOoiicPhw3V/EYhTYuoAoz6k2fAwvOGGmkclEwRneCCHtsuQ+SRzmMRb1+tTrtG42GIy+/YhISUpI8MsYcnvB1uMWExOzmF8jtpeNlUOc1lXYFaKN+b7mZnp5jyZJgJmiqYBKNJabaklUdkYLDabvUI5Ed+Fa6VyuiDQJutifZz2a0H/7rnHHMw0TtXFOulSJqjWpmtHKkQvlDQlMd58c7ZKVhcV5komH61zmDYSU4jZ1pP5Yj8JoUzaGMrLXyB+nrQWfJ7l/Z3nYFgJNlB+bHQl8/s4ZBLTijRPvM+C8LX4dLQPdqmwcEMbf1Fsdrepz6TXiXAS28zlet3mevdf0nQ6jKrSFkbVCe5TXskhOIVexmCLYQLDMiQwQJqTnTrpi5zAOhP9PXjQbMh2MBgeMtmRwoPPsnAlTCeLxKiRbg8cMGla4Ubt2OA43iQXi+npqjtxwpDPYnMAFLYt6E9TTeP1Pe+dyUieIZK0ZN7XrVFk5oWGtm+f8o5SSNX0nxSZ9DcEUglpeTlBsfKjaYaEqW2IPYKT3qFDZAGzz/QXRoXNPLmQmJ2WcaGqBTyk6R/p91XIlItIDJR+SDUsxDLhY6PSxo5P+0JBY1vBvGHISKWhdgDJlbIyG+Acp/Fjgzaa8n2nrJJ7aO4ST3KLRQ6BaSiHip61NA3IYNtYQiQyQd9iChE4TngpCx3Lsryh8i7TtdpwYRk9U1nfp4GF0qGUsaQtbRUXTGluwsuwQfH2jq67EKz3vve9/iDHuejxj3+8u/POO3PL/uqv/qr7hm/4Bu8NzeupT31qW/kXvehFTU+95PWMZzxjzf3sw4KuX115FNuKlDowDKMqgvuU7VKhS8KQtuQOxTCBKqO+oPLGk1ukvpSxlUN4LQvMwn6/7GOSQ1LoSdENf3r6mmfWePoyPhgdhxAMJ8zrTKYjDnuFo3DRII4aUlxuHqa1oD+lMWg0hv2BbTZqc3aiPRiO4DfNk77RcpySZCgpXw5uCwtT7tq1STcwUPcXIMi88dP9IdtXSAL70CuU7rIOdJkCkFjp58GDOCEl+bFZCw5qGKx+j8NjEVHn8eMJFKpgUGGemBmoF9U6TJ/kH9ieNa/8n3nRxUbMjksTv5WEh2aDOu0iJFSxyRYQC2v6pS+ZQ92Xv2yXEaRFu1gt+71JGRg9f82GngbM0ZzGDFr9oc6zZ81j2jz1E4YchoLFTDoMpQudxqDw2ZNfREyWDrMYwCh+jmMIX8w37LtTp5LUnyJdbPMShrgHO5P+4Ac/6F7xile4N7zhDe4zn/mMe+QjH+me/vSnt0KRYvr4xz/unv/857uPfexj7o477vChSk972tN86E5IMGXCjvT63d/93TX3tazT/EaV63Wbvej/2Nh4R7tyFo5vfKMukwy+DIV2K4MOTTNqwk9WSok0iOqbkKT0XBX1UVPL4WcOQO3hWByojDmcK9mQCXeJGXWYQzo++GByeGwbpjMOZeYhT/3K9qR4WzCrYUQwCUlZQvdC/Rra3LdsMbXt1BRqeEJusudJ/YvNC2GGrSzliLQqMGJ52YtpyT7Je9nby1InW6byb8s0A3PkgvCwhxHOaJcRQbMaFGyCvy7IUZgm/ycOmhe0uDjl15S5xlyR5GVO2uX4pG7yicdM19ai1or11njpRxaWN20xR9JIzc9bKlUxagGWKBucHAHZK8x3ojVqrzsrNjokPMRX86yqzvAyzEUn7yhS2F+vGPV1pe5+xzve4V760pe6F7/4xf79+973Pvenf/qn7gMf+IB79atf3Vb+t3/7t1Pv3//+97s/+qM/ch/96EfdC17wgtbnxCx3y6NYRJjWZi7X6zZ70X8erPe8Z9z9yI+YTjbWKOdJmnlwn3mwoJ1IZeK2hPGtjFll7Vphm0l2Lw4/w/mG0Z4+XS0lQYR1wajvvZcQJ+L4baJ0WMo+LZL3MNIFSTVqNcZi8aUwi7y2YbrMpVTkCwuE78w3scYtHSNMA6ZPe4wPdazZ/+2FmlsObjgvybENSFEYz9GjF93JkwnXAYt8eNguAmKI8imA+Fz5qaUeFtylwqoEp4qWIVbvo8Llcy4PMBzZuJ0bbtll88jWPNvMIdu88m4rRzd95ZGQFkD1hOYZpcA0G3zCnLWmYv5cduRIFpLGKClR3uxobFgz5oi5t0uNca48RppoZMwko0QfsvvL8Yr62Ddc4uSQZzngkzHnPVfzTXs3+0bws6a6t3C/Iip6josec/kS7N/fcO98Z9O9fR3pumHSBLJ/+tOfdq95zWtan+E1iAobKbkMAQxCgH0c34zEDQgJKvEnP/nJ7i1veYsHF+lEAuIIGX0IaYltUqAanajX5RSM3+u+dbOuvDFMTRmwQR7ZQdh+PRaAgcrEYdpJDGgaFCGLVCYrnjpk1Kh3re5y9alv2LNhDKaiXHYTEwMeaIIUmebwlF8ffQrbO3Ro2R05MuBzUSdwpMaE4n4hHeEkRB+wL5utOgmr4uDkAE+clypeTU6cNCE7Aofh0KeNwUHU+AbCIamZ7FrK2iUUNKl2BbZijNVAZ2CIN9+cGIQPH77UYth79hjSmVSuUl/DfMHUMSnd0mzKLCKtiRyg0gzayshZCyaCZCqNAH4CjKeT7wNzYwyyfY2oQ9jhaody9EHgJlmJLRRjTgwz/QdkyPJEp1W1GqOvNSMZSIIhbhevMPxLJCmVvUE8dR5amv5P/2Bk0j7Ii19mDC5rMzMG7hLOCfNrYX60p71f6Rgbzd5jjOl9G78v9xzzLCmyIialFrUwrrr7sR97invJS5oOCg92Jg36FqEVoFWFxPu77iIjUjG96lWvcnv37vWMPVR1f/u3f3sLeeu1r32te+Yzn+kZf6cbF4haIb3whS/09m0RiFidwE42qhzMTWheITjAevetm3XljWFiYr+75ZZp9zM/03CvehWfmFcuZKqxRXf5crvOe2lp2Q0OFkvJZcqpDPl0a7WhDG/outu9e6B0uBl7PtyH1Ds+PuwPdWN8S+7y5QF/KKOCnZ5edPX6Qun+GzIYiHDGAC1RRjJvST/sYNq9mwvSoMcxhwlzcNL2+fOGXgUqU71+zWeY4pAFdCN0kmMohHaBvIb3N9oPvoYhI8EzBijMTAVJwuPQByJyYWGxzXyBdA+dPLns7r+/7qWq4WHasksC2gAOeerhoiA1MPZPxstyYAmDGUlNHnv6y6lK0ixk4U/L/vLC720NEkQU/A80ZyBpYWqIke04+M+dox/J5c2YSdX3i3IwtvDiQFid5mXfvgl3+LCtA3m66WO4veRkF85nSNpiPFvkGbc+DHtGanH5SNVX3NWrQKsSO87+TjvoyXqq+GpildkfaD/C54BxcJGBqeKzkeVDSXwy/hfO6YdkXTNUwJCYj2PHKn7OFxautfb38vKo27r1UiZGfvxMxcS879wJSqM5WNpc0GfmFvS8uWYMdbkQzwcNk14r/ezP/qz7vd/7PS81h4hWz3ve81r/v+2229ztt9/uYTUp95SnPCW3PiAukbzzJGkYPvUUUa/LSfp84hOfWBju1M2+dbOuTmN429uq7s1vrrj3vte5H/7hK/4GLgnJPK+HMx1JyiT2KFMuLEO4UFZ8JowM5lWtjrdheBe1CbxmE2W2RcCOjowMeoaDF+7ExPCK+o9UefQojN7sgqdPt2sCpCYm9tWQphq+PUJZYHhbtw74v0imBw7A9M0jGmceJSbhXETqUQgYwCdW97x3mAoPawFzyGlJqt+BgYXmGNLjCEPd9u0zWEnau/vuSx59SgRzhJnCROgDFwOAWaSUYWx8JnNHGsnM2pBTnMKhYLDT0wP+O6RYWwPrXxhtoN/ABJQzG7LLB/9jAiopW3+YOpS7q+XyDtdl0qvb6Scx69JAyJ4v04E8uOOQKEhSsyG36dJreOxaewOsmfB7aHAQFTiM0ZwKQ9s07aJloB1i8pO1TuzZMGzGvn8/4WjUm40Zz16baBqJr1xB4kYD1Va0aaeveU2G9jfOkfptskeAHbU9yThZN0nqWbR/PxfSoeYFFS0YqTLpmE2YwZuuP103TBocbG4/cQIG3hfZk3/hF37BM+m/+Iu/8Ey4E4EjTVsw4U5MGgbdSeLDSa1MzO9GlEP6pExRuW622e1x5o0B/v6ud5kt89w5cIHnArtyNkOEeZcJiSpTLiyTZ+NGJT05edldubLFM7tOgCdxm1nnAgxVuahRBecx/k79lyctduqdO7kIpDNSwVzlnGQHZnLAI7lZ0g+rgwM8yU5ktlUIG6IyOYXdAGMblDLIzAH2pRgnc6Z45kqlfQxxKJA5qJkT0Z49U61QIA74+++/6CYmElUwv5FDXDjHSrhhEqAhdonkvQ4z5GXhUzbvYvCyE6ejDUxVzyVBYVWQmDH7RKFY7BFDikvC1fge+EmTVM20wG/vvrviLx+MF+ZHn8PMXvgdnDhhfaN8mPJSqGfExhuTtlCrcO3NBKDx19x99xmELnXRLr4HsuXDkOkrl0lMT2LQ8bYzAJSQQSYFtm0TXGgSVtXJjKU1GxlJ743w/+nQzGoqHDLv3j00NNC6FDAOO2p6n3TjumHSHDCPfexjvdPXs5/97JZqhvc//MM/nPu7t7/97e6tb32r+/M//3OfFKOISN4ALjfJL9ZCly5dcmN5QauboFwZ6mabvew/hwav2dkz3hySh+2dBTm41nJxmXR8ppHOpt27l93s7EBHZDKk5Li+mKQaFaNGclQ+6pX2/8YbcSizhAWoiXWxgBKnpvRhq8xFFy5Qd90zYSFthcyTumIGbWM072+YyLlzJB+xbEtcFGTzVqpCmJy8mSVpw5iQ3rgz6zIkZyTsxqH7yNjYlB/bffeZREq9CwuXUv1R7m3GhMQlRC3aRTjj90p/KOSzcC4Adsnz5kZ6g0lCzCtxzOor2gTF6GteKpUpP37mk7lkLFwM+ExxvAJqQX7BIU4x7sYsk1hf/jJP8s6X+p79YrjkJqmG+cthYNpzCisML0NcBnhxgWJuaBPfEEUe5IHYaHyMKyuyUc6Iy01Hsazfh2SXm+z9HYZm2sXCHMyoj3mTuaHo2dsoum6YNET4FbZfmO3jHvc49653vctnfpK3Nx7b+/btc29729v8+5/7uZ9zr3/9693v/M7vtDJQQahBeGETfOMb3+ie85zneGkcVesrX/lKb28mtGstBLOJ7eebqVwZ6mabm7n/K4EP7VZdoiIIUSSKrJSNWaE8JvFib4OJ2MkaMusQWrFzn+bc7OyoDwNCZQkJ/CRPKkr8LFFh1lsq+T17gOgcTGWWah+j8nUjsdQ880GtuncvWobEQQkGPztbbYUhQfxOTFPMj/IwK2V2gilRTvCaMFUuBMotTfy1SDHQMFu+Gxq62IQFNQQwCLW+YD7T/kf2houB7NnxBY06JSFTZuvWqRaiF2UVt05dqFZNU2G/lRuMpQBNEnZoTVgfmKTuuMyThVyZ+cJCooy5Mx/EAeu3N99MVi887/MBcWTmoF9I0Iw/yRaXOLKp//v3mxYlRhxTyB77evfuuo9OiMFnlGbyRNNRjDmjXVuT9D4SQyc+PtzfCfJg4mVOn4HXDW3kXFzYL7FEHT97G0WboAvl6bnPfa5P5QjjheE+6lGPch/+8IdbBzHOROHN55d/+Ze9V/h3fMd3pOohzvqnf/qnvfr8c5/7nPvN3/xNn9gBpzLiqN/85jeXslF2orI3sI0q1+s2N7L/edjem4VWgvVdhEUsqYcMQDDqUKouodFvlZP6W5m0QkjUMExHJGZFPw4cwHaH6hzgFNTn9l2en5xQz/gtQBIczqOjMA2Y9nwT+tMOVtSOIbwqc4CzWfy4KhkFtl6BtoQqXj5HUkY7oPAiYU9rbu2Qh4GbYxkHPVIyfcVvMW3fNUZo9mmrm/4qpaQI1fPx46ah4DIQJnWhfZgFFwjGrjHRf5gt/RGsq9lTE+QyrW1oDpG5BcYWwpDiDyGCOZclhTrRP/pNvXpcmUP+v22bZR9jrhgXzDyOzZaa3DJwzTV9GDTfDXfwIOksbb+Mj9tc6rLFOtKutDvS2FhsedJGGM4mUwLrEjuxlYHV3Wjqp6pcp1SVm5VWkqrygTaGkFFnYWr36gHNS1VZJs3lSvufJVGvlGDUHHSzs6oDdbSpnc1WaJmHOFTjdIMwahEH8z33ZDNrDnUYtNTKjIt6FG42MTHf8uaWGtw8hO3Qpn15hkMwO6V8zAqlQTLbt48wvqr34pXdWiklDTe8/WIDKhWqXZhvGBcMiVnCyGEejEGMAuI75geGoBSRIa65PMoVKkYd9IW+CQEPRsMFAZu1spgRMiRmqbazSJLzSpmziH7A5Lj8sBaKX9cljLzlzJ/WifVgvPwmhg5lfXD0unTpspucxMHLJgFPey6taEaUQAZiTLRD3Yxbr1AdL2Le8ZSfm7Mbi5g0bVKv7P4iXfiUljKL4lSVOoe+7uu2ugsXLqxrqsqNV7g/QOnw4cObulyv29wM/UeihpBSBHnJX0JX+FuE+rkSWNDVEIcALw4EXp3qCxGSyDiVd8FAooaQqC9cKOeNGreJVM0hhhSIUw9UrxOKQ5iTqQvpB4wyRvDau3exlUsYZkuZyOnWvye0CuZksb7mU8DhqlSIU1O1FmqZ+pElyTfftbzBsxh04jBW8fZsLg2y40odm2bQSeUwJqUK5eBPXgalKhSwLBQ7znHiglGfM2b1TWp4mA59gHET+sRnKiMbM+91AYDChC20k6UAnJ2ttyAup6drbudO4XCvbO8aKAnMr51Bz81VW8zbPPNtfbhkMVfMAxcMIYVlOUBaKJz9Jl63q00QGSX6YMzUE+579V+q91jzo0tVePHK00KE9WUx6F7S9SlKXQfUTTvnepTrdZubpf8Pfei4+8hHrmTms90sKq9Q/d2ePKJdgi4iMWriSfOcykIKGZ4cd264wRj18eNVz2QMBKTupqYG2qTndF1WmRg1kjUKKHmLS3pU7t5w7kP1ZQJHaaFBtZol7oDwJM5KexhiRGcdzLQn1akcttR+npOS+sHFxNTxaSYmdDDVE3v4Y69GfW2OeelLQTjvoXQdbmMcx5g7eY/nmTtCqRnat6/mmV+ofi9KPBMTdUuiD6JPPYMWaezh3MG8Y+1J+PuQQROHzOU5a+6vXjVVu/oSU4IPnh67YFRjS5ouRmpro5/7POoz6XUikH82c7let7lZ+s8h9fa3J9ChkB7STvlsu217L8uoFxcHW4doOozEyDyfh0vWueBmZ0cKGbXiVmOEJ5Om8d6uun377OiAKXWyOMSgETBrGLWcnJRNCrU5TFXIWiGjC5mumCLMmnkxjO15Xxd2UVTh/B5mqNjrMKtZ6MmrpQpTY6od9kEM/IH0y28pg8qZ+sVsJPWikUFtr5Akm4MERU2qYF22QmakS5e0AGIuoe2bv6jAGas0rFm25lCljde/vNHDuaC/4cW0zN5FQt62TeAwefHN1gbaEHlUh5SHmW7JX5Lfa181gnzjFmqVvefCeGvGHPdLDmQKf9O65/UpK357I6jPpFdJeJizqV/2spe5Zz3rWd5BbXR01MOLHjlypAUcgYSA/RqShzmINXxHmBdIanghA0NKfTjGQQcPHvTfAWVK+Bn2b2K3FaM9NDTUSixCbDFIXXi6E4ZAH5RNauvWrR68RZ7tONlhD+V76hXSGv3ErjI+Pu6TjOi3tIEdnr4RQ37PPff4NmCAlCdZCWPld9euXfP2GQgP+XvvvdfbbbC9Ml71nz4wX4JWpV5C3/iMdvgrRLGdO3f69hgfRH8py3vaxtmP+YaIb4eYN/2Wsppvys7OLngYwne+0ySyH/1RjGVCd2p49CwS2zM3UnfBaADkEBzp0BCpLe3//C4sa7mp6x5+FmKd6L+kevrB+ED/oj7qDstSTiAJqCZZisuX6Q/AKAPeu1bt0F8dtDCG5eWFJiSpxZDTDsT/+Zz2duy46s6cGfOqb3IiU3ZoaNDNz1tZ/s+hODe35E6cGGjGuxoSFpIn8JcjI8tu716+H/SOYWNj8ylI13BeeM946AOfMdY9ewbcyZOD3uvYQresPPHXR4/i0Yxq3hg23t0wN7aKoiIVNgOEJYfr7t21Zo5p8lfb/ABSYbmdQY0yVW8otXPQG4Qq62V/wZEmexN2XTkuiVETW430S5iTpGWFNon5U58yaCGhA31qfUGKTTzlmUtdBkKVt/poatwlPydjY6x52tlKDIz5EnyogahUvN1Ve5Z5Z/4XFw35LitXOnNBX0lUkuzv9j2r/X316nDTYQ/thdDRrD6cvNgvJFHBFEIMstn7kz1r60Vbi35eeI7OnaMde24slDBJxdpozolSdir3taWMRLuzmNrfeGSDhiaI2fBZsT1V8VJ7ctFlP7O2rB3PY/Is82wwD/zWMuWl93evwrP6jmPr5DgGQ4qhQzdDuZU4XXWzb92sq+wYsur67Ged+97vTZf7oR+63ELYwnlEiRdip6yVIo6t1HEsrz5AWcxbtZpb30032QG4kr7lOZXNzc27er3m1Y55xDwtL19yZ88mmRC2bm0fy/w8F6Q0GDoHZLpuO5GV05oLBwyQMzBEKJOaFWmIZQ8dhxQTrIN8dta8wuX1S72XL5s63dS8XDrm3exszTMWi9W23lA340vyLnN5s/nlMgCzbt4hWwTD4iJFefpPv6TIkZc3zAUkLPphUJ4m8YeJLeThDggP86Yc1kpKIual8kjLRepqNCeHDycQpMwh7eg9zJVXp72LnJH83iBhJfXKK57jEEaIBD01tehhcTs7ODbc2bPEIg94Kdpi421OQpt0o8momb/E1JLkL0/Shc67kRGw4ckQZ2k3s3C+Gw2Dti1yutR85Nmke+U4VlqSBt+6LH3oQx9abX/61Kd1JaSk225z7vOfTz57xzsG3Y//+HIrfV7okRva7jaKOCBOnKi20l0qk1ZIq0EozArTKlvf/Pyim5+/6g4cGPcSBR7gSChZjLq4r3iPJ6kzQ9UuDBrnJGXHkrpaDlwiqYXxKEbSqVaN2fAbJF9CuahfKk6Yyejootu7V9qKpC7BZ8qT2HCo7S+fszdiHO1QmgxzJmu8fA8DgsHzf34Lw4exEYqlywS43ng1G7KaVSonNS5OoYoWKY/5gal3sqdKla9LD/0Is5zB+CSBx6R4d2OUALvUW+FMqNy1DlwmmFPWirE3GizyUCaYj0je2wcPUtZwvmHQysIWMuqRZipL5kz7g+/5TTjXgoDtlNGOxDSjo9ePErl0T7uZAvHBQGURyzaqXK/b3Cz955b/utc59+Y3J4wa9d7HPz7iXvKSK22379CpbN++YtsvdXWTVN+2beZQMzpqzDpk1EjjZZxeTI2ZPrimp5fdzIwxaghmPTyM2r1zXQCWgMgkRRySkBg1JGadNR95Tj8LC5YG08ZrEi19NZxnO6hRGyP5Zjk/8TKPbvteTNU8omuteFsY1fHj8+7y5dEmEzH1dzJmw9YW4+UvkrX6DoMVspeYlOJxDRs87YxlmaOSdJI4vIWCl+zfcgTj4gASmdSpksTDeVPfLG1pftiVYtxRKMm7mYsCUjAMWH23/Z2sVYgPjx+BtB/sNcHDMp8Kg9N6KX4bzOtOpH2yYwe3I3P3t3h4Y9DMjyBWIYXTxfsyHUo11MQ4X/aXrjwpuewz2u1ned2Z9K//+q+vb08eYIQtGTvtZi1XhrrZ5mbq/803O/fOd9rBx+1/bGzZ3XrrgDtyZNwdPpwNeiJHp6Ln1qS47tmqVF+CNFZvY9QwmTJ5dLH1oTpt1xJgnzb1N4enQDk6IZvV6+3hOiEAiqRq63/6lIxR08RwzNkKe7plCxOWNYc1f+XEFkrQsVe+oEi1XhziUsXyuejgQWyPCz4xgy4sMNfTpxmX4ViLDO/a7NrUw/Kyb8JwHn6v8CQYfsjk+B1Mxy5Ilp9ZYxYmOfOh3xiTS/ZRkVaj0/cwcOaCiw7jC1XHXHyU5MOcJit+XLKP791r+y5swy5l2I3TewNpPmSIWeseM2gk6FCDkfw2kbIbTZQ0KOtYSIdgWb9CU0qWB3vZZ7Tbz/Jq6fqR+a8zwk6B49JmLVeGutnmZuu/8L2hu+8+6rZuvdV97nPOvec9dhKE3t8iYDKLAPblJLUS6mS3U31ppLGEUeO4hdOLZefp3EaIuxwzOepeXFx2y8s0bAkZhKgaHsbmgd1w587lI7iFUjWpFuNwmzAblhya+EyAFXYoV73q06jedGZLcMrjMUia5DDm0rG0NNgCCuF72pIUZvG8XFhwmEu8fGHA+/cb0plQ0CwzmGGiYwbhwoA9NlTF0nfZx0nwknWRk+d3mF6T/qIypm9iQIp1trSito/ytCTmEJXMQXrfWFuMlXkMMdMVf67LQ5JEAo1MtbVHTHNjaxX2ISuTFnUxDl2WbN8O5TJo9kjsDZUkZUkuEsPDyXMRpw/Vemu8ONLNzdULQyvLPqOUI/PWdcmk8bDt5PDSTUCK65XKZFXayHK9bvN66H9o0YFZx4x6PeIo88KqsuJX03Y2Y9B45eLhGqdujEmST178KQzN+mAq7O3bzQkKj2RsgSGsJEy8yN80yaqVHMyhvZrDl7HQphyiYCgEJcgpCJWyoX9xIWGuqNMOTTktZUmTlQqXjUFvv8wi1jlMH8mcCOqSz4SlTR8sJnnJzc8P+O9wWgthKf1sNKfCcmtnM2nmjUsA3ueEm4WXB9ZboX9ZEJdS44d7hDblzMZcmI098Z2gXl2OshDelBGLcVj6Ri5TDX8BCUl9NISwJM1nSApZKwphDBl0FukiyppzaZFdulpNLq+yVwsgRc9kDGASj6FTv4poI4FMVs2kX/7yl6feE1bxD//wDx5H+yd+4ie61bfrmsrkTt7Icr1u83rof+xUFjJqy6lbfKteCeY7IU2dbOAwsbi+0BFnYsIOj5mZIbe01Bn325JbZF9aFIca2mRxrqnVBlqSaxhyvhInNbPlJpJ1yKglHcEYkGwVNywGRtgVkmaC/azx1Vv9EbMOL1AW5pbfJ+ZPal4IiS3E0FZ98qw+cCCpj35kjT+UYGMSIIxJyAagojHRNhoL5iGUhMN1j/HajUEbo2J+5AQmhqqLRAxbGiuVlper/sLC75kT2frpC/MrsBXeMwYuLADixNoAMc5w/KFHf2fmjI3bnMYUG898MA7e15qJVyDeM288p7Gt2RDf8plpGtO83DNKOZ6rjaZVMWlig7Pove99r/vUpz611j49IIgYYeKiN2u5MtTNNq+H/mc5lcGoYdw4lRGHTBxlJyI2mXjKMpSVyjC+/Q8MFNe3ffucO3t2pGOCDnNealcVp5Gakvd2ABqjVnpIeYCT7GJs7Ab/F0ALmEWelgG7b602nOtYpjSLgGRwYIcAIjBNmJCkeHMKI0Y1GePkpCGhCS7T2pz3eZFjD2yRLgJqi+nN8tTWOqBCFbBFJxOl+pkefzpdJ17so6OGdR4z5vTv0uFQ0qJIWreQs4bP6BSnDtUFRn1FC5HUW02NW8AplDlyxGywljI0bVdXUpI9e0CZszpkElDoHKSxLCzM+5hqUb70POYZL+PR5UzJOULIzkZz78p5LEynaWao/P0d9mslz6jhDGy8RbirPXjmM5/pXvOa1zwonMyKwExOnDjhwUCKwEwA7SDergjMhPrLgJnMzs66Q4cOdQXMhH6WATNhrACJdAIzYRxlwEyYK0BHugFmQtsxmAn9h4hxB/CB+ab/fMfvGo1r7vWv3+4uXgTIZc574h44wMHUcJ/8pIGFdAIzkb2rDJgJdaH6lOrYDhhzULK6KimAkjzgEz4DdezkyYoHPqnVGm1gJnj4wkw55ATuIIJpxHmiFTYEozYbNNmNqKPqjh8HPEPZscxxjbjjkRFU5DYv5nxm+5a2BgcHfOpCvp+Zqbnz5znosaUntlWBgYQMVHmj6R8S1smTAEvwvc3n2FjFS5nHjy97j2yra8hNTS25XbsG3KlTBoYSMhup7I1oM+tgb7gtWxiDxUjzlwsDUyqAFV8qkDSFSQ3T4dJRrRI6h5RIiJn118BDBjye9t69BuIBkxGADXuJ9WTeGAvrrHVk/Ws1yle89sGYUsL82CMCWoFRM0+YK9KZnywMLYmPZpxcwqpeqme/M4/yVje1O/uQ7F3GpInzjkF1bB/Rf8CI8JwHAIiMVstuYWGxmWIy2bO2xjUvmaMNEdNnDum7pOqBAQPS0VzTFznCaQ+wL0jRChBR1v5mj1SrmCzs2eTZJdRN8w2wCpenrGfZuYHW+wcEmMnb3/5290u/9Ev+cH6wg5nAFMlRXUS9LrcSMJNu9q2bdZUdw3rM7dGjnWFLOWCLQjcEZjIwMNmWyjAklACDg8X1hW12yqSF5AUCVCi9w7BgPhz6IeORQMKhj3pROaotXhcvaJgbEqbNfwwsUWY+lF2LM9CSJlh4T6ialCIksUNyyIMElR9ic889MAGznYrJK05YzlIGkEK5Aa8GxqkLUhw220EqcLVB+0r8QX+w9QoNCwaFDVj18Bvq4L4bnuWhtNcpY1WneYPhcsRycTRmkYwNoBHaljOgZSpr+MuKGLWcslBfy3QCc7vvPjNHCKglRPoSY7/1Vi6plRRcLCQHMzHvvXvnO+5b1oW9zwUi5kDMC/HuskVXPEO3sbL/Qq/0HTvQTBD3nGS3ol9yjMvy72Buq1UL1+q0jy5ftn2UZ5PedGAmIT360Y9OqRWYQA4wpBKYdJ9Mgt3M5Xrd5vXef5WbnjZbW15+ajGtMhSHIoUkLOFKpbi+sM1OuamzAB44yDgQYdT8XxCUkLC0dXgRfwpzwNN5504ySCVnQBawhPUt39tOKFOHD+OhbufI2bNJzDd1cVALYYo2kCI7xQRDN96IY5aN/Z57Ei9iXT6YB4uVJuFIgtcNo6JtJLnQaUlMlu9hKswJjlS6UCivNQw6hLOEQYY40UaVUrb9rHkLgUV0WWAdUHnLNk+fURejVArjrxkvJE2FhXkl/UDRZg5kSXuSpMX8dOlBAo/3EZKpLgGsa73e2cuSvZKXoYzLj7KENZprJm2LgF3EoLlEMHYumZpnyhhqWzbzJYZbWOaiLGbeiUH3klbFpJ/97Gen3iP2o5Z80pOe5B7+8Id3q2/XNaFmLQODuVHlylA327ze+x+XI+1lFqNeXFwo7ZiCKg984zzvbjv8iuuL29TB0rRwtJg1alPKhV6uSlmIihNhQDZgGE+cAEJMRckLdu6sepUmTCL8PiRUnUA15s8BByr5g1ErD7SwtwEkwU5KH2CaHMTGNIrD4Gycdpm66ab0RYXkHmJ24HrDcBgrSjHmi3aYH5i0QqtQq4rpyyFLIUc2Dya5hiQ1uLJbJbbVpK5O0QL0MdamKpMYBFPEshQic0kLIOQuLj5Yd5DwxdhkR2acMDKbr3QSlXDN6T+MUKAtYVw484SPgTzRQ7tz0bp3uqDQdx4zLmfz88m8EXbIHmXMMGj6QM7oOOWnNDNZmh0LRTRcAS4CoW2d9UdKTzKhbTyDXhGTfsUrXuHe/OY3e5vlN3/zN7snPOEJmwaRpU992gjKY9QroU7whWulTlJ1ljSfZKQy1WKcGSidRpLEDQ03Pj7iD0wY9Wr7bJItdnsL+zpzBtuq2Z6xy/M9ByqHqNkoO8eZ54FopBkdTM5sxhC/pw2YAAe3kLPiVJFqLxxrVuiPDn4uAU03ghQpHjp0zgpp376lNszz9phi7MfYUZM2xUQVe64LThgnrfZDoBJDfDP1fpiHO/QPQNuArV1AI50cwooojHuOGba817n8DA6mpX9U5LrITUwsewc3UQxykxV2Zetc8fMeSvLyUtdvlM/9umLS73nPe9yrXvWqFpPGuQgnqT5lE85Rm7lcr9u83vufVy5m1FkADkXUCd+4TH2dyoSMOguvOA7vgcSgwzjUmKHLlWV+3rI14aQlJ6OQyKpVhrBzQkhAeGwb84Kh4lRlKShNHZmun0O13T462DHHdTh266+pUqW+hjkQzpMc+ukLjtpiO3CRQL2KBCYNBNK2VLWopdPhbdWWxAuzC6XjkIpgNSGcufJ8l8Rc6Vee/VhrSx0CKFG+akhjkJMZlzFzzCpmzkXrrthvZSALGTWXJdaOV60FVtLwYCUwUqRnKFTNxxoA+036/wKnkUd7CDPKWJHQuTCLDCv/OgIzwTP53e9+t3va057mH9A77rjDexln0Td+4ze6BzvhEVgmN/JGlStD3Wzzeu9/p3IhozYVWfce7DL1FZXZudMgRAGiQDKJJfVYmsfLGmYYS8YhQ7fsUHhom9c4GOBnz6YxwK1v5fxSKUf79IU+oH421TBOQFWfKpKDFOhNkTCkY5s+zktIyFnqzpDw+B0dNZCSkCyxBrm8xexMRS0J29S8xsh5j5SaeMNbmxyNOLjxPY5QXGBsbuuuVsNTv/N8dILVFBXVIa3Mvn2khhzI1dTA4FhDpGjNKf3H9MFeUIIOLnzDw0ul0LqK1l2oc5g5lIUs9INQdjMRHuX0A42LLpRiykmO8LQpJAQ60W8sD7ZdOrSXQ0YdakW6/SyvO5P++Z//efeDP/iD7m1ve5u/OX/bt31bZjkDYF9FSp4HGJ0/f74VErQZy5WhbrZ5vfe/qByMGvqbvzm7YljQTlQGwrBTmeSAwpkH9bQdUmNjJLPIluaxg5PrN4v4zf795OMlNCVRFaOGJM80B9+ZM0lmrRDeshNRjvSBSFE4sqXDpuqeaSNdX7qEN7MyHaU91VeKMlWvk9vasmCF9cCckUBxRgrxxWEE9ANpnzJ4UnOwG0oZttkkZll44XjH8ztZBm1u89XYojxYzTKXjFidzTgpl0dcILStuXDs2pVkI7PUlDB5ZQcrv55F5YaHyeO8mMJPj5kzZBe/hr9sQLpQMtfMr6T78F6g8QsON8aJ1/7iMiKTgzzkpepeDcTvetDgSpzFeBE6grv5F7/4xb66u099imjPnrPu7NkNzGsZUHxAQcL9BhRkcLBYossj4nyRWDnEFOmRIKUtt0K2Go1BL6WVIftN2sMcku2QA3lyctmDaRC+de1awx+y585lO5IVyQocyNIiKKOVHKUEWyoVKQc4nuA42CkMjXE1YQ1aADBSP1MXF6IilXsWbrv9fqhlV87zUci7ZGThbXcixkt4HowalTZjweQg1DfmGFs99WXZ19dC9fqCm5ggLWf7GkorA+3cCRpMre1CiZd9J8dLPg+/C4FsYNSKopU0Lhs4WgM5rW00rfiaADDFxz72MQ8qsRluGZuV+rCg61dXWdqIuaXMrbdW1uxQJirjKZ5XJkY0Iw45ZNT8nZiorrhN6lU8bCcJNsxXDcU5q0PCEzjBD28nGLU5Ell/sYkqXhiJVgTD1oFflkkpBCmL0fEXsBckTGW+CsOsQoIxKwxIzk9x4gvmNrSPKq5X3uuKxcY8oXbUl5jhh5eMkNHLGztJ19i+nkJ+Uz+RHkOVM0yL9YA5y0asdSpDZcvlkfbMvn26aeVrdpLxm4kmTjQSkhzQ0ikuEyAVgc5Y3cVjAMBqvWlVXPbJT35ypuMYAB989mBQdxchjoHU9YhHPKIQcezOO+/0vylCHGNONa+dEMdo63GPe1xXEMdoj7aKEMfox+23394RcezSpUutw7MT4hi/v+2227qCOEZ7zHMR4hj9Zz3D+d6/f38rUQxzwGef//zn/VrxHapv5pGLKusqNDXWAEa9ffuMO3lye0fEMeYmD0VMZamPNooQx5hjoXtR1lDPDE1LSGZCVYJZX71KZqdqC7AB0BRISGVCUxICltVLOXDC09wpREzj/7Q7MLDkf7t16wX/mzNnRv3By/e1mn0H8hOJQrQvSIpBu7HHb+i17ByxRMNubg7kpwG3axdfIPUAEGMZtxL0MtTy1IdHm8019dt4zOZIONnx45Yv2ezOtk5cALBr79mz5JaW5l21Oh4gxMV9as2E9bCpimWpsLmCeIVkRj+wDZ86NeDrl0SOxI39F4CYixcNsAMb/IULtl5ciNAwYGZgXOwbm/JGa83JgjY6OuwvXqB4XbumzoEEh8bBnPBkt6Xe3bsX3IkT7Clz3JPzmJyokKpBIGOf4I9hWODL/jwhxMr2B/s7QRFjz1KWOeb/zLnKtu9vO9coTzlepASF9u5lffHwt7I215XU/g4R9VCd85wDUoKpRvNSqXC7SCOQgb4nXHNzOAMQxbK70bVt2641E3bUWyhjeYhjz3/+f3CbEnGMznHox0waeEUOKA7rBzviGId2mdjbXpdbCeJYN/vWzbrKjmEj1iAukyVRC3EMxlqUqSvGcF5JGR7DEPyPQ0/StNC8OJxilLK4vlglC33lK0Ap2gEZj0H1ZtWFZC0KJeu5uXlXr9d8f8XgQiceQ7vioEcHWWs5jIXJJrijccc1OMlqK36X34KklZZEgTblkjXh7r03fw0YS7U65wYHRzxYCdKlYo5hrLJJW2iYOUAxHRyNzIFU5XyOhHzsGGpkQ0wLE1XAqPFJFJLWgQMWXhWOHwc0eaHTXqMx78bGmNskTp0+6vglq5UfaZOJ33wzCHFJm9JEtGakmStalxDGHudwZp3KSMllyoXPQZjVLJGeE1rtcxDPSThOc3hMQE/AaA/BS4ra5By6885Pu5e85Gs2D+IY3t0QD+X73/9+P7kibkR//dd/3QczaRIS6WYu1+s2r/f+ly0Xl1lrLHUZfOC8MjGiWV6cbBxPHdaXl0pz375Bd+KESeEhxbHVcd90AMdqcJgqNkHlE1YGphDTmbrJEx0yaDl0wZBR4uDwRb2XLiXqWzx0Ue+qX2EmriKln8GKVlM5sGW7RhFEe3h6Y6+mH3yuzwQmA9F/c9oziTVeMiUTkSo2Di/iAkLboZ20VhvyNlnxESRvGI98AJBwxZTQqMSIcFljDzUEWfHfSjRSRGXLLS+PNqXnSiZzXutzkBViCDEPIcJYVlx0mTYXFjaZuvud73xna9Hf9773paDrUAWg9uPzPpk6dTOX63Wb13v/y5bLKrMWRl3G7yOvTHxAiaFmxUCnUcoGPMPJcjyDjEFW3M6dAykwjqx6gTUVqlloL2xn1gPe/qn+ynlMDDpBYDNVcZakTTswIhKBhGTqWHMyoi6czqB6vfMBK0Y5N4eKP3EmwzYu5DGkYXlyK1YXaTjOd02fmVvGaDjl7e1JCrd5Sz4XTCl9CH+HrZyLCapeaMeOegs7PKTEnJD+PLbZxxeuLJu+IGiVfjP2yk4+N8e3LG/t0GMbIqyqSKM0uMrnQDHqyoluDn6VzH7FEKBl2hwevra5mLTseYCZfOhDH8qNk+4TKpb7SqlQN6pcGepmm9d7/8uW62b/QyjP1ZYJHWsWF4nr/f/bOxNwuYpq39c5J/MAJBASCCGCQAhDIMmVQBgf8JBBQeGqCMgggl5AZhGFKAYUGR0QB3wC6gW5gqhMFwFBUECGhJAEAwSQQYEEQoAkJCc55/T7frWzuqv32VN319699+n6f18nfbpXV62qVXuvXVVr/cs7gCEsqIobFU7SOyrQ26sNAk5q1Cj2LD0iiqAoZP8xjUEcyeKs2UeVmfW4cd6sLygth/1wTqXyz/IkoMu7EVcfZykQJ0XQmSy1Llmyrlp/fe/0LTP4TGbozJCZhQvBC7NpZsTCIV1hOWOvsr/uN7+DFtCXsipq0oYKTM5075Qm7wNvKZZYDn8sgGdTefgI48KW/vDb3GMtq/zO7Dc+DxpSUNCa2w0C5CF34QGEPpCy/DNWM2Ib54wNlKqsyNq8DjpDVoFGj64cPSqz6CCO7iR1zpt3JxFKKk3UlalNdLdz0A4OySF51M2ApKwMHEi+bHzU88iR3poqB957rEu94dGCct40EeKqV7lRs3A+98/qSLERhw2zFM6YGSQ3+eqZeW8HbSJqQhbU7vHjuzV3OO2Q5WH2cnHc/C852aI7zkkcM22WtnOkZ/VBGtXwM2LJ6VwyM5b8df7H2fFwQP3evnKpnLvsrQp4Ly+33FtV8M7k7r2HLAhyurLS4j+wxFy58KNU6gghkPH2fv1M0XyOvXHOZsR21NK2DXRHjD9OgpPx1yj9529+8xOVy+hu9p+vv/569ec//1lHxpph6+D+++9XrY6kOeTNksu6zqLrn1QuSqaeZe9GlrsbkWNmwQ1t0aJ27ahJCTIhZxmXSu0x6V9tiYhGRLewPWsBTi1s1kh53h52b53CZobmaoM3exeWM8+pfvAB7a+c883NnSVmCUYTELkszF3+GbLk4AodJY6dCaScMMXser31vLaK0x49WjIhvFO8/EeaeqdbeWlaXhR579lspe3toU63uu1eClPYEjXo7g4mT0Ef2iS7PWIDIsQBepI/Xy/61Xgd+NMPTRD57kXWe0YKO+kqSZ1ZBEnX5aRJO8JJH3TQQWq77baL3U9oRUjYfl7lsq6z6PonlYuTwVE/9hhLfMkgyRdBpBeV03qSU2/WIucFWnnOWp49xFnjEIguDruFVGbKwadWCU92hYrUY0CTNoU5a9JlNtiA4xl7H5CAg/AORyD1qi3RzFDAdzKjxNnQdq+Onl6HMOCIlyzxgtGqObk9PanLn0KmJfTJYh5piEd7Kv3tyXsz+so4EifBd/4HE9mT53OJEud7WNJYkhcaUi9vmlSi9ti2J2EJCwu0k7agk+eYK3Ynza1xSv5STTLRAYHeSg32iDqKMuh6qR6zHerznz9T5dJJ33TTTeq3v/2tOvDAA+1r1EdAHm+SQKNmySWBzTqLrn9SuSQyU6cOUg8+mMxRe/nx/UJZlTyu62T0hbXKVQeeeQQozKq5uXV0dOsc7nXWCV5fjVtSx3GZqTHcD/37l8BcFvUYzCoOG2dnHquJcyXNaaONPIrRKKrJKCDrfwgAkj9MKhQ3d3PZlz1YIuPRH0jwm0DappSX1hMWeCUwHaZpBzn0Qhw0fbCWFqCsIw7aXMJetYo85fi0qSRO2nsQ6f3Q5fFqe+c7Uz/5zebkrdFT3bq6YLHrl1gmqj729lEt7qxo//Xi3+PGDvPmHady6aSJ5LYZHNMXyUzIGYdcI47MBDkQR2bC0owQZkSRmSxatEjnqtsgM0FP6ogjM6ENY8aMiSQzoX9E/ygyE/qKttsgM6FuZOPITNDf399BZCZiqygyE9rP59LfY8eO1f0HmQv1oT86vPfePDVo0N6xZCa8XbSImaGqImXgvOXXXy/p4BshjvCTmZhkDx7JiFdPnKwQTADavdFG7H+yj8dNmIhiOLm9JWBkg8geuFnCF+3t+ckszvuNdxSiR9LR+2AMj7SDPOA1a6rL3WCDTv3/0qVEX3kpVt5Z1uS8cja3d/Pt7oYcw6MmrZBreGlBFXINj1iF8mSJ25NdrSOTSZUKAvvG9AP7mp7+FadFW//9bw72wC4VshIvhQubso/vEbkQgMXDxcCB9LfHQW7KdnWtUatXt+kxgcPjsI5Ro7A/fe/NpDs7e3QutZwk5ZHJeDzbxBPwN/Vhdz95DEQi5DJ7Nu/Qy9MyPjyCkm5fv3hkJkKaIisM6CQzWORZ1mclQw6noC5s097Og1N71fgWkiYhMzHHIado0a7u8pS4FDlm+d4c3/ThoEFCUlO5bjwHXVIjR67R/WqS9SDXv/8AtXq11y9CIOWNb/biyVTwtj3k+eOJJ4yoyDyRmVxxxRX6BvajH/2o5Za6k5KZYFwzRS0vcrWQmdjUzWZZSdvQDBvUov/gwf8Re/0wM/LvR5qAcILl1ySXoXlzaUSOGy1MWnIzHTo0uL1h0bUsnfMMFnbnMclQwuCd1lSpF4dXOXkrvp1RhDJEZ6NfUJoXTog8aPaAw4D+/mCsSr3VukkEfFgOb9D3EHDQr6VSd+BJWf76bdldZNh+MPuG5WwvatrbDzdXEPzR/PWS+tQD//iTVSD2+eW0tahz3M3+8BMDed+X1HHHPavee2+r/JCZCLjBEOH9v//7v2rbbbfVT3smSM9qdTDLY3aWV7kksFln0fVPKleL/ix7z54dzeLvUXCG38C4wTArY0YQB5tyHR2L1r4bW46QFcYyATdCZvoEG5k3QiEiMSEzGWlTGJg9oRtlmYFI5t71oEHezLFeULawUJmEKubesh9J9Tf7NvyoTT7n1Kc2nVtNPxLpLkv7gM+CZvtBAXKN2t1Mm2KGzEpHmIPjAcZL9ytpqlH5PEg+AVdIL7vXImOmH3orC56j7u5erTo7B0RuIQXZqlmoy0mzhBp2VKWDB1mKyatc1nUWXf+kcrXqHxfxLXST4d/XFhAWN3sQuaQYNcrjxBbGMr+zLpW84xT9Okch6vsw3cxAM47R5FSnuEM9wmDmD3t+tzp/OHjxpmKnKP29penKTC/8QBGWd9t0SpV/f5zPWE5/+236vOLswgLkzDqjIHKmU/b3r7cNwnJy8GqHfN7eXskxDltR2WijymEocSglGJNBMujjnc1d2YMmKj4qPVDOITfLa3RPPXMnfd1119nXpI8h6ekozZLLus6i659Urh79oxw15xSb1J7VdXlOlr0+EL98178Xj3HQcmQcHSI3Vpb32L8V51VhLKvQi4aV5acrDWpTGOJ0w5mwx8jqnjm7rsVh+wO1xPmKE8RJ904Dq8hE8V+Y+kfPzrzyggLYCJDzDtzwZtiALY+wHPgk9Jbe4Sf9dfQ5CMtjTlKWKReXL7/hhtFnZjdCCyoPjf4AMQ45SXIOuVle1JhNG3WfNcm+2l/+8hcddHTEEUfoQCICa7h4TU7vVoXLk06vrKLmSdfjqGEIC+IeNik429v7RcxWJAKc79ur2L+CZg8gLlYBJ02AobfEW/1dNb0oTqtfIj5lYfSKS5WqJV/WHxme5MjMpPnD1U7c0z9Jqhd7+IIoOS/YqbeDlskdM2hsKgdyUDf6xtUp8M+W6StzyT4MSVK0vOVlj0oVfaMcIlH4aeRJrwhx0AAe8yjIw5NZXhgH+Ec+kvCw9KwZx4im5ThBoppPPvnkckTyJZdcos4++2yVJq6++modTUvE8rRp0/RRj1G4+eab9aEfyKPzXXfdVfU9A/Mb3/iGjrRmFrTvvvuqhQsXNqynRBznVS7rOouuf1I5m/pXqAm9GzABQeb/MmMjcCeO3SvJGdBmnY2CmyOvFStI1ep9o/S3iRxhs022txyE5WrMmG6d30wSQnf3UO2A4/KHOXnLz3xWr/6sPphlBAWYeQdqyIzWmzFz+IY5UeS9mZKPMzcP3wiqU1i//Mxf8jDT6HYO9bNSQ4DVa6+V9P9B8Qcmku71rq5BtygHDdrbo1OvxM7+OoOuw+23vy6fTpq0I1KQSKExl/fYp4aFLC38z//8jzrzzDPVN7/5TTV79my1ww47qI9+9KPlVCQ/HnnkEfXZz35WHX/88eqpp55Sn/jEJ/Rr/vz5ZZlLL71Un+7FwSCPPfaYniFQJmk7Dg55oQ7101BW59SGc2yLA05y2lNS8GBLChopd3F7hUIxyo3T76zNNnGeddr7fuJEOEOb13vvrVnrsCuOqxaIE2fJ2a8//YnjJFKc/4P6N4iW04s+Rsc2HWHOsjY6UwayTHS9PPXee7BBddCuzs5++n/TKdum5Qxb1pajRMOGiW2br4hx0AByl7BdqbjtFnPMDhjQra699kqVy+Xuv/71r9oB+qPtmOES3ZoWrrzySnXCCSeo447zEshxrHfeeae69tpr1bnnnttL/gc/+IHaf//91Ve+8hX994UXXqjuvfdenTrGbxnk3//+99X555+vVwXAr371K53H+4c//EEdfvjhobqQOhAUbcgSCbN2ydn1COSDwZ6HyMXJ+rcQyOf107GCIUOG6O/4P0iWbQqeECXHFx3CZPncr5Oph8gGyflleZ+kffQHD0hRzF1mv0fJopcXNNIeKSv687/I0kf+J2mznWGyQX3BeJClM+ToN9MGJqZMqUR8S05okJ0BS5Mel3Ul9SgIQj9prmT6ZSGpkMAhM4UMHfzOAH2EDtHs3yBZvttwQ6/PFy/uV76JDhlSyV0VObOd0jazziC5INkgGfrojTe8fGQTK1d26d8SsbxoUUdVcJbnEDlCkzzzig5BMA9sCEqb8pbyyf9ur+ozZmdjx3opTczqGQ5sFZCPy760UIxSFrQCnMAFZYAX3S39VwlYq37YIHe8q5wW6Fff3HMVncLaKLKM1yCZzk76tveqBJcF++WMQf9K+uDBjG/GxoDQ8RO23xwku2YNZ5vDc84DiNm2yolbXlklNWZMqTweRC/ZboHu1ouk790fph51ZC9n56TNhHQTEEewN50GuKnNmjVLfe1rX6vqMJanH3300cDf8DkzbxPMknHAAFIJSCcoQ0AONMvo/DbKSf/kJz/RN18/uEmfccYZOngFh0BOeRiIfmSWL44jSpaL46tf/Wr578svvzx0kHBBmg8tPNz47SV9xg3q61//evlzHlqEFCAI5513Xvk9KxBCehEn+8tf/jJydUJkqZuHLsg/wnDKKafoMUi/XXPNNWVSlCCceOKJmhAFUK5szQThc5/7XDl9CtpbIXUJwn/+53+qCRMm6Pc33HBDmXwlCB/72Mf0qg+AqU+IZoLGLWOR8Tdr1qrybDUMXGs83HgpLh75RBD4nBvT4MED15JxVIHNMQIAAGb1SURBVMt6hzusVO++6z1csDpGBgfgQSLKFvAGyGE7kOnwdxgGDhykRo4coZ0hM1n0WLnSOOvS9yA2cuT6+n1n56pIGxMktv76G5TvE++807vMwYPXV++/75FrmA9GjCFUXr6cPqr87t13R5dlIRAZNqxbEwWFgXzl0aM31I4WB/3++9XXEJcUl/n6669Uq1d/oCcCgrfeqoyzIUM8PQUQjQhHN7NpqBkGDvQO3li0yDtgY8yYDu3MITfhIYC0N8C9DSKcYLRpAiKB17Z4We45b721uNe9Z+DADfTpZEBScnF+PT1sMUDY4/121aoe9d57PdpxjxzJGKCfBujvFi9etNYpQoAyVLW1DdQPV4xdKGjXW69iN+/MiO4q+0pqIA859K841LfffqvXwzm2XXfdoWrECCLVSenz0sWWLn0r8v4H6ZRMEoRgKZdOer/99tM3c26Q0mAuZpah06IKhQ2Ki9oc3IC/n3322cDfMEiD5IUNymThCpOpFRiYPPIk9JAMHFYlklBS0nbKTfIUx0A3ZaNmAJRjygY9fJkwZeN4qk3ZqIFvytJvUY4fPPHEE2X7xBHcsy3CFoY4kSjMmzev7GyjHBNYsGBB2eELy1oYnn/++XJ5Uc5GHhylr9asGR8pK7NxzmweNMg7JtEPmt7T06mpITn/efFiWMBKVd+PGtWtPvig8mPql9WAuL1AxkBFNtrGMF4hS53UsWLFuuWbq99ZMw6l3N7jrPomzkPKypWrVU+P92AchMpqAysGMtPCafPSEuUZKWUPHgxzGsd6ltTSpf3UsmUlNXCgPDT0fgggf9jTd0igHQBOdtSogapUWhG6quQPakJX75nCYxvDP6y3HodutKkNN+ww9rA/UB988K4e65Wio2d6UStbvtaVZbFL0K3HpAtFhj7dYIN2nSa2eDF9563eQC37oQ/BhLZcX484PCnbu6cRgDdCvfVWPyOGok0NGTJYLy+XSquqZtGmczZB2bK6EnT/8x4YqHf52vuDt2UUd//jfiPXRNR9telOmhkfM9JtttlGz46I7ibYiqXK3/zmN6oVQJvN4zoZbLxkuZvZEvScO+64Y2gZPOkxW0MORMniPOhvwZQpUwIHCYFLUE+aS9imLIMQJ/eRj3xEz7j9y92mLGWNh6g4ZAlbZIPk/LI8gUJfGgaRpd94CIxb7v773/+upk+frnbaaadQWfSaOHFi+YmaOIogWdHfXMKmXL+DMtsZJhvUF+ZyN7Jc6KYNwmQp84knPJrSIIiD4aFm3LjeRwjK8l3//ji0dk13uOmmpNm0a2pLqibFi3YMG1aJSqdeWSXyyDxY/uPG6h3x583alqnOzhX6GhB9RTaMYMJclkZ23XU9WW7I/fptuJZCMmy5e51yeW1t0NxWR1Z7bR2ohg7FIQzs1WednVBJVqKmveXlyvc9PR1qxIgxuj8lalvqHzSoR228sbcUDd58szIDFRBcRp+xl0z/hKUMecuxzPLMA0Aq19+qVSyvm+Qonlzl0ApWRLw2rLceM2moLKl/gBo6dFRVvVJuGBGIXzaKMERkGWtBq6XoQdS89Ns660CH7P0tWy28WMlZvLhNbbSRtwq0atWK8rWPs8QmUKtyOZk7QZ2dzODb1SabsHJU0rLeloCXq6/U6Cr9zbZJJoLA384w2aD+MGXjzppuqpOGu/jpp5/WB23MnTtXPwmxbHvkkUdazXM1wQMANzT/khN/m8s2Jvg8Sl7+5zOiu02ZKIcpnMxRM2AcLzcvWTYMAxeH3OSiZBnI5jJdGA2dX84vi5PiZsKyflBagykbVFaQbJwcwFkmSaOQfosCbRD+3qAthzD9w1IDg/Tnb/PmGSbnlw2Sefddj9qTCTdLdmPHDtTyYTYwy9111wGxrGTcLLgxslIfnCfdYcjJzU+cRO99RPpUHJT3ENDR62CBAQMGq1GjBmg7mLJSV1BZJkxZ9ge5MXIspuyI+NnL5GGmf//B6l//8nQxi62kkrUF1mkSlJiUlojxOfW+/75HHuIvG2Yvr+wO3Z9jx1Y/iJDetWbNwLXleqdcAY9XvBqwcMlzWeWQDa9cdGQ4c3Z0ZUZdcS5tbd06yltSs157DZIVrzBmqP58d+mzMBv4+zeJnIy13r+vTlEaOLBNp4d5D4KejPQ7YxSHzgPNqFGDqx7KhODFX0Vbm7d/TBnwx3uzbJbhsUV0O/2fRbXT/DyuP5LmjDeKumph9swN5KijjtLR0T/+8Y/VF77whdQcNOCJZurUqVXR48zi+HuXXXYJ/A2f+6PNCRwTeWacOGpThj01orzDykyKpMvlzZLLus6i659Uzi/DGR2nn67U8ccrRXjE5z+v1FlntatBgyqrInHlRUV9A1kej4oAN+XiYMqFRe2ypPvWWwQjRd/Ua6lT0ra81K3e0eAyo4qLZOdwijAnghM0F6DkJCn2pWlT0D2ZBwh/mpoJIqU33HCV/p/9YA53IC2O2a/5wvmuWdOjI8p5vfMOy8iU7f2/dKn3P06NgCrK4MVBECx7r7uuxynOWS5sh264oSzVV6fb+RHUH0FIIhclIylKm23mOWZe8gzq50GX/GkezMw0uPCjMEvl78MiuOsZ3zbkcumkIWw45phjtMPLal0eEAT285//XAchsSf4X//1X3rvQaK9jz766KrAMlLF7r77br08z771BRdcoJ588kkdeAR4Sjr99NPVRRddpG677Ta9J0kZnJZEqlYjSJrC1Sy5rOssuv5J5UwZZtAzZyplZPxpzJtH4N8gfZpU0vKiHHVa50kDHFOYU8SpSbCQbd38zlpuzCzTR4GbeBgNJk6ExTMcCbnHm25a0ulThBaYZztHlR2GSmS89zDAQ5I4WV44bnK0veMUiQEgBcxLBcMJI7Nqlfc9R23i7MnDRU90ZoaPnl77vJk0y8nmglrYg0SttKCNyLCbxHMq/o0X+vDyTzrl4YIZsekL41OyekJTrNK8DpqJupa7cZI33nijTltiye4zn/mMnlWz55cmqIdgHchHmGGwJI0TlsAv9m3NJQj2LNGTFCsimLfccksd2b3ddtuVZc455xzt6IkCJpqWk5UoM2oZNQmS7lc0Sy7rOouuf1I5U4Ylbr+DFjz9dEm99lqbXr5MWl4YK1lS/uN65IIcE18TRexxgUeX2ahufrrRqOJkKb+ri+MHg6lRkZEAfyLc/TEBUeXHMYQFHewgWw/oRcwAzpWVDtlPx+GiK7NOihBH29Hh5fLyN8v7YQ9JMn7kIBBx0ma7bY6PKBlz1YWAO9LsaCdt9I659GRYvTAfJswxFka/OXo0sROsRmCz+vVPQy6XR1UKiFi95ZZbdLDY/fffr88FxlnjRFv9qEr2TZPswWYtV8tRlTZ1s11WkjY0wwamzEMPeUvcYUf0XXPNMLXnnm011+l31EnoHOuVCzqiT4CT23xzAg/t1hkFnLWc07x4ceWBnJ8y42T1IoqbHIcg/OX+Y0WRlQMt/OA7kzq1Vv3NfmR53e94zVOhqIcAOC9rxtNXIHu6Xp2eLA8dspwsf5vttmmDKBkc8trj2nVbyHxcssR7mPDiGLwX7ZfVC2zw4Q93VJGLxB0xKfBz1ZvL61Gw1R9cm7vuup7O7kjzqMqGdr6J8mOp+Z577tEBZETGfetb37KnXYHxctidLSdyWddZdP2Typky7CFGYd11S3XVyYzaXP5O87QvmdkIPFIPmRF5kcW264zCiBEr9c2amzY3b16AGSXZbR4NZaVf/Xu15nK0CXFq2Mz/3aBBkIJEz6Tj9Jf6PYKayuxaXib4Tsrz1ymMY+bf4qDNGWo1JWz6p7xJwJcsb/OelQNWDQho9LYXVK/thaAxJCsR48ZxLKbnmEeP9uhxgylIK1SkMdmbidtZi1zaaMhJs18GOQP7t6TjkOMq7F4ODq0OMrG23z74ux12aFPjxjW25xUXUGYDplOTWRw3wn79vP1cbsRJbow24bF09WhnzUMEN3PSpMJCBvx7tfwepxHEhe6VV/0d+8ON7sJUlp69GR+7aeLQvPOXJY+4mpbS/5Akv8dRM+un78VBSwBcWLvTgixz+9dk+ZxVCaK8mVGjJxkOIke7sKF3HGsFbGtAekJbN964R220EQ9i3bHBjMzkw4Lnioy69qT/9Kc/6b1e9ndZioN9idn0HnvsYV/DgoK84DzLZV1n0fVPKmfKkFE3YwZ0tF6wmADHfdJJK9W66w5quE4c9aOPJrsrJVnSD5LznKLHQ01gEA5i5coe9eab3GD7aXrFsKXgeuuMk5NI9sGDvZn0669DP+q9X7So9xKl/8aNYyiVPtApc/4lTf9ZyXH77kn0F2c7YECbDqxibOC0hPiE9zhfluy9fuwXcWKYN+MnWryrq03bB2dszlDNdg8YkM5JUwKZOdNG/xGesh/NAwWzah56zOVpLzBSyIYqwWD+wLB+Rr1hwYzmnn5UopHtMZk26tKCgzSgOoTnGoYxoYFrJRAkR5AaEeQE0LE0Qgoake8QWgjjDctb7F8LtzkBbwxMAoLIzSbYje+5GVOesFhBTwnLGjzPpJ+xtfACuRd6yW+E7nM5WASSEFYxqBOyAfKthXqS9wTBmexq7IfyPeWShsZ79GRfhS0LocPkBkYd7MOjGzEHMGKxj4Q+yMPVTr3oA0mHsG9tscUWeqmWfRvK4feiPzrQX8K+RblQyvIZ2QLoIMxfUHpSn1DwoS+y/E3dROLLyVPCg06/ST8hK/2NLPoD4gnYj6S/0Z86zf6GC+Al8qfWlsNnYiu+I8hQuLexq7SNMukL6W/y6WfO7NK5tMuW4UgGqSFD3lb/+McjavHij2j7cMQrYDwItSZjh5xx2kid9CGxEMKNT+og7UKPIUNWqEmTJqmHH363zPeNXrJc51FbEkW7Rn9HX/BdmCyOmJkMN9N+/dp1NDLO4M03hSscSlbvJsrNFgYzeKRZusTefI8cBBuUSx30C58JmQz9acryvTDNebLt5XQfyoGtDFleXGemLOVDjPLyy55+o0ezQOh5K/atvb1FHFq3pu+kH4S1SnQAYf3C9wMHDtCRzcJI5Zf1cnw7jZs7ZXuHn5RK7TroifhW+K2hbhCKT8l79ghi0AnSmNVro8C9saTUarXRRh06n9ojkympfv2ICF+lOjq4Rr17ktxvKqA/esrkNGZ/0//+PhRbUSft9GSx1cAqFkB+L+lJyDI2qJZLf8MN2/TsWfahKzNm4Sf39OckKo9PfKA+Kc3TtU1vZ4Du7v6aGKd7bX+LLbxxzFID49CrQJ6xPJvS521lWW8c9q8a35RpjkMZD5Ux67VVHt7CxmxWedJ1BY4RMJYWR3dfCRzjpo2jikPWcrUEjtnUzWZZSdvQDBskkVmypFvNn8+65HpqxAiPhCSMx6bWOoOivwXcaOMi1L3DIXC6ldkjN1mCgNj7kyjiyk2aQw28mXQQV0ySOm3ImQFh3Lxh9ILgA3AvXWcdHhR6B+8FzaTr0c0vE3S+N7NMSArNQDBh4hJIP8bVKfoPHjy8TO4SFuzW1WXPBkEyZlAcbSGGilm1PHwwi+bBZNiwyuyYzz1mNVLO4iOpO416w4IZxe7M1qNm0rbGZC4Dx9h/5olEHDSzFDNPmlkI5CYODg7BYHJ+5pnt6sQT+5XJTc44w/vcBvxBZbVA9vrM5UrgkU54N98wpH3MZByCAsJYkiev1ltWDSZHSQNRe6ZyGpRHHdo75avWfmTZPioQLgu74JBZnma7YdQoVh2YaRLc16OPhcQOOGicNjb44APPFhwIst56tW+aD/Dt09dy1KRpI+xD9LwX6a9yi5pm0ixPsBTKki7g6WHOnDl6uVLoNFlSjCMpb4WZdBQPbjPlaplJ29TNZllJ29AMG0TJkB4E+9i8eb1ncexTf+97vWfUjdRZa6pWVLoVdwn2S3HW/pk0y9xhe9JppocFQag2mTsE5UmTwiXo6fF4oxtJOwqSiepH9pLZhg0KtjPTvOLq9K8E+NORzHY32rdJHm5GjOjp9WBCe9iBYifI5LySWTT/b7JJtxo2rEItm1S3zoCVCn+6XRiS/raQKVh+f54XRpY8QvZF8yqXdZ1F1z+pXJRMFLkJgWVrt9at1emfVcedWmamCYURhdQ6Y4ur07acF5C0JpQaVZjMPAwyTlpqTDeRoQ8lJUqCpkxw3jMPO3H9mLQ/BFGUsLX0rcnwZjK9Sb+tv35nFX2rvCRtSqLiYXMj2FAemExICp+XotiuObzjZrNdvjb460sahU8dbOf0Ple8d2R4rTZIC/kIX+uDYOk/z3JZ11l0/ZPKRcnEnGgZ+L2NvhVH/de/Rp9/W5l5BTNvMfvhhkiAE4FiBABBusGhEWFIShtci1zUrDFpWaNHe0dhclymOVP0H+yRtDxkZIaGo5TUJ5k1Sn9SFO9NRjJZ+hbnTnsapVs2+4hDOCQ32w+z7QSteaQw4XVH6WVGxXd2EvzGsZy95dCFBxXiRokHFb2iZsI9AfX6o/A7O3Gq0ev79AmXS1DMlz8yPEvK6yg4J50SkiyfNlMu6zqLrn9SuSiZeHIT+3WaGD9+uXrzzYGxe31BPl/2+uTGuGbNSr3k3dGxfipUpGFOuKenfzlAzNTNvLnXQueIsxZ5oR71O+tkdJn9ykeFmqlIftpPfz+GLb2OGVN/xoy/TJwv+kgf+ZevxSkn2VqpxZ5iRwLHzDO2CZ7DObPsbyYGVU4yCyJxaUtUZxy8h5bo72spL5dOmhxp9mTNU6jmr13DIx3EwQNpOnmWy7rOouufVC5KRshN5s7t/R2fBxzJbb1vN920PTQKvJKT6y0/hi3FeucFr6NnonH3sVofgKL2C1lu59SksKVKubnX+6Dn5wkXDB0aXx7pUaIXhCJEw7MLYXJXe46y+qjKoAAz/kaHKBrScD16l4m9hg/vKQeuhc2UbT9sowt9IUd3iqPGnjhpLwLcO3JSEJbnPMCSbt7Z1m1VUejYh5m1x5lee1vTRs2JXpx+BcMYL/Jiv/jFL5b/PvbYY9PRsoCQHNu8ymVdZ9H1TyoXJSPkJn4WMv7m86A0rLT6NiwKnJnWRht1BbJx1QMzvzZOLspp8Tk3UXKyg2CyayWtMwzmXitYvryyVxsGjqAU4HegOyAJRk7c4gGM90p1JjphjL3yetjCzDKFNpUXEdboTz55GJL0Wy32xMfBrMbDCn0h9KDCsR32gBe0N90ZUq8ZpU374mKWvdUi78xvyHmgYyCFjf8pw9Sp0XHUlJl0Xtboi0BmAkEF5BpxZCZCZBFHZkLfJyEzIcIeEgwbZCbomYTMhDZArhFFZkL/JCEzoa9ouw0yE+pOQmaC/v7+DiIzEVtFkZnQfj43yUzoP7gFqG/zzTdT55339togsYFq/fU71LBh76hS6X21YsWYXmQmUk4UmQl6IUufsQSNDWnfa2sTchmTfC76m/29ySZDdL899thyg+xhtSYwYXYhZA9dXRViCG5e9BN2YWzJfcFP9gAxROWs62gyE2+84Ui8WQ6zKwlgI/cVp+WRZpQCiCy8z9esKemHCTPgB7vLzVZIVZKSmcCmNmLEarV6NZWU1LJlHfpoSd5DttK/f5ch22EE3Hn6yMIi7SOQqqsLYhRvlQKnDsmJ95PKg4cQkkDk4RGhVDikJZMBnaUuk4jD61POtq54GohC+F5OLaOPOjq6AslMTIISITPx96F8bsp6cQLdVbJCnLPRRt42AJe6Z2cC5yACEduVtC29fHHPrhDPUJxJZlIqlXoRzRAXAcGOPJSgLjESG2/MGAonMxk9uk2fQGcuwUtMANkLPJx5RDT0A/2FDt3FIjNpZSRNwRIWsThkLVdLCpZN3WyWlbQNzbCBTf1t1plETpbA40774iZlPvT5j3us94SxVav6VRF9+LHxxkr9+9/haTFCYpGkziRkJrL0zgOCyMjS+9Kl7b1maP/+d++l+GpSkWqiGJbEeeYyA8tM/TbbDEKWeP3NCHXu5u++Wz2ZMlOJoog+0jrNzh9jwMe024vohnmsPfa0sS5fvSZ5jb+dcSeW0V+w0wkxjxDK4HOF/xzbm+UFBbRllYJVV+DYzTffrI+nfP755/XfW221lTriiCM0h7eDh6RUqc2Sy7rOouufVM42RW6WfSvL348+apzS0CBqCTSK239F/cGD2wIP0jBJLGwE/FQvvbcF7H9XzjX2js9kCZUZZvUxmnKDB155slLgOS6WgyWAylSb3POwLdHKsnslhUyW5iVPvPphofKAEbXNais4yy/nj8IGwkfOEnOSdL42X73BWwWVPPUo/m4JHvM3BT/LoqecRW6WFxbQlgVqmq/zBP2Zz3xGv/7xj3/oJU1ezzzzjP7s8MMPd7nTayFL0XmVy7rOouufVM6m/rbrTCo3atRiayds1ZKjG8ckxUwGZ2Q7xzhsj5NUKmJkWX414T9dCp1GjFilT2vidC7vlK0e7chh4ILnnD1hz6lUypKgKhy1edusnA7l/R2Ut0xU+vDhy/X/ZiBYEPMa9+QkDGS15IM3KicnkY0fX6o6kSws9qHLV17Q3rPpe6L2pkkdDALjjyVwkws86xPFGp5J/+AHP1D33Xefuu222/QBGyb4jLOlkTkdWiUHB4dCQxx1FB+4TQSd+ARMB9PevkaNGzewKsfYPFO5kYBcM7LcTB8aNco7NMJ0pGFOwGtD79gd85Qu01EDDt4wj7LkxTKw7JkGRWNHzYWE6KOSJ11SgwbFr1RkDe+IyuCTyOJ/q+r+HqpSeN39M3GJ8q4loC13Tvq6665Tl112WS8HDQ4++GDN2+2ctCoHc+VZLus6i65/Ujmb+tuus145nHW9jrrWFCy/g/HnSSMn+cbh6Vq1h6L7I8vlRi2OkqVQk2zG7wTi2gltZhhVaNBesbcfWn+grrnELCQlcUia5hRHJpO0rFowwFeerLqYtifAMMmy/oABHYEPg2x5m9sOUp6gWQ85NS13L1y4UO27776h3/MdMg7kAb6Ta7ms6yy6/knlbOpvu85G5Oo9uKOe5dEkFJdR6VoEZ9U66/HvcUogkThq86Yf5ATi2ilOJWg7sJ7yaoFNalYejAjY4oHD/N+frWSbUrPLV16jy/pBtKKkibFyYpYXZaOtt+7On5MmxSiKsITIZ1IyHLiwV+RaLus6i65/Ujmb+tuu04Ycjnq77ZIvTaZBCxqfY1z7/qHfqeOk5RAIr17v/zAnEKd/kFNppLxaYMsGcXnsZh/aTtftCSjP72jZ306S0y9l+R8GsbdpI/M87KxOFGt4uXuXXXZRP/nJT/QrCFdffbWWcXDR3WmWlRQuujsdOW/p8RnVv/+OsXuJ9UQDJ5GLmil7e7qqJgTdgL08ce9mzY0aZxC0tGvqFQWcB4dA9PT0i1wqTlpeUtiygTwYBYn5I6ptU2q2hZRnLuuvXt2lOjril9mjdDO3W4TONchGPKy+805n/pz0eeedp/baay+db3n22WerrbfeWi8JLFiwQF1xxRXqj3/8o3rggQfS07ZAgBwjz3JZ11l0/ZPK2dTfdp225HDSxKWQ6/0f/7GJmj27M3P+9KhZTZJ0rt7l9t7j9Mry6Dx5RZWZVP+BA/sVlsPeC0KLPia01jqTYkCG5wOI4yfdLw+oyUlPnz5d/c///I868cQT1e9+97uq72BlInd61113Va2AJIxjkyZNimUce+SRRzQTVhzjGL8Rhps4xjHsZItxjPYkYRybMmVKJOMY+snFEcc4NnnyZGuMY9SdhHFs5513jmUce+qpp3QZcYxjEyZMCGUcQ390QH/sh32ECYzx4Gcce/jhhzW7WBzj2O677x7LOEaqJPqb/T1kiMc4ZvY37ca+MmYZD7BIMY6oF7tiE7H3yJGeHZcsGdOLcWzFiuV6rMYxjpn7f55su+rqElYrgpU8WdpBX7S3d6lBg4Qvu/J7yho0qEe1t3MqVXdNjGMEnLGfzXK5MGANGlRSo0bhfdCnmlnLZMDic3FgjAk+F9kBAwaqNWtWl+ujz4NYxKRf0E8Ou/CzZcUxjpksYtLfMibM/g5iHGPsU18Y41hbG+xi9Et7VX8Tre6RgvBZe5lxjN9Jfwexk3lBaN1aHjkY3ugnr639ezGOtbe3VzGOmf3C51wP1GH2dxDjmNRn9ndFdoBavbrCrkYb/GN29Oi31auvDtB2zC3jGMbkoA0JEoPMZL/99stM6SIwjnHTxlHFIWu5WtiubOpms6ykbWiGDWzqb7NOm3Lo/+CDD+qHALgRzDgUfxQ4N0RudHGoRy4supscY1Js6mEcC4pc7uqK1y3Ndtaif5p10ievvlphTDPhZ/lKUqftNnRmYAMzeJIHbSZXuWQcwxl/8pOftK9NHwIzpDzLZV1n0fVPKmdTf9t12pJjxsJDhrw3nbQ/tzqKMtREPXJh6VrMwOo4O2ht+b2Du0qleN3SbGejsFWnBL+xSBR1SlotdSZFR0exbdAIahrJjz76qLrjjjuqPvvVr36ll/BYUmMZPC8nhzQbSaPcmyWXdZ1F1z+pnO3shmb0rY02SMpW0kMI6pULSteyffBBkvLSbmcjsFnngAFeBHXcKWlp26DbYIYT8pm0bWCLha9W1NSTM2fO1Ptagnnz5qnjjz9e50efe+656vbbb1cXX3xxGnoWDrInmVe5rOssuv5J5Wzqb7vOZthggw0WJZKTfUsbcknLSgqbddqWy7pOZKLy2GutMynWGOUlzdVOUlZSuWY56Jqd9Jw5c9Q+++xT/vumm25S06ZNUz//+c/VmWeeqX74wx+q3/72t2no6eDgUFDUS4Ti4FBLrjbHVqZB3dnssVuTkyYal8hcAcEjBxxwQPnvj3zkI+WI0lYHUb15lsu6zqLrn1TOpv6262y2DaKctctVr08u6zqbob9ZXhSJDSeMJSGxqaUNzXbQNTtpHLSksBDSPnv2bJ26IiDNxLZxigpJRcqrXNZ1Fl3/pHI29bddZ15ssNVWQ9XAgUOr9hMl1SYOSeSiZCp7mUQTD1Hd3W2p15mmXNZ1NkN/s7zoYpPRwSbV7UMfyuZgGavR3QceeKDee77kkkvUH/7wBx3lTX6mYO7cuTq304GbwPJcy2VdZ9H1TypnU3/bdebBBqSez5yp1Pz5/OXNUs45Z4UaPbpDszvFIQndZJiMP2ULB82+KpHJURk5jdSZtlzWdTZDfz+VZxhIJk4SkJ1EN2bQL7zg8UUUyklfeOGF6tBDD1V77rmnzm27/vrrq9hbrr32Wp0v7dC8dIA8pm7YLispmtG3rZB6wjU/ceJETQQTz97klQXlPzws3/qWUsSemlSLl146VG29dac6+WRoHVUqiOOdNnN8iwJyi3nQIL4pjma0r2BACDMcgKajUaIzc3k7LylYNTlpmIkeeughvYSFk/Y34uabby4zFbU64xiADSqOcYylF8gj4hjHxo8fX2a1imIc43PqtME4BjsVdcQxjgHKjGIco62ifxTjmLAx2WAco8+QjWMcA9QZxzgmtopiHIPohs9tMY4Byo5iHAPYL45xTPSPYxxDJ2lPEOMYsjvssIPuH2wu+tPftInP6C/azqxl3rzl6tJLB6k99+xQt9/ucScNHdqmxoyB5Wm1buuzz3IEZbcaOfJ1tXjxqFDGMSG98LNlmaxW/C3fC1tWV1d/9cEH/NZkN/NYxzhPZNWqkho6tL2KRcxkwOJ8Z5MBy884ZjJ4RTGOCWtWHOOYjMkoxrG2tkHqtdd6M6TRr/37e30l5ZrsamGMY9KvYYxjIivMb0EsYqasFwluh3FsoNHfY8b0V2++2a5WrBDWM89x0+6urtWqvT2acUxYx8z+Rpbl7e7uQVX3CO5nYffkXDKOMYtOgltvvVW1OuMYji/J0n/WcrWwXdnUzWZZSdvQDBvY1N9mnbblkrO+va0uumgDvbx93HEsbQezVHV2rlJXXz1I7bFH9e/rYTALkmHV3R/TiuPt6PCcE3qw9J20vKQyfgYz6EyT8HfH1YnvffVVqE/brbB/JZWzWVYjjGPddTLDBZUVFhwWdx3kknEM5+SQDEmffZoll3WdRdc/qZxN/W3XaUsOB/3YY4/p2Tjvo5z0228PWbv/DElK9MlJQbcXP4NZvYhbuUxjZTOIthS+8Y03jj9OMQ70GzPoIL39/doX0RHADFfLEdZJorZtX8uZOOnrrrsuPU36GJI+WTVLLus6i65/UjnbT9TN6Ns4OZbZ77vvvvL7KIayFSsqtxhW1KdOVWrWrMr3Emi744791Pjx8TfVxYtXqJdfjvZwQXuJUXuZfB61l1lPXECje+BxdcYFKJvfm2UFzUDl67zGltjSzRxDSZDm7Dh17m6HeLAvmGe5rOssuv5J5Wzqb7vOZthgxIjKcix+/Qtf8N6Lo+a+uv32Sn3968RPJNPNnAUFzbCDaB+FdzroQA4/73SS8uJkwvN52xLNdMPqFCcrZXmnT/WWM9tT2W8Obz8z+2bQnyZFe426Rc2Um3EdNAK7PZkiWP8/8sgj9dMNgS3QkUaliiD/5S9/WR8bSEAXm/6nnnpqr7xN9kL8L5jUGoUEX+VVLus6i65/Ujmb+tuusxk2GD58qXbC4iT+3/9T6v/8H6V++EOlfvQjuP+V+t73uMG+UpduQpBiEqWE0T7KgRzwTW+yCYFuJf1/3NJzPbSgQTNdnOm663L0ZcWJh82Ig+o06TCXLSMQyyPv8GcU+VcGvKDB6Jk93zeD/jQp1vjK89udFxS0SdjtmnEdtMRMGgdNp917773aYMcdd5w+0OPGG28MlCfilNfll1+uttlmGx0B/KUvfUl/dsstt/Raxt9///1TO8nIwaFV0d7+vpoxY5S68EK4/j1Hc+ed3ux5xgwi+z25t9+2s/8n+a1kFwTNsmUv0wta+kB1dBAtFk9oUiv8M3McNMkHS5a0qUWLZOZaPZONgt/Jvv8+5ZE54s3MccoS5Ry0MhDN1OV9b3nyWxPiHOsLa23aiiiEk16wYIG6++671RNPPKFTn8BVV12lyVVwwqTW+LHddtup3/3ud+W/idL79re/rY466qhewS44ZVJLbCJIpzzJZV1n0fVPKmdTf9t1NsMGpJWRqcJsmUw5FrIIEGP/2XwWTsMGccviSVEPXaZ/D5ztTTIxSffCGcoSddgetb88v5NlmXvx4pJab702tf76yHt1BuVJUxZBZnEPAQMHZkMLKnbxMgSeVFOn7tbnr4M+76Q5IhNHKg4acPIWexBEmSY921pC5f3RqCeffLL6whe+oPNHmW0zS49LCZAcXwEh/WZYP3XFET00Q05yJSV/MivdbJaVtA3NsIFN/W3WaVPO1Js81qh2SFmkN227rb+cdNoQJDNlSuXanDVrlXZywPs/ehZPG+PuB34ZHPFGG7WVZ7+oIw4a/2UWx/esMAweXAotr6urd/09PSX90ANYtidHutKmat3i9nT5up52JpGbOrU6sFDGS1+6DlSrO2mIISBkMIGjJU856bF6JKHDmMYSuf/4zb333lsnpt9zzz3qpJNO0nvd7F9Hwb/0cswxx6hjjz22ak88Ko+6WXJcFEJcEXfh2tTNZllJ29AMG9jU32adNuWEGAKwuhUV3Z1nG7z22qtq3LjKNlcYurq6Vb9+HTXL4KQ23BBikH6quxvCEyFQ6b0XvWYN5CcfRJTn5xn3iFg8sg8+95bvw3QbOHCwnikHzag9To41avnylXW1M7zOhfr93/6W3nXQ3t6uhgzZWK1Ysb5aunSgGjFimRo6dIn64IPXQ+k/bY01CIr6vJMWHvC4pW4bBCQHHXSQ3pu+4IILqr6bwcbYWkyePFkzPl122WWxThpGJpiowmbSsNbALhWHrOXk6W/69OmxRBo2dbNZVtI2NMMGNvW3WadNOZz0okWL9GoSbYhiXsq/DdYr24AZdlh742ZecTKc0MQyNH6jvb33TJTZ9eDBw0LLE55xc8kbB03aEcvq3rnOwWwsUtbYsZWZvUD2sPneS8lqrJ0yc/ZssFvq18Err7SrmTPbdC7+6tWdmt1t++3XUzNmbK7Gj+9JdawJC2KuGMdsA7o1ocwMA0vQ//3f/63OOuusqiVmDMwTPFSkUcvdPO189KMf1TeSO+64I/KpH9x5553qYx/7WJlKsl7GsbyiFrarvKLobSi6/n2hDXH6N0qe4gcTXiKzw/K0k3CH9z4gpEcNG9aeKPAsSZ60DdRytGOjY+jdd5U6/XQ5rKUaBCYSB5FmDHBWjGNNTcGCJ3jrrbeOfPHUtssuu2iO4lkGC8L999+vlzOmTZsWWj4OlQM/KOO2226LddBgzpw5eoachF4uCsL/mle5rOssuv5J5Wzqb7tOZ4PkMFN7hOc5CnEykqct+8aC8Gjs1caxmt7/+LF6UshM3SS6XXK0KVdSwcxtjFrb6U99yuI6eOWVagdt6kYmwVpK/0RlNSKXNgrxCMyJO6RInXDCCeqnP/2pTsE65ZRT1OGHH16OwOPggX322Uf96le/UjvttFPZQUOGzkycv3nJwwHLRLfffrtetuNMbBw46V3f+c531Nlnn92wzs06X9adQ5tenTbbmRR5PMuYGdDTTz+tUyLjaEH7gg022uid0JSuWoAz3XhjDq7oFzuTZR+bc0uClqYbTSELIzUZPbqftdlzFtfBe72OKq9+AAo7Fr1ZZ2L3aScNbrjhBu2YccQECxx22GHqhzAirAWO+7nnntNOGcyePVtHfgcFefGExOk+pAlcffXV6owzztCDHrkrr7xSPww0iqSngTVLLus6i65/Ujnbp8A1o2/j5NgKYutI3ketUPUlG0SldCVn4iolyol+8802tcq3RW6ma9WS02zqFkVqQp2bblob+1rY8nYW18G6Pq739vZqxcOOmmjGvaglnDT7v2HEJQCna26v77XXXrEE6czOTRITm3Dc3emVlRSOuztduVa2gf/gD5uc1l5ONOyH4cQjCXbuAuuMJjVpi6UrNcuK2n+mz9gzltx49oZ5AKh3j3idABuQa8/eM0vbft34PIwLvhnXQUvQghYNcu5vXuWyrrPo+ieVs6m/7TqdDepDVHmyF2uTLpPZbtQEo9ZVWLPOqN96KV3xZSWh3nzhhW4d1HX88UqdeaZSn/+8UmecodTaI9qt2GC99TzWOqGdlbOohc0u7IGgGddBS8ykHRwcHPIK2bcGSfeuwyKt0zxWs9GyaadS0VktzKC/+93+6rnnqj9nxgs9rM2o6803r7DZwcAGnYafza7ocE46JSSlGW2WXNZ1Fl3/pHK26WWb0bfOBo3VGXUGttBlRp1IhbMeMqT3nrTIJCDLCqwz7shO6owqm3YtXx7fFzjMBQv6Be6bS9R1rU50TIQNKIvX8uXdOpe8kbLqkUsbzknXCShKCaI47bTT1CGHHKLTFzhtC2Y0DvMgdw6+cJaQJBecfXMY0iQHG15jgt3IuybfjvLIHQec2gVLGoFwpJAhL+xqpIhx4S1evFj/PW7cOJ2zBxELr0mTJqkXX3xRfwedKoE98tvRo0drRjW+p1yS9XmPnuzBcDybnP7C99RPVDy6kbNO0B1RjwRVIM+SEG3ltLGVK1eWTxljVvHyyy/r6N9hw4bp/00d6C/Je6fcf/3rX/oz0ii23HLLMhMRkfjUJ8QB6Issf1M30f30N9iAEwzWsssB2kLqnvQ3spJWQYwDe1j0Nzpvv/32Vf29ySabqJfWrs3R33z2/PPPa1vxHeXSj0Q2Y1fIbQC6jh07ttxW3tN/5OtTH/qjA/pjP+zDoS+A8YD9kIetivGzcOFC3df0IXXLEhw3ENqFHug/ZcoU3WcsR9Ju2vcaiblK6THJ59iDMsz+hj+AfjP7W8agjFnGA3ZhHFEv5Qhol/T3+PHjdZsYBx55xtjy+EYf+ipozDKWuXakz+gT7EXmBTD7m9eOO+5YHrOUTRtkzGJj2kZdMmaxI+ma9CMv+hs70X7aig7S3+aYRQ/Kon+32mor3QfoAZA1+5vyRX/6mzJGjqyM2UcffV/ri1z//oN05DYxrrLvzOo2tKFvvNGmI8BHj1Zq0aJ2tWpVm6b/BHDG4De6ulaXl8Opn/foT59LGhJjjc/Qg7bSv0LDOXp0f132ihVeGd7BHG1q9GiP4rWtDZY0otA9MhBssf76b6oXXvAYzrCH2d/YxLxHLF68Sq1a1a3694cgpaO8DI2dKXPx4i71wgv/1mOL/sSW2FH6O+geMXz4cD1ewsYsenFdUQ79jY1lzFKWeY/gfiy2YswyzoLuEf7rwX9PjiLx6TNkJkVEUjITbtpJTm3JWq4WAgGbutksK2kbmmEDm/rbrNOmHDflK664Qr/nITXq1Dhng95y//73Rvq4yTCQB93e3qn69RsYSjzipWAt1w8TcVzaOCs/70PQUntXV285/95zknY+/bRSRx2F0wuObvvFL5TaYYfiXwfvtAKZiYODQ/HALIJZBQ+rSQ4qcKhGW9tQddVV4YFXErwlxCMs4Xq0n/Z0iCs7SXBYGNgTnjQp+MEhKuraIRhuJl0jHC1o81H0NhRd/yzbYDONJy82YKZJ1LMfX/7yivJMOioNqtaZdC2o1zH7wU6RnCEu8J8hXvTr4B03ky42zH27PMplXWfR9U8qZ1N/23UWzQbc6P1pPKecsjo2jSfvNpD8Xj+YXf/lL0PVDjsMTUTRmRRxZcmsecMNK7EI5kMSDxUPPaTU3LnYJNmhEh0dr+ioa5a2r7zS+5+/xUG32nXQCIr3+FIQJD1rtFlyWddZdP2Tytk+Y7YZfRsnR/APAWEE0YQdB9honTiHmTN7H57w9NM9sWk8ebeB5PeGzTT5fsyYJYH7ofVQk/oXS8Nmy/428DDkt8GECUO13nHOloA2ibruq9dBVnBOOiWwDJVnuazrLLr+SeVs6m+7TltyRLfecsst+v2+++4buS9db53+wxNM6se4NJ4i2MDM72UpHwpLM783rDxxsN5S8ZNq6tT4peI331ymxoyJ35oz6wx7SJo/vz1RrnMRbBAH222oF85Jp4SoiNc8yGVdZ9H1TypnU3/bdRbJBmGHI/Tr1xH5fS11NtsGUTPNZtsg7CGJ/k+S61wUG0TBdhvqhduTTgmS75dXuazrLLr+SeVs6m+7ziLZIOxwBNlfDfu+ljqTohVtEPYQJP0f9ZBUS52tYoNG4GbSKZGZkExP4nwcmYkQWcSRmbD3J4QZUWQmJOZDtGCDzAQ9qSOOzIQ2QHIRRWZC/4j+UWQm9BVtt0FmQt3IxpGZoL+/v4PITMRWUWQmtJ/PbZGZSDlRZCb83k+uEURmIvXEkZmgq7SnUTITqTOOzITxJ3XSJ6NHD1MTJrSpuXNLuqyuLsg1utWaNV3qP/4Dopq31QsvvBtIZsK4oixbZCbIM2ajyEz4X/QXMhNzzGI36qOv6CdzzAbdI6hT7hHS3+Y9Av15IUe5QWNW7hHI+Pub8e6/R4itsNvgwe2qs5O97DatR2enR3/mkZx0qwEDOtULL7weSGbC+KYs8x4hY9a8R9RCZtLT06P7O4rMRPSPIzNBX7FVFJkJYxtdm01mwgBxqAHvvfceI7e0ZMmSSLn3338/UXlZy61Zs6b0wAMP6P+z1M1mWUnb0Awb2NTfZp025ZYtW1a64IIL9Gvp0qWp1fnii6XSsceWSlOnVl5HH92lP291G6R9HWNWf9/zmjy5S38eY/aWsMGSJUu0L8AnpAk3k04JSVMomiWXdZ1F1z+pnE39bddZNBsEBVdtsMFKNXZsdECPs0HjdYZFoG+3XSnyhKla62wVGzQCtyedEmSZJq9yWddZdP2TytnU33adRbQBzgAKyT328P5fufJNa3UmRavaQB6SzFznGTOWJsp1djawBzeTdnBwqAnsx7FXyp6dowXt2/BHoL/wAvvJo5qpUsvB0YKmRAtKoAOBFHHIWq4WKj6butksK2kbmmEDm/rbrNO2nLNB7TI25VrpOs6rDRwtaMHhUrDSKyspXApWunLNqNPZoPWu475gg0bgnHRKcIFj6ZWVFC5wLB05SYdhBhFHC+pskK5c1nU2K+hqdcFt0Aick04JSXPomiWXdZ1F1z+pnO3cyWb0bZwceaK//vWv1Zw5c/T7POlWS1lFtkErXcd9wQaNwDnplCDEGnmVy7rOouufVM6m/rbrdDaoD84G6ZTVSjZoBC66O0XGsUmTJsUyjv3973/XLDdxjGP8RoIY4hjHpk+fbo1xjPYkYRybMmVKJOMY+kkkcBzj2OTJk60xjlF3EsaxnXfeOZZx7KmnntJlxDGOTZgwwRrj2OOPP66ZmuIYx3bfffdYxrFnnnlG6x/HOEa7sa8NxjEZ33GMY+gi5yLTJ9iLsQzM/uazXXfdtTxmgxjHaCc2s8k4xpiMYhzjc9qRhHFs6tSpsYxj6MzvbDGOTZs2LZZxbPbs2br/sJsw58mYlf6mnRMnTqzq7yDGMcYe14FNxrFBgwZFMo7Nnz9f6xPHOEZ/yuEZcYxjtMcxjvVRxrGFCxcmKi9ruVpYfmzqZrOspG1ohg1s6m+zTptytTCOORukI9dK13FebZAV45hb7k4JPN3lWS7rOouuf1I5m/rbrtPZoD44G6RTVivZoBE4J50SWH7Js1zWdRZd/6RyNvW3XaezQX1wNkinrFayQSNwTjolyH5UXuWyrrPo+ieVs6m/7TqdDeqDs0E6ZbWSDRqBc9IODg41gQAwlgIJnIljinJwcGgMjhY0JVpQlkqIjIxD1nK1UPHZ1M1mWUnb0Awb2NTfZp225ZwNapexKddK13FebeBoQQsOSavJq1zWdRZd/6RyNvW3XaezQX1wNkinrFayQSNwTjolkKOXZ7ms6yy6/knlbOpvu05bcuSsSn5xHC2os0G6clnX2Qz9+4INGoFz0imBJPo8y2VdZ9H1TypnU3/bddqSg8zhmmuuUU8++WQsLaizQbpyWdfZDP37gg0agYv6SIlxjP0M2HTiGMeYjcBWFcc4BmuOsFpFMY7BnESdNhjHCA6ijjjGMf6mzCjGMfbxRf8oxjF04n8bjGN8h2wc45iwT8UxjomtohjH6D8+t8U4Rp9SdhTjGPpjvzjGMdE/jnGMtkp7GmUckzrjGMdou9QZxTgmiGIco+8oyxbjGLK0M4pxDFnRP4pxjLbymzjGMXTCfjYYx5BD9zjGMbFVFOMYfY1cHOMYOmE/W4xj48aN0/0dxTgm+scxjjG2xFZRjGPojq6OcaxgcIxj9ck5xrG+w7TkGMfqk7Ep10rXcV5t4BjHHBwcHBwcWhzOSacEdwpWemUlhTuBKV25ZtTpbNB613FfsEEjcE7awcHBwcEhp3BOOiVIME1e5bKus+j6J5Wzqb/tOp0N6oOzQTpltZINWsJJE5l45JFH6og7ogCPP/74qmjPIOy11146atN8felLX6qSIULwoIMO0pF6RGV+5Stf0ZGZDg4OwSBKm+hhImcdLaiDQ7oozBWGgybs/95779UpDMcdd5w68cQT1Y033hj5uxNOOEHNnDmz/LcZNk/YPg6a1IBHHnlEl3/00UfrFIXvfOc7DelLaH+e5bKus+j6J5Wzqb/tOm3J4ZxPOukkTekYl0vqbJCuXNZ1NkP/vmCDPu+kFyxYoO6++271xBNP6PxkcNVVV6kDDzxQXX755Tq3LQw4ZZxwEO655x71j3/8Q9133306l27HHXdUF154ofrqV7+qLrjgAp0LFwbJ3xOQY8dLQJ4e+YZxyFqOVQJyRpOsFtjUzWZZSdvQDBvY1N9mnbblnA1ql7Ep10rXcZ5tkAUKccDGtddeq84666wqx0gH8RR/8803q09+8pOhy93PPPOMTsLHUX/84x9XM2bMKM+mv/GNb6jbbrtNzZkzp/wbEtlJyJ89e7aaPHly6AEbfhxzzDHq2GOPrVqejzqAo1lyXBQs8ZOYD/lAVrrZLCtpG5phA5v626zTphz6QyoB8QSEFFFL3s4G6ci10nWcVxtAcHPwwQenfsBGIWbSMPCwX2yCGwMdKMxOQTjiiCP0kgUz7blz5+oZ8nPPPaduvfXWcrnMoE3I31HlAhhr2JcLm0lzA4MBKA5Zy8nT3/Tp02P3E23qZrOspG1ohg1s6m+zTptyxIL84Ac/0O95QA56aG2Wbq1ig1a6jvNqg6zOm26qkz733HPVJZdcErvUXS/YsxZsv/32mmJvn3320TR00Po1Ahx01FMWNHZJjkNrhhxPrVwUcReGzTpttzNJG5rRtzb1t12nLTlTZ+ScDdx13Io26JdR0GRTo7tZwsYJR71YemapWjh/BcKNG7bfHIRp06bp/4W3ld8KP7BA/q6l3CAI/2te5bKus+j6J5Wzqb/tOp0N6oOzQTpltZINCjuThhidVxx22WUXTcQ+a9YsNXXqVP3Z/fffr/c0xPEmgew9M6OWcr/97W/rBwBZTid6nP2FbbbZps5WOTg4ODg4tFCe9MSJE9X++++v06kef/xx9fDDD6tTTjlFHX744eXIbk5a2XrrrfX3gCVtIrVx7JwiQ4AY6VV77LGHmjRpkpbZb7/9tDP+3Oc+p55++mn1pz/9SZ1//vnq5JNPrtpfrgdJAhOaKZd1nUXXP6mcTf1t1+lsUB+cDdIpq5Vs0OedNLjhhhu0E2ZPmdSr3XbbTZ9pKyB3mqAwOd+W9ClSq3DE/I6l9cMOO0zdfvvt5d+w33DHHXfo/5lVH3XUUdqRm3nV9SLJnkcz5bKus+j6J5Wzqb/tOp0N6oOzQTpltZIN+nx0tzzVRBGXkApiZpOx6f/ggw/Glkv091133aVsgzNIo6Jemy2XBDbrLLr+SeVs6m+7TmeD+uBskE5ZrWSDlphJOzg45ANEtQ4fPlyvVjlaUAeHdOGcdEogQT/PclnXWXT9k8rZ1N92nbbkIBE69dRT9RZRHC2os0G6clnX2Qz9+4INGoF7DK4T0JOS43faaaepQw45RDMwDR48WEeJv/LKK2rJkiVqwoQJegme97IkD0nKqlWrdGAaUeZPPfWUWn/99fWL8lhikQHCKSzssTNj4Tt+Jzna8ItLWhpL+6SjrVixQrPfTJkyRQfOAQ4j4UYq5CyQtUBGwfeUu9lmm+n36ElU+9ChQzWHOaDOYcOGaZY13pMOR1oCnOfMpJAnYI/2bbvttmrlypW6frDFFlvogD1S5SgD3YW8AB3oL2GQo1yIA/iMNhAoCBMRIPqf+oQ4AH2R5W/qJnCQ/jbPf5XTa8x+o7+RlbQKtk/Yc6K/0R92ObO/ITF46aWXyv3NZ1DIYie+I9uAfmQmiV0lra+zs1NvoUh/QytI/8FORH3ojw7oj/2wD/SDgPFA+5HnMBhy+QlopH76kKU32iwpgrQLPdB/p5120n1GbAY2pH2vvfaalmVM8jk6or/Z37Dv0W9mf2N/mSHTNv6mXYwj6sWu2ETsLXak3bSJz+kv2i7jG30oM2jMMpb5jt/JmMVekg5p9jf/c+3JmKVPaIOMWWyM3dBVxix/kwnCmOVFf6M/7cf+6CD9bY5Z9KCf6F8CTOkD6gfImv3NdzK+6W9JEZUxi92ojzZwrZhjNugeIWObMYFOwLxHoD8v2kK5QWNW7hGUDeWx2d/cX/z3CNgZqQO7YVd0lTEr/c2Y42+zv5Hz3yOol7bIPULGrHmP4Hv6k7Kxo/R30D1i0KBBZaa7oDFLXyxcuFDrj370i4xZyjL7m3El+82MWf4OukdgQ/o/7J5sngORKqAFdUiO9957j43v0pIlSyLlFi5cmKi8rOXWrFlTeuCBB/T/Wepms6ykbWiGDWzqb7NO23LOBrXL2JRrpes4rzbAB+AL8Alpws2kU0LU4Rx5kMu6zqLrn1TOpv6267Qlx8zpiiuu0O+ZoTEDyotutZRVZBu00nXcF2zQCNyedEpIwg3bTLms6yy6/knlbOpvu05ng/rgbJBOWa1kg0bgnHRKkL2hvMplXWfR9U8qZ1N/23U6G9QHZ4N0ymolGzQC56QdHBwcHBxyCuekU4J5jGUe5bKus+j6J5Wzqb/tOp0N6oOzQTpltZINGoFz0inBBY6lV1ZSuKCldOWaUaezQetdx33BBo3AOemU4D8CM29yWddZdP2TytnU33adzgb1wdkgnbJayQaNwDlpBweHmgDxCEQOQkLi4OCQHtwVlhLjGH/DphPHOIacMEFFMY7BhCOsVlGMY9THywbjGCxR1BHHOCZMSlGMY7ACif5RjGMwAfG/DcYx6kE2jnGM+njFMY6JraIYx+hrPrfFOEbbKTuKcQy9sF8c45joH8c4Rl3SniDGMX578MEH6/7hc2lrEOOY1BnHOIas1BnFOAbrFIhiHOO3lGWLcQw57BfFOGaO7yjGMXTiN3GMY8hiPxuMY3yP7nGMY2KrKMYx+pYy4hjHKAv72WIc23jjjXV/RzGOif5xjGOMAbFVFOMYdkVXxzjWRxnH3njjjUTlZS1XC8uPTd1slpW0Dc2wgU39bdZpW87ZoHYZm3KtdB3n1QZZMY655e6UwNNlnuWyrrPo+ieVs6m/7TqdDeqDs0E6ZbWSDRqBW+5OCUn36poll3WdRdc/qZztPdpm9G2cXC20oM4G6cplXWcz9O8LNmgEbiadEthvybNc1nUWXf+kcjb1t12ns0F9cDZIp6xWskEjcE46JUhgQl7lsq6z6PonlbOpv+06nQ3qg7NBOmW1kg0agXPSDg4ODg4OOYVz0imBlII8y2VdZ9H1TypnU3/bdTob1Adng3TKaiUbNALnpFMCOdN5lsu6zqLrn1TOpv6263Q2qA/OBumU1Uo2aAT5CF/rg2QmJNNPmjQplsxk9uzZOoE+jsyE3/B9HJkJifnTp0+3QmZCnbQnjsyEtk6ZMiWSzAT9hAs3isyEvpo8ebIVMhOpO47MBP133nnnWDKTp556SpcRRWZC+ydMmGCNzGTOnDmaBCKKzITf77777rFkJs8884zWP47MhHZj3zAyEyHXALRL+juIzETGdxyZCbrQ5jgyEz7bddddI8lMFixYoG1mi8wEecZkFJkJn9OOODIT+mrq1KmxZCbozO9skJkgM23atFgyE7FVFJkJ7Zw4cWIsmQljj+vAFplJT09PmXAljMxk/vz5Wp84MhP6E/vGkZkwtmmPIzPpo2QmCxcuTFRe1nK1EAjY1M1mWUnb0Awb2NTfZp025VasWFG6+OKLSxdeeGEskYOzQTpyrXQd59UGjsyk4OBpNM9yWddZdP2TytnU33adtuSYQZx99tlqt912i51NOBukK5d1nc3Qvy/YoBE4J50SWP7Js1zWdRZd/6RyNvW3XaezQX1wNkinrFayQSNwTjolsJ+RZ7ms6yy6/knlbOpvu05ng/rgbJBOWa1kg0bgAsdSAgEXeZbLus6i659Uzqb+tuu0JVcLLaizQbpyWdfZDP37gg0agZtJpwQiDPMsl3WdRdc/qZxN/W3X6WxQH5wN0imrlWzQCJyTTgmSCpFXuazrLLr+SeVs6m+7TmeD+uBskE5ZrWSDRuCcdEqQw+nzKpd1nUXXP6mcTf1t1+lsUB+cDdIpq5Vs0Aick04JJPHnWS7rOouuf1I5m/rbrtPZoD44G6RTVivZoBG4wLGUGMdgu4GVJo5xDDYbQv3jGMdgbBJWqyjGMZh3YOCxwTjGb6kjjnGMtpIvG8U4RntF/yjGMeqhXhuMY/wW2TjGMfSnzjjGMbFVFOMY9fC5LcYxxg5/RzGOoT9tjWMcE/3jGMewmbSnUcYxqTOOcQwdpM4oxjF0oz1RjGPUT1m2GMdoO9d2FOMY34v+UYxj1MN4imMc4zqiPhuMY5RDP8UxjomtohjHqIcy4hjHuI7knmKDcWzMmDG6v6MYx0T/OMYxZMVWUYxj2BRdHeNYweAYx+qTc4xjfYdpadmyZaULLrhAv5YuXZor3VrFBq10HefVBo5xzMHBIZdgxsRsjP+FT97BwSEduOXulMAyVZ7lsq6z6PonlbOpv+06bcmxzHfOOeeov/3tb7FLfs4G6cplXWcz9O8LNmgE7jE4JbBvk2e5rOssuv5J5Wzqb7tOZ4P64GyQTlmtZING4Jx0SnDc3emVlRSONzpduWbU6WzQetdxX7BBSzhpIhOPPPJIHS1IFODxxx+vowrDQEQkkZRBr5tvvrksF/T9TTfd1LC+cjZuXuWyrrPo+ieVs6m/7TptyRHd+p3vfEc9+OCD+n2edKulrCLboJWu475gg0bQRvSYKgAOOOAAHfb/s5/9TKc9HHfcceojH/mIuvHGGwPlJSTfxDXXXKMuu+wyXY4c+o0hrrvuOrX//vuX5SQlIezpinQBUiZIKykaSA1hL5FjBkmJKSKK3oai629yd5OCGMXdnVcU3QZF178vtOGdd97RaXGkaaWZU12InlmwYIG6++671RNPPKHzk8FVV12lDjzwQHX55Zfr3DY/yO8jt87E73//e/XpT3+67KAF3GT8snGQ/D0BOXa8zJk8eYBxyFqOC4OcUf7PUjebZSVtQzNsYFN/m3XalDP15mE4qh3OBunItdJ1nGcbZIFCzKSvvfZaddZZZ1U5RjqI2S5L15/85Cdjy5g1a5Z28A8//LCaPn16+XNm0jh5Et9JxP/Sl76kZ+lhSx0yk/bjmGOOUccee2zVU1aSmXbWclwUkACQmB+XPmNTN5tlJW1DM2xgU3+bddqUg1Di0Ucf1e+nTZsWuurUDN1axQatdB3n1QYQ+Rx88MFuJg1g4IHFxwTLI3SgMDvF4Re/+IWaOHFilYMGM2fOVHvvvbdOJbnnnnvUSSedpJfzTj311MjyYKyB1SdsJg2LDUw3cchaTp7+6Ie4JSabutksK2kbmmEDm/rbrNOmHNeHOGm2nIIeWpulW6vYoJWu47zaQBjl0kZTnfS5556rLrnkktil7kYBFR171zNmzOj1nfnZ5MmTddg9+9ZxThoHHfWUJTSIcWiGHE+tyMTJ2azTdjuTtKEZfWtTf9t12pIzv2NbydnAXcetaIN+Ge2jNzW6myVsnHDUiyVo9ouF81cg3LhJ9pJvueUWHYV69NFHx8qyfCd8vY1A+GrzKpd1nUXXP6mcTf1t1+lsUB+cDdIpq5VsUNiZNMTovOKwyy67aCJ29pWnTp2qP7v//vv1ngZONclSN3sHSeqaM2eOniWbS9cODg7Vsx9m0Fx/jhbUwSFdFGJPmr1kUqROOOEE9dOf/lSnYJ1yyinq8MMPL0d289Szzz77qF/96ldqp512qto7fuihh9Rdd93Vq9zbb79d7zvsvPPOOvjl3nvv1fmfZ599dsM6J40Wb5Zc1nUWXf+kcjb1t12nLTniN9iqSkIL6myQrlzWdTZD/75gg0ZQmMfgG264QW299dbaEZN6RW4dec8CHPdzzz3Xi1yByHCOcNtvv/16lckhAVdffbWeqe+44446B/vKK69U3/zmNxvWl6PP8iyXdZ1F1z+pnE39bdfpbFAfnA3SKauVbNASTppNfIK/CHsn5B3na+Y7k89GNtlee+1V9TtmxoT5By3LMTt/6qmndJlErLLU/cUvftHKEp6cO5tXuazrLLr+SeVs6m+7TmeD+uBskE5ZrWSDPr/c7eDgkB+wWsWKE3vSU6ZMSTVH1MGh1eGcdEr48Ic/nGu5rOssuv5J5Wzqb7tOW3I4Z5jG5H2edKulrCLboJWu475gg0bgnHSdgL2MZXG4iw855BDNwjR48GBNuvLKK6/olLFtttlGL8HD8y1L8pCvsNdB9DjnlT7++OP6N3DAUp7wjcPC8/bbb+tZy4ABA/RNUW6MRJ+zny5paePGjdPpaOR4UxeBcy+++GIVD7mQvpCcz9I+31PuZpttpt+jJzOioUOHam5zQH3UBcsaupEO989//lN/Pnz4cC1PwB56TJo0SeejsxUBtthiC02rR6oc2xJsKQiLGzrQX8IgR7mkvfEZv99+++31FgUgIp/6hDgAfZHlb+omcJD+BhtssIH+n34D1Ec/S38ji/6yfUKEMv2N/tjT7G/iGF566aVyf/PZvHnztK34jqUw+pFcSexKgCLABlzc0t9jx47V/Uf7qQ/90QH9qRf7vP7661qW8cDvkUd3ynnsscd0u+hDSEMkLYSgFtqFHpRDXAV9RmwGNqR9r732mpZFZ4nZ4L3Z3wR+Ub7Z3/xOWMRoG+OBlEQ+o17sKqBd0t/jx4/XujAO6C/ajv7UKTmnQWOWscz4E3IL+gR7EdQJzP6mLgI9ZczSJ7RBxiw2fv7553V/yZjFjjxMMGZ50d+MKdqP/dFB+tscs+hBP6EzY5I+kCVQZM3+No81pL2SIipjFrtRH7/fYYcdqsZs0D2CsukjxoT0t3mPkPsBbaHcoDEr9wheZMWY/c39xX+PmDt3rtYdu3Ed0t8yZqW/+WzLLbes6m8+898j+J4+knuEjFnzHkFb6Q/Kxo7S30H3iI6ODv0KG7P0xbPPPqv1R1/6RcYsZZn9TR0yvhmzjLOgewS60k9h9+S4oElrgBbUITnee+89aFRLS5YsiZRbuHBhovKylluzZk3pgQce0P9nqZvNspK2oRk2sKm/zTptyi1btqx0wQUX6NfSpUtzpVur2KCVruO82gAfgC/AJ6SJwgSOFQ08XedZLus6i65/Ujmb+tuu09mgPjgbpFNWK9mgETgnnRKSHmPZLLms6yy6/knlbB9f2oy+dTZIr86i26AZ+vcFGzQC56RTguwH5lUu6zqLrn9SOZv6267T2aA+OBukU1Yr2aAROCft4OBQEwgQkiBARwvq4JAuXHR3SvAfrZk3uazrLLr+SeVs6m+7TltyRLV+/etfT0QL6myQrlzWdTZD/75gg0bgHoNTAqkZeZbLus6i659Uzqb+tut0NqgPzgbplNVKNmgEzkmnBMnvy6tc1nUWXf+kcjb1t12ns0F9cDZIp6xWskEjcMvdDg4ONQEyh+9///uaQMLRgjo4pAvnpFNiHIOdBzadOMYxvoetKo5xzGS1imIcg5WHMm0wjsEQRB1xjGPCRBTFOAaTj+gfxTiGrvxvg3GMfkE2jnEM/akzjnFMbBXFOEa/8LktxjH+p+woxjH04hXHOCb6xzGO0V5pTxDjGP0iS4HYW8ZhEOOY1BnHOEa/SJ1RjGMSqBbFOEb7KcsW4xj1YL8oxjHaKvpHMY7RLn4TxzjGZ9jPBuMYZaF7HOOY2CqKcYx+oYw4xjFksZ8txrEPfehDur+jGMdE/zjGMcai2CqKcQx90NUxjvVRxrFXXnklUXlZy9XC8mNTN5tlJW1DM2xgU3+bddqUq4VxzNkgHblWuo7zagPHOFZw8MSXZ7ms6yy6/knlbOpvu05ng/rgbJBOWa1kg0bgnHRKSLoU0iy5rOssuv5J5WwvgTWjb50N0quz6DZohv59wQaNwDnplCD7o3mVy7rOouufVM6m/rbrdDaoD84G6ZTVSjZoBM5JpwQJasirXNZ1Fl3/pHI29bddp7NBfXA2SKesVrJBI3BO2sHBwcHBIadwKVgpgbSAPMtlXWfR9U8qZ1N/23XakiMN57zzztO0oLzPk261lFVkG7TSddwXbNAI3Ew6JZCXmWe5rOssuv5J5Wzqb7tOZ4P64GyQTlmtZING4GbSKZGZkEw/adKkWDKTefPm6QT6ODITfiPlRJGZkJg/ffp0K2Qm1AnxQhyZCW2FeSqKzAT9RP8oMhNkJk+ebIXMhLrpkzgyE/TfeeedY8lMxFZRZCa0f8KECdbITJ555hlNAhFFZsLvd99991gyE8pC/zgyE+xmjlk/mQnfYxOxt9gxiMxE+iyOzERsH0dmwme77rprJJkJtsBmtshMkGdMRpGZ8LnoH0VmwhibOnVqLJkJOvM7G2QmyEybNi2WzERsFUVmQjsnTpwYS2bC2OM6sEVm0tPTo6+hKDKT+fPna33iyEx4L/0dRWZCfVH3ZEdmUnAyk4ULFyYqL2u5WggEbOpms6ykbWiGDWzqb7NOm3IrVqwoXXzxxaULL7wwlsjB2SAduVa6jvNqg6zITNxMOiXwlJhnuazrLLr+SeVs6m+7TltyzGqYpcj7POlWS1lFtkErXcd9wQaNwO1JpwRZDsqrXNZ1Fl3/pHI29bddp7NBfXA2SKesVrJBI3BOOiXITCOvclnXWXT9k8rZ1N92nc4G9cHZIJ2yWskGjcA56ZRAwEKe5bKus+j6J5Wzqb/tOp0N6oOzQTpltZINGoFz0imBSMY8y2VdZ9H1TypnU3/bdTob1Adng3TKaiUbNALnpFOCpE3kVS7rOouuf1I5m/rbrtPZoD44G6RTVivZoBE4J+3g4ODg4JBTuBSslADxQJ7lsq6z6PonlbOpv+06bcnVQgvqbJCuXNZ1NkP/vmCDRuCcdEqMY8JiFMc4BmsO38cxjnEzFFarKMYxIhJh7rHBOMZ76ohjHBMWoyjGMfpC9I9iHKNs6rXBOCYMRnGMY+hPnXGMY2KrKMYx+om+sMU4hv7UGcU4xm8oJ45xTMqKYxyjfluMY9JncYxjjCXpwyjGMdqMPaIYx6iPly3GMb5H9yjGMWwr+kcxjknZcYxj/E17bTCOoT99FMc4JraKYhyjn6grjnGMv9HLFuPYyJEjtX5RjGOMd/SPYxzjWkzCOIbNHeNYAeEYx+qTc4xjfYdpCTgb1C5jU66VruO82sAxjjk4OOQSzDquuuoqPYtkRSluydvBwaF+OCedElhGybNc1nUWXf+kcjb1t12nLTmWbFnyk/d50q2Wsopsg1a6jvuCDRqBi+5OCbL3lle5rOssuv5J5Wzqb7tOZ4P64GyQTlmtZING4Jx0jRCquDjKOAIkkiBrOfS+/vrrE1He2dTNZllJ29AMG9jU32adacgBAnmyrNPZoPWu4zzbwPxftbqT/va3v63PSSaijijAJCAa8Rvf+IaOkCTyet9991ULFy6skiHi8cgjj9RRiJR7/PHH62jFRg1D9F8SZC2H3r/85S8TDSybutksK2kbmmEDm/rbrDMNuSRO2tkgHblWuo7zbAPzf9XqTpqbwac+9Sn1X//1X4l/c+mll6of/vCH6qc//al67LHHdKrERz/6UR34IsBBP/PMM+ree+9Vd9xxh3rooYfUiSee2LC+f/zjH3Mtl3WdRdc/qZxN/W3X6WxQH5wN0imrlWzQEEoFw3XXXVdad911Y+V6enpKY8aMKV122WXlz959993SwIEDS7/5zW/03//4xz90CP0TTzxRlvnf//3fUltbW+nf//53YLmvvfaa/s0///nPyPo//OEPJ2pP1nKSNhCXQmZbN5tlJW1DM2xgU3+bddqUW7ZsWemCCy7QL66HPOnWKjZopes4rzbAB6B/3DXQKPpsdDcJ6ZACsMQtIGF+2rRp6tFHH1WHH364/p8lbtJIBMiTwM7M+5Of/GTgEjqQBH0BifC8TDkhM4hC1nKQB9A+IWXISjebZSVtQzNsYFN/m3XalBNSEUAbokgdnA3SkWul6zivNhBSFvEJqaHUR2fSDz/8sH7Kef3116s+/9SnPlX69Kc/rd9/+9vfLm211Va9fjtq1KjSj3/848ByX3zxRV2ue7mXe7mXe7nXiy++WOqzM+lzzz1XXXLJJZEyCxYsUFtvvbXKC6Cyg8YOaj0o8sJm0g4ODg4OfRelUklTjEIlmiaa6qTPOussdeyxx0bKwAVbD+QsUHhZie4W8PeOO+5YlvHnwgnnbthZosJP6+Dg4ODQ2lh33XVTr6OpThpidF5pAJJ1HO2f//znslOG9J29ZokQ32WXXfR+yKxZs9TUqVP1Z/fff78m42fv2sHBwcHBoZkoTAoWJ57MmTNH/8+JJ7znZeY0syz++9//Xr9nKfr0009XF110kbrtttvUvHnz1NFHH62XJj7xiU9omYkTJ6r9999fnXDCCerxxx9XDz/8sDrllFN0UFnaSxgODg4ODg5xKEx0N6QkJL4LJk+erP9/4IEH1F577aXfP/fcc+WIO3DOOefoo9nIe2bGvNtuu6m7775bH18muOGGG7Rj3mefffRS9mGHHaZzqx0cHBwcHJqOVMPSCoiLLrqotMsuu5QGDx6cKIpccrJnzJih87IHDRpU2meffUrPP/98lQy5gEcccURp+PDhutzPf/7zOt80DdRal+T7Bb1++9vfluWCvpec82bqD/bcc89eun3xi1+sknnllVdKBx54oLYtEfxnn312omPy0tYf+VNOOUVnGjB+xo0bV/ryl7+s8/pNpNn/P/rRj0rjx4/XPAI77bRT6bHHHouUZ1xMmDBBy2+33XalO++8s+ZrwjZqacM111xT2m233UrrrbeefqGfX/6YY47p1d8f/ehHc6E/WS5+3fhdM21Qi/5B1ysvrs9m9P+DDz5Y+tjHPlbaaKONdD2///3vY3/DMZuTJ08uDRgwQOdUY5NGr6sgOCftwze+8Y3SlVdeWTrzzDMTO+nvfve7WvYPf/hD6emnny4dfPDBpc0226y0cuXKssz+++9f2mGHHUp///vfS3/9619LW2yxRemzn/1sKm2ota6urq7SG2+8UfX61re+VRo2bFiVc2HwMhBNObONzdJfLvoTTjihSjfznFfaiDPZd999S0899VTprrvuKm2wwQalr33ta03Xf968eaVDDz20dNttt5VeeOGF0p///OfSlltuWTrssMOq5NLq/5tuuknfaK699trSM888o/sRx7Vo0aLQ9MaOjo7SpZdeqgmBzj///FL//v11O2q5Jmyi1jbwEHX11VfrsbBgwYLSscceq/X917/+VeUksKXZ3++8804u9GccrLPOOlW6vfnmm1UyWdqgVv15MDV1nz9/vh5TpqPLsv/vuuuu0nnnnVe69dZbEznpl156qTRkyBDtJ7gGrrrqKq3/3XffXXefhME56Zwym9ULW3XtuOOOegZoIukTZjP0x0mfdtppkRdhe3t71Y3sJz/5ib7RdXZ2Nl3/oJkqF7g500+r/3nCP/nkk8t/d3d3lzbeeOPSxRdfHCgPz8BBBx1U9dm0adPKKxdJrolmt8EPHuJY+fjlL39Z5SQOOeSQUhaoVf+4+1PWNmi0/7/3ve/p/l++fHlT+t9EkuvsnHPOKW277bZVn33mM5+pmuk32ieCwgSOFZXZDMQxm9mEjbqIdicoj8NG/Dj55JPVBhtsoHbaaSd17bXXWmfbaUR/4gvQbbvttlNf+9rXymceS7nbb7+9Gj16dPkzeNyJ+Ie7PQ/6myC2gkNf+vXrl2r/w4mPvc3xi678LePXDz435aUvRT7JNWET9bTBD8bKmjVr1MiRI6s+/8tf/qI23HBDNWHCBJ0VsmTJktzoT9AsZx6PGzdOHXLIIVXjOEsb2Oj/X/ziFzpgl/MVsu7/ehB3Ddjok8IFjuUVXAjAvPnL3/Id/zPQTHDz5YYgMjb1abQuLhgi3zl1zMTMmTPV3nvvrWkg77nnHnXSSSfpG8Wpp57adP2POOIIfcMiKn/u3Lnqq1/9qg4kvPXWW8vlBtlIvmu2/ibefvttdeGFF/Y66CWN/qcusiWC+ubZZ58N/E1YX5rjXT4Lk7GJetrgB+OFsWPeVMn8OPTQQ3U6JwRGX//619UBBxygb7IdHR1N1R+nxUPapEmT9APd5Zdfrq9XHPUmm2ySqQ0a7X8ya+bPn6/vOyay6v96EHYN8NDPEZdLly5teEy2lJMuIrNZvW1oFAywG2+8Uc2YMaPXd+ZnRNcTOX/ZZZclchJp6286NGbMENgQsc/F/eEPf1gVpf+5yA866CC1zTbbqAsuuMBa/zuE47vf/a666aab9KzNzPxgZmeOKRwiYwk5xlYzAccDLwEOmgfrn/3sZ/oBr0jAOdO/rA6ZyHP/Z4mWcNJFZDartw2N1nXLLbfopT9yyuPA0hk3BM5TjaNEzUp/Uzfwwgsv6Aub3/LEbgIbgSTlZqE/FIPMHoYPH67z/aGetdX/YWDpnFmJ9IWAv8P05fMo+STXhE3U0wYBM1Cc9H333aedQJx9qYsxZdNJNKK/gLHCgxu6ZW2DRvTnQZMHJFaJ4rB5Sv1fD8KuAbaoBg8erPujUZuWUdMOdguh1sCxyy+/vPwZUcVBgWNPPvlkWeZPf/pTqoFj9dZFAJY/qjgqXW3EiBElm7DVV3/72990OUS1moFjZmTlz372Mx04tmrVqqbrz5jZeeeddf+vWLEi0/4nwIUUMDPAZezYsZGBY6SrmCBt0R84FnVN2EatbQCXXHKJtv+jjz6aqA6OJMSOf/zjH0t50N8f+EZK3BlnnNEUG9SrP/dZdHr77beb2v/1BI6RLWKCDA5/4FgjNi3rU5N0C4BcWtIyJAWJ97zMVCQuBkL1zVQHQusZPHPnztURiUEpWOTUkSeHAyHFJs0UrKi6SDOhDf6cvYULF+qLgGhkP0gP+vnPf67TbJDjlDBSEEhZa7b+pC3NnDlTO0ZyvrHD5ptvXtpjjz16pWDtt99+pTlz5uhUCXKl00rBqkV/bp5ER2+//fa6LWbKCXqn3f+kinCjvP766/VDxoknnqjHs0TCf+5znyude+65VSlY/fr10w6A9KVvfvObgSlYcdeETdTaBvQjev6WW26p6m+5zvmfPHocOGPqvvvuK02ZMkXb0uZDXb36c3/i4Y8TmGbNmlU6/PDDdS40qT7NsEGt+gvIVScq2o+s+3/ZsmXlez1OmjRc3uMPALrTBn8K1le+8hV9DZDOF5SCFdUnSeGctA9BCfS8SFz356v6SQNGjx6tjQJpwHPPPdcrL5AbNY6fp/fjjjsuVTKTqLqEvMRsE8BhQaTBE58fOG7Ssihz6NChOg/4pz/9aaBs1vq/+uqr2iGPHDlS9z95yVw8Zp40ePnll0sHHHCAJjMhR/qss85KjcykFv35P4xMBtks+p88z0033VQ7LmYA5HgLmN1zXfhTxCBfQZ5UlDAyk6hrwjZqaQMEE0H9zQMH+OCDD/QDHQ9yPIAgT55rrTfYtPQ//fTTy7L0MSQgs2fPbqoNah1Dzz77rO7ze+65p1dZWff/AyHXoOjM/7TB/xuuSdrLpCCIzCSqT5KijX9srNE7ODg4ODg42IXLk3ZwcHBwcMgpnJN2cHBwcHDIKZyTdnBwcHBwyCmck3ZwcHBwcMgpnJN2cHBwcHDIKZyTdnBwcHBwyCmck3ZwcHBwcMgpnJN2cHBwcHDIKZyTdnBwqMJee+2lTj/99Gar4eDg4Jy0g0M+wYlbn/jEJ/T7t956Sx94v+mmm+rTrjhFhwPmH3744arfPPLII+rAAw9UI0aM0EcucrzflVdeqc+1NdHW1lZ+rbvuumrXXXdV999/vyoyOL6Q9rz77rvNVsXBwSqck3ZwyDkOO+ww9dRTT6lf/vKX6vnnn1e33Xabnu0uWbKkLMPRlnvuuafaZJNN1AMPPKAPlj/ttNPURRddpM/l9bP/XnfddeqNN97Qjp7j/z72sY+pl156qQmtc3BwiET9lOQODg5pAUJ/Ti1aunSpJvr/y1/+Eiq7fPny0vrrr1869NBDe33H6Vn8nhN5wo7i4whNPuPADsBBAqeddlr5e04d4jCSjTfeWJ/8w0EB5uEsHDPIKUx8z+ElnDZ24403Vulx88036885qYmDUDjsAb3B448/Xtp33311GziQhMNSONnJBPpxCtgnPvEJXQeHqMiRhXJgSdDBCA4ORYebSTs45BjDhg3Trz/84Q+qs7MzUOaee+7Rs+qzzz6713cf//jH1VZbbaV+85vfhNbBIfVg9erVgd+fcsop6tFHH1U33XSTmjt3rvrUpz6l9t9/f7Vw4UL9/apVq9TUqVPVnXfeqebPn69OPPFE9bnPfU49/vjj+ntm7J/97GfV5z//ebVgwQK9NH3ooYeWZ/fLli1TxxxzjPrb3/6m/v73v6stt9xSL9vzuYlvfetb6tOf/rTWge+PPPJI9c4776hx48ap3/3ud1rmueee0/X94Ac/SNjDDg45R7OfEhwcHMJn0oAzj0eMGKFnodOnT9dHij799NNV5wZzKTPrDsLBBx9cmjhxYuBMesWKFaWTTjpJn4UrZZozac7T5Ttm2yaYCUedxX3QQQfp2TdgVkydHBWaBBy/OXz48NLtt99epfP5559f/ptZOJ/J2edy1GBYHzg4FBVuJu3gUIA96ddff13vRTODZSY6ZcoUdf3111fJ1XLqLDNbZujDhw/Xs9Bf/OIXatKkSb3k5s2bpwPPmI3LrJ7Xgw8+qF588UUtw/cXXnihDlQbOXKk/v5Pf/qTevXVV/X3O+ywg9pnn33098zCf/7zn6ulS5eW61i0aJE64YQT9AyaQLZ11llHLV++vPx7ganf0KFDtdzixYtr6EkHh+KhX7MVcHBwiAfR2v/3//5f/ZoxY4b6whe+oL75zW/qKHAcKGApefr06b1+y+fbbLNN1Wff+9731L777qud4qhRo0LrxVl2dHSoWbNm6f9N4IzBZZddppeXv//972tHjAMlhUuWz/ndvffeq6PPWZq/6qqr1Hnnnacee+wxtdlmm+mlbpbrKWP8+PE6gn2XXXbptfzev3//qr+J5u7p6am5Lx0cigQ3k3ZwKCBwuitWrNDv99tvPz2DveKKK3rJMftm75iZswnSuLbYYotIBw0mT56sZ8rMWJE3X5QBiBA/5JBD1FFHHaVnzZtvvrmOQvc7VFK92FcmUn3AgAE6Il1+f+qpp+p95m233VY76bfffrum/qA84E83c3AoOpyTdnDIMZhh7r333uq///u/dcDUP//5T3XzzTerSy+9VDtGwMz1Zz/7mfrjH/+og7aQe/nll/USNjPt//zP/9QBV/WAWToBWkcffbS69dZbdf0EhF188cU6UAywTC0zZWbtX/ziF/UStoAZ83e+8x315JNP6iVsyiH3e+LEieXf//rXv9a/RZb6JJgtKZiB8yBwxx136LJZAXBw6AtwTtrBIcdgSXnatGl6eXqPPfZQ2223nV7uZg/3Rz/6UVkOR0x+NE5w9913VxMmTNC/YVmZqGwcWL0gpxonfdZZZ+lyIVl54oknNLkKOP/88/UeOQQr5G8zwxYiFsDe8UMPPaRnyjh95Jn1H3DAAfp7HibYo6YMosKZVW+44YY16Th27Fg9Sz/33HPV6NGjdUS6g0NfQBvRY81WwsHBwcHBwaE33EzawcHBwcEhp3BO2sHBwcHBIadwTtrBwcHBwSGncE7awcHBwcEhp3BO2sHBwcHBIadwTtrBwcHBwSGncE7awcHBwcEhp3BO2sHBwcHBIadwTtrBwcHBwSGncE7awcHBwcEhp3BO2sHBwcHBQeUT/x+MwoOokuR+kAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "msn2 = MultiSkewNorm()\n", + "msn2.define_dp(\n", + " xi=np.array([0.06534, 0.628637]),\n", + " omega=np.array([[0.14890315, -0.06423752], [-0.06423752, 0.10139612]]),\n", + " alpha=np.array([0.79105, -0.767217]),\n", + ")\n", + "msn2.sample(n=1000, return_sample=True)\n", + "msn2.summary()\n", + "msn2.sspy_plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74ee3dde", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv (3.12.5)", + "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.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/example_settings.yaml b/docs/tutorials/example_settings.yaml index 93a79159..3c18843c 100644 --- a/docs/tutorials/example_settings.yaml +++ b/docs/tutorials/example_settings.yaml @@ -7,120 +7,120 @@ version: "1.1" # Supported metrics: LAeq, LZeq, LCeq, SEL # Supported stats: avg/mean, max, min, kurt, skew, any integer from 1-99 AcousticToolbox: - # A-weighted equivalent continuous sound level - LAeq: - run: true # Set to false to skip this metric - main: "avg" # Main statistic to calculate - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] # List of statistics to calculate - channel: ["Left", "Right"] # Channels to analyze - label: "LAeq" # Label for the metric in output - func_args: # Additional arguments for the metric function - time: 0.125 # Time interval for calculation (seconds) - method: "average" # Method for calculating Leq + # A-weighted equivalent continuous sound level + LAeq: + run: true # Set to false to skip this metric + main: "avg" # Main statistic to calculate + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] # List of statistics to calculate + channel: ["Left", "Right"] # Channels to analyze + label: "LAeq" # Label for the metric in output + func_args: # Additional arguments for the metric function + time: 0.125 # Time interval for calculation (seconds) + method: "average" # Method for calculating Leq - # Z-weighted (unweighted) equivalent continuous sound level - LZeq: - run: true - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "LZeq" - func_args: - time: 0.125 - method: "average" + # Z-weighted (unweighted) equivalent continuous sound level + LZeq: + run: true + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "LZeq" + func_args: + time: 0.125 + method: "average" - # C-weighted equivalent continuous sound level - LCeq: - run: true - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "LCeq" - func_args: - time: 0.125 - method: "average" + # C-weighted equivalent continuous sound level + LCeq: + run: true + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "LCeq" + func_args: + time: 0.125 + method: "average" - # Sound Exposure Level - SEL: - run: true - channel: ["Left", "Right"] - label: "SEL" - # Note: SEL doesn't use main or statistics as it's a single value metric + # Sound Exposure Level + SEL: + run: true + channel: ["Left", "Right"] + label: "SEL" + # Note: SEL doesn't use main or statistics as it's a single value metric # MoSQITo Library Settings # Supported metrics: loudness_zwtv, sharpness_din_from_loudness, sharpness_din_perseg, sharpness_din_tv, roughness_dw # Supported statistics: avg/mean, max, min, kurt, skew, any integer from 1-99 MoSQITo: - # Zwicker Time-Varying Loudness - loudness_zwtv: - run: false # Disabled by default as it's used in sharpness calculation - main: 5 # N5 (loudness exceeded 5% of the time) - statistics: [10, 50, 90, 95, "min", "max", "kurt", "skew", "avg"] - channel: ["Left", "Right"] - label: "N" - parallel: true # Enable parallel processing - func_args: - field_type: "free" # Free field condition + # Zwicker Time-Varying Loudness + loudness_zwtv: + run: false # Disabled by default as it's used in sharpness calculation + main: 5 # N5 (loudness exceeded 5% of the time) + statistics: [10, 50, 90, 95, "min", "max", "kurt", "skew", "avg"] + channel: ["Left", "Right"] + label: "N" + parallel: true # Enable parallel processing + func_args: + field_type: "free" # Free field condition - # Sharpness (DIN 45692) calculated from Zwicker Loudness - sharpness_din_from_loudness: - run: false - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "S_L" - parallel: true - func_args: - weighting: "din" # DIN 45692 weighting - field_type: "free" + # Sharpness (DIN 45692) calculated from Zwicker Loudness + sharpness_din_from_loudness: + run: false + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "S_L" + parallel: true + func_args: + weighting: "din" # DIN 45692 weighting + field_type: "free" - # Sharpness (DIN 45692) calculated per segment - sharpness_din_perseg: - run: false - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "S_perseg" - parallel: false # Parallel processing not necessary for this method - func_args: - weighting: "din" - nperseg: 4096 # Number of samples per segment - field_type: "free" + # Sharpness (DIN 45692) calculated per segment + sharpness_din_perseg: + run: false + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "S_perseg" + parallel: false # Parallel processing not necessary for this method + func_args: + weighting: "din" + nperseg: 4096 # Number of samples per segment + field_type: "free" - # Sharpness (DIN 45692) time-varying - # Note: It's recommended to use sharpness_din_from_loudness instead - sharpness_din_tv: - run: false - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "S_din_tv" - parallel: true - func_args: - weighting: "din" - field_type: "free" - skip: 0.5 # Skip time at the beginning (seconds) + # Sharpness (DIN 45692) time-varying + # Note: It's recommended to use sharpness_din_from_loudness instead + sharpness_din_tv: + run: false + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "S_din_tv" + parallel: true + func_args: + weighting: "din" + field_type: "free" + skip: 0.5 # Skip time at the beginning (seconds) - # Roughness (Daniel & Weber method) - roughness_dw: - run: false - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "R" - parallel: true + # Roughness (Daniel & Weber method) + roughness_dw: + run: false + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "R" + parallel: true # scikit-maad Library Settings # These are collections of multiple acoustic indices scikit-maad: - # Temporal alpha diversity indices - all_temporal_alpha_indices: - run: true - channel: ["Left", "Right"] - # Note: This calculates multiple indices, so no individual statistics are specified + # Temporal alpha diversity indices + all_temporal_alpha_indices: + run: true + channel: ["Left", "Right"] + # Note: This calculates multiple indices, so no individual statistics are specified - # Spectral alpha diversity indices - all_spectral_alpha_indices: - run: true - channel: ["Left", "Right"] - # Note: This calculates multiple indices, so no individual statistics are specified + # Spectral alpha diversity indices + all_spectral_alpha_indices: + run: true + channel: ["Left", "Right"] + # Note: This calculates multiple indices, so no individual statistics are specified diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 2648a050..274258f3 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -1,4 +1,3 @@ # Tutorials Each of the tutorials provided will walk you through the basic functionality of _Soundscapy_ as well as the basic concepts of Soundscape analysis. - diff --git a/mkdocs.yml b/mkdocs.yml index a14e2322..c25ca9ee 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,36 +2,57 @@ site_name: "Soundscapy" site_url: https://soundscapy.readthedocs.io/en/latest/ site_dir: site +site_author: "Andrew Mitchell" +site_description: "Documentation website for Soundscapy" repo_name: "MitchellAcoustics/Soundscapy" repo_url: https://github.com/MitchellAcoustics/Soundscapy -copyright: Copyright © 2024 Andrew Mitchell +copyright: Copyright © 2025 Andrew Mitchell + +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + nav: - - Home: index.md - - About: - - 'License': license.md - - Tutorials: - - tutorials/index.md - - '`Soundscapy` - Quick Start': tutorials/QuickStart.ipynb - - 'How To Analyse and Represent Soundscape Perception': tutorials/HowToAnalyseAndRepresentSoundscapes.ipynb - - 'Using Soundscapy for Binaural Recording Analysis': tutorials/BinauralAnalysis.ipynb - - 'API reference': - - 'Survey Analysis': reference/surveys.md - - 'Plotting': reference/plotting.md - - 'Binaural Analysis': reference/audio.md - - 'Databases': reference/databases.md - - 'News': + - Home: index.md + - About: + - "License": license.md + - Tutorials: + - tutorials/index.md + - "Quick Start": tutorials/QuickStart.ipynb + - "How To Analyse and Represent Soundscape Perception": tutorials/HowToAnalyseAndRepresentSoundscapes.ipynb + - "The Soundscape Perception Index (SPI)": tutorials/SoundscapePerceptionIndex-SPI.ipynb + - "Using Soundscapy for Binaural Recording Analysis": tutorials/BinauralAnalysis.ipynb + - "API reference": + - "Core": reference/api.md + - "Survey Analysis": reference/surveys.md + - "Plotting": + - reference/plotting.md + - reference/plotting/backends.md + - "Soundscape Perception Index (SPI)": reference/spi.md + - "Binaural Analysis": reference/audio.md + - "Databases": reference/databases.md + - "News": - news.md - - 'Changelog': CHANGELOG.md + - "Changelog": CHANGELOG.md + theme: name: material -# logo: img/DarkLogo.png + # logo: img/DarkLogo.png features: - navigation.tabs - navigation.expand - navigation.path - navigation.top + - content.action.edit + icon: + repo: fontawesome/brands/github palette: # Palette toggle for light mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode - media: "(prefers-color-scheme: light)" scheme: default toggle: @@ -43,37 +64,51 @@ theme: scheme: slate toggle: icon: material/brightness-4 - name: Switch to light mode + name: Switch to system preference + extra_css: - stylesheets/extra.css - https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.css plugins: - - search - - mkdocstrings: - default_handler: python - handlers: - python: - paths: [.] - options: - docstring_section_style: spacy - docstring_style: "numpy" - separate_signature: true - show_if_no_docstring: false - merge_init_into_class: true - show_symbol_type_heading: true # waiting for general release - show_symbol_type_toc: true - - mkdocs-jupyter: - include_source: true - theme: default + - search + - mkdocstrings: + default_handler: python + handlers: + python: + paths: [.] + options: + docstring_section_style: spacy + docstring_style: "numpy" + separate_signature: true + show_if_no_docstring: false + merge_init_into_class: true + show_symbol_type_heading: true # waiting for general release + show_symbol_type_toc: true + modernize_annotations: true + summary: true + inventories: + - "https://docs.python.org/3/objects.inv" + - include-markdown: + opening_tag: "{!" + closing_tag: "!}" + - mkdocs-jupyter: + include_source: true + theme: default # execute: true markdown_extensions: - admonition + - pymdownx.tasklist - pymdownx.arithmatex: - generic: true + generic: true extra_javascript: - javascripts/katex.js - https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.js - https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/contrib/auto-render.min.js + +extra: + social: + - icon: fontawesome/brands/github + link: "https://github.com/MitchellAcoustics" diff --git a/pyproject.toml b/pyproject.toml index 3348e788..1d7d6163 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,137 +1,156 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools-scm>=8", "setuptools>=64"] + +[dependency-groups] +dev = [ + "build>=1.2.2.post1", + "mypy>=1.15.0", + "pandas-stubs>=2.2.3.250308", + "pre-commit>=4.2.0", + "ruff>=0.7.2", + "scipy-stubs>=1.15.2.2", + "setuptools-scm>=8.3.1", + "tox>=4.25.0", + "twine>=6.1.0", + "types-pyyaml>=6.0.12.20250402", +] +docs = [ + "ipywidgets>=8.1.3", + "jupyter-dash>=0.4.2", + "jupyter>=1.1.1", + "mkdocs-include-markdown-plugin>=7.1.5", + "mkdocs-jupyter>=0.24.8", + "mkdocs-material>=9.5.31", + "mkdocs>=1.6.0", + "mkdocstrings[python]>=0.25.2", + "pymdown-extensions>=10.9", +] +test = [ + "nbmake>=1.5.4", + "pytest-cov>=6.0.0", + "pytest-mpl>=0.17.0", + "pytest-xdist>=3.6.1", + "pytest>=8.3.3", + "setuptools>=72.1.0", + "xdoctest[all]>=1.1.6", +] + [project] -name = "soundscapy" -version = "0.7.9dev0" -description = "A python library for analysing and visualising soundscape assessments." authors = [ - { name = "Andrew Mitchell", email = "a.j.mitchell@ucl.ac.uk" } + {email = "mitchellacoustics15@gmail.com", name = "Andrew Mitchell"}, +] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Typing :: Typed", + "Operating System :: OS Independent", + "License :: OSI Approved :: BSD License", ] dependencies = [ + "loguru>=0.7.2", + "numpy!=1.26", "pandas[excel]>=2.2.2", - "seaborn>=0.13.2", "plotly>=5.23.0", - "scipy>=1.14.1", - "pyyaml>=6.0.2", "pydantic>=2.8.2", - "loguru>=0.7.2", - "numpy!=1.26", + "pyyaml>=6.0.2", + "scipy>=1.14.1", + "seaborn>=0.13.2", ] +description = "A python library for analysing and visualising soundscape assessments." +dynamic = ["version"] +keywords = ["acoustics", "audio analysis", "psychoacoustics", "soundscape"] +license = {file = "LICENSE.md"} +name = "soundscapy" readme = "README.md" requires-python = ">= 3.10" -license = { text = "BSD-3-Clause" } -keywords = [ - "soundscape", - "psychoacoustics", - "acoustics", - "audio analysis", -] -classifiers = [ - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", - "License :: OSI Approved :: BSD License", -] - -[project.urls] -repository = "https://github.com/MitchellAcoustics/Soundscapy" -documentation = "https://soundscapy.readthedocs.io/en/latest/" [project.optional-dependencies] -all = [ - "soundscapy[audio]", -] +all = ["soundscapy[audio]", "soundscapy[spi]"] audio = [ + "acoustic-toolbox>=0.1.2", "mosqito>=1.2.1", + "numba>=0.59", "scikit-maad>=1.4.3", "tqdm>=4.66.5", - "numba>=0.59", - "acoustic-toolbox>=0.1.2", ] +spi = ["rpy2>=3.5.0"] -[tool.uv] -default-groups = ["dev", "test", "docs"] +[project.urls] +documentation = "https://soundscapy.readthedocs.io/en/latest/" +repository = "https://github.com/MitchellAcoustics/Soundscapy" -[dependency-groups] -dev = [ - "bumpver>=2023.1129", - "ruff>=0.7.2", -] -test = [ - "pytest>=8.3.3", - "setuptools>=72.1.0", - "nbmake>=1.5.4", - "pytest-xdist>=3.6.1", - "xdoctest[all]>=1.1.6", - "pytest-mpl>=0.17.0", - "pytest-cov>=6.0.0", -] -docs = [ - "jupyter>=1.1.1", - "mkdocs>=1.6.0", - "mkdocs-material>=9.5.31", - "mkdocs-jupyter>=0.24.8", - "mkdocstrings[python]>=0.25.2", - "pymdown-extensions>=10.9", - "ipywidgets>=8.1.3", - "jupyter-dash>=0.4.2", -] +[tool.coverage] +report = {sort = "cover"} +run = {branch = true, parallel = true, source = ["soundscapy"]} +paths.source = ["src", ".tox*/*/lib/python*/site-packages"] -[tool.uv.pip] -universal = true +[tool.mypy] +explicit_package_bases = true [tool.pytest.ini_options] -addopts = "-v --tb=short --durations=5 --xdoctest -n 6 --cov=src/soundscapy --cov-report=term" -testpaths = ["test", "src/soundscapy"] -python_files = "test_*.py" -python_classes = "Test*" -python_functions = "test_*" -console_output_style = "count" +addopts = ["--color=yes", "--import-mode=importlib", "--verbose", "--xdoctest"] doctest_optionflags = "NUMBER NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL" markers = [ "optional_deps(group): mark tests that depend on optional dependencies. group can be 'audio', etc.", - "slow: mark test as slow", + "parametrize: mark test as parametrized", "skip: mark test as skipped", "skipif: mark test as skipped if condition is met", + "slow: mark test as slow", "xfail: mark test as expected to fail", - "parametrize: mark test as parametrized" ] +python_classes = "Test*" +python_files = "test_*.py" +python_functions = "test_*" +testpaths = ["src/soundscapy", "test"] -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +[tool.ruff] +fix = true +force-exclude = true +lint.ignore = [ + "COM812", # trailing commas (ruff-format recommended) + "D203", # no-blank-line-before-class + "D212", # multi-line-summary-first-line + "D407", # removed dashes lines under sections + "D417", # argument description in docstring (unreliable) + "FIX002", # fixme (ruff-format recommended) + "ISC001", # simplify implicit str concatenation (ruff-format recommended) + "PLR0913", # too many arguments +] +lint.per-file-ignores = {"test*" = [ + "INP001", # File is part of an implicit namespace package. + "S101", # Use of `assert` detected +]} +lint.select = ["ALL"] +lint.isort.known-first-party = ["soundscapy"] +lint.mccabe.max-complexity = 18 +lint.pep8-naming.classmethod-decorators = ["classmethod"] -[tool.hatch.metadata] -allow-direct-references = true +[tool.setuptools.package-data] +soundscapy = ["*.csv", "*.yaml", "py.typed"] -[tool.hatch.build.targets.wheel] -packages = ["src/soundscapy"] -exclude = [ - "test/data", - "test/test_audio_files", - "*.wav", - "test/baseline", - "docs/tutorials", - "docs/img" -] +[tool.setuptools.packages.find] +where = ["src"] -[tool.hatch.build.targets.sdist] -exclude = [ - "test/test_audio_files", - "test/data", - "*.wav", - "docs/tutorials", - "docs/img" -] +[tool.setuptools_scm] +local_scheme = "no-local-version" +write_to = "src/soundscapy/_version.py" -[tool.bumpver] -current_version = "v0.7.9dev0" -version_pattern = "vMAJOR.MINOR.PATCH[[-]PYTAGNUM]" -commit_message = "bump version {old_version} -> {new_version}" -tag_message = "{new_version}" -commit = true -tag = true -push = true +[tool.tomlsort] +all = true +spaces_indent_inline_array = 4 +trailing_comma_inline_array = true +overrides."project.classifiers".inline_arrays = false +overrides."tool.coverage.paths.source".inline_arrays = false +overrides."tool.tox.env.docs.commands".inline_arrays = false +overrides."tool.tox.env_run_base.commands".inline_arrays = false -[tool.bumpver.file_patterns] -"pyproject.toml" = [ - '^current_version = "{version}"', - '^version = "{pep440_version}"', -] +[tool.uv] +default-groups = ["dev", "docs", "test"] + +[tool.uv.pip] +universal = true diff --git a/schemas/github-issue-forms.json b/schemas/github-issue-forms.json new file mode 100644 index 00000000..b890ff13 --- /dev/null +++ b/schemas/github-issue-forms.json @@ -0,0 +1,2377 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + + "$id": "https://json.schemastore.org/github-issue-forms.json", + + "$comment": "https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms", + + "additionalProperties": false, + + "definitions": { + "type": { + "description": "A form item type\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#keys", + + "type": "string", + + "enum": ["checkboxes", "dropdown", "input", "markdown", "textarea"] + }, + + "id": { + "type": "string", + + "pattern": "^[a-zA-Z0-9_-]+$", + + "examples": ["SampleId"] + }, + + "validations": { + "title": "validation options", + + "type": "object", + + "properties": { + "required": { + "description": "Specify whether require a form item", + + "type": "boolean", + + "default": false + } + }, + + "additionalProperties": false + }, + + "assignee": { + "type": "string", + + "maxLength": 39, + + "pattern": "^[a-zA-Z0-9](-?[a-zA-Z0-9])*$", + + "examples": ["SampleAssignee"] + }, + + "label": { + "type": "string", + + "minLength": 1, + + "examples": ["Sample label"] + }, + + "description": { + "type": "string", + + "default": "", + + "examples": ["Sample description"] + }, + + "placeholder": { + "type": "string", + + "default": "", + + "examples": ["Sample placeholder"] + }, + + "value": { + "type": "string", + + "minLength": 1, + + "examples": ["Sample value"] + }, + + "form_item": { + "title": "form item", + + "description": "A form item\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#about-githubs-form-schema", + + "type": "object", + + "required": ["type"], + + "properties": { + "type": { + "$ref": "#/definitions/type" + } + }, + + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "markdown" + } + } + }, + + "then": { + "$comment": "For `additionalProperties` to work `type` must also be present here.", + + "title": "markdown", + + "description": "Markdown\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown", + + "type": "object", + + "required": ["type", "attributes"], + + "properties": { + "type": { + "$ref": "#/definitions/type" + }, + + "attributes": { + "title": "markdown attributes", + + "description": "Markdown attributes\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes", + + "type": "object", + + "required": ["value"], + + "properties": { + "value": { + "description": "A markdown code\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes", + + "type": "string", + + "minLength": 1, + + "examples": ["Sample code"] + } + }, + + "additionalProperties": false + } + }, + + "additionalProperties": false + } + }, + + { + "if": { + "properties": { + "type": { + "const": "textarea" + } + } + }, + + "then": { + "$comment": "For `additionalProperties` to work `type` must also be present here.", + + "title": "textarea", + + "description": "Textarea\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#textarea", + + "type": "object", + + "required": ["type", "attributes"], + + "properties": { + "type": { + "$ref": "#/definitions/type" + }, + + "id": { + "$ref": "#/definitions/id", + + "description": "A textarea id\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#keys" + }, + + "attributes": { + "title": "textarea attributes", + + "description": "Textarea attributes\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-1", + + "type": "object", + + "required": ["label"], + + "properties": { + "label": { + "$ref": "#/definitions/label", + + "description": "A short textarea description\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-1" + }, + + "description": { + "$ref": "#/definitions/description", + + "description": "A long textarea description\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-1" + }, + + "placeholder": { + "$ref": "#/definitions/placeholder", + + "description": "A textarea placeholder\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-1" + }, + + "value": { + "$ref": "#/definitions/value", + + "description": "A textarea value\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-1" + }, + + "render": { + "description": "A textarea syntax highlighting mode\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-1", + + "type": "string", + + "enum": [ + "1C Enterprise", + + "4D", + + "ABAP CDS", + + "ABAP", + + "ABNF", + + "AFDKO", + + "AGS Script", + + "AIDL", + + "AL", + + "AMPL", + + "ANTLR", + + "API Blueprint", + + "APL", + + "ASL", + + "ASN.1", + + "ASP.NET", + + "ATS", + + "ActionScript", + + "Ada", + + "Alloy", + + "Alpine Abuild", + + "Altium Designer", + + "AngelScript", + + "Ant Build System", + + "ApacheConf", + + "Apex", + + "Apollo Guidance Computer", + + "AppleScript", + + "Arc", + + "AsciiDoc", + + "AspectJ", + + "Assembly", + + "Astro", + + "Asymptote", + + "Augeas", + + "AutoHotkey", + + "AutoIt", + + "AutoIt3", + + "AutoItScript", + + "Avro IDL", + + "Awk", + + "BASIC", + + "Ballerina", + + "Batchfile", + + "Beef", + + "Befunge", + + "BibTeX", + + "Bicep", + + "Bison", + + "BitBake", + + "Blade", + + "BlitzBasic", + + "BlitzMax", + + "Boo", + + "Boogie", + + "Brainfuck", + + "Brightscript", + + "Browserslist", + + "C", + + "C#", + + "C++", + + "C-ObjDump", + + "C2hs Haskell", + + "CIL", + + "CLIPS", + + "CMake", + + "COBOL", + + "CODEOWNERS", + + "COLLADA", + + "CSON", + + "CSS", + + "CSV", + + "CUE", + + "CWeb", + + "Cabal Config", + + "Cabal", + + "Cap'n Proto", + + "Carto", + + "CartoCSS", + + "Ceylon", + + "Chapel", + + "Charity", + + "ChucK", + + "Cirru", + + "Clarion", + + "Classic ASP", + + "Clean", + + "Click", + + "Clojure", + + "Closure Templates", + + "Cloud Firestore Security Rules", + + "CoNLL", + + "CoNLL-U", + + "CoNLL-X", + + "ColdFusion CFC", + + "ColdFusion", + + "Common Lisp", + + "Common Workflow Language", + + "Component Pascal", + + "Containerfile", + + "Cool", + + "Coq", + + "Cpp-ObjDump", + + "Crystal", + + "Csound Document", + + "Csound Score", + + "Csound", + + "Cuda", + + "Cue Sheet", + + "Cycript", + + "Cython", + + "D-ObjDump", + + "DIGITAL Command Language", + + "DM", + + "DTrace", + + "Dafny", + + "Darcs Patch", + + "Dart", + + "DataWeave", + + "Dhall", + + "Diff", + + "Dlang", + + "Dockerfile", + + "Dogescript", + + "Dylan", + + "E", + + "E-mail", + + "EBNF", + + "ECL", + + "ECLiPSe", + + "EJS", + + "EQ", + + "Eagle", + + "Earthly", + + "Easybuild", + + "Ecere Projects", + + "EditorConfig", + + "Eiffel", + + "Elixir", + + "Elm", + + "Emacs Lisp", + + "EmberScript", + + "Erlang", + + "F#", + + "F*", + + "FIGfont", + + "FIGlet Font", + + "FLUX", + + "Factor", + + "Fancy", + + "Fantom", + + "Faust", + + "Fennel", + + "Filebench WML", + + "Filterscript", + + "Fluent", + + "Formatted", + + "Forth", + + "Fortran Free Form", + + "Fortran", + + "FreeBasic", + + "Frege", + + "Futhark", + + "G-code", + + "GAML", + + "GAMS", + + "GAP", + + "GCC Machine Description", + + "GDB", + + "GDScript", + + "GEDCOM", + + "GLSL", + + "GN", + + "Game Maker Language", + + "Gemfile.lock", + + "Genie", + + "Genshi", + + "Gentoo Eclass", + + "Gerber Image", + + "Gettext Catalog", + + "Gherkin", + + "Git Config", + + "Glyph Bitmap Distribution Format", + + "Glyph", + + "Gnuplot", + + "Go Checksums", + + "Go Module", + + "Go", + + "Golo", + + "Gosu", + + "Grace", + + "Gradle", + + "Grammatical Framework", + + "Graph Modeling Language", + + "GraphQL", + + "Graphviz (DOT)", + + "Groovy Server Pages", + + "Groovy", + + "HAProxy", + + "HCL", + + "HTML", + + "HTML+ECR", + + "HTML+EEX", + + "HTML+ERB", + + "HTML+PHP", + + "HTML+Razor", + + "HTTP", + + "HXML", + + "Hack", + + "Haml", + + "Handlebars", + + "Harbour", + + "HashiCorp Configuration Language", + + "Haskell", + + "Haxe", + + "HiveQL", + + "HolyC", + + "Hy", + + "IDL", + + "IGOR Pro", + + "IPython Notebook", + + "Idris", + + "Ignore List", + + "ImageJ Macro", + + "Inform 7", + + "Io", + + "Ioke", + + "Isabelle ROOT", + + "Isabelle", + + "J", + + "JAR Manifest", + + "JFlex", + + "JSON with Comments", + + "JSON", + + "JSON5", + + "JSONLD", + + "JSONiq", + + "Jasmin", + + "Java Properties", + + "Java Server Pages", + + "Java", + + "JavaScript", + + "JavaScript+ERB", + + "Jest Snapshot", + + "Jinja", + + "Jison Lex", + + "Jison", + + "Jolie", + + "Jsonnet", + + "Julia", + + "Jupyter Notebook", + + "Kaitai Struct", + + "KakouneScript", + + "KiCad Layout", + + "KiCad Legacy Layout", + + "KiCad Schematic", + + "Kit", + + "Kotlin", + + "Kusto", + + "LFE", + + "LLVM", + + "LOLCODE", + + "LSL", + + "LTspice Symbol", + + "LabVIEW", + + "Lark", + + "Lasso", + + "Lean", + + "Less", + + "Lex", + + "LilyPond", + + "Limbo", + + "Linker Script", + + "Linux Kernel Module", + + "Liquid", + + "Literate Agda", + + "Literate CoffeeScript", + + "Literate Haskell", + + "LiveScript", + + "Logos", + + "Logtalk", + + "LookML", + + "LoomScript", + + "Lua", + + "M", + + "M4", + + "M4Sugar", + + "MATLAB", + + "MAXScript", + + "MLIR", + + "MQL4", + + "MQL5", + + "MTML", + + "MUF", + + "Macaulay2", + + "Makefile", + + "Mako", + + "Markdown", + + "Marko", + + "Mathematica", + + "Max", + + "Mercury", + + "Meson", + + "Metal", + + "Microsoft Developer Studio Project", + + "Microsoft Visual Studio Solution", + + "MiniD", + + "Mirah", + + "Modelica", + + "Modula-2", + + "Modula-3", + + "Module Management System", + + "Monkey", + + "Moocode", + + "MoonScript", + + "Motoko", + + "Motorola 68K Assembly", + + "Muse", + + "Myghty", + + "NASL", + + "NCL", + + "NEON", + + "NPM Config", + + "NSIS", + + "NWScript", + + "Nearley", + + "Nemerle", + + "NeoSnippet", + + "NetLinx", + + "NetLinx+ERB", + + "NetLogo", + + "NewLisp", + + "Nextflow", + + "Nginx", + + "Ninja", + + "Nit", + + "Nix", + + "NumPy", + + "Nunjucks", + + "ObjDump", + + "Object Data Instance Notation", + + "ObjectScript", + + "Objective-C", + + "Objective-C++", + + "Objective-J", + + "Odin", + + "Omgrofl", + + "Opa", + + "Opal", + + "Open Policy Agent", + + "OpenCL", + + "OpenEdge ABL", + + "OpenQASM", + + "OpenRC runscript", + + "OpenSCAD", + + "OpenStep Property List", + + "OpenType Feature File", + + "Org", + + "Ox", + + "Oxygene", + + "Oz", + + "P4", + + "PEG.js", + + "PHP", + + "PLpgSQL", + + "POV-Ray SDL", + + "Pan", + + "Papyrus", + + "Parrot Assembly", + + "Parrot Internal Representation", + + "Parrot", + + "Pascal", + + "Pawn", + + "Pep8", + + "Perl", + + "Pickle", + + "PicoLisp", + + "PigLatin", + + "Pike", + + "PlantUML", + + "Pod 6", + + "Pod", + + "PogoScript", + + "Pony", + + "PostCSS", + + "PostScript", + + "PowerShell", + + "Prisma", + + "Processing", + + "Proguard", + + "Prolog", + + "Promela", + + "Propeller Spin", + + "Protocol Buffer", + + "Protocol Buffers", + + "Public Key", + + "Pug", + + "Puppet", + + "Pure Data", + + "PureBasic", + + "PureScript", + + "Python", + + "Q#", + + "QMake", + + "Qt Script", + + "Quake", + + "R", + + "RAML", + + "RDoc", + + "REALbasic", + + "REXX", + + "RMarkdown", + + "RPC", + + "RPM Spec", + + "Racket", + + "Ragel", + + "Raw token data", + + "ReScript", + + "Readline Config", + + "Reason", + + "Rebol", + + "Record Jar", + + "Red", + + "Redirect Rules", + + "Regular Expression", + + "RenderScript", + + "Rich Text Format", + + "Ring", + + "Riot", + + "RobotFramework", + + "Roff", + + "Rouge", + + "Rscript", + + "Ruby", + + "Rust", + + "SAS", + + "SCSS", + + "SELinux Kernel Policy Language", + + "SELinux Policy", + + "SMT", + + "SPARQL", + + "SQF", + + "SQL", + + "SQLPL", + + "SRecode Template", + + "SSH Config", + + "STON", + + "SVG", + + "SWIG", + + "Sage", + + "SaltStack", + + "Sass", + + "Scala", + + "Scaml", + + "Scheme", + + "Scilab", + + "Self", + + "ShaderLab", + + "Shell", + + "ShellCheck Config", + + "Sieve", + + "Singularity", + + "Slash", + + "Slice", + + "Slim", + + "SmPL", + + "Smalltalk", + + "SnipMate", + + "Solidity", + + "Soong", + + "SourcePawn", + + "Spline Font Database", + + "Squirrel", + + "Stan", + + "Standard ML", + + "Starlark", + + "StringTemplate", + + "Stylus", + + "SubRip Text", + + "SugarSS", + + "SuperCollider", + + "Svelte", + + "Swift", + + "SystemVerilog", + + "TI Program", + + "TLA", + + "TOML", + + "TSQL", + + "TSV", + + "TSX", + + "TXL", + + "Tcl", + + "Tcsh", + + "TeX", + + "Tea", + + "Terra", + + "Texinfo", + + "Text", + + "TextMate Properties", + + "Textile", + + "Thrift", + + "Turing", + + "Turtle", + + "Twig", + + "Type Language", + + "TypeScript", + + "UltiSnip", + + "UltiSnips", + + "Unified Parallel C", + + "Unity3D Asset", + + "Unix Assembly", + + "Uno", + + "UnrealScript", + + "Ur", + + "Ur/Web", + + "UrWeb", + + "V", + + "VBA", + + "VCL", + + "VHDL", + + "Vala", + + "Valve Data Format", + + "Verilog", + + "Vim Help File", + + "Vim Script", + + "Vim Snippet", + + "Visual Basic .NET", + + "Vue", + + "Wavefront Material", + + "Wavefront Object", + + "Web Ontology Language", + + "WebAssembly", + + "WebVTT", + + "Wget Config", + + "Wikitext", + + "Windows Registry Entries", + + "Wollok", + + "World of Warcraft Addon Data", + + "X BitMap", + + "X Font Directory Index", + + "X PixMap", + + "X10", + + "XC", + + "XCompose", + + "XML Property List", + + "XML", + + "XPages", + + "XProc", + + "XQuery", + + "XS", + + "XSLT", + + "Xojo", + + "Xonsh", + + "Xtend", + + "YAML", + + "YANG", + + "YARA", + + "YASnippet", + + "Yacc", + + "ZAP", + + "ZIL", + + "Zeek", + + "ZenScript", + + "Zephir", + + "Zig", + + "Zimpl", + + "abl", + + "abuild", + + "acfm", + + "aconf", + + "actionscript 3", + + "actionscript3", + + "ada2005", + + "ada95", + + "adobe composite font metrics", + + "adobe multiple font metrics", + + "advpl", + + "ags", + + "ahk", + + "altium", + + "amfm", + + "amusewiki", + + "apache", + + "apkbuild", + + "arexx", + + "as3", + + "asm", + + "asp", + + "aspx", + + "aspx-vb", + + "ats2", + + "au3", + + "autoconf", + + "b3d", + + "bash session", + + "bash", + + "bat", + + "batch", + + "bazel", + + "blitz3d", + + "blitzplus", + + "bmax", + + "bplus", + + "bro", + + "bsdmake", + + "byond", + + "bzl", + + "c++-objdump", + + "c2hs", + + "cURL Config", + + "cake", + + "cakescript", + + "cfc", + + "cfm", + + "cfml", + + "chpl", + + "clipper", + + "coccinelle", + + "coffee", + + "coffee-script", + + "coldfusion html", + + "console", + + "cperl", + + "cpp", + + "csharp", + + "csound-csd", + + "csound-orc", + + "csound-sco", + + "cucumber", + + "curlrc", + + "cwl", + + "dcl", + + "delphi", + + "desktop", + + "dircolors", + + "django", + + "dosbatch", + + "dosini", + + "dpatch", + + "dtrace-script", + + "eC", + + "ecr", + + "editor-config", + + "edn", + + "eeschema schematic", + + "eex", + + "elisp", + + "emacs muse", + + "emacs", + + "email", + + "eml", + + "erb", + + "fb", + + "fish", + + "flex", + + "foxpro", + + "fsharp", + + "fstar", + + "ftl", + + "fundamental", + + "gf", + + "git-ignore", + + "gitattributes", + + "gitconfig", + + "gitignore", + + "gitmodules", + + "go mod", + + "go sum", + + "go.mod", + + "go.sum", + + "golang", + + "groff", + + "gsp", + + "hbs", + + "heex", + + "help", + + "html+django", + + "html+jinja", + + "html+ruby", + + "htmlbars", + + "htmldjango", + + "hylang", + + "i7", + + "ignore", + + "igor", + + "igorpro", + + "ijm", + + "inc", + + "inform7", + + "inputrc", + + "irc logs", + + "irc", + + "java server page", + + "jq", + + "jruby", + + "js", + + "jsonc", + + "jsp", + + "kak", + + "kakscript", + + "keyvalues", + + "ksy", + + "lassoscript", + + "latex", + + "leex", + + "lhaskell", + + "lhs", + + "lisp", + + "litcoffee", + + "live-script", + + "ls", + + "m2", + + "m68k", + + "mIRC Script", + + "macruby", + + "mail", + + "make", + + "man page", + + "man", + + "man-page", + + "manpage", + + "markojs", + + "max/msp", + + "maxmsp", + + "mbox", + + "mcfunction", + + "mdoc", + + "mediawiki", + + "mf", + + "mma", + + "mumps", + + "mupad", + + "nanorc", + + "nasm", + + "ne-on", + + "nesC", + + "nette object notation", + + "nginx configuration file", + + "nixos", + + "njk", + + "node", + + "npmrc", + + "nroff", + + "nush", + + "nvim", + + "obj-c", + + "obj-c++", + + "obj-j", + + "objc", + + "objc++", + + "objectivec", + + "objectivec++", + + "objectivej", + + "objectpascal", + + "objj", + + "octave", + + "odin-lang", + + "odinlang", + + "oncrpc", + + "ooc", + + "openedge", + + "openrc", + + "osascript", + + "pandoc", + + "pasm", + + "pcbnew", + + "perl-6", + + "perl6", + + "pir", + + "plain text", + + "posh", + + "postscr", + + "pot", + + "pov-ray", + + "povray", + + "progress", + + "protobuf", + + "pwsh", + + "pycon", + + "pyrex", + + "python3", + + "q", + + "ql", + + "qsharp", + + "ragel-rb", + + "ragel-ruby", + + "rake", + + "raw", + + "razor", + + "rb", + + "rbx", + + "reStructuredText", + + "readline", + + "red/system", + + "redirects", + + "regex", + + "regexp", + + "renpy", + + "rhtml", + + "robots txt", + + "robots", + + "robots.txt", + + "rpcgen", + + "rs", + + "rs-274x", + + "rss", + + "rst", + + "rusthon", + + "salt", + + "saltstate", + + "sed", + + "sepolicy", + + "sh", + + "shell-script", + + "shellcheckrc", + + "sml", + + "snippet", + + "sourcemod", + + "soy", + + "specfile", + + "splus", + + "squeak", + + "terraform", + + "tl", + + "tm-properties", + + "troff", + + "ts", + + "udiff", + + "vb .net", + + "vb.net", + + "vb6", + + "vbnet", + + "vdf", + + "vim", + + "vimhelp", + + "viml", + + "visual basic 6", + + "visual basic for applications", + + "visual basic", + + "vlang", + + "wasm", + + "wast", + + "wdl", + + "wgetrc", + + "wiki", + + "winbatch", + + "wisp", + + "wl", + + "wolfram lang", + + "wolfram language", + + "wolfram", + + "wsdl", + + "xBase", + + "xbm", + + "xdr", + + "xhtml", + + "xml+genshi", + + "xml+kid", + + "xpm", + + "xsd", + + "xsl", + + "xten", + + "yas", + + "yml", + + "zsh" + ] + } + }, + + "additionalProperties": false + }, + + "validations": { + "$ref": "#/definitions/validations", + + "title": "textarea validations", + + "description": "Textarea validations\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#validations" + } + }, + + "additionalProperties": false + } + }, + + { + "if": { + "properties": { + "type": { + "const": "input" + } + } + }, + + "then": { + "$comment": "For `additionalProperties` to work `type` must also be present here.", + + "title": "input", + + "description": "Input\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#input", + + "type": "object", + + "required": ["type", "attributes"], + + "properties": { + "type": { + "$ref": "#/definitions/type" + }, + + "id": { + "$ref": "#/definitions/id", + + "description": "An input id\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#keys" + }, + + "attributes": { + "title": "input attributes", + + "description": "Input attributes\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-2", + + "type": "object", + + "required": ["label"], + + "properties": { + "label": { + "$ref": "#/definitions/label", + + "description": "A short input description\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-2" + }, + + "description": { + "$ref": "#/definitions/description", + + "description": "A long input description\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-2" + }, + + "placeholder": { + "$ref": "#/definitions/placeholder", + + "description": "An input placeholder\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-2" + }, + + "value": { + "$ref": "#/definitions/value", + + "description": "An input value\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-2" + } + }, + + "additionalProperties": false + }, + + "validations": { + "$ref": "#/definitions/validations", + + "title": "input validations", + + "description": "Input validations\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#validations-1" + } + }, + + "additionalProperties": false + } + }, + + { + "if": { + "properties": { + "type": { + "const": "dropdown" + } + } + }, + + "then": { + "$comment": "For `additionalProperties` to work `type` must also be present here.", + + "title": "dropdown", + + "description": "dropdown\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#dropdown", + + "type": "object", + + "required": ["type", "attributes"], + + "properties": { + "type": { + "$ref": "#/definitions/type" + }, + + "id": { + "$ref": "#/definitions/id", + + "description": "A dropdown id\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#keys" + }, + + "attributes": { + "title": "dropdown attributes", + + "description": "Dropdown attributes\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-3", + + "type": "object", + + "required": ["label", "options"], + + "properties": { + "label": { + "$ref": "#/definitions/label", + + "description": "A short dropdown description\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-3" + }, + + "description": { + "$ref": "#/definitions/description", + + "description": "A long dropdown description\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-3" + }, + + "multiple": { + "description": "Specify whether allow a multiple choices\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-3", + + "type": "boolean", + + "default": false + }, + + "options": { + "description": "Dropdown choices\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-3", + + "type": "array", + + "minItems": 1, + + "uniqueItems": true, + + "items": { + "type": "string", + + "minLength": 1, + + "examples": ["Sample choice"] + } + }, + + "default": { + "description": "Index of the default option\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-3", + + "type": "integer", + + "examples": [0] + } + }, + + "additionalProperties": false + }, + + "validations": { + "$ref": "#/definitions/validations", + + "title": "dropdown validations", + + "description": "Dropdown validations\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#validations-2" + } + }, + + "additionalProperties": false + } + }, + + { + "if": { + "properties": { + "type": { + "const": "checkboxes" + } + } + }, + + "then": { + "$comment": "For `additionalProperties` to work `type` must also be present here.", + + "title": "checkboxes", + + "description": "Checkboxes\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#checkboxes", + + "type": "object", + + "required": ["type", "attributes"], + + "properties": { + "type": { + "$ref": "#/definitions/type" + }, + + "id": { + "$ref": "#/definitions/id", + + "description": "Checkbox list id\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#keys" + }, + + "attributes": { + "title": "checkbox list attributes", + + "description": "Checkbox list attributes\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-4", + + "type": "object", + + "required": ["label", "options"], + + "properties": { + "label": { + "$ref": "#/definitions/label", + + "description": "A short checkbox list description\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-4" + }, + + "description": { + "$ref": "#/definitions/description", + + "description": "A long checkbox list description\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-4" + }, + + "options": { + "description": "Checkbox list choices\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-4", + + "type": "array", + + "minItems": 1, + + "items": { + "title": "checkbox list choice", + + "description": "Checkbox list choice\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-4", + + "type": "object", + + "required": ["label"], + + "properties": { + "label": { + "description": "A short checkbox list choice description\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-4", + + "type": "string", + + "minLength": 1, + + "examples": ["Sample label"] + }, + + "required": { + "description": "Specify whether a choice is required\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-4", + + "type": "boolean", + + "default": false + } + }, + + "additionalProperties": false + } + } + }, + + "additionalProperties": false + } + }, + + "additionalProperties": false + } + } + ] + } + }, + + "properties": { + "name": { + "description": "An issue template name\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms#top-level-syntax", + + "type": "string", + + "minLength": 1, + + "examples": ["Sample name"] + }, + + "description": { + "description": "An issue template description\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms#top-level-syntax", + + "type": "string", + + "minLength": 1, + + "examples": ["Sample description"] + }, + + "body": { + "description": "An issue template body\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms#top-level-syntax", + + "type": "array", + + "minItems": 1, + + "items": { + "$ref": "#/definitions/form_item" + } + }, + + "assignees": { + "description": "An issue template assignees\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms#top-level-syntax", + + "oneOf": [ + { + "$ref": "#/definitions/assignee" + }, + + { + "type": "array", + + "minItems": 1, + + "uniqueItems": true, + + "items": { + "$ref": "#/definitions/assignee" + } + } + ] + }, + + "labels": { + "description": "An issue template labels\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms#top-level-syntax", + + "type": "array", + + "minItems": 1, + + "uniqueItems": true, + + "items": { + "type": "string", + + "minLength": 1, + + "examples": [ + "Sample label", + + "bug", + + "documentation", + + "duplicate", + + "enhancement", + + "good first issue", + + "help wanted", + + "invalid", + + "question", + + "wontfix" + ] + } + }, + + "title": { + "description": "An issue template title\nhttps://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms#top-level-syntax", + + "type": "string", + + "minLength": 1, + + "examples": ["Sample title", "Bug: ", "Feature: "] + } + }, + + "required": ["name", "description", "body"], + + "title": "GitHub issue forms config file schema", + + "type": "object" +} diff --git a/scratch/plotting_examples.py b/scratch/plotting_examples.py new file mode 100644 index 00000000..ad5c6129 --- /dev/null +++ b/scratch/plotting_examples.py @@ -0,0 +1,449 @@ +# %% +""" +# Soundscapy Plotting Module: Developer's Guide + +This notebook demonstrates how the Soundscapy plotting module works, starting with the internal +components and building up to the user-facing API. It uses the newly fixed CircumplexPlot class +that implements the grammar of graphics approach with Seaborn Objects. + +RECENT FIXES TO CIRCUMPLEX PLOT: +1. Fixed palette handling to work with the latest Seaborn Objects API +2. Fixed the show() method to properly display plots in notebook contexts +3. Added property access to the underlying Seaborn Objects plot +4. Added a method to get matplotlib objects directly + +These changes allow the CircumplexPlot class to work correctly while providing +a clean, builder-pattern API for creating layered visualizations. + +See the proposed implementation details in the comment block below. + +# CircumplexPlot enhancements - IMPLEMENTED +The CircumplexPlot class has been fixed with the following improvements: + +1. Added @property to directly access the Seaborn Objects plot: + @property + def seaborn_plot(self): + return self.plot + +2. Added get_matplotlib_objects() method: + def get_matplotlib_objects(self): + fig, ax = plt.subplots(figsize=(6, 6)) + self.plot.plot(ax=ax) + return fig, ax + +3. Fixed the show() method to use pyplot=True: + def show(self): + self.plot.plot(pyplot=True) + +4. Fixed palette handling to work with Seaborn Objects API by: + - Storing palette_name instead of directly using palette + - Using .scale(color=so.Nominal(palette_name)) instead of the palette parameter + +These changes make CircumplexPlot work correctly in notebook contexts +while providing direct access to both the Seaborn Objects and matplotlib objects. + +Structure: +1. Data preparation +2. Backend implementation with Seaborn Objects API +3. Builder pattern and grammar of graphics approach +4. Simple user-facing API +5. Advanced examples and integrations +""" + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import seaborn.objects as so + +# Import from soundscapy +import soundscapy as sspy +from soundscapy.plotting import ( + CircumplexPlot, + create_circumplex_subplots, + density_plot, + joint_plot, + scatter_plot, +) +from soundscapy.plotting.plotting_utils import DEFAULT_XLIM, DEFAULT_YLIM +from soundscapy.surveys.processing import add_iso_coords, simulation + +# %% +# ## 1. Data Preparation + + +# Option 1: Load real data from the ISD database +def load_real_data(): + """Load real data from the International Soundscape Database.""" + # Load ISD data + isd_data = sspy.isd.load() + + # Add ISO coordinates if not already present + if "ISOPleasant" not in isd_data.columns: + isd_data = add_iso_coords(isd_data) + + # Get data for a specific location + location_data = sspy.isd.select_location_ids(isd_data, ["CamdenTown"]) + + return isd_data, location_data + + +# Option 2: Create simulated data for demonstration +def create_simulated_data(n=200): + """Generate random data for demonstration.""" + return simulation(n=n, incl_iso_coords=True) + + +# Create sample data for our examples +data = create_simulated_data(n=200) +print(f"Generated simulated data with {len(data)} samples") +# Look at the first few rows of our data +data.head() + + +# %% +# ## 2. Low-Level Backend Implementation + +""" +This section shows the core building blocks of the Seaborn Objects-based implementation. +We'll demonstrate how to use Seaborn Objects directly to create plots, which is what +the CircumplexPlot builder class uses internally. +""" + +# Create a basic seaborn objects plot manually +plot = so.Plot(data, x="ISOPleasant", y="ISOEventful") + +# Add a scatter layer using Dots mark +plot = plot.add(so.Dots()) + +# Add styling manually (the raw way) +plot = plot.limit(x=DEFAULT_XLIM, y=DEFAULT_YLIM) +plot = plot.label(title="Direct Seaborn Objects Example") + +# Display the plot +plot.show() + +# %% +# Add a KDE layer to our plot (density plot) +# Note: In Seaborn Objects, KDE is a transform, not a mark +plot = so.Plot(data, x="ISOPleasant", y="ISOEventful") +plot = plot.add( + so.Area(alpha=0.4, fill=True), # Mark (what to draw) + so.KDE(bw_adjust=1.2), # Transform (how to process the data) +) + +# To style our plot with grid lines, we can use matplotlib commands after rendering +# First, create a figure and axes to draw on +fig, ax = plt.subplots(figsize=(6, 6)) + +# Apply seaborn objects plot to this axes +plot = plot.on(ax) + +# Now manually add styling to the axes +ax.grid(True, which="major", color="grey", alpha=0.5) +ax.axhline(y=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) +ax.axvline(x=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + +# Set axis limits and title +plot = plot.limit(x=DEFAULT_XLIM, y=DEFAULT_YLIM) +plot = plot.label(title="KDE Transform with Area Mark") + +# Display the plot +plot.show() + +# %% +# Add grouping with hue +# Create a categorical column to use for grouping +data["group"] = np.random.choice(["Group A", "Group B", "Group C"], size=len(data)) + +# Create a plot with color grouping using hue +plot = so.Plot(data, x="ISOPleasant", y="ISOEventful") + +# Add dots with color grouping by 'group' column +plot = plot.add( + so.Dots(pointsize=30, alpha=0.7), + color="group", # Use 'group' column for colors +) + +# Apply a colorblind-friendly palette +plot = plot.scale(color=so.Nominal("colorblind")) + +# Add a title and legend (in Seaborn Objects, legend labels come from the data) +plot = plot.label(title="Grouping with Color", group="Group") + + +# Apply styling through the .on() method with a function +def style_axes(ax): + ax.grid(True, which="major", color="grey", alpha=0.5) + ax.axhline(y=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + ax.axvline(x=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + return ax + + +# Create a new figure +plt.figure(figsize=(6, 6)) +# Apply the function through pyplot +plot.plot(pyplot=True) +# Get the current axes and style it +ax = plt.gca() +style_axes(ax) + +# We don't need this since we've already displayed the plot +# The axes have already been styled above + +# %% +# ## 3. Builder Pattern / Grammar of Graphics Approach + +""" +The CircumplexPlot class provides a builder pattern interface that wraps the +Seaborn Objects API. It lets you build plots layer by layer with a fluent interface. +""" + +# Example 1: Basic layer composition +print("Builder pattern - basic composition") + +# Create a CircumplexPlot with default parameters +plot = CircumplexPlot(data) +plot.add_scatter(pointsize=30, alpha=0.7) +plot.add_grid(diagonal_lines=True) +plot.add_title("Grammar of Graphics: Basic Layers") + +# Now we can simply use show() since it's been fixed +plot.show() + +# %% +# Example 2: Multiple layer types - adding density + scatter +print("Builder pattern - multiple layer types") + +plot = CircumplexPlot(data) +# Add a density layer first (background) +plot.add_density(alpha=0.3, fill=True, simple=True) +# Add scatter points on top +plot.add_scatter(pointsize=15, alpha=0.6) +# Add styling elements +plot.add_grid(diagonal_lines=True) +plot.add_title("Grammar of Graphics: Multiple Layers") + +# Direct access to the Seaborn Objects plot (this is what a fixed CircumplexPlot.show() would do) +plot.plot.plot(pyplot=True) + +# %% +# Example 3: Using hue for grouping with the builder pattern +print("Builder pattern - using hue for grouping") + +# Step by step construction with assignments +plot = CircumplexPlot(data, hue="group") + +# Add layers in order from background to foreground +plot.add_density(alpha=0.3, simple=True) +plot.add_scatter(pointsize=25, alpha=0.7) + +# Add styling elements +plot.add_grid() +plot.add_title("Grouped by Category") +plot.add_legend(title="Category") + +# Direct access to the Seaborn Objects plot (this is what a fixed CircumplexPlot.show() would do) +plot.plot.plot(pyplot=True) + +# %% +# Example 4: Advanced customization with annotations +print("Builder pattern - advanced customization with annotations") + +# Create a plot with both scatter and annotations +plot = CircumplexPlot(data) +plot.add_scatter(pointsize=20, alpha=0.7) +plot.add_grid(diagonal_lines=True) + +# Add annotations for points of interest +plot.add_annotation(0, text="Point of interest", x_offset=0.2, y_offset=0.1) +plot.add_annotation(10, text="Another point", x_offset=-0.2, y_offset=0.2) + +# Add title +plot.add_title("Grammar of Graphics: Adding Annotations") + +# Direct access to the Seaborn Objects plot (this is what a fixed CircumplexPlot.show() would do) +plot.plot.plot(pyplot=True) + +# %% +# Example 5: Faceting with the builder pattern +print("Builder pattern - faceting") + +# Create a categorical variable for faceting +data["facet_var"] = np.random.choice(["Segment 1", "Segment 2"], size=len(data)) + +# Create a faceted plot using the builder pattern +plot = CircumplexPlot(data) # Avoid hue for now to prevent palette issues +plot.add_scatter(pointsize=20, alpha=0.7) +plot.add_grid(diagonal_lines=True) +plot.facet(column="facet_var") # The 'column' parameter creates column facets +plot.add_title("Faceted Plot by Segment") + +# Direct access to the Seaborn Objects plot (this is what a fixed CircumplexPlot.show() would do) +plot.plot.plot(pyplot=True) + +# %% +# ## 4. Simple User-Facing API Examples + +""" +The module provides simplified high-level functions for common use cases. +These functions use the CircumplexPlot class internally, but provide a simpler +interface for basic use cases. +""" + +# Simple scatter plot with the high-level API +plt.figure(figsize=(10, 5)) +plt.subplot(1, 2, 1) +scatter_plot(data, title="Simple API: Scatter Plot") + +# Simple density plot with the high-level API +plt.subplot(1, 2, 2) +density_plot( + data, + title="Simple API: Density Plot", + diagonal_lines=True, + simple_density=True, + incl_scatter=True, + scatter_alpha=0.3, +) +plt.tight_layout() + +# %% +# Joint plot with marginals using the simple API +joint_plot( + data, + title="Simple API: Joint Plot with Marginals", + plot_type="density", + incl_scatter=True, +) + +# %% +# Multiple subplots comparing different random data samples +datasets = [create_simulated_data(n=50) for _ in range(4)] +subtitles = ["Sample A", "Sample B", "Sample C", "Sample D"] + +# Create multiple subplots with a single function call +create_circumplex_subplots( + datasets, + subtitles=subtitles, + title="Simple API: Multiple Subplot Comparison", + plot_type="density", + incl_scatter=True, + diagonal_lines=True, +) + +# %% +# ## 5. Integration with Matplotlib + +""" +The plotting functions can integrate with existing matplotlib workflows by +returning matplotlib objects or working with provided axes. +""" + +# Create a custom layout with matplotlib +fig, axes = plt.subplots(1, 2, figsize=(12, 5)) + +# Plot directly on the first axis +scatter_plot(data, title="Scatter on Custom Axes", diagonal_lines=True, ax=axes[0]) + +# Plot on the second axis +density_plot( + data, + title="Density on Custom Axes", + simple_density=True, + incl_scatter=True, + ax=axes[1], +) + +# Add a global title +fig.suptitle("Integration with Matplotlib", fontsize=16) +plt.tight_layout(rect=[0, 0, 1, 0.95]) # Adjust for the suptitle + +# %% +# Integration with Seaborn Objects with as_objects=True parameter + +# Get a Seaborn Objects plot directly +so_plot = scatter_plot(data, title="Getting Seaborn Objects Directly", as_objects=True) + +# You can further customize the Seaborn Objects plot +so_plot = so_plot.theme({"axes.grid": True, "grid.color": ".8", "font.family": "serif"}) + +# And display it +so_plot.show() + +# %% +# ## 6. Advanced Composite Examples + +""" +These examples show more complex visualizations that combine multiple elements and techniques. +""" + +# Create a dataset with groups for more complex examples +advanced_data = create_simulated_data(n=150) +advanced_data["group"] = np.random.choice(["A", "B", "C"], size=len(advanced_data)) + +# Example: Combine density contours with annotations +# Create a plot with both density contours and data points +plot = ( + CircumplexPlot(advanced_data, hue="group") + .add_density(alpha=0.2, fill=True) + .add_scatter(pointsize=15, alpha=0.6) +) + +# Add grid with diagonal lines and quadrant labels +plot.add_grid(diagonal_lines=True) + +# Add annotations for the group centroids +for group in advanced_data["group"].unique(): + group_data = advanced_data[advanced_data["group"] == group] + x_mean = group_data["ISOPleasant"].mean() + y_mean = group_data["ISOEventful"].mean() + + # Find the closest point to the centroid + distances = np.sqrt( + (group_data["ISOPleasant"] - x_mean) ** 2 + + (group_data["ISOEventful"] - y_mean) ** 2 + ) + closest_idx = distances.idxmin() + + # Add annotation + plot.add_annotation( + closest_idx, + text=f"Group {group} centroid", + x_offset=0.1 if group != "B" else -0.2, + y_offset=0.1, + ) + +# Add title and legend +plot.add_title("Advanced Example: Annotating Group Centroids") +plot.add_legend(title="Group") + +# Direct access to the Seaborn Objects plot (this is what a fixed CircumplexPlot.show() would do) +plot.plot.plot(pyplot=True) + +# %% +# Statistical comparison example between two datasets + +# Create two datasets to compare +data1 = create_simulated_data(n=50) +data1["dataset"] = "Urban" +data2 = create_simulated_data(n=50) +data2["dataset"] = "Rural" + +# Slightly shift data2 to make it more interesting +data2["ISOPleasant"] = data2["ISOPleasant"] + 0.3 +data2["ISOEventful"] = data2["ISOEventful"] - 0.2 + +# Combine datasets +combined_data = pd.concat([data1, data2]) + +# Create a comparative plot without using hue in the constructor +plot = CircumplexPlot(combined_data) # Avoid using hue in constructor +plot.hue = "dataset" # Set hue after construction to avoid palette issue +plot.add_density(alpha=0.2, simple=True) +plot.add_scatter(pointsize=20, alpha=0.6) +plot.add_grid(diagonal_lines=True) +plot.add_title("Advanced Example: Comparing Urban vs Rural Soundscapes") +plot.add_legend(title="Location Type") + +# Direct access to the Seaborn Objects plot (this is what a fixed CircumplexPlot.show() would do) +plot.plot.plot(pyplot=True) diff --git a/scratch/plotting_examples_minimal.py b/scratch/plotting_examples_minimal.py new file mode 100644 index 00000000..c55da2e5 --- /dev/null +++ b/scratch/plotting_examples_minimal.py @@ -0,0 +1,62 @@ +# %% +""" +# Soundscapy Plotting Module - Minimal Examples + +This notebook demonstrates how the Soundscapy plotting module works +with minimal examples focused on the high-level API. +""" + +import matplotlib.pyplot as plt + +# Import from soundscapy +from soundscapy.plotting import ( + create_circumplex_subplots, + density_plot, + joint_plot, + scatter_plot, +) +from soundscapy.surveys.processing import simulation + +# %% +# Create sample data for our examples +data = simulation(n=200, incl_iso_coords=True) +print(f"Generated simulated data with {len(data)} samples") +data.head() + +# %% +# Simple scatter and density plots + +plt.figure(figsize=(10, 5)) +plt.subplot(1, 2, 1) +scatter_plot(data, title="Scatter Plot") + +plt.subplot(1, 2, 2) +density_plot( + data, + title="Density Plot", + diagonal_lines=True, + simple_density=True, + incl_scatter=True, + scatter_alpha=0.3, +) +plt.tight_layout() + +# %% +# Joint plot with marginals +joint_plot( + data, title="Joint Plot with Marginals", plot_type="density", incl_scatter=True +) + +# %% +# Multiple subplots comparison +datasets = [simulation(n=50, incl_iso_coords=True) for _ in range(4)] +subtitles = ["Sample A", "Sample B", "Sample C", "Sample D"] + +create_circumplex_subplots( + datasets, + subtitles=subtitles, + title="Multiple Subplot Comparison", + plot_type="density", + incl_scatter=True, + diagonal_lines=True, +) diff --git a/scratch/plotting_minimal.py b/scratch/plotting_minimal.py new file mode 100644 index 00000000..6e6ca2e8 --- /dev/null +++ b/scratch/plotting_minimal.py @@ -0,0 +1,32 @@ +# %% +""" +# Minimal CircumplexPlot Example with Fixed Implementation +""" + +import matplotlib.pyplot as plt + +from soundscapy.plotting import CircumplexPlot +from soundscapy.surveys.processing import simulation + +# Create sample data +data = simulation(n=200, incl_iso_coords=True) +print(f"Generated simulated data with {len(data)} samples") + +# Example 1: Basic Plot +plot = CircumplexPlot(data) +plot.add_scatter(pointsize=30, alpha=0.7) +plot.add_grid(diagonal_lines=True) +plot.add_title("Basic CircumplexPlot Example") +plot.show() # This has been fixed to correctly display the plot + +plt.figure() # Create a new figure to avoid subplot errors + +# Example 2: Adding hue +import numpy as np # noqa: E402 + +data["group"] = np.random.choice(["Group A", "Group B", "Group C"], size=len(data)) +plot2 = CircumplexPlot(data, hue="group") +plot2.add_scatter(pointsize=30, alpha=0.7) +plot2.add_grid(diagonal_lines=True) +plot2.add_title("CircumplexPlot with Hue Grouping") +plot2.show() # Now works with color grouping diff --git a/src/soundscapy/__init__.py b/src/soundscapy/__init__.py index c9a902f5..42d7e428 100644 --- a/src/soundscapy/__init__.py +++ b/src/soundscapy/__init__.py @@ -1,62 +1,93 @@ -""" -Soundscapy is a Python library for soundscape analysis and visualisation. -""" +"""Soundscapy is a Python library for soundscape analysis and visualisation.""" # ruff: noqa: E402 -from typing import Any from loguru import logger # https://loguru.readthedocs.io/en/latest/resources/recipes.html#configuring-loguru-to-be-used-by-a-library-or-an-application logger.disable("soundscapy") -import importlib.metadata - -__version__ = importlib.metadata.version("soundscapy") - -from soundscapy._optionals import import_optional - # Always available core modules -from soundscapy import surveys -from soundscapy import databases -from soundscapy import plotting -from soundscapy.logging import ( - setup_logging, - enable_debug, +from soundscapy import databases, plotting, surveys +from soundscapy._version import __version__ # noqa: F401 +from soundscapy.databases import isd, satp +from soundscapy.plotting import density_plot, scatter_plot +from soundscapy.sspylogging import ( disable_logging, + enable_debug, get_logger, + setup_logging, ) -from soundscapy.databases import araus, isd, satp from soundscapy.surveys import processing -from soundscapy.plotting import scatter_plot, density_plot __all__ = [ - # Core modules - "surveys", "databases", - "plotting", - "araus", + "density_plot", + "disable_logging", + "enable_debug", + "get_logger", "isd", - "satp", + "plotting", "processing", + "satp", "scatter_plot", - "density_plot", # Logging functions "setup_logging", - "enable_debug", - "disable_logging", - "get_logger", - # Optional modules listed explicitly for IDE/typing support - "Binaural", - "AudioAnalysis", - "AnalysisSettings", - "ConfigManager", - "process_all_metrics", - "prep_multiindex_df", - "add_results", - "parallel_process", + # Core modules + "surveys", ] +# Try to import optional audio module +try: + from soundscapy import audio + from soundscapy.audio import ( + AnalysisSettings, + AudioAnalysis, + Binaural, + ConfigManager, + add_results, + parallel_process, + prep_multiindex_df, + process_all_metrics, + ) + + __all__ += [ + "AnalysisSettings", + "AudioAnalysis", + "Binaural", + "ConfigManager", + "add_results", + "audio", + "parallel_process", + "prep_multiindex_df", + "process_all_metrics", + ] + +except ImportError: + # Audio module not available - this is expected if dependencies aren't installed + pass + +# Try to import optional SPI module +try: + from soundscapy import spi + from soundscapy.spi import ( + CentredParams, + DirectParams, + MultiSkewNorm, + cp2dp, + dp2cp, + msn, + ) + + __all__ += [ + "CentredParams", + "DirectParams", + "MultiSkewNorm", + "cp2dp", + "dp2cp", + "msn", + "spi", + ] -def __getattr__(name: str) -> Any: - """Lazy import handling for optional components.""" - return import_optional(name) +except ImportError: + # SPI module not available + pass diff --git a/src/soundscapy/_optionals.py b/src/soundscapy/_optionals.py deleted file mode 100644 index 55ab25c3..00000000 --- a/src/soundscapy/_optionals.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Optional dependency handling for soundscapy. - -This module provides utilities for managing optional dependencies across the package. -It allows graceful handling of missing dependencies and provides helpful feedback -about which dependencies are missing and how to install them. -""" - -import importlib -from typing import Any, Dict - -# Map module groups to their pip install targets -OPTIONAL_DEPENDENCIES = { - "audio": { - "packages": ("mosqito", "maad", "tqdm", "acoustic_toolbox"), - "install": "soundscapy[audio]", - "description": "audio analysis functionality", - }, - # Add other groups as needed -} -"""Dict[str, Dict]: Mapping of feature groups to their required dependencies. - -Each group contains: - modules (Tuple[str]): Required module names - install (str): pip install command/target - description (str): Human-readable feature description -""" - -OPTIONAL_IMPORTS = { - "Binaural": ("soundscapy.audio", "Binaural"), - "AudioAnalysis": ("soundscapy.audio", "AudioAnalysis"), - "AnalysisSettings": ("soundscapy.audio", "AnalysisSettings"), - "ConfigManager": ("soundscapy.audio", "ConfigManager"), - "process_all_metrics": ("soundscapy.audio", "process_all_metrics"), - "prep_multiindex_df": ("soundscapy.audio", "prep_multiindex_df"), - "add_results": ("soundscapy.audio", "add_results"), - "parallel_process": ("soundscapy.audio", "parallel_process"), -} - - -def require_dependencies(group: str) -> Dict[str, Any]: - """Import and return all packages required for a dependency group. - - Parameters - ---------- - group : str - The name ofthe dependency group to import - - Returns - ------- - Dict[str, Any] - Dictionary mapping package names to imported modules - - Raises - ------ - ImportError - If any required package is not available - KeyError - If the group name is not recognized - """ - if group not in OPTIONAL_DEPENDENCIES: - raise KeyError(f"Unknown dependency group: {group}") - - packages = {} - try: - for package in OPTIONAL_DEPENDENCIES[group]["packages"]: - packages[package] = importlib.import_module(package) - return packages - except ImportError as e: - raise ImportError( - f"{OPTIONAL_DEPENDENCIES[group]['description']} requires additional dependencies. " - f"Install with: pip install {OPTIONAL_DEPENDENCIES[group]['install']}" - ) from e - - -def import_optional(name: str) -> Any: - """Import an optional component by name.""" - if name not in OPTIONAL_IMPORTS: - raise AttributeError(f"module 'soundscapy' has no attribute '{name}'") - - module_name, attr_name = OPTIONAL_IMPORTS[name] - try: - module = importlib.import_module(module_name) - return getattr(module, attr_name) - except ImportError as e: - group = "audio" # Can be made dynamic if we add more groups - raise ImportError( - f"The {name} component requires {OPTIONAL_DEPENDENCIES[group]['description']}. " - f"Install with: pip install {OPTIONAL_DEPENDENCIES[group]['install']}" - ) from e diff --git a/src/soundscapy/_utils.py b/src/soundscapy/_utils.py new file mode 100644 index 00000000..bec33e52 --- /dev/null +++ b/src/soundscapy/_utils.py @@ -0,0 +1,49 @@ +from pathlib import Path +from typing import Literal + +from soundscapy.sspylogging import get_logger + +logger = get_logger() + + +def ensure_path_type(filepath: str | Path) -> Path: + if isinstance(filepath, str): + return Path(filepath) + if isinstance(filepath, Path): + return filepath + msg = "`filepath` must be either a valid path str or Path object." + raise TypeError(msg) + + +def ensure_input_path(filepath: str | Path) -> Path: + filepath = ensure_path_type(filepath) + if filepath.exists(): + return filepath + msg = f"{filepath.as_posix()} does not exist." + raise Warning(msg) + + +def ensure_output_filepath_exists( + filepath: str | Path, *, create_missing: bool = True +) -> Path | Literal[False]: + filepath = ensure_path_type(filepath) + if not filepath.exists(): + logger.info("Output file %s does not exist.", filepath) + if not create_missing: + return False + logger.info("Creating new file at %s", filepath.absolute()) + filepath.touch() + return filepath + + +def ensure_output_dirpath_exists( + dirpath: str | Path, *, create_missing: bool = True +) -> Path | Literal[False]: + dirpath = ensure_path_type(dirpath) + if not dirpath.exists(): + logger.info("Output directory %s does not exist.", dirpath) + if not create_missing: + return False + logger.info("Creating new directory at %s", dirpath.absolute()) + dirpath.mkdir() + return dirpath diff --git a/src/soundscapy/_version.py b/src/soundscapy/_version.py new file mode 100644 index 00000000..ab601ee4 --- /dev/null +++ b/src/soundscapy/_version.py @@ -0,0 +1,21 @@ +# file generated by setuptools-scm +# don't change, don't track in version control + +__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + from typing import Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '0.7.9.dev70' +__version_tuple__ = version_tuple = (0, 7, 9, 'dev70') diff --git a/src/soundscapy/audio/__init__.py b/src/soundscapy/audio/__init__.py index a3574c92..d5005e9f 100644 --- a/src/soundscapy/audio/__init__.py +++ b/src/soundscapy/audio/__init__.py @@ -1,15 +1,12 @@ """ -soundscapy.audio -================ - -This module provides tools for working with audio signals, particularly binaural recordings. +Provides tools for working with audio signals, particularly binaural recordings. Key Components: - Binaural: A class for processing and analyzing binaural audio signals. - Various metric calculation functions for audio analysis. -The module integrates with external libraries such as mosqito, maad, and acoustic_toolbox -to provide a comprehensive suite of audio analysis tools. +The module integrates with external libraries such as mosqito, maad, +and acoustic_toolbox to provide a comprehensive suite of audio analysis tools. Example: >>> # xdoctest: +SKIP @@ -20,19 +17,30 @@ See Also: soundscapy.audio.binaural: For detailed Binaural class documentation. soundscapy.audio.metrics: For individual metric calculation functions. -""" -# ruff: noqa: E402 -# ignore module level import order because we need to run require_dependencies first -from soundscapy._optionals import require_dependencies +""" -# This will raise an ImportError if the required dependencies are not installed -required = require_dependencies("audio") +# ruff: noqa: E402 +# ignore module level import order because we need to check dependencies first + +# Check for required dependencies directly +# This will raise ImportError if any dependency is missing +try: + import acoustic_toolbox # noqa: F401 + import maad # noqa: F401 + import mosqito # noqa: F401 + import tqdm # noqa: F401 +except ImportError as e: + msg = ( + "Audio analysis functionality requires additional dependencies. " + "Install with: pip install soundscapy[audio]" + ) + raise ImportError(msg) from e # Now we can import our modules that depend on the optional packages -from .binaural import Binaural -from .analysis_settings import AnalysisSettings, ConfigManager +from soundscapy.audio.analysis_settings import AnalysisSettings, ConfigManager from soundscapy.audio.audio_analysis import AudioAnalysis +from soundscapy.audio.binaural import Binaural from soundscapy.audio.metrics import ( add_results, prep_multiindex_df, @@ -41,12 +49,12 @@ from soundscapy.audio.parallel_processing import parallel_process __all__ = [ + "AnalysisSettings", "AudioAnalysis", "Binaural", - "AnalysisSettings", "ConfigManager", - "process_all_metrics", - "prep_multiindex_df", "add_results", "parallel_process", + "prep_multiindex_df", + "process_all_metrics", ] diff --git a/src/soundscapy/audio/analysis_settings.py b/src/soundscapy/audio/analysis_settings.py index b9a0712d..05a0fe65 100644 --- a/src/soundscapy/audio/analysis_settings.py +++ b/src/soundscapy/audio/analysis_settings.py @@ -1,8 +1,18 @@ +""" +Module for managing audio analysis settings using Pydantic models. + +This module defines Pydantic models for configuring analysis settings for different +audio processing libraries (AcousticToolbox, MoSQITo, scikit-maad). +It includes classes for individual metric settings, library settings, and overall +analysis settings. It also provides a ConfigManager class for loading, saving, +merging, and managing configurations from YAML files or dictionaries. +""" + from __future__ import annotations from importlib import resources from pathlib import Path -from typing import Any, Dict +from typing import Any import yaml from loguru import logger @@ -17,6 +27,13 @@ ) +def _ensure_path(value: str | Path) -> Path: + """Ensure the value is a Path object.""" + if isinstance(value, str): + return Path(value) + return value + + class MetricSettings(BaseModel): """ Settings for an individual metric. @@ -37,6 +54,7 @@ class MetricSettings(BaseModel): Whether to run the metric in parallel. func_args : dict[str, Any] Additional arguments for the metric function. + """ run: bool = True @@ -49,7 +67,7 @@ class MetricSettings(BaseModel): @model_validator(mode="before") @classmethod - def check_main_in_statistics(cls, values): + def check_main_in_statistics(cls, values: dict[str, Any]) -> dict[str, Any]: """Check that the main statistic is in the statistics list.""" main = values.get("main") statistics = values.get("statistics", []) @@ -82,11 +100,13 @@ def get_metric_settings(self, metric: str) -> MetricSettings: ------ KeyError If the specified metric is not found. + """ if metric in self.root: return self.root[metric] logger.error(f"Metric '{metric}' not found in library") - raise KeyError(f"Metric '{metric}' not found in library") + msg = f"Metric '{metric}' not found in library" + raise KeyError(msg) class AnalysisSettings(BaseModel): @@ -103,6 +123,7 @@ class AnalysisSettings(BaseModel): Settings for MoSQITo metrics. scikit_maad : LibrarySettings | None Settings for scikit-maad metrics. + """ version: str = "1.0" @@ -110,13 +131,15 @@ class AnalysisSettings(BaseModel): None, validation_alias=AliasChoices("AcousticToolbox", "PythonAcoustics") ) MoSQITo: LibrarySettings | None = None - scikit_maad: LibrarySettings | None = Field(None, alias="scikit-maad") + scikit_maad: LibrarySettings | None = Field( + None, validation_alias=AliasChoices("scikit-maad", "scikit_maad") + ) model_config = ConfigDict(populate_by_name=True, extra="forbid") @field_validator("*", mode="before") @classmethod - def validate_library_settings(cls, v): + def validate_library_settings(cls, v: dict | LibrarySettings) -> LibrarySettings: """Validate library settings.""" if isinstance(v, dict): return LibrarySettings(root=v) @@ -136,21 +159,24 @@ def from_yaml(cls, filepath: str | Path) -> AnalysisSettings: ------- AnalysisSettings An instance of AnalysisSettings. + """ + filepath = _ensure_path(filepath) logger.info(f"Loading configuration from {filepath}") - with open(filepath, "r") as f: + with Path.open(filepath) as f: config_dict = yaml.safe_load(f) return cls(**config_dict) @classmethod def default(cls) -> AnalysisSettings: """ - Create a default AnalysisSettings object using the package's default configuration file. + Create a default AnalysisSettings using the package default configuration file. Returns ------- AnalysisSettings An instance of AnalysisSettings with default settings. + """ config_resource = resources.files("soundscapy.data").joinpath( "default_settings.yaml" @@ -173,6 +199,7 @@ def from_dict(cls, d: dict) -> AnalysisSettings: ------- AnalysisSettings An instance of AnalysisSettings. + """ return cls(**d) @@ -184,12 +211,14 @@ def to_yaml(self, filepath: str | Path) -> None: ---------- filepath : str | Path Path to save the YAML file. + """ + filepath = _ensure_path(filepath) logger.info(f"Saving configuration to {filepath}") - with open(filepath, "w") as f: + with Path.open(filepath, "w") as f: yaml.dump(self.model_dump(by_alias=True), f) - def update_setting(self, library: str, metric: str, **kwargs) -> None: + def update_setting(self, library: str, metric: str, **kwargs: dict) -> None: """ Update the settings for a specific metric. @@ -206,6 +235,7 @@ def update_setting(self, library: str, metric: str, **kwargs) -> None: ------ KeyError If the specified library or metric is not found. + """ library_settings = getattr(self, library) if library_settings and metric in library_settings.root: @@ -217,7 +247,8 @@ def update_setting(self, library: str, metric: str, **kwargs) -> None: logger.error(f"Invalid setting '{key}' for metric '{metric}'") else: logger.error(f"Metric '{metric}' not found in library '{library}'") - raise KeyError(f"Metric '{metric}' not found in library '{library}'") + msg = f"Metric '{metric}' not found in library '{library}'" + raise KeyError(msg) def get_metric_settings(self, library: str, metric: str) -> MetricSettings: """ @@ -239,12 +270,14 @@ def get_metric_settings(self, library: str, metric: str) -> MetricSettings: ------ KeyError If the specified library or metric is not found. + """ library_settings = getattr(self, library) if library_settings and metric in library_settings.root: return library_settings.root[metric] logger.error(f"Metric '{metric}' not found in library '{library}'") - raise KeyError(f"Metric '{metric}' not found in library '{library}'") + msg = f"Metric '{metric}' not found in library '{library}'" + raise KeyError(msg) def get_enabled_metrics(self) -> dict[str, dict[str, MetricSettings]]: """ @@ -254,6 +287,7 @@ def get_enabled_metrics(self) -> dict[str, dict[str, MetricSettings]]: ------- dict[str, dict[str, MetricSettings]] A dictionary of enabled metrics grouped by library. + """ enabled_metrics = {} for library in ["AcousticToolbox", "MoSQITo", "scikit_maad"]: @@ -276,10 +310,11 @@ class ConfigManager: ---------- default_config_path : str | Path | None Path to the default configuration file. + """ - def __init__(self, config_path: str | Path | None = None): - self.config_path = Path(config_path) if config_path else None + def __init__(self, config_path: str | Path | None = None) -> None: # noqa: D107 + self.config_path = _ensure_path(config_path) if config_path else None self.current_config: AnalysisSettings | None = None def load_config(self, config_path: str | Path | None = None) -> AnalysisSettings: @@ -295,6 +330,7 @@ def load_config(self, config_path: str | Path | None = None) -> AnalysisSettings ------- AnalysisSettings The loaded configuration. + """ if config_path: logger.info(f"Loading configuration from {config_path}") @@ -320,17 +356,19 @@ def save_config(self, filepath: str | Path) -> None: ------ ValueError If no current configuration is loaded. + """ if self.current_config: logger.info(f"Saving configuration to {filepath}") self.current_config.to_yaml(filepath) else: logger.error("No current configuration to save") - raise ValueError("No current configuration to save.") + msg = "No current configuration to save." + raise ValueError(msg) - def merge_configs(self, override_config: Dict) -> AnalysisSettings: + def merge_configs(self, override_config: dict) -> AnalysisSettings: """ - Merge the current configuration with override values and update the current_config. + Merge the current config with override values and update the current_config. Parameters ---------- @@ -346,10 +384,12 @@ def merge_configs(self, override_config: Dict) -> AnalysisSettings: ------ ValueError If no base configuration is loaded. + """ if not self.current_config: logger.error("No base configuration loaded") - raise ValueError("No base configuration loaded.") + msg = "No base configuration loaded." + raise ValueError(msg) logger.info("Merging configurations") merged_dict = self.current_config.model_dump() self._deep_update(merged_dict, override_config) @@ -357,7 +397,7 @@ def merge_configs(self, override_config: Dict) -> AnalysisSettings: self.current_config = merged_config # Update the current_config return merged_config - def _deep_update(self, base_dict: Dict, update_dict: Dict) -> None: + def _deep_update(self, base_dict: dict, update_dict: dict) -> None: """Recursively update a nested dictionary.""" for key, value in update_dict.items(): if ( @@ -382,9 +422,11 @@ def generate_minimal_config(self) -> dict: ------ ValueError If no current configuration is loaded. + """ if not self.current_config: - raise ValueError("No current configuration loaded.") + msg = "No current configuration loaded." + raise ValueError(msg) default_config = AnalysisSettings.default() current_dict = self.current_config.model_dump() default_dict = default_config.model_dump() @@ -490,12 +532,12 @@ def _get_diff(self, current: dict, default: dict) -> dict: ) # Print the created configuration - print(analysis_settings.model_dump_json(indent=2)) + print(analysis_settings.model_dump_json(indent=2)) # noqa: T201 # Save the configuration to a YAML file output_path = Path("my_custom_config.yaml") analysis_settings.to_yaml(output_path) - print(f"Configuration saved to {output_path}") + print(f"Configuration saved to {output_path}") # noqa: T201 # To use this configuration: diff --git a/src/soundscapy/audio/audio_analysis.py b/src/soundscapy/audio/audio_analysis.py index b2e2d326..1fc4ffbe 100644 --- a/src/soundscapy/audio/audio_analysis.py +++ b/src/soundscapy/audio/audio_analysis.py @@ -1,18 +1,64 @@ +""" +Audio analysis module for psychoacoustic analysis of audio files. + +This module provides functionality for analyzing audio files using psychoacoustic +metrics. It includes the AudioAnalysis class for processing single files or entire +folders. +""" + import json from concurrent.futures import ProcessPoolExecutor, as_completed from pathlib import Path -from typing import Dict, List, Optional, Union import pandas as pd from loguru import logger from tqdm.auto import tqdm +from soundscapy._utils import ensure_input_path, ensure_path_type from soundscapy.audio.analysis_settings import ConfigManager from soundscapy.audio.parallel_processing import load_analyse_binaural class AudioAnalysis: - def __init__(self, config_path: Optional[Union[str, Path]] = None): + """ + A class for performing psychoacoustic analysis on audio files. + + This class provides methods to analyze single audio files or entire folders + of audio files using parallel processing. It handles configuration management, + calibration, and saving of analysis results. + + Attributes + ---------- + config_manager : ConfigManager + Manages the configuration settings for audio analysis + settings : dict + The current configuration settings + + Methods + ------- + analyze_file(file_path, calibration_levels, resample) + Analyze a single audio file + analyze_folder(folder_path, calibration_file, max_workers, resample) + Analyze all audio files in a folder using parallel processing + save_results(results, output_path) + Save analysis results to a file + update_config(new_config) + Update the current configuration + save_config(config_path) + Save the current configuration to a file + + """ + + def __init__(self, config_path: str | Path | None = None) -> None: + """ + Initialize the AudioAnalysis with a configuration. + + Parameters + ---------- + config_path : str, Path, or None + Path to the configuration file. If None, uses default configuration. + + """ self.config_manager = ConfigManager(config_path) self.settings = self.config_manager.load_config() logger.info( @@ -22,8 +68,8 @@ def __init__(self, config_path: Optional[Union[str, Path]] = None): def analyze_file( self, file_path: str | Path, - calibration_levels: Optional[Dict[str, float] | List[float]] = None, - resample: Optional[int] = None, + calibration_levels: dict[str, float] | list[float] | None = None, + resample: int | None = None, ) -> pd.DataFrame: """ Analyze a single audio file using the current configuration. @@ -40,9 +86,10 @@ def analyze_file( ------- pd.DataFrame DataFrame containing the analysis results. + """ - if isinstance(file_path, str): - file_path = Path(file_path) + file_path = ensure_input_path(file_path) + logger.info(f"Analyzing file: {file_path}") return load_analyse_binaural( file_path, @@ -55,9 +102,9 @@ def analyze_file( def analyze_folder( self, folder_path: str | Path, - calibration_file: Optional[str | Path] = None, - max_workers: Optional[int] = None, - resample: Optional[int] = None, + calibration_file: str | Path | None = None, + max_workers: int | None = None, + resample: int | None = None, ) -> pd.DataFrame: """ Analyze all audio files in a folder using parallel processing. @@ -70,26 +117,29 @@ def analyze_folder( calibration_file : str or Path, optional Path to a JSON file containing calibration levels for each audio file. max_workers : int, optional - Maximum number of worker processes to use. If None, it will use the number of CPU cores. + Maximum number of worker processes to use. + If None, it will use the number of CPU cores. Returns ------- pd.DataFrame DataFrame containing the analysis results for all files. - """ - folder_path = Path(folder_path) + """ + folder_path = ensure_input_path(folder_path) audio_files = list(folder_path.glob("*.wav")) logger.info( - f"Analyzing folder: {folder_path.name} of {len(audio_files)} files in parallel (max_workers={max_workers})" + f"Analyzing folder: {folder_path.name} of {len(audio_files)}" + f"files in parallel (max_workers={max_workers})" ) if max_workers else logger.info( f"Analyzing folder: {folder_path}, {len(audio_files)} files" ) calibration_levels = {} if calibration_file: - with open(calibration_file, "r") as f: + calibration_file = ensure_input_path(calibration_file) + with calibration_file.open() as f: calibration_levels = json.load(f) logger.debug(f"Loaded calibration levels from: {calibration_file}") @@ -102,8 +152,8 @@ def analyze_folder( file, calibration_levels, self.settings, - False, resample, + parallel_mosqito=False, ) futures.append(future) @@ -114,7 +164,7 @@ def analyze_folder( result = future.result() all_results.append(result) except Exception as e: - logger.error(f"Error processing file: {str(e)}") + logger.error(f"Error processing file: {e!s}") combined_results = pd.concat(all_results) logger.info( @@ -122,7 +172,7 @@ def analyze_folder( ) return combined_results - def save_results(self, results: pd.DataFrame, output_path: Union[str, Path]): + def save_results(self, results: pd.DataFrame, output_path: str | Path) -> None: """ Save analysis results to a file. @@ -132,17 +182,21 @@ def save_results(self, results: pd.DataFrame, output_path: Union[str, Path]): DataFrame containing the analysis results. output_path : str or Path Path to save the results file. + """ - output_path = Path(output_path) + output_path = ensure_path_type( + output_path + ) # If doesn't already exist, pandas will create the file. if output_path.suffix == ".csv": results.to_csv(output_path) elif output_path.suffix == ".xlsx": results.to_excel(output_path) else: - raise ValueError("Unsupported file format. Use .csv or .xlsx") + msg = "Unsupported file format. Use .csv or .xlsx" + raise ValueError(msg) logger.info(f"Results saved to: {output_path}") - def update_config(self, new_config: Dict): + def update_config(self, new_config: dict) -> "AudioAnalysis": """ Update the current configuration. @@ -150,11 +204,13 @@ def update_config(self, new_config: Dict): ---------- new_config : dict Dictionary containing the new configuration settings. + """ self.settings = self.config_manager.merge_configs(new_config) logger.info("Configuration updated") + return self - def save_config(self, config_path: Union[str, Path]): + def save_config(self, config_path: str | Path) -> None: """ Save the current configuration to a file. @@ -162,6 +218,7 @@ def save_config(self, config_path: Union[str, Path]): ---------- config_path : str or Path Path to save the configuration file. + """ self.config_manager.save_config(config_path) logger.info(f"Configuration saved to: {config_path}") @@ -169,7 +226,7 @@ def save_config(self, config_path: Union[str, Path]): # Example usage if __name__ == "__main__": - from soundscapy.logging import setup_logging + from soundscapy.sspylogging import setup_logging setup_logging("INFO") diff --git a/src/soundscapy/audio/binaural.py b/src/soundscapy/audio/binaural.py index a5d18b3f..90c1c152 100644 --- a/src/soundscapy/audio/binaural.py +++ b/src/soundscapy/audio/binaural.py @@ -1,8 +1,5 @@ """ -soundscapy.audio.binaural -========================= - -This module provides tools for working with binaural audio signals. +Provides tools for working with binaural audio signals. The main class, Binaural, extends the Signal class from the Acoustic Toolbox library to provide specialized functionality for binaural recordings. It supports @@ -27,20 +24,23 @@ >>> from soundscapy.audio import Binaural >>> signal = Binaural.from_wav("audio.wav") >>> results = signal.process_all_metrics(analysis_settings) + """ import warnings from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union +from typing import Literal import numpy as np import pandas as pd import scipy.signal from acoustic_toolbox import Signal from loguru import logger +from scipy.io import wavfile -from .analysis_settings import AnalysisSettings, MetricSettings -from .metrics import ( +from soundscapy._utils import ensure_input_path +from soundscapy.audio.analysis_settings import AnalysisSettings, MetricSettings +from soundscapy.audio.metrics import ( acoustics_metric_1ch, acoustics_metric_2ch, maad_metric_1ch, @@ -50,6 +50,8 @@ process_all_metrics, ) +ALLOWED_BINAURAL_CHANNELS = 2 + class Binaural(Signal): """ @@ -70,9 +72,12 @@ class Binaural(Signal): Notes ----- This class only supports 2-channel (stereo) audio signals. + """ - def __new__(cls, data, fs, recording="Rec"): + def __new__( + cls, data: np.ndarray, fs: float | None, recording: str = "Rec" + ) -> "Binaural": """ Create a new Binaural object. @@ -94,18 +99,20 @@ def __new__(cls, data, fs, recording="Rec"): ------ ValueError If the input signal is not 2-channel. + """ obj = super().__new__(cls, data, fs).view(cls) obj.recording = recording - if obj.channels != 2: + if obj.channels != ALLOWED_BINAURAL_CHANNELS: logger.error( f"Attempted to create Binaural object with {obj.channels} channels" ) - raise ValueError("Binaural class only supports 2 channels.") + msg = "Binaural class only supports 2 channels." + raise ValueError(msg) logger.debug(f"Created new Binaural object: {recording}, fs={fs}") return obj - def __array_finalize__(self, obj): + def __array_finalize__(self, obj: "Binaural | None") -> None: """ Finalize the new Binaural object. @@ -115,16 +122,17 @@ def __array_finalize__(self, obj): ---------- obj : Binaural or None The object from which the new object was created. + """ if obj is None: return self.fs = getattr(obj, "fs", None) - self.recording = getattr(obj, "recording", None) + self.recording = getattr(obj, "recording", "Rec") def calibrate_to( self, - decibel: Union[float, List[float], Tuple[float, float]], - inplace: bool = False, + decibel: float | list[float] | tuple[float, float] | np.ndarray | pd.Series, + inplace: bool = False, # noqa: FBT001, FBT002 TODO(MitchellAcoustics): Change to keyword-only in acoustic_toolbox.Signal ) -> "Binaural": """ Calibrate the binaural signal to predefined Leq/dB levels. @@ -136,10 +144,13 @@ def calibrate_to( ---------- decibel : float or List[float] or Tuple[float, float] Target calibration value(s) in dB (Leq). - If a single value is provided, both channels will be calibrated to this level. - If two values are provided, they will be applied to the left and right channels respectively. + If a single value is provided, both channels will be calibrated + to this level. + If two values are provided, they will be applied to the left and right + channels respectively. inplace : bool, optional - If True, modify the signal in place. If False, return a new calibrated signal. + If True, modify the signal in place. + If False, return a new calibrated signal. Default is False. Returns @@ -156,43 +167,48 @@ def calibrate_to( -------- >>> # xdoctest: +SKIP >>> signal = Binaural.from_wav("audio.wav") - >>> calibrated_signal = signal.calibrate_to([60, 62]) # Calibrate left channel to 60 dB and right to 62 dB + >>> # Calibrate left channel to 60 dB and right to 62 dB + >>> calibrated_signal = signal.calibrate_to([60, 62]) + """ logger.info(f"Calibrating Binaural signal to {decibel} dB") - if isinstance(decibel, (np.ndarray, pd.Series)): # Force into tuple + if isinstance(decibel, np.ndarray | pd.Series): # Force into tuple decibel = tuple(decibel) - if isinstance(decibel, (list, tuple)): - if len(decibel) == 2: # Per-channel calibration (recommended) + if isinstance(decibel, list | tuple): + if ( + len(decibel) == ALLOWED_BINAURAL_CHANNELS + ): # Per-channel calibration (recommended) logger.debug( - f"Calibrating channels separately: Left={decibel[0]}dB, Right={decibel[1]}dB" + "Calibrating channels separately: " + f"Left={decibel[0]}dB, Right={decibel[1]}dB" ) decibel = np.array(decibel) decibel = decibel[..., None] - return super().calibrate_to(decibel, inplace) - elif ( + return super().calibrate_to(decibel, inplace) # type: ignore[reportReturnType] + if ( len(decibel) == 1 ): # if one value given in tuple, assume same for both channels logger.debug(f"Calibrating both channels to {decibel[0]}dB") decibel = decibel[0] else: logger.error(f"Invalid calibration value: {decibel}") - raise ValueError( - "decibel must either be a single value or a 2 value tuple" - ) - if isinstance(decibel, (int, float)): # Calibrate both channels to same value + msg = "decibel must either be a single value or a 2 value tuple" + raise TypeError(msg) + if isinstance(decibel, int | float): # Calibrate both channels to same value logger.debug(f"Calibrating both channels to {decibel}dB") - return super().calibrate_to(decibel, inplace) - else: - logger.error(f"Invalid calibration value: {decibel}") - raise ValueError("decibel must be a single value or a 2 value tuple") + return super().calibrate_to(decibel, inplace) # type: ignore[reportReturnType] + logger.error(f"Invalid calibration value: {decibel}") + msg = "decibel must be a single value or a 2 value tuple" + raise TypeError(msg) @classmethod def from_wav( cls, - filename: Union[Path, str], - calibrate_to: Optional[Union[float, List, Tuple]] = None, - normalize: bool = False, - resample: Optional[int] = None, + filename: Path | str, + normalize: bool = False, # noqa: FBT001, FBT002 + calibrate_to: float | list | tuple | None = None, + resample: int | None = None, + recording: str | None = None, ) -> "Binaural": """ Load a wav file and return a Binaural object. @@ -221,10 +237,23 @@ def from_wav( See Also -------- acoustic_toolbox.Signal.from_wav : Base method for loading wav files. + """ + filename = ensure_input_path(filename) + if not filename.exists(): + logger.error(f"File not found: {filename}") + msg = f"File not found: {filename}" + raise FileNotFoundError(msg) + logger.info(f"Loading WAV file: {filename}") - s = super().from_wav(filename, normalize) - b = cls(s, s.fs, recording=Path(filename).stem) + fs, data = wavfile.read(filename) + data = data.astype(np.float32, copy=False).T + if normalize: + data /= np.max(np.abs(data)) + + recording = recording if recording is not None else Path(filename).stem + b = cls(data, fs, recording=recording) + if calibrate_to is not None: logger.info(f"Calibrating loaded signal to {calibrate_to} dB") b.calibrate_to(calibrate_to, inplace=True) @@ -236,6 +265,7 @@ def from_wav( def fs_resample( self, fs: float, + original_fs: float | None = None, ) -> "Binaural": """ Resample the signal to a new sampling frequency. @@ -244,7 +274,9 @@ def fs_resample( ---------- fs : float New sampling frequency. - + original_fs : float or None, optional + Original sampling frequency. + If None, it will be inferred from the signal (`Binaural.fs`). Returns ------- @@ -254,20 +286,33 @@ def fs_resample( See Also -------- acoustic_toolbox.Signal.resample : Base method for resampling signals. + """ - if fs == self.fs: - logger.info(f"Signal already at {fs} Hz. No resampling needed.") + current_fs: float + + if original_fs is None: + if hasattr(self, "fs") and self.fs is not None: + current_fs = self.fs + else: + logger.error("Original sampling frequency not provided.") + msg = "Original sampling frequency not provided." + raise ValueError(msg) + else: + current_fs = original_fs + + if fs == current_fs: + logger.info(f"Signal already at {current_fs} Hz. No resampling needed.") return self + logger.info(f"Resampling signal to {fs} Hz") resampled_channels = [ - scipy.signal.resample(channel, int(fs * len(channel) / self.fs)) + scipy.signal.resample(channel, int(fs * len(channel) / current_fs)) for channel in self ] resampled_channels = np.stack(resampled_channels) - resampled_b = Binaural(resampled_channels, fs, recording=self.recording) - return resampled_b + return Binaural(resampled_channels, fs, recording=self.recording) - def _get_channel(self, channel): + def _get_channel(self, channel: int | str | None) -> Signal: """ Get a single channel from the signal. @@ -280,35 +325,33 @@ def _get_channel(self, channel): ------- Signal Single channel signal. + """ if self.channels == 1: logger.debug("Returning single channel signal") return self - elif ( - channel is None - or channel == "both" - or channel == ("Left", "Right") - or channel == ["Left", "Right"] - ): + if channel is None or channel in ("both", ("Left", "Right"), ["Left", "Right"]): logger.debug("Returning both channels") return self - elif channel in ["Left", 0, "L"]: + if channel in ["Left", 0, "L"]: logger.debug("Returning left channel") return self[0] - elif channel in ["Right", 1, "R"]: + if channel in ["Right", 1, "R"]: logger.debug("Returning right channel") return self[1] - else: - logger.warning( - f"Unrecognized channel specification: {channel}. Returning full Binaural object." - ) - warnings.warn("Channel not recognised. Returning Binaural object as is.") - return self + logger.warning( + f"Unrecognized channel specification: {channel}." + "Returning full Binaural object." + ) + warnings.warn( + "Channel not recognised. Returning Binaural object as is.", stacklevel=2 + ) + return self def pyacoustics_metric( self, - metric: str, - statistics: Union[Tuple, List] = ( + metric: Literal["LZeq", "Leq", "LAeq", "LCeq", "SEL"], + statistics: tuple | list = ( 5, 10, 50, @@ -320,16 +363,60 @@ def pyacoustics_metric( "kurt", "skew", ), - label: Optional[str] = None, - channel: Union[str, int, List, Tuple] = ("Left", "Right"), - as_df: bool = True, - return_time_series: bool = False, - metric_settings: Optional[MetricSettings] = None, - func_args: Dict = {}, - ) -> Union[Dict, pd.DataFrame]: + label: str | None = None, + channel: str | int | list | tuple = ("Left", "Right"), + as_df: bool = True, # noqa: FBT001, FBT002 + return_time_series: bool = False, # noqa: FBT001, FBT002 + metric_settings: MetricSettings | None = None, + func_args: dict | None = None, + ) -> dict | pd.DataFrame | None: + """ + Run a metric from the pyacoustics library (deprecated). + + This method has been deprecated. Use `acoustics_metric` instead. + All parameters are passed directly to `acoustics_metric`. + + Parameters + ---------- + metric : {"LZeq", "Leq", "LAeq", "LCeq", "SEL"} + The metric to run. + statistics : tuple or list, optional + List of level statistics to calculate (e.g. L_5, L_90, etc.). + Default is (5, 10, 50, 90, 95, "avg", "max", "min", "kurt", "skew"). + label : str, optional + Label to use for the metric. + If None, will pull from default label for that metric. + channel : tuple, list, or str, optional + Which channels to process. Default is ("Left", "Right"). + as_df : bool, optional + Whether to return a dataframe or not. Default is True. + If True, returns a MultiIndex Dataframe with + ("Recording", "Channel") as the index. + return_time_series : bool, optional + Whether to return the time series of the metric. Default is False. + Cannot return time series if as_df is True. + metric_settings : MetricSettings, optional + Settings for metric analysis. Default is None. + func_args : dict, optional + Any settings given here will override those in the other options. + Can pass any *args or **kwargs to the underlying acoustic_toolbox method. + + Returns + ------- + dict or pd.DataFrame + Results of the metric calculation. + + See Also + -------- + Binaural.acoustics_metric + + """ + if func_args is None: + func_args = {} warnings.warn( "pyacoustics has been deprecated. Use acoustics_metric instead.", DeprecationWarning, + stacklevel=2, ) return self.acoustics_metric( metric, @@ -344,8 +431,8 @@ def pyacoustics_metric( def acoustics_metric( self, - metric: str, - statistics: Union[Tuple, List] = ( + metric: Literal["LZeq", "Leq", "LAeq", "LCeq", "SEL"], + statistics: tuple | list = ( 5, 10, 50, @@ -357,13 +444,13 @@ def acoustics_metric( "kurt", "skew", ), - label: Optional[str] = None, - channel: Union[str, int, List, Tuple] = ("Left", "Right"), + label: str | None = None, + channel: str | int | list | tuple = ("Left", "Right"), as_df: bool = True, return_time_series: bool = False, - metric_settings: Optional[MetricSettings] = None, - func_args: Dict = {}, - ) -> Union[Dict, pd.DataFrame]: + metric_settings: MetricSettings | None = None, + func_args: dict | None = None, + ) -> dict | pd.DataFrame | None: """ Run a metric from the acoustic_toolbox library. @@ -375,12 +462,14 @@ def acoustics_metric( List of level statistics to calculate (e.g. L_5, L_90, etc.). Default is (5, 10, 50, 90, 95, "avg", "max", "min", "kurt", "skew"). label : str, optional - Label to use for the metric. If None, will pull from default label for that metric. + Label to use for the metric. + If None, will pull from default label for that metric. channel : tuple, list, or str, optional Which channels to process. Default is ("Left", "Right"). as_df : bool, optional Whether to return a dataframe or not. Default is True. - If True, returns a MultiIndex Dataframe with ("Recording", "Channel") as the index. + If True, returns a MultiIndex Dataframe with + ("Recording", "Channel") as the index. return_time_series : bool, optional Whether to return the time series of the metric. Default is False. Cannot return time series if as_df is True. @@ -398,10 +487,16 @@ def acoustics_metric( See Also -------- metrics.acoustics_metric - acoustic_toolbox.standards_iso_tr_25417_2007.equivalent_sound_pressure_level : Base method for Leq calculation. - acoustic_toolbox.standards.iec_61672_1_2013.sound_exposure_level : Base method for SEL calculation. - acoustic_toolbox.standards.iec_61672_1_2013.time_weighted_sound_level : Base method for Leq level time series calculation. + acoustic_toolbox.standards_iso_tr_25417_2007.equivalent_sound_pressure_level : + Base method for Leq calculation. + acoustic_toolbox.standards.iec_61672_1_2013.sound_exposure_level : + Base method for SEL calculation. + acoustic_toolbox.standards.iec_61672_1_2013.time_weighted_sound_level : + Base method for Leq level time series calculation. + """ + if func_args is None: + func_args = {} if metric_settings: logger.debug("Using provided analysis settings") if not metric_settings.run: @@ -421,23 +516,22 @@ def acoustics_metric( return acoustics_metric_1ch( s, metric, statistics, label, as_df, return_time_series, func_args ) - else: - logger.debug("Processing both channels") - return acoustics_metric_2ch( - s, - metric, - statistics, - label, - channel, - as_df, - return_time_series, - func_args, - ) + logger.debug("Processing both channels") + return acoustics_metric_2ch( + s, + metric, + statistics, + label, + channel, + as_df, + return_time_series, + func_args, + ) def mosqito_metric( self, metric: str, - statistics: Union[Tuple, List] = ( + statistics: tuple | list = ( 5, 10, 50, @@ -449,14 +543,14 @@ def mosqito_metric( "kurt", "skew", ), - label: Optional[str] = None, - channel: Union[int, Tuple, List, str] = ("Left", "Right"), + label: str | None = None, + channel: int | tuple | list | str = ("Left", "Right"), as_df: bool = True, return_time_series: bool = False, parallel: bool = True, - metric_settings: Optional[MetricSettings] = None, - func_args: Dict = {}, - ) -> Union[Dict, pd.DataFrame]: + metric_settings: MetricSettings | None = None, + func_args: dict = {}, + ) -> dict | pd.DataFrame: """ Run a metric from the mosqito library. @@ -495,6 +589,7 @@ def mosqito_metric( -------- binaural.mosqito_metric_2ch : Method for running metrics on 2 channels. binaural.mosqito_metric_1ch : Method for running metrics on 1 channel. + """ logger.info(f"Running mosqito metric: {metric}") if metric_settings: @@ -515,30 +610,35 @@ def mosqito_metric( if s.channels == 1: logger.debug("Processing single channel") return mosqito_metric_1ch( - s, metric, statistics, label, as_df, return_time_series, func_args - ) - else: - logger.debug("Processing both channels") - return mosqito_metric_2ch( s, metric, statistics, label, - channel, - as_df, - return_time_series, - parallel, - func_args, + as_df=as_df, + return_time_series=return_time_series, + **func_args, ) + logger.debug("Processing both channels") + return mosqito_metric_2ch( + s, + metric, + statistics, + label, + channel, + as_df=as_df, + return_time_series=return_time_series, + parallel=parallel, + func_args=func_args, + ) def maad_metric( self, metric: str, - channel: Union[int, Tuple, List, str] = ("Left", "Right"), + channel: int | tuple | list | str = ("Left", "Right"), as_df: bool = True, - metric_settings: Optional[MetricSettings] = None, - func_args: Dict = {}, - ) -> Union[Dict, pd.DataFrame]: + metric_settings: MetricSettings | None = None, + func_args: dict = {}, + ) -> dict | pd.DataFrame: """ Run a metric from the scikit-maad library. @@ -572,6 +672,7 @@ def maad_metric( -------- metrics.maad_metric_1ch metrics.maad_metric_2ch + """ logger.info(f"Running maad metric: {metric}") if metric_settings: @@ -593,9 +694,8 @@ def maad_metric( if s.channels == 1: logger.debug("Processing single channel") return maad_metric_1ch(s, metric, as_df) - else: - logger.debug("Processing both channels") - return maad_metric_2ch(s, metric, channel, as_df, func_args) + logger.debug("Processing both channels") + return maad_metric_2ch(s, metric, channel, as_df, func_args) def process_all_metrics( self, @@ -633,6 +733,7 @@ def process_all_metrics( >>> signal = Binaural.from_wav("audio.wav") >>> settings = AnalysisSettings.from_yaml("settings.yaml") >>> results = signal.process_all_metrics(settings) + """ logger.info(f"Processing all metrics for {self.recording}") logger.debug(f"Parallel processing: {parallel}") diff --git a/src/soundscapy/audio/metrics.py b/src/soundscapy/audio/metrics.py index 0f863c4a..c7f591ae 100644 --- a/src/soundscapy/audio/metrics.py +++ b/src/soundscapy/audio/metrics.py @@ -1,40 +1,52 @@ """ -soundscapy.audio.metrics -======================== +Functions for calculating various acoustic and psychoacoustic metrics for audio signals. -This module provides functions for calculating various acoustic and psychoacoustic metrics -for audio signals. It includes implementations for single-channel and two-channel signals, +It includes implementations for single-channel and two-channel signals, as well as wrapper functions for different libraries such as Acoustic Toolbox, MoSQITo, and scikit-maad. Functions --------- _stat_calcs : Calculate various statistics for a time series array. -mosqito_metric_1ch : Calculate a MoSQITo psychoacoustic metric for a single channel signal. +mosqito_metric_1ch : Calculate a MoSQITo psychoacoustic metric for a single channel + signal. maad_metric_1ch : Run a metric from the scikit-maad library on a single channel signal. -acoustics_metric_1ch : Run a metric from the Acoustic Toolbox on a single channel object. +acoustics_metric_1ch : Run a metric from the Acoustic Toolbox on a single channel + object. acoustics_metric_2ch : Run a metric from the Acoustic Toolbox on a Binaural object. -pyacoustics_metric_1ch: Deprecated function for running a metric from the PyAcoustics library (replaced with `acoustics_metric_1ch`). -pyacoustics_metric_2ch: Deprecated function for running a metric from the PyAcoustics library (replaced with `acoustics_metric_2ch`). +pyacoustics_metric_1ch: Deprecated function for running a metric from the PyAcoustics + library +pyacoustics_metric_2ch: Deprecated function for running a metric from the PyAcoustics + library (replaced with `acoustics_metric_2ch`). +pyacoustics_metric_2ch: Deprecated function for running a metric from the PyAcoustics + library (replaced with `acoustics_metric_2ch`). mosqito_metric_2ch : Calculate metrics from MoSQITo for a two-channel signal. maad_metric_2ch : Run a metric from the scikit-maad library on a binaural signal. prep_multiindex_df : Prepare a MultiIndex dataframe from a dictionary of results. add_results : Add results to a MultiIndex dataframe. -process_all_metrics : Process all metrics specified in the analysis settings for a binaural signal. +process_all_metrics : Process all metrics specified in the analysis settings for a + binaural signal. Notes ----- -This module relies on external libraries such as numpy, pandas, maad, mosqito, and scipy. -Ensure these dependencies are installed before using this module. +This module relies on external libraries such as numpy, pandas, maad, mosqito, +and scipy. Ensure these dependencies are installed before using this module. + """ import concurrent.futures import multiprocessing as mp import warnings -from typing import Dict, List, Optional, Tuple, Union +from typing import Literal, TypedDict + +try: + from typing import Unpack +except ImportError: + from typing_extensions import Unpack import numpy as np import pandas as pd +from acoustic_toolbox import Signal from loguru import logger from maad.features import all_spectral_alpha_indices, all_temporal_alpha_indices from maad.sound import spectrogram @@ -45,9 +57,10 @@ sharpness_din_perseg, sharpness_din_tv, ) +from numpy.typing import NDArray from scipy import stats -from .analysis_settings import AnalysisSettings +from soundscapy.audio.analysis_settings import AnalysisSettings DEFAULT_LABELS = { "LZeq": "LZeq", @@ -64,10 +77,13 @@ def _stat_calcs( - label: str, ts_array: np.ndarray, res: dict, statistics: List[Union[int, str]] + label: str, + ts_array: NDArray[np.float64], + res: dict, + statistics: tuple[int | str, ...], ) -> dict: """ - Calculate various statistics for a time series array and add them to a results dictionary. + Calculate various statistics for a time series array and add them to a dictionary. Parameters ---------- @@ -95,6 +111,7 @@ def _stat_calcs( >>> updated_res = _stat_calcs("metric", ts, res, [50, "avg", "max"]) >>> print(updated_res) {'metric_50': 3.0, 'metric_avg': 3.0, 'metric_max': 5} + """ logger.debug(f"Calculating statistics for {label}") for stat in statistics: @@ -111,18 +128,38 @@ def _stat_calcs( res[f"{label}_{stat}"] = stats.skew(ts_array) elif stat == "std": res[f"{label}_{stat}"] = np.std(ts_array) - else: + elif isinstance(stat, int): res[f"{label}_{stat}"] = np.percentile(ts_array, 100 - stat) - except Exception as e: - logger.error(f"Error calculating {stat} for {label}: {str(e)}") + else: + logger.error(f"Unrecognized statistic: {stat} for {label}") + res[f"{label}_{stat}"] = np.nan + except Exception as e: # noqa: BLE001, PERF203 + logger.error(f"Error calculating {stat} for {label}: {e!s}") res[f"{label}_{stat}"] = np.nan return res +class _MosqitoMetricParams(TypedDict, total=False): + field_type: str # loudness_zwtv, sharpness_din_from_loudness, sharpness_din_tv + weighting: ( + str # sharpness_din_from_loudness, sharpness_din_perseg, sharpness_din_tv + ) + overlap: float # roughness_dw + nperseg: int # sharpness_din_perseg + noverlap: int | None # sharpness_din_perseg + skip: float # sharpness_din_tv + + def mosqito_metric_1ch( - s, - metric: str, - statistics: Tuple[Union[int, str]] = ( + s: Signal, + metric: Literal[ + "loudness_zwtv", + "roughness_dw", + "sharpness_din_from_loudness", + "sharpness_din_perseg", + "sharpness_din_tv", + ], + statistics: tuple[int | str, ...] = ( 5, 10, 50, @@ -134,11 +171,12 @@ def mosqito_metric_1ch( "kurt", "skew", ), - label: Optional[str] = None, + label: str | None = None, + *, as_df: bool = False, return_time_series: bool = False, - func_args: Dict = {}, -) -> Union[Dict, pd.DataFrame]: + **kwargs: Unpack[_MosqitoMetricParams], +) -> dict | pd.DataFrame: """ Calculate a MoSQITo psychoacoustic metric for a single channel signal. @@ -169,7 +207,8 @@ def mosqito_metric_1ch( Raises ------ ValueError - If the input signal is not single-channel or if an unrecognized metric is specified. + If the input signal is not single-channel + or if an unrecognized metric is specified. Examples -------- @@ -177,18 +216,21 @@ def mosqito_metric_1ch( >>> from soundscapy.audio import Binaural >>> signal = Binaural.from_wav("audio.wav", resample=480000) >>> results = mosqito_metric_1ch(signal[0], "loudness_zwtv", as_df=True) + """ logger.debug(f"Calculating MoSQITo metric: {metric}") # Checks and warnings if s.channels != 1: logger.error("Signal must be single channel") - raise ValueError("Signal must be single channel") + msg = "Signal must be single channel" + raise ValueError(msg) try: label = label or DEFAULT_LABELS[metric] except KeyError as e: logger.error(f"Metric {metric} not recognized") - raise ValueError(f"Metric {metric} not recognized.") from e + msg = f"Metric {metric} not recognized." + raise ValueError(msg) from e if as_df and return_time_series: logger.warning( "Cannot return both a dataframe and time series. Returning dataframe only." @@ -199,54 +241,102 @@ def mosqito_metric_1ch( res = {} try: if metric == "loudness_zwtv": - N, N_spec, bark_axis, time_axis = loudness_zwtv(s, s.fs, **func_args) + # Prepare args specifically for loudness_zwtv + loudness_args = {} + if "field_type" in kwargs: + loudness_args["field_type"] = kwargs["field_type"] + # Call with filtered args + N, N_spec, _, time_axis = loudness_zwtv(s, s.fs, **loudness_args) # noqa: N806 + # TODO(MitchellAcoustics): Add the bark_axis back in + # when we implement time series calcs + # https://github.com/MitchellAcoustics/Soundscapy/issues/113 res = _stat_calcs(label, N, res, statistics) if return_time_series: res[f"{label}_ts"] = (time_axis, N) + elif metric == "roughness_dw": - R, R_spec, bark_axis, time_axis = roughness_dw(s, s.fs, **func_args) - res = _stat_calcs(label, R, res, statistics) + # Prepare args specifically for roughness_dw + roughness_args = {} + if "overlap" in kwargs: + roughness_args["overlap"] = kwargs["overlap"] + # Call with filtered args + R, _, _, time_axis = roughness_dw(s, s.fs, **roughness_args) # noqa: N806 + # TODO(MitchellAcoustics): Add the R_spec and bark_axis back in + # when we implement time series calcs + # https://github.com/MitchellAcoustics/Soundscapy/issues/113 + if isinstance(R, float | int): + res[label] = R + elif isinstance(R, np.ndarray) and len(R) == 1: + res[label] = R[0] + else: + res = _stat_calcs(label, R, res, statistics) if return_time_series: res[f"{label}_ts"] = (time_axis, R) + elif metric == "sharpness_din_from_loudness": - field_type = func_args.get("field_type", "free") - N, N_spec, bark_axis, time_axis = loudness_zwtv( - s, s.fs, field_type=field_type - ) + # Prepare args for loudness_zwtv (needed first) + loudness_args = {} + if "field_type" in kwargs: + loudness_args["field_type"] = kwargs["field_type"] + N, N_spec, _, time_axis = loudness_zwtv(s, s.fs, **loudness_args) # noqa: N806 + # TODO(MitchellAcoustics): Add the R_spec and bark_axis back in + # when we implement time series calcs + # https://github.com/MitchellAcoustics/Soundscapy/issues/113 res = _stat_calcs("N", N, res, statistics) if return_time_series: res["N_ts"] = time_axis, N - func_args.pop("field_type", None) - S = sharpness_din_from_loudness(N, N_spec, **func_args) + # Prepare args specifically for sharpness_din_from_loudness + sharpness_args = {} + if "weighting" in kwargs: + sharpness_args["weighting"] = kwargs["weighting"] + # Call with filtered args + S = sharpness_din_from_loudness(N, N_spec, **sharpness_args) # noqa: N806 res = _stat_calcs(label, S, res, statistics) if return_time_series: res[f"{label}_ts"] = (time_axis, S) + elif metric == "sharpness_din_perseg": - S, time_axis = sharpness_din_perseg(s, s.fs, **func_args) + # Prepare args specifically for sharpness_din_perseg + sharpness_args = {} + if "weighting" in kwargs: + sharpness_args["weighting"] = kwargs["weighting"] + if "nperseg" in kwargs: + sharpness_args["nperseg"] = kwargs["nperseg"] + if "noverlap" in kwargs: + sharpness_args["noverlap"] = kwargs["noverlap"] + # Call with filtered args + S, time_axis = sharpness_din_perseg(s, s.fs, **sharpness_args) # noqa: N806 res = _stat_calcs(label, S, res, statistics) if return_time_series: res[f"{label}_ts"] = (time_axis, S) + elif metric == "sharpness_din_tv": - S, time_axis = sharpness_din_tv(s, s.fs, **func_args) + # Prepare args specifically for sharpness_din_tv + sharpness_args = {} + if "weighting" in kwargs: + sharpness_args["weighting"] = kwargs["weighting"] + if "skip" in kwargs: + sharpness_args["skip"] = kwargs["skip"] + # Call with filtered args + S, time_axis = sharpness_din_tv(s, s.fs, **sharpness_args) # noqa: N806 res = _stat_calcs(label, S, res, statistics) if return_time_series: res[f"{label}_ts"] = (time_axis, S) else: - logger.error(f"Metric {metric} not recognized") - raise ValueError(f"Metric {metric} not recognized.") + msg = f"Metric {metric} not recognized." + logger.error(msg) + raise ValueError(msg) except Exception as e: - logger.error(f"Error calculating {metric}: {str(e)}") + logger.error(f"Error calculating {metric}: {e!s}") raise # Return the results in the requested format if not as_df: return res - try: - rec = s.recording - return pd.DataFrame(res, index=[rec]) - except AttributeError: - return pd.DataFrame(res, index=[0]) + + rec = getattr(s, "recording", None) + return pd.DataFrame(res, index=[rec]) def maad_metric_1ch(s, metric: str, as_df: bool = False, func_args={}): @@ -281,6 +371,7 @@ def maad_metric_1ch(s, metric: str, as_df: bool = False, func_args={}): -------- maad.features.all_spectral_alpha_indices maad.features.all_temporal_alpha_indices + """ logger.debug(f"Calculating MAAD metric: {metric}") @@ -302,7 +393,7 @@ def maad_metric_1ch(s, metric: str, as_df: bool = False, func_args={}): logger.error(f"Metric {metric} not recognized") raise ValueError(f"Metric {metric} not recognized.") except Exception as e: - logger.error(f"Error calculating {metric}: {str(e)}") + logger.error(f"Error calculating {metric}: {e!s}") raise if not as_df: @@ -318,7 +409,7 @@ def maad_metric_1ch(s, metric: str, as_df: bool = False, func_args={}): def pyacoustics_metric_1ch( s, metric: str, - statistics: List[Union[int, str]] = ( + statistics: list[int | str] = ( 5, 10, 50, @@ -353,7 +444,7 @@ def pyacoustics_metric_1ch( def acoustics_metric_1ch( s, metric: str, - statistics: List[Union[int, str]] = ( + statistics: list[int | str] = ( 5, 10, 50, @@ -406,6 +497,7 @@ def acoustics_metric_1ch( See Also -------- acoustic_toolbox + """ logger.debug(f"Calculating acoustics metric: {metric}") @@ -453,7 +545,7 @@ def acoustics_metric_1ch( logger.error(f"Metric {metric} not recognized") raise ValueError(f"Metric {metric} not recognized.") except Exception as e: - logger.error(f"Error calculating {metric}: {str(e)}") + logger.error(f"Error calculating {metric}: {e!s}") raise if not as_df: @@ -468,7 +560,7 @@ def acoustics_metric_1ch( def pyacoustics_metric_2ch( b, metric: str, - statistics: Union[Tuple, List] = ( + statistics: tuple | list = ( 5, 10, 50, @@ -481,7 +573,7 @@ def pyacoustics_metric_2ch( "skew", ), label: str = None, - channel_names: Tuple[str, str] = ("Left", "Right"), + channel_names: tuple[str, str] = ("Left", "Right"), as_df: bool = False, return_time_series: bool = False, func_args={}, @@ -505,7 +597,7 @@ def pyacoustics_metric_2ch( def acoustics_metric_2ch( b, metric: str, - statistics: Union[Tuple, List] = ( + statistics: tuple | list = ( 5, 10, 50, @@ -518,7 +610,7 @@ def acoustics_metric_2ch( "skew", ), label: str | None = None, - channel_names: Tuple[str, str] = ("Left", "Right"), + channel_names: tuple[str, str] = ("Left", "Right"), as_df: bool = False, return_time_series: bool = False, func_args={}, @@ -561,6 +653,7 @@ def acoustics_metric_2ch( See Also -------- acoustics_metric_1ch + """ logger.debug(f"Calculating acoustics metric for 2 channels: {metric}") @@ -595,7 +688,7 @@ def acoustics_metric_2ch( res = {channel_names[0]: res_l, channel_names[1]: res_r} except Exception as e: - logger.error(f"Error calculating {metric} for 2 channels: {str(e)}") + logger.error(f"Error calculating {metric} for 2 channels: {e!s}") raise if not as_df: @@ -614,7 +707,7 @@ def acoustics_metric_2ch( def _parallel_mosqito_metric_2ch( b, metric: str, - statistics: Union[Tuple, List] = ( + statistics: tuple | list = ( 5, 10, 50, @@ -627,7 +720,7 @@ def _parallel_mosqito_metric_2ch( "skew", ), label: str | None = None, - channel_names: Tuple[str, str] = ("Left", "Right"), + channel_names: tuple[str, str] = ("Left", "Right"), return_time_series: bool = False, func_args={}, ): @@ -660,6 +753,7 @@ def _parallel_mosqito_metric_2ch( See Also -------- mosqito_metric_1ch + """ logger.debug(f"Calculating MoSQITo metric in parallel: {metric}") @@ -684,7 +778,7 @@ def _parallel_mosqito_metric_2ch( pool.close() return {channel: result_objects[i] for i, channel in enumerate(channel_names)} except Exception as e: - logger.error(f"Error in parallel MoSQITo calculation: {str(e)}") + logger.error(f"Error in parallel MoSQITo calculation: {e!s}") pool.close() raise finally: @@ -694,7 +788,7 @@ def _parallel_mosqito_metric_2ch( def mosqito_metric_2ch( b, metric: str, - statistics: Union[Tuple, List] = ( + statistics: tuple | list = ( 5, 10, 50, @@ -707,7 +801,7 @@ def mosqito_metric_2ch( "skew", ), label: str = None, - channel_names: Tuple[str, str] = ("Left", "Right"), + channel_names: tuple[str, str] = ("Left", "Right"), as_df: bool = False, return_time_series: bool = False, parallel: bool = True, @@ -751,6 +845,7 @@ def mosqito_metric_2ch( ------ ValueError If the input signal is not 2-channel. + """ logger.debug(f"Calculating MoSQITo metric for 2 channels: {metric}") @@ -774,9 +869,9 @@ def mosqito_metric_2ch( metric, statistics, label, - False, - return_time_series, - func_args, + as_df=False, + return_time_series=return_time_series, + **func_args, ) future_r = executor.submit( mosqito_metric_1ch, @@ -784,9 +879,9 @@ def mosqito_metric_2ch( metric, statistics, label, - False, - return_time_series, - func_args, + as_df=False, + return_time_series=return_time_series, + **func_args, ) res_l = future_l.result() res_r = future_r.result() @@ -796,25 +891,23 @@ def mosqito_metric_2ch( metric, statistics, label, - False, - return_time_series, - func_args, + as_df=False, + return_time_series=return_time_series, + **func_args, ) res_r = mosqito_metric_1ch( b[1], metric, statistics, label, - False, - return_time_series, - func_args, + as_df=False, + return_time_series=return_time_series, + **func_args, ) res = {channel_names[0]: res_l, channel_names[1]: res_r} except Exception as e: - logger.error( - f"Error calculating MoSQITo metric {metric} for 2 channels: {str(e)}" - ) + logger.error(f"Error calculating MoSQITo metric {metric} for 2 channels: {e!s}") raise if not as_df: @@ -833,7 +926,7 @@ def mosqito_metric_2ch( def maad_metric_2ch( b, metric: str, - channel_names: Tuple[str, str] = ("Left", "Right"), + channel_names: tuple[str, str] = ("Left", "Right"), as_df: bool = False, func_args={}, ): @@ -870,6 +963,7 @@ def maad_metric_2ch( -------- scikit-maad library maad_metric_1ch + """ logger.debug(f"Calculating MAAD metric for 2 channels: {metric}") @@ -880,11 +974,11 @@ def maad_metric_2ch( logger.debug(f"Calculating scikit-maad {metric}") try: - res_l = maad_metric_1ch(b[0], metric, as_df=False) - res_r = maad_metric_1ch(b[1], metric, as_df=False) + res_l = maad_metric_1ch(b[0], metric, as_df=False, **func_args) + res_r = maad_metric_1ch(b[1], metric, as_df=False, **func_args) res = {channel_names[0]: res_l, channel_names[1]: res_r} except Exception as e: - logger.error(f"Error calculating MAAD metric {metric} for 2 channels: {str(e)}") + logger.error(f"Error calculating MAAD metric {metric} for 2 channels: {e!s}") raise if not as_df: @@ -925,6 +1019,7 @@ def prep_multiindex_df(dictionary: dict, label: str = "Leq", incl_metric: bool = ------ ValueError If the input dictionary is not in the expected format. + """ logger.info("Preparing MultiIndex DataFrame") try: @@ -940,7 +1035,7 @@ def prep_multiindex_df(dictionary: dict, label: str = "Leq", incl_metric: bool = logger.debug("MultiIndex DataFrame prepared successfully") return df except Exception as e: - logger.error(f"Error preparing MultiIndex DataFrame: {str(e)}") + logger.error(f"Error preparing MultiIndex DataFrame: {e!s}") raise ValueError("Invalid input dictionary format") from e @@ -964,6 +1059,7 @@ def add_results(results_df: pd.DataFrame, metric_results: pd.DataFrame): ------ ValueError If the input DataFrames are not in the expected format. + """ logger.info("Adding results to MultiIndex DataFrame") try: @@ -978,7 +1074,7 @@ def add_results(results_df: pd.DataFrame, metric_results: pd.DataFrame): logger.debug("Results added successfully") return results_df except Exception as e: - logger.error(f"Error adding results to DataFrame: {str(e)}") + logger.error(f"Error adding results to DataFrame: {e!s}") raise ValueError("Invalid input DataFrame format") from e @@ -1024,6 +1120,7 @@ def process_all_metrics( >>> signal = Binaural.from_wav("audio.wav", resample=480000) >>> settings = AnalysisSettings.from_yaml("settings.yaml") >>> results = process_all_metrics(signal,settings) + """ logger.info(f"Processing all metrics for {b.recording}") logger.debug(f"Parallel processing: {parallel}") @@ -1074,7 +1171,7 @@ def process_all_metrics( logger.info("All metrics processed successfully") return results_df except Exception as e: - logger.error(f"Error processing metrics: {str(e)}") + logger.error(f"Error processing metrics: {e!s}") raise ValueError("Error processing metrics") from e diff --git a/src/soundscapy/audio/parallel_processing.py b/src/soundscapy/audio/parallel_processing.py index 9d06aa7a..7dc2a58b 100644 --- a/src/soundscapy/audio/parallel_processing.py +++ b/src/soundscapy/audio/parallel_processing.py @@ -1,8 +1,5 @@ """ -soundscapy.audio.parallel_processing -==================================== - -This module provides functions for parallel processing of binaural audio files. +Functions for parallel processing of binaural audio files. It includes functions to load and analyze binaural files, as well as to process multiple files in parallel using concurrent.futures. @@ -14,14 +11,14 @@ Note: This module requires the tqdm library for progress bars and concurrent.futures for parallel processing. It uses loguru for logging. + """ import concurrent.futures from pathlib import Path -from typing import Dict, List, Optional -from loguru import logger import pandas as pd +from loguru import logger from tqdm.auto import tqdm from .analysis_settings import AnalysisSettings @@ -33,21 +30,22 @@ ) -def tqdm_write_sink(message): +def tqdm_write_sink(message: str) -> None: """ - A custom sink for loguru that writes messages using tqdm.write(). + Custom sink for loguru that writes messages using tqdm.write(). This ensures that log messages don't interfere with tqdm progress bars. - """ + """ # noqa: D401 tqdm.write(message, end="") def load_analyse_binaural( wav_file: Path, - levels: Dict | List[float], + levels: dict[str, float] | list[float] | None, analysis_settings: AnalysisSettings, + resample: int | None = None, + *, parallel_mosqito: bool = True, - resample: Optional[int] = None, ) -> pd.DataFrame: """ Load and analyze a single binaural audio file. @@ -68,6 +66,7 @@ def load_analyse_binaural( ------- pd.DataFrame DataFrame with analysis results. + """ logger.info(f"Processing {wav_file}") try: @@ -85,18 +84,19 @@ def load_analyse_binaural( logger.warning(f"No calibration levels found for {wav_file}") return process_all_metrics(b, analysis_settings, parallel=parallel_mosqito) except Exception as e: - logger.error(f"Error processing {wav_file}: {str(e)}") + logger.error(f"Error processing {wav_file}: {e!s}") raise def parallel_process( - wav_files: List[Path], + wav_files: list[Path], results_df: pd.DataFrame, - levels: Dict, + levels: dict, analysis_settings: AnalysisSettings, - max_workers: Optional[int] = None, + max_workers: int | None = None, + resample: int | None = None, + *, parallel_mosqito: bool = True, - resample: Optional[int] = None, ) -> pd.DataFrame: """ Process multiple binaural files in parallel. @@ -121,6 +121,7 @@ def parallel_process( ------- pd.DataFrame Updated results DataFrame with analysis results for all files. + """ logger.info(f"Starting parallel processing of {len(wav_files)} files") @@ -135,8 +136,8 @@ def parallel_process( wav_file, levels, analysis_settings, - parallel_mosqito, resample, + parallel_mosqito=parallel_mosqito, ) futures.append(future) @@ -146,7 +147,7 @@ def parallel_process( result = future.result() results_df = add_results(results_df, result) except Exception as e: - logger.error(f"Error processing file: {str(e)}") + logger.error(f"Error processing file: {e!s}") finally: pbar.update(1) @@ -159,13 +160,13 @@ def parallel_process( if __name__ == "__main__": # Example usage - from datetime import datetime import json import warnings + from datetime import datetime warnings.filterwarnings("ignore") - from soundscapy.logging import setup_logging + from soundscapy.sspylogging import setup_logging setup_logging("DEBUG") diff --git a/src/soundscapy/data/__init__.py b/src/soundscapy/data/__init__.py index e69de29b..0928964b 100644 --- a/src/soundscapy/data/__init__.py +++ b/src/soundscapy/data/__init__.py @@ -0,0 +1,6 @@ +""" +Soundscape data module. + +This module serves as the package initializer for the soundscape data processing +functionalities. +""" diff --git a/src/soundscapy/data/default_settings.yaml b/src/soundscapy/data/default_settings.yaml index 7eea121f..ad01f75b 100644 --- a/src/soundscapy/data/default_settings.yaml +++ b/src/soundscapy/data/default_settings.yaml @@ -7,120 +7,120 @@ version: "1.1" # Supported metrics: LAeq, LZeq, LCeq, SEL # Supported statistics: avg/mean, max, min, kurt, skew, any integer from 1-99 AcousticToolbox: - # A-weighted equivalent continuous sound level - LAeq: - run: true # Set to false to skip this metric - main: "avg" # Main statistic to calculate - statistics: [5, 10, 50, 90, 95, "avg", "min", "max", "kurt", "skew"] # List of statistics to calculate - channel: ["Left", "Right"] # Channels to analyze - label: "LAeq" # Label for the metric in output - func_args: # Additional arguments for the metric function - time: 0.125 # Time interval for calculation (seconds) - method: "average" # Method for calculating Leq + # A-weighted equivalent continuous sound level + LAeq: + run: true # Set to false to skip this metric + main: "avg" # Main statistic to calculate + statistics: [5, 10, 50, 90, 95, "avg", "min", "max", "kurt", "skew"] # List of statistics to calculate + channel: ["Left", "Right"] # Channels to analyze + label: "LAeq" # Label for the metric in output + func_args: # Additional arguments for the metric function + time: 0.125 # Time interval for calculation (seconds) + method: "average" # Method for calculating Leq - # Z-weighted (unweighted) equivalent continuous sound level - LZeq: - run: true - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "LZeq" - func_args: - time: 0.125 - method: "average" + # Z-weighted (unweighted) equivalent continuous sound level + LZeq: + run: true + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "LZeq" + func_args: + time: 0.125 + method: "average" - # C-weighted equivalent continuous sound level - LCeq: - run: true - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "LCeq" - func_args: - time: 0.125 - method: "average" + # C-weighted equivalent continuous sound level + LCeq: + run: true + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "LCeq" + func_args: + time: 0.125 + method: "average" - # Sound Exposure Level - SEL: - run: true - channel: ["Left", "Right"] - label: "SEL" - # Note: SEL doesn't use main or statistics as it's a single value metric + # Sound Exposure Level + SEL: + run: true + channel: ["Left", "Right"] + label: "SEL" + # Note: SEL doesn't use main or statistics as it's a single value metric # MoSQITo Library Settings # Supported metrics: loudness_zwtv, sharpness_din_from_loudness, sharpness_din_perseg, sharpness_din_tv, roughness_dw # Supported statistics: avg/mean, max, min, kurt, skew, any integer from 1-99 MoSQITo: - # Zwicker Time-Varying Loudness - loudness_zwtv: - run: false # Disabled by default as it's used in sharpness calculation - main: 5 # N5 (loudness exceeded 5% of the time) - statistics: [10, 50, 90, 95, "min", "max", "kurt", "skew", "avg"] - channel: ["Left", "Right"] - label: "N" - parallel: true # Enable parallel processing - func_args: - field_type: "free" # Free field condition + # Zwicker Time-Varying Loudness + loudness_zwtv: + run: false # Disabled by default as it's used in sharpness calculation + main: 5 # N5 (loudness exceeded 5% of the time) + statistics: [10, 50, 90, 95, "min", "max", "kurt", "skew", "avg"] + channel: ["Left", "Right"] + label: "N" + parallel: true # Enable parallel processing + func_args: + field_type: "free" # Free field condition - # Sharpness (DIN 45692) calculated from Zwicker Loudness - sharpness_din_from_loudness: - run: true - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "S_L" - parallel: true - func_args: - weighting: "din" # DIN 45692 weighting - field_type: "free" + # Sharpness (DIN 45692) calculated from Zwicker Loudness + sharpness_din_from_loudness: + run: true + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "S_L" + parallel: true + func_args: + weighting: "din" # DIN 45692 weighting + field_type: "free" - # Sharpness (DIN 45692) calculated per segment - sharpness_din_perseg: - run: true - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "S_perseg" - parallel: false # Parallel processing not necessary for this method - func_args: - weighting: "din" - nperseg: 4096 # Number of samples per segment - field_type: "free" + # Sharpness (DIN 45692) calculated per segment + sharpness_din_perseg: + run: true + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "S_perseg" + parallel: false # Parallel processing not necessary for this method + func_args: + weighting: "din" + nperseg: 4096 # Number of samples per segment + field_type: "free" - # Sharpness (DIN 45692) time-varying - # Note: It's recommended to use sharpness_din_from_loudness instead - sharpness_din_tv: - run: false - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "S_din_tv" - parallel: true - func_args: - weighting: "din" - field_type: "free" - skip: 0.5 # Skip time at the beginning (seconds) + # Sharpness (DIN 45692) time-varying + # Note: It's recommended to use sharpness_din_from_loudness instead + sharpness_din_tv: + run: false + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "S_din_tv" + parallel: true + func_args: + weighting: "din" + field_type: "free" + skip: 0.5 # Skip time at the beginning (seconds) - # Roughness (Daniel & Weber method) - roughness_dw: - run: true - main: "avg" - statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] - channel: ["Left", "Right"] - label: "R" - parallel: true + # Roughness (Daniel & Weber method) + roughness_dw: + run: true + main: "avg" + statistics: [5, 10, 50, 90, 95, "min", "max", "kurt", "skew"] + channel: ["Left", "Right"] + label: "R" + parallel: true # scikit-maad Library Settings # These are collections of multiple acoustic indices scikit-maad: - # Temporal alpha diversity indices - all_temporal_alpha_indices: - run: true - channel: ["Left", "Right"] - # Note: This calculates multiple indices, so no individual statistics are specified + # Temporal alpha diversity indices + all_temporal_alpha_indices: + run: true + channel: ["Left", "Right"] + # Note: This calculates multiple indices, so no individual statistics are specified - # Spectral alpha diversity indices - all_spectral_alpha_indices: - run: true - channel: ["Left", "Right"] - # Note: This calculates multiple indices, so no individual statistics are specified + # Spectral alpha diversity indices + all_spectral_alpha_indices: + run: true + channel: ["Left", "Right"] + # Note: This calculates multiple indices, so no individual statistics are specified diff --git a/src/soundscapy/databases/__init__.py b/src/soundscapy/databases/__init__.py index e69de29b..f5310b2a 100644 --- a/src/soundscapy/databases/__init__.py +++ b/src/soundscapy/databases/__init__.py @@ -0,0 +1,10 @@ +""" +Soundscapy Databases Module. + +This module handles connections to and operations on soundscape databases, +primarily focused on the International Soundscape Database (ISD). +""" + +from soundscapy.databases import isd, satp + +__all__ = ["isd", "satp"] diff --git a/src/soundscapy/databases/araus.py b/src/soundscapy/databases/araus.py index 19f9e5d7..abf66abf 100644 --- a/src/soundscapy/databases/araus.py +++ b/src/soundscapy/databases/araus.py @@ -1,5 +1,9 @@ -# Customized functions specifically for the ARAUS dataset +"""Customized functions specifically for the ARAUS dataset.""" -# Add soundscapy to the Python path +import warnings -# Constants and Labels +warnings.warn( + "The ARAUS dataset module is not yet implemented", + FutureWarning, + stacklevel=2, +) diff --git a/src/soundscapy/databases/isd.py b/src/soundscapy/databases/isd.py index 02d0a6fb..3aaba8ba 100644 --- a/src/soundscapy/databases/isd.py +++ b/src/soundscapy/databases/isd.py @@ -19,13 +19,14 @@ True >>> 'PAQ1' in df.columns True + """ from importlib import resources -from typing import Dict, List, Optional, Tuple import pandas as pd from loguru import logger + from soundscapy.surveys.processing import ( calculate_iso_coords, likert_data_quality, @@ -75,6 +76,7 @@ def load() -> pd.DataFrame: True >>> set(PAQ_IDS).issubset(df.columns) True + """ isd_resource = resources.files("soundscapy.data").joinpath("ISD v1.0 Data.csv") with resources.as_file(isd_resource) as f: @@ -116,6 +118,7 @@ def load_zenodo(version: str = "latest") -> pd.DataFrame: True >>> set(PAQ_IDS).issubset(df.columns) # doctest: +SKIP True + """ version = version.lower() version = "v1.0.1" if version == "latest" else version @@ -130,7 +133,8 @@ def load_zenodo(version: str = "latest") -> pd.DataFrame: } if version not in url_mapping: - raise ValueError(f"Version {version} not recognised.") + msg = f"Version {version} not recognised." + raise ValueError(msg) url = url_mapping[version] file_type = "csv" if version in ["v1.0.0", "v1.0.1"] else "excel" @@ -148,10 +152,11 @@ def load_zenodo(version: str = "latest") -> pd.DataFrame: def validate( df: pd.DataFrame, - paq_aliases: List | Dict = _PAQ_ALIASES, + paq_aliases: list | dict = _PAQ_ALIASES, + val_range: tuple[int, int] = (1, 5), + *, allow_paq_na: bool = False, - val_range: Tuple[int, int] = (1, 5), -) -> Tuple[pd.DataFrame, Optional[pd.DataFrame]]: +) -> tuple[pd.DataFrame, pd.DataFrame | None]: """ Perform data quality checks and validate that the dataset fits the expected format. @@ -168,8 +173,9 @@ def validate( Returns ------- - Tuple[pd.DataFrame, Optional[pd.DataFrame]] - Tuple containing the cleaned dataframe and optionally a dataframe of excluded samples. + Tuple[pd.DataFrame, pd.DataFrame | None] + Tuple containing the cleaned dataframe + and optionally a dataframe of excluded samples. Notes ----- @@ -190,27 +196,28 @@ def validate( 2 >>> excl_df.shape[0] 2 + """ logger.info("Validating ISD data") - df = rename_paqs(df, paq_aliases) + data = rename_paqs(df, paq_aliases) invalid_indices = likert_data_quality( - df, allow_na=allow_paq_na, val_range=val_range + data, val_range=val_range, allow_na=allow_paq_na ) if invalid_indices: - excl_df = df.iloc[invalid_indices] - df = df.drop(df.index[invalid_indices]) + excl_data = data.iloc[invalid_indices] + data = data.drop(data.index[invalid_indices]) logger.info(f"Removed {len(invalid_indices)} rows with invalid PAQ data") else: - excl_df = None + excl_data = None logger.info("All PAQ data passed quality checks") - return df, excl_df + return data, excl_data def _isd_select( - data: pd.DataFrame, select_by: str, condition: str | int | List | Tuple + data: pd.DataFrame, select_by: str, condition: str | int | list | tuple ) -> pd.DataFrame: """ General function to select by ID variables. @@ -244,17 +251,18 @@ def _isd_select( ID Value 0 A 1 2 C 3 + """ - if isinstance(condition, (str, int)): + if isinstance(condition, str | int): return data.query(f"{select_by} == @condition", engine="python") - elif isinstance(condition, (list, tuple)): + if isinstance(condition, list | tuple): return data.query(f"{select_by} in @condition") - else: - raise TypeError("Should be either a str, int, list, or tuple.") + msg = "Should be either a str, int, list, or tuple." + raise TypeError(msg) def select_record_ids( - data: pd.DataFrame, record_ids: str | int | List | Tuple + data: pd.DataFrame, record_ids: str | int | list | tuple ) -> pd.DataFrame: """ Filter the dataframe by RecordID. @@ -281,12 +289,13 @@ def select_record_ids( RecordID Value 0 A 1 2 C 3 + """ return _isd_select(data, "RecordID", record_ids) def select_group_ids( - data: pd.DataFrame, group_ids: str | int | List | Tuple + data: pd.DataFrame, group_ids: str | int | list | tuple ) -> pd.DataFrame: """ Filter the dataframe by GroupID. @@ -313,12 +322,13 @@ def select_group_ids( GroupID Value 0 G1 1 1 G1 2 + """ return _isd_select(data, "GroupID", group_ids) def select_session_ids( - data: pd.DataFrame, session_ids: str | int | List | Tuple + data: pd.DataFrame, session_ids: str | int | list | tuple ) -> pd.DataFrame: """ Filter the dataframe by SessionID. @@ -347,12 +357,13 @@ def select_session_ids( 1 S1 2 2 S2 3 3 S2 4 + """ return _isd_select(data, "SessionID", session_ids) def select_location_ids( - data: pd.DataFrame, location_ids: str | int | List | Tuple + data: pd.DataFrame, location_ids: str | int | list | tuple ) -> pd.DataFrame: """ Filter the dataframe by LocationID. @@ -379,6 +390,7 @@ def select_location_ids( LocationID Value 2 L2 3 3 L2 4 + """ return _isd_select(data, "LocationID", location_ids) @@ -389,7 +401,7 @@ def describe_location( calc_type: str = "percent", pl_threshold: float = 0, ev_threshold: float = 0, -) -> Dict[str, int | float]: +) -> dict[str, int | float]: """ Return a summary of the data for a specific location. @@ -427,10 +439,14 @@ def describe_location( ... }) >>> df = add_iso_coords(df) >>> result = describe_location(df, 'L1') - >>> set(result.keys()) == {'count', 'ISOPleasant', 'ISOEventful', 'pleasant', 'eventful', 'vibrant', 'chaotic', 'monotonous', 'calm'} + >>> set(result.keys()) == { + ... 'count', 'ISOPleasant', 'ISOEventful', 'pleasant', 'eventful', + ... 'vibrant', 'chaotic', 'monotonous', 'calm' + ... } True >>> result['count'] 2 + """ loc_df = select_location_ids(data, location_ids=location) count = len(loc_df) @@ -483,7 +499,8 @@ def describe_location( } ) else: - raise ValueError("Type must be either 'percent' or 'count'") + msg = "Type must be either 'percent' or 'count'" + raise ValueError(msg) return {k: round(v, 3) if isinstance(v, float) else v for k, v in res.items()} @@ -528,11 +545,15 @@ def soundscapy_describe( True >>> result.index.tolist() ['L1', 'L2'] - >>> set(result.columns) == {'count', 'ISOPleasant', 'ISOEventful', 'pleasant', 'eventful', 'vibrant', 'chaotic', 'monotonous', 'calm'} + >>> set(result.columns) == { + ... 'count', 'ISOPleasant', 'ISOEventful', 'pleasant', 'eventful', + ... 'vibrant', 'chaotic', 'monotonous', 'calm' + ... } True >>> result = soundscapy_describe(df, calc_type="count") >>> result.loc['L1', 'count'] 2 + """ res = { location: describe_location(df, location, calc_type=calc_type) diff --git a/src/soundscapy/databases/satp.py b/src/soundscapy/databases/satp.py index 276b1c9a..c2f91995 100644 --- a/src/soundscapy/databases/satp.py +++ b/src/soundscapy/databases/satp.py @@ -18,6 +18,7 @@ True >>> 'Country' in participants.columns # doctest: +SKIP True + """ import pandas as pd @@ -53,11 +54,11 @@ def _url_fetch(version: str) -> str: Traceback (most recent call last): ... ValueError: Invalid version. Should be either 'latest', 'v1.2.1', or 'v1.2'. + """ if version.lower() not in ["latest", "v1.2.1", "v1.2"]: - raise ValueError( - "Invalid version. Should be either 'latest', 'v1.2.1', or 'v1.2'." - ) + msg = "Invalid version. Should be either 'latest', 'v1.2.1', or 'v1.2'." + raise ValueError(msg) version = "v1.2.1" if version.lower() == "latest" else version.lower() url = "https://zenodo.org/record/7143599/files/SATP%20Dataset%20v1.2.xlsx" @@ -79,11 +80,12 @@ def load_zenodo(version: str = "latest") -> pd.DataFrame: ------- pd.DataFrame DataFrame containing the SATP dataset. + """ url = _url_fetch(version) - df = pd.read_excel(url, engine="openpyxl", sheet_name="Main Merge") + data = pd.read_excel(url, engine="openpyxl", sheet_name="Main Merge") logger.info(f"Loaded SATP dataset version {version} from Zenodo") - return df + return data def load_participants(version: str = "latest") -> pd.DataFrame: @@ -99,12 +101,13 @@ def load_participants(version: str = "latest") -> pd.DataFrame: ------- pd.DataFrame DataFrame containing the SATP participants dataset. + """ url = _url_fetch(version) - df = pd.read_excel(url, engine="openpyxl", sheet_name="Participants") - df = df.drop(columns=["Unnamed: 3", "Unnamed: 4"]) + data = pd.read_excel(url, engine="openpyxl", sheet_name="Participants") + data = data.drop(columns=["Unnamed: 3", "Unnamed: 4"]) logger.info(f"Loaded SATP participants dataset version {version} from Zenodo") - return df + return data if __name__ == "__main__": diff --git a/src/soundscapy/plotting/CLAUDE.local.md b/src/soundscapy/plotting/CLAUDE.local.md new file mode 100644 index 00000000..cc71f83c --- /dev/null +++ b/src/soundscapy/plotting/CLAUDE.local.md @@ -0,0 +1,16 @@ +# Soundscapy Plotting Module Development Guide + +- Maintain consistency with the general design principles of Soundscapy given in `CLAUDE.md` and `design.md` + +## Plotting Module Design + +- User-facing API should have explicit parameters (avoid **kwargs where possible) +- Use element-based architecture inspired by grammar of graphics +- Support combinations of plot elements (scatter, density, marginals) +- Support consistent grouping via 'hue' parameter across all elements +- Use explicit parameters named for clarity (e.g., scatter_alpha, density_fill) +- Maintain backend-independent interface with consistent return types +- High-level API functions should be simple with good defaults +- Use protocols for backend interfaces rather than inheritance +- Keep styling separate from data visualization +- Provide convenience functions for common use cases \ No newline at end of file diff --git a/src/soundscapy/plotting/__init__.py b/src/soundscapy/plotting/__init__.py index 966e5f6c..8c260ff9 100644 --- a/src/soundscapy/plotting/__init__.py +++ b/src/soundscapy/plotting/__init__.py @@ -1,41 +1,90 @@ """ -Soundscapy Plotting Module +Soundscapy Plotting Module. This module provides tools for creating circumplex plots for soundscape analysis. -It supports various plot types and backends, with a focus on flexibility and ease of use. +It utilizes the Grammar of Graphics approach with Seaborn Objects API to create +flexible, composable visualizations. Main components: -- CircumplexPlot: Main class for creating customizable circumplex plots -- scatter_plot: Function to quickly create scatter plots -- density_plot: Function to quickly create density plots -- create_circumplex_subplots: Function to create multiple circumplex plots in subplots -- Backend: Enum for selecting the plotting backend (Seaborn or Plotly) -- PlotType: Enum for specifying the type of plot to create +- Custom marks for soundscape plots (SoundscapeCircumplex, SoundscapeQuadrantLabels) +- Custom stats for coordinate calculations (SoundscapeCoordinates) +- Function-based API for creating plots (scatter_plot, density_plot) +- CircumplexPlot builder class for backward compatibility Example usage: - from soundscapy.plotting import scatter_plot, density_plot, Backend, PlotType - # Create a scatter plot using Seaborn backend - scatter_plot(data, x='ISOPleasant', y='ISOEventful', backend=Backend.SEABORN) +```python +import pandas as pd +import seaborn.objects as so +from soundscapy.plotting import ( + scatter_plot, density_plot, SoundscapeCircumplex, SoundscapeQuadrantLabels +) - # Create a density plot using Plotly backend - density_plot(data, x='ISOPleasant', y='ISOEventful', backend=Backend.PLOTLY) +# Function-based API (recommended for new code) +scatter_plot(data, x='ISOPleasant', y='ISOEventful') +density_plot(data, x='ISOPleasant', y='ISOEventful', incl_scatter=True) + +# Direct use of custom components with so.Plot +plot = ( + so.Plot(data, x='ISOPleasant', y='ISOEventful') + .add(so.Dots(), color='LocationID') + .add(SoundscapeCircumplex()) + .add(SoundscapeQuadrantLabels()) +) + +# Legacy builder API (maintained for backwards compatibility) +(CircumplexPlot(data) + .add_density(simple=True) + .add_scatter() + .add_grid(diagonal_lines=True) + .add_title("Custom Plot") + .show()) +``` """ -from . import likert -from .circumplex_plot import CircumplexPlot, CircumplexPlotParams -from .plot_functions import create_circumplex_subplots, density_plot, scatter_plot -from .plotting_utils import Backend, PlotType -from .stylers import StyleOptions +from soundscapy.plotting import likert +from soundscapy.plotting.circumplex_plot import ( + CircumplexPlot, + add_annotation, + apply_circumplex_grid, +) +from soundscapy.plotting.marks import ( + SoundscapeCircumplex, + SoundscapePointAnnotation, + SoundscapeQuadrantLabels, +) +from soundscapy.plotting.plotting_utils import DEFAULT_XLIM, DEFAULT_YLIM, PlotType +from soundscapy.plotting.soundscape_functions import ( + add_calculated_coords, + create_circumplex_subplots, + density_plot, + joint_plot, + scatter_plot, + use_soundscapy_style, +) +from soundscapy.plotting.stats import SoundscapeCoordinates __all__ = [ - "CircumplexPlot", - "CircumplexPlotParams", + # Public API - function based (recommended) "scatter_plot", "density_plot", + "joint_plot", "create_circumplex_subplots", - "Backend", + "add_calculated_coords", + "use_soundscapy_style", + # Custom components + "SoundscapeCircumplex", + "SoundscapeQuadrantLabels", + "SoundscapePointAnnotation", + "SoundscapeCoordinates", + # Legacy API (maintained for backwards compatibility) + "CircumplexPlot", + "add_annotation", + "apply_circumplex_grid", + # Utility classes and constants "PlotType", - "StyleOptions", + "DEFAULT_XLIM", + "DEFAULT_YLIM", + # Sub-modules "likert", ] diff --git a/src/soundscapy/plotting/backends.py b/src/soundscapy/plotting/backends.py index c48a6aaa..508986b4 100644 --- a/src/soundscapy/plotting/backends.py +++ b/src/soundscapy/plotting/backends.py @@ -1,3 +1,23 @@ +""" +Backend classes for plotting. + +This module provides classes for different plotting backends, including Seaborn +and Plotly. Each backend class implements methods for creating scatter and +density plots, as well as applying styling to the plots. + +Example usage: + +```python +from soundscapy.plotting import scatter_plot, density_plot, Backend, PlotType + +# Create a scatter plot using Seaborn backend +scatter_plot(data, x='ISOPleasant', y='ISOEventful', backend=Backend.SEABORN) + +# Create a density plot using Plotly backend +density_plot(data, x='ISOPleasant', y='ISOEventful', backend=Backend.PLOTLY) +``` +""" + import warnings from abc import ABC, abstractmethod @@ -30,8 +50,8 @@ def create_scatter(self, data, params): Returns ------- The created plot object. + """ - pass @abstractmethod def create_density(self, data, params): @@ -46,8 +66,8 @@ def create_density(self, data, params): Returns ------- The created plot object. + """ - pass @abstractmethod def apply_styling(self, plot_obj, params): @@ -62,16 +82,14 @@ def apply_styling(self, plot_obj, params): Returns ------- The styled plot object. + """ - pass class SeabornBackend(PlotBackend): - """ - Backend for creating plots using Seaborn and Matplotlib. - """ + """Backend for creating plots using Seaborn and Matplotlib.""" - def __init__(self, style_options: StyleOptions = StyleOptions()): + def __init__(self, style_options: StyleOptions = StyleOptions()) -> None: self.style_options = style_options def create_scatter(self, data, params, ax=None): @@ -86,6 +104,7 @@ def create_scatter(self, data, params, ax=None): Returns ------- tuple: A tuple containing the figure and axes objects. + """ if ax is None: fig, ax = plt.subplots(figsize=self.style_options.figsize) @@ -123,11 +142,13 @@ def create_density(self, data, params, ax=None): Returns ------- tuple: A tuple containing the figure and axes objects. + """ if len(data) < 30: warnings.warn( "Density plots are not recommended for small datasets (<30 samples).", UserWarning, + stacklevel=2, ) if ax is None: @@ -175,11 +196,12 @@ def create_jointplot(self, data, params): >>> import soundscapy as sspy >>> from soundscapy.plotting import Backend, CircumplexPlot, StyleOptions, CircumplexPlotParams >>> data = sspy.isd.load() - >>> data = sspy.surveys.add_iso_coords(data, overwrite=True) + >>> data = sspy.surveys.add_iso_coords(data,overwrite=True) >>> sample_data = sspy.isd.select_location_ids(data, ['CamdenTown']) >>> plot = CircumplexPlot(data=sample_data, backend=Backend.SEABORN) >>> g = plot.jointplot() >>> g.show() # doctest: +SKIP + """ g = sns.JointGrid(xlim=params.xlim, ylim=params.ylim) joint_params = params @@ -249,33 +271,34 @@ def apply_styling(self, plot_obj, params): Returns ------- tuple: The styled figure and axes objects. + """ fig, ax = plot_obj styler = SeabornStyler(params, self.style_options) return styler.apply_styling(fig, ax) - def show(self, plot_obj): + def show(self, plot_obj) -> None: """ Display the Matplotlib figure. Parameters ---------- fig: The figure to display. + """ fig, _ = plot_obj plt.show() class PlotlyBackend(PlotBackend): - """ - Backend for creating plots using Plotly. - """ + """Backend for creating plots using Plotly.""" - def __init__(self): + def __init__(self) -> None: warnings.warn( - "PlotlyBackend is very experimental and not fully implemented.", UserWarning + "PlotlyBackend is very experimental and not fully implemented.", + UserWarning, + stacklevel=2, ) - pass def create_scatter(self, data, params): """ @@ -289,6 +312,7 @@ def create_scatter(self, data, params): Returns ------- go.Figure: A Plotly figure object. + """ fig = px.scatter( data, @@ -303,8 +327,8 @@ def create_scatter(self, data, params): fig.update_layout( width=600, height=600, - xaxis=dict(scaleanchor="y", scaleratio=1), - yaxis=dict(scaleanchor="x", scaleratio=1), + xaxis={"scaleanchor": "y", "scaleratio": 1}, + yaxis={"scaleanchor": "x", "scaleratio": 1}, ) return fig @@ -320,11 +344,13 @@ def create_density(self, data, params): Returns ------- go.Figure: A Plotly figure object. + """ if len(data) < 30: warnings.warn( "Density plots are not recommended for small datasets (<30 samples). Consider using a scatter plot instead.", UserWarning, + stacklevel=2, ) fig = px.density_heatmap( @@ -339,8 +365,8 @@ def create_density(self, data, params): fig.update_layout( width=600, height=600, - xaxis=dict(scaleanchor="y", scaleratio=1), - yaxis=dict(scaleanchor="x", scaleratio=1), + xaxis={"scaleanchor": "y", "scaleratio": 1}, + yaxis={"scaleanchor": "x", "scaleratio": 1}, ) scatter_trace = px.scatter( data, x=params.x, y=params.y, color=params.hue, opacity=0.5 @@ -360,6 +386,7 @@ def apply_styling(self, plot_obj, params): Returns ------- go.Figure: The styled Plotly figure object. + """ fig = plot_obj if params.diagonal_lines: @@ -368,7 +395,7 @@ def apply_styling(self, plot_obj, params): x=[params.xlim[0], params.xlim[1]], y=[params.ylim[0], params.ylim[1]], mode="lines", - line=dict(color="gray", dash="dash"), + line={"color": "gray", "dash": "dash"}, showlegend=False, ) ) @@ -377,18 +404,19 @@ def apply_styling(self, plot_obj, params): x=[params.xlim[0], params.xlim[1]], y=[params.ylim[1], params.ylim[0]], mode="lines", - line=dict(color="gray", dash="dash"), + line={"color": "gray", "dash": "dash"}, showlegend=False, ) ) return fig - def show(self, fig): + def show(self, fig) -> None: """ Display the Plotly figure. Parameters ---------- fig (go.Figure): The Plotly figure to display. + """ fig.show() diff --git a/src/soundscapy/plotting/circumplex_plot.py b/src/soundscapy/plotting/circumplex_plot.py index 05beee97..bc8c5652 100644 --- a/src/soundscapy/plotting/circumplex_plot.py +++ b/src/soundscapy/plotting/circumplex_plot.py @@ -1,209 +1,739 @@ """ -Main module for creating circumplex plots using different backends. +Main module for creating circumplex plots using Seaborn Objects API. + +This module provides the CircumplexPlot class, which is a builder for creating +circumplex plots with a grammar of graphics approach. It allows for layering +of different plot elements (scatter, density) and customization of styling. + +Note: This class is maintained for backwards compatibility. For new code, +consider using the direct function-based API instead. """ -import copy -from dataclasses import dataclass, field -from typing import Optional, Tuple +import warnings +from typing import Any import matplotlib.pyplot as plt import pandas as pd +import seaborn as sns +import seaborn.objects as so -from soundscapy.plotting.backends import PlotlyBackend, SeabornBackend -from soundscapy.plotting.plotting_utils import ( - Backend, - DEFAULT_XLIM, - DEFAULT_YLIM, - ExtraParams, - PlotType, +from soundscapy.plotting.marks import ( + SoundscapeCircumplex, + SoundscapePointAnnotation, + SoundscapeQuadrantLabels, ) -from soundscapy.plotting.stylers import StyleOptions - - -@dataclass -class CircumplexPlotParams: - """Parameters for customizing CircumplexPlot.""" - - x: str = "ISOPleasant" - y: str = "ISOEventful" - hue: Optional[str] = None - title: str = "Soundscape Plot" - xlim: Tuple[float, float] = DEFAULT_XLIM - ylim: Tuple[float, float] = DEFAULT_YLIM - alpha: float = 0.8 - fill: bool = True - palette: Optional[str] = None - incl_outline: bool = False # Fixed from (False,) - diagonal_lines: bool = False - show_labels: bool = True - legend: bool = "auto" - legend_location: str = "best" - extra_params: ExtraParams = field(default_factory=dict) - - def __post_init__(self): - if self.palette is None: - self.palette = "colorblind" if self.hue else None +from soundscapy.plotting.plotting_utils import DEFAULT_XLIM, DEFAULT_YLIM +from soundscapy.plotting.soundscape_functions import use_soundscapy_style -class CircumplexPlot: +def apply_circumplex_grid( + plot: so.Plot, + xlim: tuple[float, float] = DEFAULT_XLIM, + ylim: tuple[float, float] = DEFAULT_YLIM, + x_label: str | None = None, + y_label: str | None = None, + *, + diagonal_lines: bool = False, + show_labels: bool = True, +) -> so.Plot: """ - A class for creating circumplex plots using different backends. + Apply circumplex grid styling to a Seaborn Objects plot. - This class provides methods for creating scatter plots and density plots - based on the circumplex model of soundscape perception. It supports multiple - backends (currently Seaborn and Plotly) and offers various customization options. + Parameters + ---------- + plot : so.Plot + The plot to style + xlim, ylim : tuple + Axis limits + diagonal_lines : bool + Whether to draw diagonal lines and quadrant labels + show_labels : bool + Whether to keep axis labels + x_label, y_label : str, optional + Custom labels for axes + + Returns + ------- + so.Plot + The styled plot """ + warnings.warn( + "The apply_circumplex_grid function is deprecated. " + "Use SoundscapeCircumplex and SoundscapeQuadrantLabels marks instead.", + DeprecationWarning, + stacklevel=2, + ) + + # Apply limits and axes appearance + plot = plot.limit(x=xlim, y=ylim) + + # Apply square aspect ratio and layout + plot = plot.layout(size=(6, 6)) + + # Add the circumplex grid mark + plot = plot.add(SoundscapeCircumplex(xlim=xlim, ylim=ylim)) + + # Add quadrant labels if requested + if diagonal_lines: + plot = plot.add(SoundscapeQuadrantLabels(xlim=xlim, ylim=ylim)) + + # Apply axis label changes if requested + if not show_labels: + plot = plot.label(x=None, y=None) + elif x_label is not None or y_label is not None: + labels = {} + if x_label is not None: + labels["x"] = x_label + if y_label is not None: + labels["y"] = y_label + plot = plot.label(**labels) + + return plot + + +def add_annotation( + plot: so.Plot, + data: pd.DataFrame, + idx: int | str, + x: str = "ISOPleasant", + y: str = "ISOEventful", + text: str | None = None, + x_offset: float = 0.1, + y_offset: float = 0.1, + **kwargs: Any, +) -> so.Plot: + """ + Add an annotation to a Seaborn Objects plot. + + Parameters + ---------- + plot : so.Plot + The plot to annotate + data : pd.DataFrame + Data containing the point to annotate + idx : int or str + Index of the point to annotate + x, y : str + Column names for coordinates + text : str, optional + Text to display (defaults to index value if None) + x_offset, y_offset : float + Offsets for annotation position + **kwargs + Additional keyword arguments for annotation + + Returns + ------- + so.Plot + The plot with annotation added + + """ + warnings.warn( + "The add_annotation function is deprecated. " + "Use SoundscapePointAnnotation mark instead.", + DeprecationWarning, + stacklevel=2, + ) + + # Get the text if not provided + if text is None: + text = str(data.index[idx]) if isinstance(idx, int) else str(idx) + + # Add the annotation using the mark + plot = plot.add( + SoundscapePointAnnotation( + points=[idx], texts=[text], offsets=(x_offset, y_offset), **kwargs + ) + ) - # TODO: Implement jointplot method for Seaborn backend. - # TODO: Implement density plots for Plotly backend. - # TODO: Improve Plotly backend to support more customization options. + return plot + + +class CircumplexPlot: + """ + A builder class for creating circumplex plots using Seaborn Objects API. + + This class allows for a layered grammar of graphics approach to building + circumplex plots including scatter, density, and other elements. + + Parameters + ---------- + data : pd.DataFrame + Data to plot + x, y : str + Column names for coordinates + hue : str, optional + Column name for color grouping + xlim, ylim : tuple + Axis limits for the plot + palette : str + Color palette to use + + """ def __init__( self, data: pd.DataFrame, - params: CircumplexPlotParams = CircumplexPlotParams(), - backend: Backend = Backend.SEABORN, - style_options: StyleOptions = StyleOptions(), - ): + x: str = "ISOPleasant", + y: str = "ISOEventful", + hue: str | None = None, + xlim: tuple[float, float] = DEFAULT_XLIM, + ylim: tuple[float, float] = DEFAULT_YLIM, + palette: str = "colorblind", + ) -> None: + """ + Initialize a CircumplexPlot instance. + + Parameters + ---------- + data : pd.DataFrame + Data to plot + x : str, default="ISOPleasant" + Column name for x-axis + y : str, default="ISOEventful" + Column name for y-axis + hue : str, optional + Column name for color grouping + xlim : tuple[float, float] + Axis limits for x-axis + ylim : tuple[float, float] + Axis limits for y-axis + palette : str, default="colorblind" + Color palette to use + + """ + warnings.warn( + "The CircumplexPlot class is maintained for backwards compatibility. " + "For new code, consider using the function-based API instead.", + PendingDeprecationWarning, + stacklevel=2, + ) + + # Apply the soundscapy style + use_soundscapy_style() + self.data = data - self.params = params - self.style_options = style_options - self._backend = self._create_backend(backend) - self._plot = None - - def _create_backend(self, backend: Backend): - """Create the appropriate backend based on the backend enum.""" - if backend == Backend.SEABORN: - return SeabornBackend(style_options=self.style_options) - elif backend == Backend.PLOTLY: - return PlotlyBackend() - else: - raise ValueError(f"Unsupported backend: {backend}") + self.x = x + self.y = y + self.hue = hue + self.xlim = xlim + self.ylim = ylim + self.palette_name = palette + + # Initialize the plot + self.plot = so.Plot(data, x=x, y=y) + + # Track what's been added + self.has_scatter = False + self.has_density = False + self.has_grid = False + + def add_scatter( + self, + pointsize: float = 30, + alpha: float = 0.7, + marker: str = "o", + color: str | None = None, # Overrides hue if provided + ) -> "CircumplexPlot": + """ + Add a scatter layer to the plot. - def _create_plot( + Parameters + ---------- + pointsize : int or float + Size of the scatter points + alpha : float + Opacity of the points + marker : str + Marker style + color : str, optional + Override for hue variable + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + color_var = color if color is not None else self.hue + + # Add the dots mark + self.plot = self.plot.add( + so.Dots(pointsize=pointsize, alpha=alpha, marker=marker), color=color_var + ) + + # If we have a color variable and palette, apply it using scale + if color_var and hasattr(self, "palette_name"): + self.plot = self.plot.scale(color=so.Nominal(self.palette_name)) + + self.has_scatter = True + return self + + def add_density( self, - plot_type: PlotType, - apply_styling: bool = True, - ax: Optional[plt.Axes] = None, + alpha: float = 0.5, + fill: bool = True, + levels: int = 8, + bw_adjust: float = 1.2, + color: str | None = None, # Overrides hue if provided + simple: bool = False, + **kwargs: Any, # For backwards compatibility ) -> "CircumplexPlot": - """Create a plot based on the specified plot type.""" - if plot_type == PlotType.SCATTER: - if isinstance(self._backend, SeabornBackend): - self._plot = self._backend.create_scatter(self.data, self.params, ax) - else: - self._plot = self._backend.create_scatter(self.data, self.params) - elif plot_type == PlotType.DENSITY: - if isinstance(self._backend, SeabornBackend): - self._plot = self._backend.create_density(self.data, self.params, ax) - else: - raise NotImplementedError( - "Density plots are only available for the Seaborn backend." - ) - elif plot_type == PlotType.SIMPLE_DENSITY: - if isinstance(self._backend, SeabornBackend): - self._plot = self._backend.create_simple_density( - self.data, self.params, ax - ) - else: - raise NotImplementedError( - "Simple density plots are only available for the Seaborn backend." - ) - elif plot_type == PlotType.JOINT: - if isinstance(self._backend, SeabornBackend): - self._plot = self._backend.create_jointplot(self.data, self.params) - else: - raise NotImplementedError( - "Joint plots are only available for the Seaborn backend." - ) + """ + Add a density layer to the plot. + + Parameters + ---------- + alpha : float + Opacity of the fill color for the density + fill : bool + Whether to fill the contours + levels : int + Number of contour levels + bw_adjust : float + Bandwidth adjustment factor + color : str, optional + Override for hue variable + simple : bool + If True, use simplified density with fewer levels + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + color_var = color if color is not None else self.hue + + if simple: + # For simple density, use just a few levels + self.plot = self.plot.add( + so.Area(alpha=alpha, fill=fill), + so.KDE(bw_adjust=bw_adjust, levels=2), + color=color_var, + ) + + # Apply palette if needed + if color_var and hasattr(self, "palette_name"): + self.plot = self.plot.scale(color=so.Nominal(self.palette_name)) + + # Add outline + self.plot = self.plot.add( + so.Line(alpha=1.0), + so.KDE(bw_adjust=bw_adjust, levels=2), + color=color_var, + ) + + # Apply palette to the outline too if needed + if color_var and hasattr(self, "palette_name"): + self.plot = self.plot.scale(color=so.Nominal(self.palette_name)) else: - raise ValueError(f"Unsupported plot type: {plot_type}") + # Use the Area mark with KDE stat for regular density plots + self.plot = self.plot.add( + so.Area(alpha=alpha, fill=fill), + so.KDE(bw_adjust=bw_adjust, levels=levels), + color=color_var, + ) - if apply_styling: - self._plot = self._backend.apply_styling(self._plot, self.params) + # Apply palette if needed + if color_var and hasattr(self, "palette_name"): + self.plot = self.plot.scale(color=so.Nominal(self.palette_name)) + + self.has_density = True return self + def add_grid( + self, *, diagonal_lines: bool = False, show_labels: bool = True + ) -> "CircumplexPlot": + """ + Add circumplex grid to the plot. + + Parameters + ---------- + diagonal_lines : bool + Whether to show diagonal lines and quadrant labels + show_labels : bool + Whether to show axis labels + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + # Add the circumplex grid + self.plot = self.plot.add(SoundscapeCircumplex(xlim=self.xlim, ylim=self.ylim)) + + # Add quadrant labels if requested + if diagonal_lines: + self.plot = self.plot.add( + SoundscapeQuadrantLabels(xlim=self.xlim, ylim=self.ylim) + ) + + # Hide labels if requested + if not show_labels: + self.plot = self.plot.label(x=None, y=None) + + self.has_grid = True + return self + + def add_annotation( + self, + idx: int | str, + text: str | None = None, + x_offset: float = 0.1, + y_offset: float = 0.1, + **kwargs: Any, + ) -> "CircumplexPlot": + """ + Add an annotation to the plot. + + Parameters + ---------- + idx : int or str + Index of the point to annotate + text : str, optional + Text to display (defaults to index value if None) + x_offset, y_offset : float + Offsets for annotation position + **kwargs + Additional keyword arguments for annotation + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + # Get text if not provided + if text is None: + text = str(self.data.index[idx]) if isinstance(idx, int) else str(idx) + + # Add annotation + self.plot = self.plot.add( + SoundscapePointAnnotation( + points=[idx], texts=[text], offsets=(x_offset, y_offset), **kwargs + ) + ) + + return self + + def add_title(self, title: str) -> "CircumplexPlot": + """ + Add a title to the plot. + + Parameters + ---------- + title : str + Title text + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + self.plot = self.plot.label(title=title) + return self + + def add_legend( + self, title: str | None = None, loc: str = "best" + ) -> "CircumplexPlot": + """ + Customize the legend appearance. + + Parameters + ---------- + title : str, optional + Legend title (defaults to hue variable name) + loc : str + Legend location + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + # Store the legend parameters for when we create the final plot + self._legend_title = title if title is not None else self.hue + self._legend_loc = loc + + return self + + def facet( + self, + column: str | None = None, + row: str | None = None, + col_wrap: int | None = None, + ) -> "CircumplexPlot": + """ + Add faceting to the plot. + + Parameters + ---------- + column : str, optional + Variable for column faceting + row : str, optional + Variable for row faceting + col_wrap : int, optional + Number of columns to wrap facets + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + self.plot = self.plot.facet(col=column, row=row, wrap=col_wrap) + return self + + def build(self, as_objects: bool = True) -> so.Plot | tuple[plt.Figure, plt.Axes]: + """ + Complete the plot with any default elements that haven't been added. + + Parameters + ---------- + as_objects : bool + If True, return the Seaborn Objects plot; if False, convert to Matplotlib axes + + Returns + ------- + so.Plot or (plt.Figure, plt.Axes) + The completed plot object or (figure, axes) tuple + + """ + # Add grid if not already added + if not self.has_grid: + self.add_grid() + + # Ensure correct aspect ratio + self.plot = self.plot.layout(size=(6, 6)) + + # Apply legend customization if requested + if hasattr(self, "_legend_title") and self.hue is not None: + # Create a label mapping for the hue variable + # This sets the legend title to the specified value or the hue variable name + self.plot = self.plot.label(**{self.hue: self._legend_title}) + + if as_objects: + return self.plot + + # Create a new figure with the right size + fig, ax = plt.subplots(figsize=(6, 6)) + + # Draw to the axes + self.plot.plot(ax) + + # Apply legend location if needed (after plotting) + if hasattr(self, "_legend_loc") and ax.get_legend() is not None: + ax.legend(loc=self._legend_loc) + + # Return the figure and axes to be compatible with the legacy API + return fig, ax + + @property + def seaborn_plot(self) -> so.Plot: + """ + Return the underlying Seaborn Objects plot. + + Returns + ------- + so.Plot + The Seaborn Objects plot + + """ + return self.plot + + def get_matplotlib_objects(self) -> tuple[plt.Figure, plt.Axes]: + """ + Return matplotlib figure and axes objects. + + Returns + ------- + Tuple[plt.Figure, plt.Axes] + Figure and axes for the plot + + """ + fig, ax = plt.subplots(figsize=(6, 6)) + self.plot.plot(ax) + return fig, ax + + def show(self) -> None: + """ + Build and display the plot. + + Uses the proper pyplot=True approach which works in both + notebook and non-notebook contexts. + """ + # Ensure any default elements are added + if not self.has_grid: + self.add_grid() + + # Draw the plot + fig, ax = plt.subplots(figsize=(6, 6)) + self.plot.plot(ax) + plt.show() + + # Legacy API compatibility methods def scatter( - self, apply_styling: bool = True, ax: Optional[plt.Axes] = None + self, apply_styling: bool = True, ax: plt.Axes | None = None ) -> "CircumplexPlot": - """Create a scatter plot.""" - return self._create_plot(PlotType.SCATTER, apply_styling, ax) + """ + Create a scatter plot (legacy API compatibility). + + Parameters + ---------- + apply_styling : bool + Whether to apply styling (always True in this implementation) + ax : plt.Axes, optional + Axes to plot on (ignored in objects implementation) + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + self.add_scatter() + self.add_grid() + return self def density( - self, apply_styling: bool = True, ax: Optional[plt.Axes] = None + self, apply_styling: bool = True, ax: plt.Axes | None = None ) -> "CircumplexPlot": - """Create a density plot.""" - return self._create_plot(PlotType.DENSITY, apply_styling, ax) + """ + Create a density plot (legacy API compatibility). - def jointplot(self, apply_styling: bool = True) -> "CircumplexPlot": - """Create a joint plot.""" - return self._create_plot(PlotType.JOINT, apply_styling) + Parameters + ---------- + apply_styling : bool + Whether to apply styling (always True in this implementation) + ax : plt.Axes, optional + Axes to plot on (ignored in objects implementation) + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + self.add_density() + self.add_grid() + return self def simple_density( - self, apply_styling: bool = True, ax: Optional[plt.Axes] = None + self, apply_styling: bool = True, ax: plt.Axes | None = None ) -> "CircumplexPlot": - """Create a simple density plot.""" - return self._create_plot(PlotType.SIMPLE_DENSITY, apply_styling, ax) - - def show(self): - """Display the plot.""" - if self._plot is None: - raise ValueError( - "No plot has been created yet. Call scatter(), density(), or simple_density() first." - ) - self._backend.show(self._plot) + """ + Create a simple density plot (legacy API compatibility). - def get_figure(self): - """Get the figure object of the plot.""" - if self._plot is None: - raise ValueError( - "No plot has been created yet. Call scatter(), density(), or simple_density() first." - ) - return self._plot + Parameters + ---------- + apply_styling : bool + Whether to apply styling (always True in this implementation) + ax : plt.Axes, optional + Axes to plot on (ignored in objects implementation) - def get_axes(self): - """Get the axes object of the plot (only for Seaborn backend).""" - if self._plot is None: - raise ValueError( - "No plot has been created yet. Call scatter(), density(), or simple_density() first." - ) - if isinstance(self._backend, SeabornBackend): - return self._plot[1] # Return the axes object - else: - raise AttributeError("Axes object is not available for Plotly backend") - - def get_style_options(self) -> StyleOptions: - """Get the current StyleOptions.""" - return copy.deepcopy(self.style_options) - - def update_style_options(self, **kwargs) -> "CircumplexPlot": - """Update the StyleOptions with new values.""" - new_style_options = copy.deepcopy(self.style_options) - for key, value in kwargs.items(): - if hasattr(new_style_options, key): - setattr(new_style_options, key, value) - else: - raise ValueError(f"Invalid StyleOptions attribute: {key}") - - self.style_options = new_style_options - self._backend.style_options = new_style_options + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + self.add_density(simple=True) + self.add_grid() return self - def iso_annotation(self, location, x_adj: float = 0, y_adj: float = 0, **kwargs): - """Add an annotation to the plot (only for Seaborn backend).""" - if isinstance(self._backend, SeabornBackend): - ax = self.get_axes() - x = self.data[self.params.x].iloc[location] - y = self.data[self.params.y].iloc[location] - ax.annotate( - text=self.data.index[location], - xy=(x, y), - xytext=(x + x_adj, y + y_adj), - ha="center", - va="center", - arrowprops=dict(arrowstyle="-", ec="black"), - **kwargs, - ) - else: - raise AttributeError("iso_annotation is not available for Plotly backend") + def jointplot(self, apply_styling: bool = True) -> "CircumplexPlot": + """ + Create a joint plot (legacy API compatibility). + + Parameters + ---------- + apply_styling : bool + Whether to apply styling (not used in this implementation) + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + # Fall back to traditional seaborn for jointplot + g = sns.jointplot( + data=self.data, + x=self.x, + y=self.y, + hue=self.hue, + kind="kde", + ) + + # Add grid elements to the central plot + ax = g.ax_joint + ax.set_xlim(self.xlim) + ax.set_ylim(self.ylim) + + # Add zero lines + ax.axhline(y=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + ax.axvline(x=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + # Add grid + ax.grid(True, which="major", color="grey", alpha=0.5) + # Store for get_figure and get_axes + self._joint_grid = g + return self + + def get_figure(self) -> plt.Figure | tuple[plt.Figure, plt.Axes]: + """ + Get the figure object (legacy API compatibility). + + Returns + ------- + plt.Figure or tuple + Figure object or (figure, axes) tuple depending on plotting method + + """ + if hasattr(self, "_joint_grid"): + return self._joint_grid.fig + fig, ax = self.build(as_objects=False) + return fig + + def get_axes(self) -> plt.Axes: + """ + Get the axes object (legacy API compatibility). + + Returns + ------- + plt.Axes + Axes object + + """ + if hasattr(self, "_joint_grid"): + return self._joint_grid.ax_joint + fig, ax = self.build(as_objects=False) + return ax + + def iso_annotation( + self, location: int | str, x_adj: float = 0, y_adj: float = 0, **kwargs: Any + ) -> "CircumplexPlot": + """ + Add an annotation to the plot (legacy API compatibility). + + Parameters + ---------- + location : int + Index of the point to annotate + x_adj, y_adj : float + Offsets for annotation position + **kwargs + Additional keyword arguments for annotation + + Returns + ------- + CircumplexPlot + Self for method chaining + + """ + return self.add_annotation(location, x_offset=x_adj, y_offset=y_adj, **kwargs) diff --git a/src/soundscapy/plotting/likert.py b/src/soundscapy/plotting/likert.py index e5a7e027..69f3ddc3 100644 --- a/src/soundscapy/plotting/likert.py +++ b/src/soundscapy/plotting/likert.py @@ -1,16 +1,15 @@ -""" -Plotting functions for visualising Likert scale data. -""" +"""Plotting functions for visualising Likert scale data.""" from math import pi from matplotlib import pyplot as plt -from soundscapy.surveys import PAQ_LABELS +from soundscapy.surveys.survey_utils import PAQ_LABELS def paq_radar_plot(data, ax=None, index=None): - """Generate a radar/spider plot of PAQ values + """ + Generate a radar/spider plot of PAQ values. Parameters ---------- @@ -24,6 +23,7 @@ def paq_radar_plot(data, ax=None, index=None): ------- plt.Axes matplotlib Axes with radar plot + """ # TODO: Resize the plot # TODO WARNING: Likely broken now diff --git a/src/soundscapy/plotting/marks.py b/src/soundscapy/plotting/marks.py new file mode 100644 index 00000000..a97d8172 --- /dev/null +++ b/src/soundscapy/plotting/marks.py @@ -0,0 +1,375 @@ +""" +Custom Mark components for Soundscape plots using seaborn.objects. + +This module contains custom Mark classes that extend the functionality of +seaborn.objects.Plot for creating soundscape plots. These marks handle +specialized plot elements like circumplex grids, quadrant labels, and +point annotations. +""" + +import seaborn.objects as so + +from soundscapy.plotting.plotting_utils import DEFAULT_XLIM, DEFAULT_YLIM + + +class SoundscapeCircumplex(so.Mark): + """Mark for adding circumplex grid elements to a plot.""" + + def __init__( + self, + xlim=DEFAULT_XLIM, + ylim=DEFAULT_YLIM, + primary_color="grey", + primary_alpha=1.0, + primary_linewidth=1.5, + grid_color="grey", + grid_alpha=0.5, + **kwargs, + ): + """ + Initialize the circumplex grid mark. + + Parameters + ---------- + xlim : tuple, default=(-1, 1) + X-axis limits + ylim : tuple, default=(-1, 1) + Y-axis limits + primary_color : str, default="grey" + Color for primary elements (zero lines) + primary_alpha : float, default=1.0 + Alpha transparency for primary elements + primary_linewidth : float, default=1.5 + Line width for primary elements + grid_color : str, default="grey" + Color for grid lines + grid_alpha : float, default=0.5 + Alpha transparency for grid lines + **kwargs : + Additional keyword arguments passed to parent class + + """ + self.xlim = xlim + self.ylim = ylim + self.primary_color = primary_color + self.primary_alpha = primary_alpha + self.primary_linewidth = primary_linewidth + self.grid_color = grid_color + self.grid_alpha = grid_alpha + super().__init__(**kwargs) + + def _plot(self, axes, data, x, y, **kwargs): + """ + Draw circumplex grid elements on the plot. + + Parameters + ---------- + axes : matplotlib.axes.Axes + The axes to draw on + data : pd.DataFrame + The plot data + x, y : str + Column names for coordinates + **kwargs : + Additional keyword arguments + + Returns + ------- + self : Mark + The mark instance for method chaining + + """ + # Apply limits + axes.set_xlim(self.xlim) + axes.set_ylim(self.ylim) + + # Add major and minor grid + axes.grid( + visible=True, which="major", color=self.grid_color, alpha=self.grid_alpha + ) + axes.grid( + visible=True, + which="minor", + color=self.grid_color, + linestyle="dashed", + linewidth=0.5, + alpha=self.grid_alpha * 0.8, + ) + axes.minorticks_on() + + # Add zero lines + axes.axhline( + y=0, + color=self.primary_color, + linestyle="dashed", + alpha=self.primary_alpha, + linewidth=self.primary_linewidth, + ) + axes.axvline( + x=0, + color=self.primary_color, + linestyle="dashed", + alpha=self.primary_alpha, + linewidth=self.primary_linewidth, + ) + + return self + + +class SoundscapeQuadrantLabels(so.Mark): + """Mark for adding diagonal lines and quadrant labels to a soundscape plot.""" + + def __init__( + self, + xlim=DEFAULT_XLIM, + ylim=DEFAULT_YLIM, + line_color="grey", + line_alpha=0.5, + line_width=1.5, + label_color="black", + label_alpha=0.5, + label_size="small", + labels=("(vibrant)", "(chaotic)", "(monotonous)", "(calm)"), + **kwargs, + ): + """ + Initialize the quadrant labels mark. + + Parameters + ---------- + xlim : tuple, default=(-1, 1) + X-axis limits + ylim : tuple, default=(-1, 1) + Y-axis limits + line_color : str, default="grey" + Color for diagonal lines + line_alpha : float, default=0.5 + Alpha transparency for diagonal lines + line_width : float, default=1.5 + Line width for diagonal lines + label_color : str, default="black" + Color for quadrant labels + label_alpha : float, default=0.5 + Alpha transparency for quadrant labels + label_size : str or int, default="small" + Font size for quadrant labels + labels : tuple, default=("(vibrant)", "(chaotic)", "(monotonous)", "(calm)") + Text for each quadrant label + **kwargs : + Additional keyword arguments passed to parent class + + """ + self.xlim = xlim + self.ylim = ylim + self.line_color = line_color + self.line_alpha = line_alpha + self.line_width = line_width + self.label_color = label_color + self.label_alpha = label_alpha + self.label_size = label_size + self.labels = labels + super().__init__(**kwargs) + + def _plot(self, axes, data, x, y, **kwargs): + """ + Draw diagonal lines and quadrant labels. + + Parameters + ---------- + axes : matplotlib.axes.Axes + The axes to draw on + data : pd.DataFrame + The plot data + x, y : str + Column names for coordinates + **kwargs : + Additional keyword arguments + + Returns + ------- + self : Mark + The mark instance for method chaining + + """ + # Draw diagonal lines + axes.plot( + [self.xlim[0], self.xlim[1]], + [self.ylim[0], self.ylim[1]], + linestyle="dashed", + color=self.line_color, + alpha=self.line_alpha, + linewidth=self.line_width, + ) + axes.plot( + [self.xlim[0], self.xlim[1]], + [self.ylim[1], self.ylim[0]], + linestyle="dashed", + color=self.line_color, + alpha=self.line_alpha, + linewidth=self.line_width, + ) + + # Add quadrant labels + label_style = { + "fontstyle": "italic", + "fontsize": self.label_size, + "fontweight": "bold", + "color": self.label_color, + "alpha": self.label_alpha, + } + + # Upper right (vibrant) + axes.text( + self.xlim[1] / 2, + self.ylim[1] / 2, + self.labels[0], + ha="center", + va="center", + fontdict=label_style, + ) + + # Upper left (chaotic) + axes.text( + self.xlim[0] / 2, + self.ylim[1] / 2, + self.labels[1], + ha="center", + va="center", + fontdict=label_style, + ) + + # Lower left (monotonous) + axes.text( + self.xlim[0] / 2, + self.ylim[0] / 2, + self.labels[2], + ha="center", + va="center", + fontdict=label_style, + ) + + # Lower right (calm) + axes.text( + self.xlim[1] / 2, + self.ylim[0] / 2, + self.labels[3], + ha="center", + va="center", + fontdict=label_style, + ) + + return self + + +class SoundscapePointAnnotation(so.Mark): + """Mark for adding annotations to specific points in a soundscape plot.""" + + def __init__( + self, + points=None, + texts=None, + offsets=(0.1, 0.1), + fontsize=9, + ha="center", + va="center", + arrow_style="-", + arrow_color="black", + arrow_alpha=0.7, + **kwargs, + ): + """ + Initialize the point annotation mark. + + Parameters + ---------- + points : list, default=None + List of point indices to annotate + texts : list, default=None + List of texts to use for annotations + offsets : tuple, default=(0.1, 0.1) + (x_offset, y_offset) for annotation position + fontsize : int, default=9 + Font size for annotation text + ha : str, default="center" + Horizontal alignment + va : str, default="center" + Vertical alignment + arrow_style : str, default="-" + Style of the annotation arrow + arrow_color : str, default="black" + Color of the annotation arrow + arrow_alpha : float, default=0.7 + Alpha transparency of the annotation arrow + **kwargs : + Additional keyword arguments passed to parent class + + """ + self.points = points if points is not None else [] + self.texts = texts if texts is not None else [] + self.offsets = offsets + self.fontsize = fontsize + self.ha = ha + self.va = va + self.arrow_props = { + "arrowstyle": arrow_style, + "color": arrow_color, + "alpha": arrow_alpha, + } + super().__init__(**kwargs) + + def _plot(self, axes, data, x, y, **kwargs): + """ + Add annotations to specific points. + + Parameters + ---------- + axes : matplotlib.axes.Axes + The axes to draw on + data : pd.DataFrame + The plot data + x, y : str + Column names for coordinates + **kwargs : + Additional keyword arguments + + Returns + ------- + self : Mark + The mark instance for method chaining + + """ + if not self.points: + return self + + # Add annotations for each point + for i, point_idx in enumerate(self.points): + try: + # Get coordinates from data + if isinstance(point_idx, int): + x_val = data[x].iloc[point_idx] + y_val = data[y].iloc[point_idx] + else: + x_val = data.loc[point_idx, x] + y_val = data.loc[point_idx, y] + + # Get text (default to index if not provided) + if i < len(self.texts): + text = self.texts[i] + else: + text = str(point_idx) + + # Add annotation + axes.annotate( + text=text, + xy=(x_val, y_val), + xytext=(x_val + self.offsets[0], y_val + self.offsets[1]), + fontsize=self.fontsize, + ha=self.ha, + va=self.va, + arrowprops=self.arrow_props, + ) + except (KeyError, IndexError): + # Skip invalid indices + pass + + return self diff --git a/src/soundscapy/plotting/plot_functions.py b/src/soundscapy/plotting/plot_functions.py index d7bd0cd0..048a29ae 100644 --- a/src/soundscapy/plotting/plot_functions.py +++ b/src/soundscapy/plotting/plot_functions.py @@ -1,289 +1,502 @@ """ -Utility functions for creating various types of circumplex plots. +High level functions for creating various types of circumplex plots. + +These functions provide a high-level interface for creating common plot types +using the CircumplexPlot class with the Seaborn Objects API. """ -from typing import Any, List, Optional, Tuple +from typing import Any import matplotlib.pyplot as plt -import numpy as np import pandas as pd -import plotly.graph_objects as go import seaborn as sns +import seaborn.objects as so -from .backends import SeabornBackend -from .circumplex_plot import CircumplexPlot, CircumplexPlotParams -from .plotting_utils import ( - Backend, - DEFAULT_FIGSIZE, +from soundscapy.plotting.circumplex_plot import CircumplexPlot +from soundscapy.plotting.plotting_utils import ( DEFAULT_XLIM, DEFAULT_YLIM, - ExtraParams, - PlotType, ) -from .stylers import StyleOptions def scatter_plot( data: pd.DataFrame, x: str = "ISOPleasant", y: str = "ISOEventful", - hue: Optional[str] = None, + hue: str | None = None, title: str = "Soundscape Scatter Plot", - xlim: Tuple[float, float] = DEFAULT_XLIM, - ylim: Tuple[float, float] = DEFAULT_YLIM, + xlim: tuple[float, float] = DEFAULT_XLIM, + ylim: tuple[float, float] = DEFAULT_YLIM, palette: str = "colorblind", diagonal_lines: bool = False, show_labels: bool = True, - legend=True, - legend_location: str = "best", - backend: Backend = Backend.SEABORN, - apply_styling: bool = True, - figsize: Tuple[int, int] = DEFAULT_FIGSIZE, - ax: Optional[plt.Axes] = None, - extra_params: ExtraParams = {}, + pointsize: int = 30, + alpha: float = 0.7, + ax: plt.Axes | None = None, + as_objects: bool = False, **kwargs: Any, -) -> plt.Axes | go.Figure: +) -> so.Plot | plt.Axes: """ - Create a scatter plot using the CircumplexPlot class. + Create a scatter plot using the circumplex model. Parameters ---------- - data (pd.DataFrame): The data to plot. - x (str): Column name for x-axis data. - y (str): Column name for y-axis data. - hue (Optional[str]): Column name for color-coding data points. - title (str): Title of the plot. - xlim (Tuple[float, float]): x-axis limits. - ylim (Tuple[float, float]): y-axis limits. - palette (str): Color palette to use. - diagonal_lines (bool): Whether to draw diagonal lines. - show_labels (bool): Whether to show axis labels. - legend (bool): Whether to show the legend. - legend_location (str): Location of the legend. - backend (Backend): The plotting backend to use. - apply_styling (bool): Whether to apply circumplex-specific styling. - figsize (Tuple[int, int]): Size of the figure. - ax (Optional[plt.Axes]): A matplotlib Axes object to plot on. - extra_params (ExtraParams): Additional parameters for backend-specific functions. - **kwargs: Additional keyword arguments to pass to the backend. + data : pd.DataFrame + Data to plot + x, y : str + Column names for coordinates + hue : str, optional + Column name for color grouping + title : str + Title for the plot + xlim, ylim : tuple + Axis limits + palette : str or list or dict + Color palette to use for hue + diagonal_lines : bool + Whether to show diagonal lines and quadrant labels + show_labels : bool + Whether to show axis labels + pointsize : int + Size of scatter points + alpha : float + Opacity of scatter points + ax : plt.Axes, optional + Axes to plot on (for matplotlib compatibility) + as_objects : bool + If True, return Seaborn Objects plot; if False, return Matplotlib axes + **kwargs + Additional keyword arguments for scatter plot Returns ------- - plt.Axes | go.Figure: The resulting plot object. + so.Plot | plt.Axes + The completed plot object or axes """ - params = CircumplexPlotParams( - x=x, - y=y, - hue=hue, - title=title, - xlim=xlim, - ylim=ylim, - palette=palette if hue else None, - diagonal_lines=diagonal_lines, - show_labels=show_labels, - legend=legend, - legend_location=legend_location, - extra_params={**extra_params, **kwargs}, + plot = ( + CircumplexPlot(data, x, y, hue, xlim, ylim, palette) + .add_scatter(pointsize=pointsize, alpha=alpha, **kwargs) + .add_grid(diagonal_lines=diagonal_lines, show_labels=show_labels) + .add_title(title) ) - style_options = StyleOptions(figsize=figsize) - - plot = CircumplexPlot(data, params, backend, style_options) - plot.scatter(apply_styling=apply_styling, ax=ax) + if as_objects: + return plot.build(as_objects=True) + if ax is not None: + # If an axes is provided, draw directly on it + plot.build(as_objects=True) + # Clear previous contents + ax.clear() + # Use the ax limits and title from our plot + ax.set_xlim(xlim) + ax.set_ylim(ylim) + ax.set_title(title) + ax.set_xlabel(x) + ax.set_ylabel(y) + # Draw points and style - only use palette if hue is provided + sns.scatterplot( + data=data, + x=x, + y=y, + hue=hue, + palette=palette if hue else None, + s=pointsize, + alpha=alpha, + ax=ax, + **kwargs, + ) + # Add grid lines + ax.grid(True, which="major", color="grey", alpha=0.5) + ax.axhline(y=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + ax.axvline(x=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + + # Add diagonal lines if requested + if diagonal_lines: + ax.plot( + [xlim[0], xlim[1]], + [ylim[0], ylim[1]], + linestyle="dashed", + color="grey", + alpha=0.5, + linewidth=1.5, + ) + ax.plot( + [xlim[0], xlim[1]], + [ylim[1], ylim[0]], + linestyle="dashed", + color="grey", + alpha=0.5, + linewidth=1.5, + ) - if isinstance(plot._backend, SeabornBackend): - return plot.get_axes() - else: - return plot.get_figure() + return ax + return plot.get_axes() def density_plot( data: pd.DataFrame, x: str = "ISOPleasant", y: str = "ISOEventful", - hue: Optional[str] = None, + hue: str | None = None, title: str = "Soundscape Density Plot", - xlim: Tuple[float, float] = DEFAULT_XLIM, - ylim: Tuple[float, float] = DEFAULT_YLIM, + xlim: tuple[float, float] = DEFAULT_XLIM, + ylim: tuple[float, float] = DEFAULT_YLIM, palette: str = "colorblind", fill: bool = True, - incl_outline: bool = False, - incl_scatter: bool = True, + alpha: float = 0.5, + levels: int = 8, + bw_adjust: float = 1.2, + simple_density: bool = False, + incl_scatter: bool = False, + scatter_size: int = 15, + scatter_alpha: float = 0.5, diagonal_lines: bool = False, show_labels: bool = True, - legend=True, - legend_location: str = "best", - backend: Backend = Backend.SEABORN, - apply_styling: bool = True, - figsize: Tuple[int, int] = DEFAULT_FIGSIZE, - simple_density: bool = False, - simple_density_thresh: float = 0.5, - simple_density_levels: int = 2, - simple_density_alpha: float = 0.5, - ax: Optional[plt.Axes] = None, - extra_params: ExtraParams = {}, + ax: plt.Axes | None = None, + as_objects: bool = False, **kwargs: Any, -) -> plt.Axes | go.Figure: +) -> so.Plot | plt.Axes: """ - Create a density plot using the CircumplexPlot class. + Create a density plot using the circumplex model. Parameters ---------- - data (pd.DataFrame): The data to plot. - x (str): Column name for x-axis data. - y (str): Column name for y-axis data. - hue (Optional[str]): Column name for color-coding data points. - title (str): Title of the plot. - xlim (Tuple[float, float]): x-axis limits. - ylim (Tuple[float, float]): y-axis limits. - palette (str): Color palette to use. - fill (bool): Whether to fill the density contours. - incl_outline (bool): Whether to include an outline for the density contours. - diagonal_lines (bool): Whether to draw diagonal lines. - show_labels (bool): Whether to show axis labels. - legend (bool): Whether to show the legend. - legend_location (str): Location of the legend. - backend (Backend): The plotting backend to use. - apply_styling (bool): Whether to apply circumplex-specific styling. - figsize (Tuple[int, int]): Size of the figure. - simple_density (bool): Whether to use simple density plot (Seaborn only). - simple_density_thresh (float): Threshold for simple density plot. - simple_density_levels (int): Number of levels for simple density plot. - simple_density_alpha (float): Alpha value for simple density plot. - ax (Optional[plt.Axes]): A matplotlib Axes object to plot on. - extra_params (ExtraParams): Additional parameters for backend-specific functions. - **kwargs: Additional keyword arguments to pass to the backend. + data : pd.DataFrame + Data to plot + x, y : str + Column names for coordinates + hue : str, optional + Column name for color grouping + title : str + Title for the plot + xlim, ylim : tuple + Axis limits + palette : str or list or dict + Color palette to use for hue + fill : bool + Whether to fill the contours + alpha : float + Opacity of the fill + levels : int + Number of contour levels + bw_adjust : float + Bandwidth adjustment factor + simple_density : bool + If True, use simplified density with fewer levels and an outline + incl_scatter : bool + Whether to include scatter points with the density + scatter_size : int + Size of scatter points (if included) + scatter_alpha : float + Opacity of scatter points (if included) + diagonal_lines : bool + Whether to show diagonal lines and quadrant labels + show_labels : bool + Whether to show axis labels + ax : plt.Axes, optional + Axes to plot on (for matplotlib compatibility) + as_objects : bool + If True, return Seaborn Objects plot; if False, return Matplotlib axes + **kwargs + Additional keyword arguments for density plot Returns ------- - plt.Axes | go.Figure: The resulting plot object. + so.Plot | plt.Axes + The completed plot object or axes """ - params = CircumplexPlotParams( - x=x, - y=y, - hue=hue, - title=title, - xlim=xlim, - ylim=ylim, - palette=palette if hue else None, - fill=fill, - incl_outline=incl_outline, - diagonal_lines=diagonal_lines, - show_labels=show_labels, - legend=legend, - legend_location=legend_location, - extra_params={**extra_params, **kwargs}, - ) + cp = CircumplexPlot(data, x, y, hue, xlim, ylim, palette) - style_options = StyleOptions( - figsize=figsize, - simple_density=dict( - thresh=simple_density_thresh, - levels=simple_density_levels, - alpha=simple_density_alpha, - ) - if simple_density - else None, + # Add density layer + cp.add_density( + alpha=alpha, + fill=fill, + levels=levels, + bw_adjust=bw_adjust, + simple=simple_density, ) - plot = CircumplexPlot(data, params, backend, style_options) + # Add scatter if requested + if incl_scatter: + cp.add_scatter(pointsize=scatter_size, alpha=scatter_alpha) + + # Complete the plot + cp.add_grid(diagonal_lines=diagonal_lines, show_labels=show_labels) + cp.add_title(title) + + if as_objects: + return cp.build(as_objects=True) + if ax is not None: + # If an axes is provided, draw directly on it + # Clear previous contents + ax.clear() + # Use the ax limits and title + ax.set_xlim(xlim) + ax.set_ylim(ylim) + ax.set_title(title) + ax.set_xlabel(x) + ax.set_ylabel(y) + + # Draw the KDE + if simple_density: + # Simple density with fewer levels + sns.kdeplot( + data=data, + x=x, + y=y, + hue=hue, + fill=fill, + alpha=alpha, + levels=2, + bw_adjust=bw_adjust, + ax=ax, + **kwargs, + ) + # Add outline + sns.kdeplot( + data=data, + x=x, + y=y, + hue=hue, + fill=False, + alpha=1.0, + levels=2, + bw_adjust=bw_adjust, + ax=ax, + ) + else: + # Regular density + sns.kdeplot( + data=data, + x=x, + y=y, + hue=hue, + fill=fill, + alpha=alpha, + levels=levels, + bw_adjust=bw_adjust, + ax=ax, + **kwargs, + ) - if incl_scatter and backend == Backend.SEABORN: - plot.scatter(apply_styling=True, ax=ax) - ax = plot.get_axes() - elif incl_scatter and backend == Backend.PLOTLY: - # TODO: Implement overlaying scatter on density plot for Plotly backend - raise NotImplementedError( - "Overlaying a scatter on a density plot is not yet supported for Plotly backend. " - "Please change to Seaborn backend or use `incl_scatter=False`." - ) + # Add scatter if requested + if incl_scatter: + sns.scatterplot( + data=data, x=x, y=y, hue=hue, s=scatter_size, alpha=scatter_alpha, ax=ax + ) - if simple_density: - plot.simple_density(apply_styling=apply_styling, ax=ax) - else: - plot.density(apply_styling=apply_styling, ax=ax) + # Add grid lines + ax.grid(True, which="major", color="grey", alpha=0.5) + ax.axhline(y=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + ax.axvline(x=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + + # Add diagonal lines if requested + if diagonal_lines: + ax.plot( + [xlim[0], xlim[1]], + [ylim[0], ylim[1]], + linestyle="dashed", + color="grey", + alpha=0.5, + linewidth=1.5, + ) + ax.plot( + [xlim[0], xlim[1]], + [ylim[1], ylim[0]], + linestyle="dashed", + color="grey", + alpha=0.5, + linewidth=1.5, + ) - if isinstance(plot._backend, SeabornBackend): - return plot.get_axes() - else: - return plot.get_figure() + return ax + return cp.get_axes() -def create_circumplex_subplots( - data_list: List[pd.DataFrame], - plot_type: PlotType | str = PlotType.DENSITY, - incl_scatter: bool = True, - subtitles: Optional[List[str]] = None, - title: str = "Circumplex Subplots", - nrows: int = None, - ncols: int = None, - figsize: Tuple[int, int] = (10, 10), +def joint_plot( + data: pd.DataFrame, + x: str = "ISOPleasant", + y: str = "ISOEventful", + hue: str | None = None, + title: str = "Soundscape Joint Plot", + plot_type: str = "scatter", **kwargs: Any, -) -> plt.Figure: +) -> sns.JointGrid: """ - Create a figure with subplots containing circumplex plots. + Create a joint plot with marginals using matplotlib. + + This function falls back to matplotlib/seaborn because + Seaborn Objects does not yet fully support joint plots with marginals. Parameters ---------- - data_list (List[pd.DataFrame]): List of DataFrames to plot. - plot_type (PlotType): Type of plot to create. - incl_scatter (bool): Whether to include scatter points on density plots. - nrows (int): Number of rows in the subplot grid. - ncols (int): Number of columns in the subplot grid. - figsize (tuple): Figure size (width, height) in inches. - **kwargs: Additional keyword arguments to pass to scatter_plot or density_plot. + data : pd.DataFrame + Data to plot + x, y : str + Column names for coordinates + hue : str, optional + Column name for color grouping + title : str + Title for the plot + plot_type : str + Type of plot: "scatter", "density", or "simple_density" + **kwargs + Additional parameters for sns.jointplot Returns ------- - matplotlib.figure.Figure: A figure containing the subplots. + sns.JointGrid + The joint plot grid - Example - ------- - >>> import pandas as pd - >>> import numpy as np - >>> np.random.seed(42) - >>> data1 = pd.DataFrame({'ISOPleasant': np.random.uniform(-1, 1, 50), - ... 'ISOEventful': np.random.uniform(-1, 1, 50)}) - >>> data2 = pd.DataFrame({'ISOPleasant': np.random.uniform(-1, 1, 50), - ... 'ISOEventful': np.random.uniform(-1, 1, 50)}) - >>> fig = create_circumplex_subplots([data1, data2], plot_type=PlotType.SCATTER, nrows=1, ncols=2) - >>> isinstance(fig, plt.Figure) - True """ - if isinstance(plot_type, str): - plot_type = PlotType[plot_type.upper()] + # Fall back to traditional seaborn for jointplot + kind = "scatter" if plot_type == "scatter" else "kde" + + g = sns.jointplot(data=data, x=x, y=y, hue=hue, kind=kind, **kwargs) + + # Add grid elements to the central plot + ax = g.ax_joint + ax.set_xlim((-1, 1)) + ax.set_ylim((-1, 1)) + + # Add zero lines + ax.axhline(y=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + ax.axvline(x=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + + # Add grid + ax.grid(True, which="major", color="grey", alpha=0.5) + + # Add title + g.fig.suptitle(title, y=1.05) + + # Add scatter if requested + if plot_type in ["density", "simple_density"] and kwargs.get("incl_scatter", False): + sns.scatterplot( + data=data, + x=x, + y=y, + hue=hue, + ax=ax, + s=kwargs.get("scatter_size", 15), + alpha=kwargs.get("scatter_alpha", 0.5), + ) - if nrows is None and ncols is None: - nrows = 2 - ncols = len(data_list) // nrows - elif nrows is None: - nrows = len(data_list) // ncols - elif ncols is None: - ncols = len(data_list) // nrows + return g - if subtitles is None: - subtitles = [f"({i + 1})" for i in range(len(data_list))] - elif len(subtitles) != len(data_list): - raise ValueError("Number of subtitles must match number of dataframes") - - fig, axes = plt.subplots(nrows, ncols, figsize=figsize) - axes = axes.flatten() if isinstance(axes, np.ndarray) else [axes] - - color = kwargs.get("color", sns.color_palette("colorblind", 1)[0]) - - for data, ax, subtitle in zip(data_list, axes, subtitles): - if plot_type == PlotType.SCATTER or incl_scatter: - scatter_plot(data, title=subtitle, ax=ax, color=color, **kwargs) - if plot_type == PlotType.DENSITY: - density_plot(data, title=subtitle, ax=ax, color=color, **kwargs) - elif plot_type == PlotType.SIMPLE_DENSITY: - density_plot( - data, title=subtitle, simple_density=True, ax=ax, color=color, **kwargs - ) - plt.suptitle(title) +def create_circumplex_subplots( + data_list: list[pd.DataFrame], + x: str = "ISOPleasant", + y: str = "ISOEventful", + hue: str | None = None, + subtitles: list[str] | None = None, + title: str = "Circumplex Subplots", + plot_type: str = "density", + incl_scatter: bool = False, + cols: int = 2, + as_objects: bool = False, + **kwargs: Any, +) -> so.Plot | plt.Figure: + """ + Create a figure with multiple circumplex plots. + + Parameters + ---------- + data_list : list of DataFrames + List of data sources to plot + x, y : str + Column names for coordinates + hue : str, optional + Column name for color grouping + subtitles : list of str, optional + Titles for individual subplots + title : str + Main title for the plot + plot_type : str + Type of plot: "scatter", "density", or "simple_density" + incl_scatter : bool + Whether to include scatter points on density plots + cols : int + Number of columns for subplots + as_objects : bool + If True, return Seaborn Objects plot; if False, return Matplotlib figure + **kwargs + Additional arguments for plot functions + + Returns + ------- + so.Plot | plt.Figure + The plot object or figure + """ + # Generate subplot titles if not provided + if subtitles is None: + subtitles = [f"Plot {i + 1}" for i in range(len(data_list))] + + # Remove any layout parameters that don't belong in plot functions + plotting_kwargs = kwargs.copy() + if "nrows" in plotting_kwargs: + plotting_kwargs.pop("nrows") + if "ncols" in plotting_kwargs: + plotting_kwargs.pop("ncols") + + # For the refactored version, we'll create a matplotlib figure directly + # instead of using the faceting in Seaborn Objects + nrows = (len(data_list) - 1) // cols + 1 + + # Create a new figure + fig, axes = plt.subplots(nrows, cols, figsize=(cols * 6, nrows * 6), squeeze=False) + axes = axes.flatten() + + # Create individual plots + for i, (data, subtitle) in enumerate(zip(data_list, subtitles, strict=False)): + if i < len(axes): + # Create a plot for this axis + if plot_type == "scatter": + scatter_plot( + data, + x=x, + y=y, + hue=hue, + title=subtitle, + ax=axes[i], + **plotting_kwargs, + ) + elif plot_type == "simple_density": + density_plot( + data, + x=x, + y=y, + hue=hue, + title=subtitle, + simple_density=True, + incl_scatter=incl_scatter, + ax=axes[i], + **plotting_kwargs, + ) + else: + density_plot( + data, + x=x, + y=y, + hue=hue, + title=subtitle, + incl_scatter=incl_scatter, + ax=axes[i], + **plotting_kwargs, + ) + + # Hide any unused axes + for i in range(len(data_list), len(axes)): + axes[i].set_visible(False) + + # Add a title to the figure + fig.suptitle(title, fontsize=16) + + # Adjust layout plt.tight_layout() + + # We'll return the figure directly for legacy compatibility return fig diff --git a/src/soundscapy/plotting/plotting_utils.py b/src/soundscapy/plotting/plotting_utils.py index 7d219530..2160ecb7 100644 --- a/src/soundscapy/plotting/plotting_utils.py +++ b/src/soundscapy/plotting/plotting_utils.py @@ -1,6 +1,4 @@ -""" -Utility functions and constants for the soundscapy plotting module. -""" +"""Utility functions and constants for the soundscapy plotting module.""" from enum import Enum from typing import Any, TypedDict @@ -15,13 +13,6 @@ class PlotType(Enum): JOINT = "joint" -class Backend(Enum): - """Enum for supported plotting backends.""" - - SEABORN = "seaborn" - PLOTLY = "plotly" - - class ExtraParams(TypedDict, total=False): """TypedDict for extra parameters passed to plotting functions.""" diff --git a/src/soundscapy/plotting/soundscape_functions.py b/src/soundscapy/plotting/soundscape_functions.py new file mode 100644 index 00000000..2c317d80 --- /dev/null +++ b/src/soundscapy/plotting/soundscape_functions.py @@ -0,0 +1,564 @@ +""" +Function-based API for creating soundscape plots with Seaborn Objects. + +This module provides high-level functions for creating various types of +soundscape plots using seaborn.objects API and custom Mark/Stat components. +These functions offer a more direct, functional approach compared to the +CircumplexPlot builder class. +""" + +from collections.abc import Sequence +from pathlib import Path +from typing import Any + +import matplotlib.pyplot as plt +import pandas as pd +import seaborn as sns +import seaborn.objects as so + +from soundscapy.plotting.marks import ( + SoundscapeCircumplex, + SoundscapeQuadrantLabels, +) +from soundscapy.plotting.plotting_utils import DEFAULT_XLIM, DEFAULT_YLIM +from soundscapy.plotting.stats import SoundscapeCoordinates + +# Path to soundscapy.mplstyle +STYLE_PATH = Path(__file__).parent / "soundscapy.mplstyle" + + +def use_soundscapy_style(): + """ + Apply the soundscapy matplotlib style. + + This function activates the built-in style sheet for consistent + soundscape plot appearance. + """ + if STYLE_PATH.exists(): + plt.style.use(str(STYLE_PATH)) + else: + # Fall back to built-in style if custom style file isn't found + plt.style.use("seaborn-v0_8-colorblind") + + +def scatter_plot( + data: pd.DataFrame, + x: str = "ISOPleasant", + y: str = "ISOEventful", + hue: str | None = None, + title: str = "Soundscape Scatter Plot", + xlim: tuple[float, float] = DEFAULT_XLIM, + ylim: tuple[float, float] = DEFAULT_YLIM, + palette: str = "colorblind", + diagonal_lines: bool = False, + show_labels: bool = True, + point_size: float = 30, + alpha: float = 0.7, + marker: str = "o", + ax: plt.Axes | None = None, + as_objects: bool = False, + **kwargs: Any, +) -> so.Plot | plt.Axes: + """ + Create a scatter plot using the soundscape circumplex model. + + Parameters + ---------- + data : pd.DataFrame + Data to plot + x : str, default="ISOPleasant" + Column name for x-axis + y : str, default="ISOEventful" + Column name for y-axis + hue : str, optional + Column name for color grouping + title : str, default="Soundscape Scatter Plot" + Title for the plot + xlim : tuple, default=(-1, 1) + X-axis limits + ylim : tuple, default=(-1, 1) + Y-axis limits + palette : str, default="colorblind" + Color palette to use + diagonal_lines : bool, default=False + Whether to show diagonal lines and quadrant labels + show_labels : bool, default=True + Whether to show axis labels + point_size : float, default=30 + Size of scatter points + alpha : float, default=0.7 + Opacity of scatter points + marker : str, default="o" + Marker style for scatter points + ax : plt.Axes, optional + Axes to plot on (for matplotlib compatibility) + as_objects : bool, default=False + If True, return seaborn.objects.Plot; if False, return Matplotlib axes + **kwargs : Any + Additional keyword arguments for scatter plot + + Returns + ------- + so.Plot | plt.Axes + The completed plot object or axes + + """ + use_soundscapy_style() + + # Create base plot + plot = so.Plot(data, x=x, y=y) + + # Add scatter points + plot = plot.add( + so.Dots(pointsize=point_size, alpha=alpha, marker=marker), color=hue + ) + + # Apply color palette if needed + if hue: + plot = plot.scale(color=so.Nominal(palette)) + + # Add circumplex grid + plot = plot.add(SoundscapeCircumplex(xlim=xlim, ylim=ylim)) + + # Add quadrant labels if requested + if diagonal_lines: + plot = plot.add(SoundscapeQuadrantLabels(xlim=xlim, ylim=ylim)) + + # Add title and labels + plot = plot.label(title=title) + + # Set layout + plot = plot.layout(size=(6, 6)) + + # Hide labels if requested + if not show_labels: + plot = plot.label(x=None, y=None) + + if as_objects: + return plot + + if ax is not None: + # If an axes is provided, clear and draw on it + ax.clear() + plot.plot(ax) + return ax + + # Create a new figure and axes, draw on it, and return the axes + fig, ax = plt.subplots(figsize=(6, 6)) + plot.plot(ax) + return ax + + +def density_plot( + data: pd.DataFrame, + x: str = "ISOPleasant", + y: str = "ISOEventful", + hue: str | None = None, + title: str = "Soundscape Density Plot", + xlim: tuple[float, float] = DEFAULT_XLIM, + ylim: tuple[float, float] = DEFAULT_YLIM, + palette: str = "colorblind", + fill: bool = True, + alpha: float = 0.5, + bw_adjust: float = 1.2, + levels: int = 8, + simple_density: bool = False, + incl_scatter: bool = False, + scatter_size: float = 15, + scatter_alpha: float = 0.5, + diagonal_lines: bool = False, + show_labels: bool = True, + ax: plt.Axes | None = None, + as_objects: bool = False, + **kwargs: Any, +) -> so.Plot | plt.Axes: + """ + Create a density plot using the soundscape circumplex model. + + Parameters + ---------- + data : pd.DataFrame + Data to plot + x : str, default="ISOPleasant" + Column name for x-axis + y : str, default="ISOEventful" + Column name for y-axis + hue : str, optional + Column name for color grouping + title : str, default="Soundscape Density Plot" + Title for the plot + xlim : tuple, default=(-1, 1) + X-axis limits + ylim : tuple, default=(-1, 1) + Y-axis limits + palette : str, default="colorblind" + Color palette to use + fill : bool, default=True + Whether to fill the contours + alpha : float, default=0.5 + Opacity of the fill + bw_adjust : float, default=1.2 + Bandwidth adjustment factor + levels : int, default=8 + Number of contour levels + simple_density : bool, default=False + If True, use simplified density with fewer levels and an outline + incl_scatter : bool, default=False + Whether to include scatter points + scatter_size : float, default=15 + Size of scatter points (if included) + scatter_alpha : float, default=0.5 + Opacity of scatter points (if included) + diagonal_lines : bool, default=False + Whether to show diagonal lines and quadrant labels + show_labels : bool, default=True + Whether to show axis labels + ax : plt.Axes, optional + Axes to plot on (for matplotlib compatibility) + as_objects : bool, default=False + If True, return seaborn.objects.Plot; if False, return Matplotlib axes + **kwargs : Any + Additional keyword arguments for density plot + + Returns + ------- + so.Plot | plt.Axes + The completed plot object or axes + + """ + use_soundscapy_style() + + # Create base plot + plot = so.Plot(data, x=x, y=y) + + # Add density layer + if simple_density: + # Simple density with fewer levels and outline + plot = plot.add( + so.Area(fill=fill, alpha=alpha), + so.KDE(bw_adjust=bw_adjust, levels=2), + color=hue, + ) + # Add outline + plot = plot.add( + so.Line(alpha=1.0), so.KDE(bw_adjust=bw_adjust, levels=2), color=hue + ) + else: + # Regular density + plot = plot.add( + so.Area(fill=fill, alpha=alpha), + so.KDE(bw_adjust=bw_adjust, levels=levels), + color=hue, + ) + + # Apply color palette if needed + if hue: + plot = plot.scale(color=so.Nominal(palette)) + + # Add scatter if requested + if incl_scatter: + plot = plot.add(so.Dots(pointsize=scatter_size, alpha=scatter_alpha), color=hue) + # Apply color palette again for scatter + if hue: + plot = plot.scale(color=so.Nominal(palette)) + + # Add circumplex grid + plot = plot.add(SoundscapeCircumplex(xlim=xlim, ylim=ylim)) + + # Add quadrant labels if requested + if diagonal_lines: + plot = plot.add(SoundscapeQuadrantLabels(xlim=xlim, ylim=ylim)) + + # Add title and labels + plot = plot.label(title=title) + + # Set layout + plot = plot.layout(size=(6, 6)) + + # Hide labels if requested + if not show_labels: + plot = plot.label(x=None, y=None) + + if as_objects: + return plot + + if ax is not None: + # If an axes is provided, clear and draw on it + ax.clear() + plot.plot(ax) + return ax + + # Create a new figure and axes, draw on it, and return the axes + fig, ax = plt.subplots(figsize=(6, 6)) + plot.plot(ax) + return ax + + +def joint_plot( + data: pd.DataFrame, + x: str = "ISOPleasant", + y: str = "ISOEventful", + hue: str | None = None, + title: str = "Soundscape Joint Plot", + kind: str = "scatter", + xlim: tuple[float, float] = DEFAULT_XLIM, + ylim: tuple[float, float] = DEFAULT_YLIM, + palette: str = "colorblind", + diagonal_lines: bool = False, + **kwargs: Any, +) -> sns.JointGrid: + """ + Create a joint plot with marginals using traditional seaborn. + + This function falls back to traditional seaborn because + seaborn.objects does not yet fully support joint plots with marginals. + + Parameters + ---------- + data : pd.DataFrame + Data to plot + x : str, default="ISOPleasant" + Column name for x-axis + y : str, default="ISOEventful" + Column name for y-axis + hue : str, optional + Column name for color grouping + title : str, default="Soundscape Joint Plot" + Title for the plot + kind : str, default="scatter" + Type of plot: "scatter", "kde", or "hex" + xlim : tuple, default=(-1, 1) + X-axis limits + ylim : tuple, default=(-1, 1) + Y-axis limits + palette : str, default="colorblind" + Color palette to use + diagonal_lines : bool, default=False + Whether to show diagonal lines + **kwargs : Any + Additional parameters for sns.jointplot + + Returns + ------- + sns.JointGrid + The joint plot grid + + """ + use_soundscapy_style() + + # Create joint plot using traditional seaborn + g = sns.jointplot( + data=data, x=x, y=y, hue=hue, kind=kind, palette=palette, **kwargs + ) + + # Apply limits + g.ax_joint.set_xlim(xlim) + g.ax_joint.set_ylim(ylim) + + # Add zero lines + g.ax_joint.axhline(y=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + g.ax_joint.axvline(x=0, color="grey", linestyle="dashed", alpha=1, linewidth=1.5) + + # Add grid + g.ax_joint.grid(True, which="major", color="grey", alpha=0.5) + + # Add title + g.fig.suptitle(title, y=1.05) + + # Add diagonal lines if requested + if diagonal_lines: + g.ax_joint.plot( + [xlim[0], xlim[1]], + [ylim[0], ylim[1]], + linestyle="dashed", + color="grey", + alpha=0.5, + linewidth=1.5, + ) + g.ax_joint.plot( + [xlim[0], xlim[1]], + [ylim[1], ylim[0]], + linestyle="dashed", + color="grey", + alpha=0.5, + linewidth=1.5, + ) + + return g + + +def create_circumplex_subplots( + data_list: Sequence[pd.DataFrame], + x: str = "ISOPleasant", + y: str = "ISOEventful", + hue: str | None = None, + subtitles: Sequence[str] | None = None, + title: str = "Circumplex Subplots", + plot_type: str = "density", + incl_scatter: bool = False, + cols: int = 2, + **kwargs: Any, +) -> plt.Figure: + """ + Create a figure with multiple circumplex plots. + + Parameters + ---------- + data_list : Sequence[pd.DataFrame] + List of data sources to plot + x : str, default="ISOPleasant" + Column name for x-axis + y : str, default="ISOEventful" + Column name for y-axis + hue : str, optional + Column name for color grouping + subtitles : Sequence[str], optional + Titles for individual subplots + title : str, default="Circumplex Subplots" + Main title for the figure + plot_type : str, default="density" + Type of plot: "scatter", "density", or "simple_density" + incl_scatter : bool, default=False + Whether to include scatter points on density plots + cols : int, default=2 + Number of columns for subplots + **kwargs : Any + Additional arguments for plot functions + + Returns + ------- + plt.Figure + The matplotlib Figure with subplots + + """ + use_soundscapy_style() + + # Generate subplot titles if not provided + if subtitles is None: + subtitles = [f"Plot {i + 1}" for i in range(len(data_list))] + + # Remove any layout parameters that don't belong in plot functions + plotting_kwargs = kwargs.copy() + if "nrows" in plotting_kwargs: + plotting_kwargs.pop("nrows") + if "ncols" in plotting_kwargs: + plotting_kwargs.pop("ncols") + + # Calculate rows needed + nrows = (len(data_list) - 1) // cols + 1 + + # Create figure and axes + fig, axes = plt.subplots(nrows, cols, figsize=(cols * 6, nrows * 6), squeeze=False) + axes = axes.flatten() + + # Create individual plots + for i, (data, subtitle) in enumerate(zip(data_list, subtitles, strict=False)): + if i < len(axes): + # Create plot for this axis + if plot_type == "scatter": + scatter_plot( + data, + x=x, + y=y, + hue=hue, + title=subtitle, + ax=axes[i], + **plotting_kwargs, + ) + elif plot_type == "simple_density": + density_plot( + data, + x=x, + y=y, + hue=hue, + title=subtitle, + simple_density=True, + incl_scatter=incl_scatter, + ax=axes[i], + **plotting_kwargs, + ) + else: # "density" or default + density_plot( + data, + x=x, + y=y, + hue=hue, + title=subtitle, + incl_scatter=incl_scatter, + ax=axes[i], + **plotting_kwargs, + ) + + # Hide any unused axes + for i in range(len(data_list), len(axes)): + axes[i].set_visible(False) + + # Add a title to the figure + fig.suptitle(title, fontsize=16) + + # Adjust layout + fig.tight_layout() + + return fig + + +def add_calculated_coords( + data: pd.DataFrame, + paq_cols=("PAQ1", "PAQ2", "PAQ3", "PAQ4", "PAQ5", "PAQ6", "PAQ7", "PAQ8"), + angles=(0, 45, 90, 135, 180, 225, 270, 315), + val_range=(5, 1), + output_cols=("ISOPleasant", "ISOEventful"), + *, + overwrite: bool = False, +) -> pd.DataFrame: + """ + Calculate and add ISO coordinates to a dataframe. + + This is a convenience function to use the SoundscapeCoordinates + stat outside of a plotting context. + + Parameters + ---------- + data : pd.DataFrame + Input data with PAQ columns + paq_cols : tuple, default=("PAQ1", "PAQ2", "PAQ3", "PAQ4", "PAQ5", "PAQ6", "PAQ7", "PAQ8") + Column names for PAQ data + angles : tuple, default=(0, 45, 90, 135, 180, 225, 270, 315) + Angles for each PAQ in degrees + val_range : tuple, default=(5, 1) + (max, min) range of original PAQ responses + output_cols : tuple, default=("ISOPleasant", "ISOEventful") + Column names for output coordinates + overwrite : bool, default=False + Whether to overwrite existing coordinate columns + + Returns + ------- + pd.DataFrame + Data with added ISO coordinate columns + + Raises + ------ + ValueError + If coordinate columns already exist and overwrite=False + + """ + # Check if columns already exist + for col in output_cols: + if col in data.columns and not overwrite: + raise ValueError( + f"{col} already exists in dataframe. Use overwrite=True to replace." + ) + + # Create the stat and apply it to the data + stat = SoundscapeCoordinates( + paq_cols=paq_cols, + angles=angles, + val_range=val_range, + output_cols=output_cols, + ) + + # Apply the stat + result = stat._apply(data) + + return result diff --git a/test/plotting/__init__.py b/src/soundscapy/plotting/soundscape_plot.py similarity index 100% rename from test/plotting/__init__.py rename to src/soundscapy/plotting/soundscape_plot.py diff --git a/src/soundscapy/plotting/soundscapy.mplstyle b/src/soundscapy/plotting/soundscapy.mplstyle new file mode 100644 index 00000000..42950791 --- /dev/null +++ b/src/soundscapy/plotting/soundscapy.mplstyle @@ -0,0 +1,45 @@ +# soundscapy.mplstyle +# Matplotlib style sheet for Soundscapy plots + +# Figure properties +figure.figsize: 6, 6 +figure.constrained_layout.use: True + +# Axes properties +axes.grid: True +axes.linewidth: 1.0 +axes.axisbelow: True +axes.labelsize: 10 +axes.titlesize: 12 + +# Grid properties +grid.alpha: 0.5 +grid.color: grey +grid.linestyle: - +grid.linewidth: 0.5 + +# Tick properties +xtick.direction: in +ytick.direction: in +xtick.minor.visible: True +ytick.minor.visible: True +xtick.labelsize: 9 +ytick.labelsize: 9 + +# Line properties +lines.linewidth: 1.5 +lines.solid_capstyle: round +lines.dashed_pattern: 2.8, 1.2 +lines.dashdot_pattern: 4.8, 1.2, 0.8, 1.2 + +# Font properties +font.size: 10 +font.family: sans-serif + +# Legend properties +legend.frameon: True +legend.framealpha: 0.8 +legend.fontsize: 9 + +# Colors for different groups +axes.prop_cycle: cycler('color', ['4C72B0', '55A868', 'C44E52', '8172B2', 'CCB974', '64B5CD']) diff --git a/src/soundscapy/plotting/stats.py b/src/soundscapy/plotting/stats.py new file mode 100644 index 00000000..e179cf0a --- /dev/null +++ b/src/soundscapy/plotting/stats.py @@ -0,0 +1,149 @@ +""" +Custom Stat components for Soundscape plots using seaborn.objects. + +This module contains custom Stat classes that extend the functionality of +seaborn.objects.Plot for creating soundscape plots. These stats handle +specialized data transformations like converting PAQ data to ISO coordinates. +""" + +import numpy as np +import seaborn.objects as so + +from soundscapy.surveys.survey_utils import EQUAL_ANGLES + + +class SoundscapeCoordinates(so.Stat): + """ + Stat for calculating ISO coordinates from PAQ data. + + This stat transforms PAQ data (perceptual attribute questions) into + ISOPleasant and ISOEventful coordinates based on the ISO 12913-3 standard. + """ + + def __init__( + self, + paq_cols=("PAQ1", "PAQ2", "PAQ3", "PAQ4", "PAQ5", "PAQ6", "PAQ7", "PAQ8"), + angles=EQUAL_ANGLES, + val_range=(5, 1), + output_cols=("ISOPleasant", "ISOEventful"), + **kwargs, + ): + """ + Initialize the ISO coordinates stat. + + Parameters + ---------- + paq_cols : tuple, default=("PAQ1", "PAQ2", "PAQ3", "PAQ4", "PAQ5", "PAQ6", "PAQ7", "PAQ8") + Column names for PAQ data + angles : tuple, default=EQUAL_ANGLES + Angles for each PAQ in degrees + val_range : tuple, default=(5, 1) + (max, min) range of original PAQ responses + output_cols : tuple, default=("ISOPleasant", "ISOEventful") + Column names for output coordinates + **kwargs : + Additional keyword arguments passed to parent class + + """ + self.paq_cols = paq_cols + self.angles = angles + self.val_range = val_range + self.output_cols = output_cols + super().__init__(**kwargs) + + def _apply(self, data, **kwargs): + """ + Calculate ISO coordinates from PAQ data. + + Parameters + ---------- + data : pd.DataFrame + Input data with PAQ columns + **kwargs : + Additional keyword arguments + + Returns + ------- + pd.DataFrame + Data with added ISO coordinate columns + + """ + # Check if required columns exist + if not all(col in data.columns for col in self.paq_cols): + # Return original data if PAQ columns aren't present + return data + + # Get only PAQ columns + paq_df = data[list(self.paq_cols)] + + # Calculate scale factor + scale = max(self.val_range) - min(self.val_range) + + # Calculate coordinates + iso_pleasant = paq_df.apply( + lambda row: self._adj_iso_pl(row, self.angles, scale), axis=1 + ) + iso_eventful = paq_df.apply( + lambda row: self._adj_iso_ev(row, self.angles, scale), axis=1 + ) + + # Add calculated coordinates to data + transformed = data.assign( + **{self.output_cols[0]: iso_pleasant, self.output_cols[1]: iso_eventful} + ) + + return transformed + + def _adj_iso_pl(self, values, angles, scale): + """ + Calculate adjusted ISOPleasant value. + + Parameters + ---------- + values : pd.Series + PAQ values for a single sample + angles : tuple + Angles for each PAQ in degrees + scale : float + Scale factor for normalization + + Returns + ------- + float + Adjusted ISOPleasant value + + """ + iso_pl = sum( + np.cos(np.deg2rad(angle)) * value + for angle, value in zip(angles, values, strict=False) + ) + return iso_pl / ( + scale / 2 * sum(abs(np.cos(np.deg2rad(angle))) for angle in angles) + ) + + def _adj_iso_ev(self, values, angles, scale): + """ + Calculate adjusted ISOEventful value. + + Parameters + ---------- + values : pd.Series + PAQ values for a single sample + angles : tuple + Angles for each PAQ in degrees + scale : float + Scale factor for normalization + + Returns + ------- + float + Adjusted ISOEventful value + + """ + iso_ev = sum( + np.sin(np.deg2rad(angle)) * value + for angle, value in zip(angles, values, strict=False) + ) + return iso_ev / ( + scale / 2 * sum(abs(np.sin(np.deg2rad(angle))) for angle in angles) + ) diff --git a/src/soundscapy/plotting/stylers.py b/src/soundscapy/plotting/stylers.py index 5ad22860..cc16ce2c 100644 --- a/src/soundscapy/plotting/stylers.py +++ b/src/soundscapy/plotting/stylers.py @@ -1,14 +1,12 @@ -""" -Styling utilities for circumplex plots using Seaborn and Matplotlib. -""" +"""Styling utilities for circumplex plots using Seaborn and Matplotlib.""" from dataclasses import dataclass, field -from typing import Any, Dict, Tuple +from typing import Any import matplotlib as mpl import seaborn as sns -from .plotting_utils import DEFAULT_FIGSIZE +from soundscapy.plotting.plotting_utils import DEFAULT_FIGSIZE @dataclass @@ -24,6 +22,7 @@ class StyleOptions: bw_adjust (float): Bandwidth adjustment for kernel density estimation. figsize (Tuple[int, int]): Figure size (width, height) in inches. simple_density (Dict[str, Any]): Configuration for simple density plots. + """ diag_lines_zorder: int = 1 @@ -31,8 +30,8 @@ class StyleOptions: prim_lines_zorder: int = 2 data_zorder: int = 3 bw_adjust: float = 1.2 - figsize: Tuple[int, int] = DEFAULT_FIGSIZE - simple_density: Dict[str, Any] = field( + figsize: tuple[int, int] = DEFAULT_FIGSIZE + simple_density: dict[str, Any] = field( default_factory=lambda: { "thresh": 0.5, "levels": 2, @@ -43,17 +42,17 @@ class StyleOptions: class SeabornStyler: - """ - Class for applying Seaborn styles to circumplex plots. - """ + """Class for applying Seaborn styles to circumplex plots.""" - def __init__(self, params: Any, style_options: StyleOptions = StyleOptions()): + def __init__( + self, params: Any, style_options: StyleOptions = StyleOptions() + ) -> None: self.params = params self.style_options = style_options def apply_styling( self, fig: mpl.figure.Figure, ax: mpl.axes.Axes - ) -> Tuple[mpl.figure.Figure, mpl.axes.Axes]: + ) -> tuple[mpl.figure.Figure, mpl.axes.Axes]: """ Apply styling to the plot. @@ -65,6 +64,7 @@ def apply_styling( Returns ------- Tuple[mpl.figure.Figure, mpl.axes.Axes]: The styled figure and axes. + """ self.set_style() self.circumplex_grid(ax) diff --git a/src/soundscapy/py.typed b/src/soundscapy/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/soundscapy/spi/__init__.py b/src/soundscapy/spi/__init__.py new file mode 100644 index 00000000..1931d597 --- /dev/null +++ b/src/soundscapy/spi/__init__.py @@ -0,0 +1,33 @@ +""" +Soundscapy Psychoacoustic Indicator (SPI) calculation module. + +This module provides functions and classes for calculating SPI, +based on the R implementation. Requires optional dependencies. +""" +# ruff: noqa: E402 +# ignore module level import order because we need to check dependencies first + +# Check for required dependencies directly +# This will raise ImportError if any dependency is missing +try: + import rpy2 # noqa: F401 + +except ImportError as e: + msg = ( + "SPI functionality requires additional dependencies. " + "Install with: pip install soundscapy[spi]" + ) + raise ImportError(msg) from e + +# Now we can import our modules that depend on the optional packages +from soundscapy.spi import msn +from soundscapy.spi.msn import CentredParams, DirectParams, MultiSkewNorm, cp2dp, dp2cp + +__all__ = [ + "CentredParams", + "DirectParams", + "MultiSkewNorm", + "cp2dp", + "dp2cp", + "msn", +] diff --git a/src/soundscapy/spi/_r_wrapper.py b/src/soundscapy/spi/_r_wrapper.py new file mode 100644 index 00000000..1aaf819c --- /dev/null +++ b/src/soundscapy/spi/_r_wrapper.py @@ -0,0 +1,439 @@ +""" +R integration for skew-normal distribution calculations. + +This module provides functions for: +1. Checking R and R package dependencies +2. Initializing and managing R sessions +3. Converting data between R and Python +4. Executing R functions for skew-normal calculations + +It is not intended to be used directly by end users. +""" + +import sys +import warnings +from typing import Any, NoReturn + +from rpy2 import robjects +from rpy2.robjects import numpy2ri, pandas2ri + +# These are used in the docstring examples but not in the code +# They will be used by code that imports and uses this module +from soundscapy.sspylogging import get_logger + +logger = get_logger() + +# Cached values to avoid repeated checks +_r_checked = False +_sn_checked = False + +# Session state +_r_session = None +_sn_package = None +_stats_package = None +_base_package = None +_session_active = False + +REQUIRED_R_VERSION = 3.6 + + +def check_r_availability() -> None: + """ + Check if R is installed and accessible through rpy2. + + Raises + ------ + ImportError + If R is not installed or cannot be accessed. + + """ + global _r_checked # noqa: PLW0603 + + def _raise_r_not_found_error() -> NoReturn: + msg = ( + "rpy2 is installed but it cannot find an R installation. " + "Please ensure R is installed and correctly configured. " + "On Linux: Install R with your package manager (e.g., apt-get install r-base)." # noqa: E501 + "On macOS: Install R from CRAN (https://cran.r-project.org/bin/macosx/). " + "On Windows: Install R from CRAN (https://cran.r-project.org/bin/windows/base/)." + ) + raise ImportError(msg) + + def _raise_r_access_error(e: Exception) -> NoReturn: + msg = ( + f"Error accessing R installation: {e!s}. " + "Please ensure R is installed and correctly configured." + ) + raise ImportError(msg) + + def _raise_r_version_too_old_error(r_version_num: float) -> NoReturn: + msg = ( + f"R version {r_version_num} is too old." + f"The 'sn' package requires R >= {REQUIRED_R_VERSION}." + "Please upgrade your R installation." + ) + raise ImportError(msg) + + if _r_checked: + return + + try: + from rpy2 import robjects + + # Basic check to ensure R is running by getting R version + r_version = robjects.r("R.version.string")[0] # type: ignore[index] + logger.debug("R version: %s", r_version) + + # Check if minimum R version requirements are met + # The 'sn' package requires R >= 3.6.0 + r_version_num = robjects.r( + "as.numeric(R.version$major) + as.numeric(R.version$minor)/10" + )[0] # type: ignore[index] + + if r_version_num < REQUIRED_R_VERSION: + _raise_r_version_too_old_error(r_version_num) + + _r_checked = True + except ImportError: + _raise_r_not_found_error() # Call the handler + except Exception as e: # noqa: BLE001 + _raise_r_access_error(e) # Call the handler + + +def check_sn_package() -> None: + """ + Check if the R 'sn' package is installed. + + Raises + ------ + ImportError + If the 'sn' package is not installed. + + """ + global _sn_checked # noqa: PLW0603 + + def _raise_sn_version_too_old_error(version: str) -> NoReturn: + msg = ( + f"R 'sn' package version {version} is too old. " + "The SPI feature requires 'sn' >= 2.0.0. " + "Please upgrade the package by running in R: install.packages('sn')" + ) + raise ImportError(msg) + + def _raise_sn_not_installed_error() -> NoReturn: + msg = ( + "R package 'sn' is not installed. " + "Please install it by running in R: install.packages('sn')" + ) + raise ImportError(msg) + + def _raise_sn_check_error(e: Exception) -> NoReturn: + msg = ( + f"Error checking for R 'sn' package: {e!s}. " + "Please ensure the package is installed by running in R: install.packages('sn')" # noqa: E501 + ) + raise ImportError(msg) + + if _sn_checked: + return + + # First ensure R is available + check_r_availability() + + try: + import rpy2.robjects.packages as rpackages + + # Check if 'sn' package is installed + try: + # Just importing to verify it exists + _ = rpackages.importr("sn") + + # Get package version using R to verify compatibility + from rpy2 import robjects + + # Use R code to get the package version + version = robjects.r('as.character(packageVersion("sn"))')[0] # type: ignore[index] + logger.debug("R 'sn' package version: %s", version) + + # Check if package version meets requirements + # The SPI implementation requires 'sn' >= 2.0.0 + if version < "2.0.0": + _raise_sn_version_too_old_error(version) + + _sn_checked = True + except rpackages.PackageNotInstalledError: + _raise_sn_not_installed_error() + except Exception as e: + if "sn" in str(e): + # Already a more specific error about the sn package + raise # Re-raising is okay here + _raise_sn_check_error(e) + + +def check_dependencies() -> dict[str, Any]: + """ + Check all required R dependencies for the SPI module. + + This function checks: + 1. R installation accessibility + 2. R version compatibility + 3. 'sn' package availability + 4. 'sn' package version compatibility + + Returns + ------- + dict[str, Any] + Dictionary with dependency information. + + Raises + ------ + ImportError + If any dependency check fails. + + """ + # Check R availability first + check_r_availability() + + # Then check for the sn package + check_sn_package() + + # If we get here, all dependencies are available + + # Return information about the dependencies + return { + "rpy2_version": sys.modules["rpy2"].__version__, + "r_version": robjects.r("R.version.string")[0], # type: ignore[index] + "sn_version": robjects.r('as.character(packageVersion("sn"))')[0], # type: ignore[index] + } + + +# === SESSION MANAGEMENT === + + +def initialize_r_session() -> dict[str, Any]: + """ + Initialize an R session for skew-normal distribution calculations. + + This function: + 1. Checks for R and package dependencies + 2. Imports required R packages + 3. Sets up the R environment + 4. Updates global session state + + Returns + ------- + dict[str, Any] + Session information including R and package versions. + + Raises + ------ + ImportError + If dependencies are missing. + RuntimeError + If session initialization fails. + + """ + global _r_session, _sn_package, _stats_package, _base_package, _session_active # noqa: PLW0603 + + # If session is already active, just return the state + if _session_active: + logger.debug("R session already initialized") + return { + "r_session": "active", + "sn_package": "loaded", + "stats_package": "loaded", + "base_package": "loaded", + } + + # First check all dependencies + dep_info = check_dependencies() + logger.debug("Dependencies verified: %s", dep_info) + + try: + import rpy2.robjects.packages as rpackages + from rpy2 import robjects + + # Import required packages + _sn_package = rpackages.importr("sn") + _stats_package = rpackages.importr("stats") + _base_package = rpackages.importr("base") + logger.debug("Imported R packages: sn, stats, base") + + # Set R random seed for reproducibility + robjects.r("set.seed(42)") + + # Store R session + _r_session = robjects + + # Update session state + _session_active = True + logger.info("R session successfully initialized") + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # Activate numpy and pandas conversion + logger.debug("Activating numpy and pandas conversion") + logger.info( + "rpy2 throws a DeprecationWarning about global activation, which we're ignoring for now." # noqa: E501 + ) + # TODO(MitchellAcoustics): Remove global conversion, as recommended by rpy2 + # https://github.com/MitchellAcoustics/Soundscapy/issues/111 + numpy2ri.activate() + pandas2ri.activate() + + return { + "r_session": "active", + "sn_package": str(_sn_package), + "stats_package": str(_stats_package), + "base_package": str(_base_package), + **dep_info, + } + + except Exception as e: + logger.exception("Failed to initialize R session") + _session_active = False + _r_session = None + _sn_package = None + _stats_package = None + _base_package = None + msg = f"Failed to initialize R session: {e!s}" + raise RuntimeError(msg) from e + + +def shutdown_r_session() -> bool: + """ + Shutdown the R session and clean up resources. + + This function: + 1. Deactivates numpy conversion + 2. Resets global session state + 3. Performs garbage collection + + Returns + ------- + bool + True if successful, False otherwise. + + """ + global _r_session, _sn_package, _stats_package, _base_package, _session_active # noqa: PLW0603 + + if not _session_active: + logger.debug("No active R session to shutdown") + return True + + try: + import gc + + # Clear references to R objects + _r_session = None + _sn_package = None + _stats_package = None + _base_package = None + + # Update session state + _session_active = False + + # Force garbage collection to release R resources + gc.collect() + logger.info("R session successfully shutdown") + + except Exception: + logger.exception("Error during R session shutdown") + return False + else: + return True + + +def get_r_session() -> tuple[Any, Any, Any, Any]: + """ + Get the current R session and package objects. + + This function: + 1. Initializes the session if not already active + 2. Returns the session and package references + + Returns + ------- + tuple[Any, Any, Any, Any] + (r_session, sn_package, stats_package, base_package) + + Raises + ------ + RuntimeError + If session initialization fails. + + """ + global _r_session, _sn_package, _stats_package, _base_package, _session_active # noqa: PLW0602 + + if not _session_active: + logger.debug("R session not active, initializing") + initialize_r_session() + + if ( + not _session_active + or not _r_session + or not _sn_package + or not _stats_package + or not _base_package + ): + msg = "Failed to initialize R session" + raise RuntimeError(msg) + + return _r_session, _sn_package, _stats_package, _base_package + + +def install_r_packages(packages: list[str] | None = None) -> None: + """ + Install R packages if not already installed. + + Parameters + ---------- + packages : list[str] | None, optional + List of R package names to install. Defaults to ["sn", "tvtnorm"]. + + Raises + ------ + ImportError + If R is not available or package installation fails. + + """ + if packages is None: + packages = ["sn", "tvtnorm"] + + check_r_availability() + + try: + import rpy2.robjects.packages as rpackages + from rpy2.robjects.vectors import StrVector + + utils = rpackages.importr("utils") + utils.chooseCRANmirror(ind=1) + + # Check if packages are installed + packnames_to_install = [x for x in packages if not rpackages.isinstalled(x)] + logger.debug("Packages to install: %s", packnames_to_install) + + # Install missing packages + if len(packnames_to_install) > 0: + utils.install_packages(StrVector(packnames_to_install)) + logger.info("Installed missing R packages: %s", packnames_to_install) + else: + logger.debug("All required R packages are already installed") + + except Exception as e: + msg = f"Failed to install R packages: {e!s}" + raise ImportError(msg) from e + + +def is_session_active() -> bool: + """ + Check if the R session is currently active. + + Returns + ------- + bool + True if the session is active, False otherwise. + + """ + global _session_active # noqa: PLW0602 + return _session_active diff --git a/src/soundscapy/spi/_rsn_wrapper.py b/src/soundscapy/spi/_rsn_wrapper.py new file mode 100644 index 00000000..e577dfec --- /dev/null +++ b/src/soundscapy/spi/_rsn_wrapper.py @@ -0,0 +1,222 @@ +from typing import Literal + +import numpy as np +import pandas as pd +from rpy2 import robjects +from rpy2.robjects.methods import RS4 + +from soundscapy import get_logger +from soundscapy.spi._r_wrapper import get_r_session + +logger = get_logger() + +_, sn, _, _ = get_r_session() +logger.debug("R session and packages retrieved successfully.") + + +def selm(x: str, y: str, data: pd.DataFrame) -> RS4: + formula = f"cbind({x}, {y}) ~ 1" + return sn.selm(formula, data=data, family="SN") + + +def calc_cp(x: str, y: str, data: pd.DataFrame) -> tuple: + selm_model = selm(x, y, data) + return extract_cp(selm_model) + + +def calc_dp(x: str, y: str, data: pd.DataFrame) -> tuple: + selm_model = selm(x, y, data) + return extract_dp(selm_model) + + +def extract_cp(selm_model: RS4) -> tuple: + cp = tuple(selm_model.slots["param"][1]) + return (cp[0].flatten(), cp[1], cp[2].flatten()) + + +def extract_dp(selm_model: RS4) -> tuple: + dp = tuple(selm_model.slots["param"][0]) + return (dp[0].flatten(), dp[1], dp[2].flatten()) + + +def sample_msn( + selm_model: RS4 | None = None, + xi: np.ndarray | None = None, + omega: np.ndarray | None = None, + alpha: np.ndarray | None = None, + n: int = 1000, +) -> np.ndarray: + if selm_model is not None: + return sn.rmsn(n, dp=selm_model.slots["param"][0]) + if xi is not None and omega is not None and alpha is not None: + r_xi = robjects.FloatVector(xi.T) # Transpose to make it a column vector + r_omega = robjects.r.matrix( + robjects.FloatVector(omega.flatten()), + nrow=omega.shape[0], + ncol=omega.shape[1], + ) # type: ignore[reportCallIssue] + r_alpha = robjects.FloatVector(alpha) # Transpose to make it a column vector + return sn.rmsn(n, xi=r_xi, Omega=r_omega, alpha=r_alpha) + msg = "Either selm_model or xi, omega, and alpha must be provided." + raise ValueError(msg) + + +def sample_mtsn( + selm_model: RS4 | None = None, + xi: np.ndarray | None = None, + omega: np.ndarray | None = None, + alpha: np.ndarray | None = None, + a: float = -1, + b: float = 1, + n: int = 1000, +) -> np.ndarray: + """ + Sample from a multivariate truncated skew-normal distribution. + + Uses rejection sampling to ensure that the samples are within the bounds [a, b] + for both dimensions. + + Parameters + ---------- + selm_model : optional + Fitted SELM model from R's 'sn' package. If provided, parameters `xi`, + `omega`, and `alpha` are ignored. + xi : np.ndarray, optional + Location parameter (2x1 array). + omega : np.ndarray, optional + Scale matrix (2x2 array). + alpha : np.ndarray, optional + Skewness parameter (2x1 array). + a : float, optional + Lower truncation bound for both dimensions, by default -1. + b : float, optional + Upper truncation bound for both dimensions, by default 1. + n : int, optional + Number of samples to generate, by default 1000. + + Returns + ------- + np.ndarray + Array of samples (n x 2). + + Raises + ------ + ValueError + If neither `selm_model` nor all of `xi`, `omega`, and `alpha` are provided. + + """ + samples = np.array([[0, 0]]) + n_samples = 0 + while n_samples < n: + if selm_model is not None: + sample = sample_msn(selm_model, n=1) + elif xi is not None and omega is not None and alpha is not None: + sample = sample_msn(xi=xi, omega=omega, alpha=alpha, n=1) + else: + msg = "Either selm_model or xi, omega, and alpha must be provided." + raise ValueError(msg) + if a <= sample[0][0] <= b and a <= sample[0][1] <= b: + samples = np.append(samples, sample, axis=0) + if n_samples == 0: + samples = samples[1:] + n_samples += 1 + + # Ensure the sample is within the bounds [a, b] for both dimensions + if not np.all((a <= samples[:, 0]) & (samples[:, 0] <= b)): + msg = f"Sample x-values are out of bounds: [{a}, {b}]" + raise ValueError(msg) + if not np.all((a <= samples[:, 1]) & (samples[:, 1] <= b)): + msg = f"Sample y-values are out of bounds: [{a}, {b}]" + raise ValueError(msg) + return samples + + +def dp2cp( + xi: np.ndarray, + omega: np.ndarray, + alpha: np.ndarray, + family: Literal["SN", "ESN", "ST", "SC"] = "SN", +) -> tuple: + """ + Convert Direct Parameters (DP) to Centred Parameters (CP). + + Parameters + ---------- + xi : np.ndarray + Location parameter (2x1 array). + omega : np.ndarray + Scale matrix (2x2 array). + alpha : np.ndarray + Skewness parameter (2x1 array). + family : str, optional + Distribution family, by default "SN". + + Returns + ------- + tuple + Tuple containing the centred parameters (mean, sigma, skew). + + """ + r_xi = robjects.FloatVector(xi.T) # Transpose to make it a column vector + r_omega = robjects.r.matrix( + robjects.FloatVector(omega.flatten()), + nrow=omega.shape[0], + ncol=omega.shape[1], + ) # type: ignore[reportCallIssue] + r_alpha = robjects.FloatVector(alpha) # Transpose to make it a column vector + + dp_r = robjects.ListVector( + { + "xi": r_xi, + "Omega": r_omega, + "alpha": r_alpha, + } + ) + + cp_r = sn.dp2cp(dp_r, family=family) + + return tuple(cp_r) + + +def cp2dp( + mean: np.ndarray, + sigma: np.ndarray, + skew: np.ndarray, + family: Literal["SN", "ESN", "ST", "SC"] = "SN", +) -> tuple: + """ + Convert Centred Parameters (CP) to Direct Parameters (DP). + + Parameters + ---------- + mean : np.ndarray + Mean vector (2x1 array). + sigma : np.ndarray + Covariance matrix (2x2 array). + skew : np.ndarray + Skewness vector (2x1 array). + family : str, optional + Distribution family, by default "SN". + + Returns + ------- + tuple + Tuple containing the direct parameters (xi, omega, alpha). + + """ + r_mean = robjects.FloatVector(mean.T) # Transpose to make it a column vector + r_sigma = robjects.r.matrix( + robjects.FloatVector(sigma.flatten()), + nrow=sigma.shape[0], + ncol=sigma.shape[1], + ) # type: ignore[reportCallIssue] + r_skew = robjects.FloatVector(skew) # Transpose to make it a column vector + cp_r = robjects.ListVector( + { + "mean": r_mean, + "Sigma": r_sigma, + "skew": r_skew, + } + ) + dp_r = sn.cp2dp(cp_r, family=family) + return tuple(dp_r) diff --git a/src/soundscapy/spi/ks2d.py b/src/soundscapy/spi/ks2d.py new file mode 100644 index 00000000..2140921c --- /dev/null +++ b/src/soundscapy/spi/ks2d.py @@ -0,0 +1,403 @@ +# ruff: noqa: PGH004 +# ruff: noqa +# type: ignore +# Code créé par Gabriel Taillon le 7 Mai 2018 +# From https://github.com/Gabinou/2DKS +# Kolmogorov-Smyrnov Test extended to two dimensions. +# References:s +# [1] Peacock, J. A. (1983). Two-dimensional goodness-of-fit testing +# in astronomy. Monthly Notices of the Royal Astronomical Society, +# 202(3), 615-627. +# [2] Fasano, G., & Franceschini, A. (1987). A multidimensional version of +# the Kolmogorov–Smirnov test. Monthly Notices of the Royal Astronomical +# Society, 225(1), 155-170. +# [3] Flannery, B. P., Press, W. H., Teukolsky, S. A., & Vetterling, W. +# (1992). Numerical recipes in C. Press Syndicate of the University +# of Cambridge, New York, 24, 78. +import inspect + +import numpy as np +import scipy.stats + + +def CountQuads( + Arr2D: np.ndarray, point: np.ndarray +) -> tuple[float, float, float, float]: + """ + Compute probabilities by counting points in quadrants. + + Computes the probabilities of finding points in each of the 4 quadrants + defined by a vertical and horizontal line crossing the given `point`. + The probabilities are determined by counting the proportion of points + from `Arr2D` that fall into each quadrant. + + Parameters + ---------- + Arr2D : np.ndarray + Array of 2D points (shape N x 2) to be counted. + point : np.ndarray + A 1D array or list with 2 elements representing the center (x, y) + of the 4 quadrants. + + Returns + ------- + tuple[float, float, float, float] + A tuple containing four floats (fpp, fnp, fpn, fnn), representing the + normalized fractions (probabilities) of points in each quadrant: + - fpp: Fraction in the positive-x, positive-y quadrant. + - fnp: Fraction in the negative-x, positive-y quadrant. + - fpn: Fraction in the positive-x, negative-y quadrant. + - fnn: Fraction in the negative-x, negative-y quadrant. + + Raises + ------ + TypeError + If `point` or `Arr2D` are not list-like or numpy arrays, or if + `point` does not have 2 elements, or if `Arr2D` is not 2D. + + """ + if isinstance(point, list): + point = np.asarray(np.ravel(point)) + elif type(point).__module__ + type(point).__name__ == "numpyndarray": + point = np.ravel(point.copy()) + else: + raise TypeError("Input point is neither list nor numpyndarray") + if len(point) != 2: + raise TypeError("Input point must have exactly 2 elements") + if isinstance(Arr2D, list): + Arr2D = np.asarray(Arr2D) + elif type(Arr2D).__module__ + type(Arr2D).__name__ == "numpyndarray": + pass + else: + raise TypeError("Input Arr2D is neither list nor numpyndarray") + if Arr2D.shape[1] > Arr2D.shape[0]: # Reshape to A[row,column] + Arr2D = Arr2D.copy().T + if Arr2D.shape[1] != 2: + raise TypeError("Input Arr2D is not 2D") + # The pp of Qpp refer to p for 'positive' and n for 'negative' quadrants. + # In order. first subscript is x, second is y. + Qpp = Arr2D[(Arr2D[:, 0] > point[0]) & (Arr2D[:, 1] > point[1]), :] + Qnp = Arr2D[(Arr2D[:, 0] < point[0]) & (Arr2D[:, 1] > point[1]), :] + Qpn = Arr2D[(Arr2D[:, 0] > point[0]) & (Arr2D[:, 1] < point[1]), :] + Qnn = Arr2D[(Arr2D[:, 0] < point[0]) & (Arr2D[:, 1] < point[1]), :] + # Normalized fractions: + ff = 1.0 / len(Arr2D) + fpp = len(Qpp) * ff + fnp = len(Qnp) * ff + fpn = len(Qpn) * ff + fnn = len(Qnn) * ff + # NOTE: all the f's are supposed to sum to 1.0. Float representation + # cause SOMETIMES sum to 1.000000002 or something. I don't know how to + # test for that reliably, OR what to do about it yet. Keep in mind. + return fpp, fnp, fpn, fnn + + +def FuncQuads(func2D, point, xlim, ylim, rounddig=4): + """ + Compute probabilities by integrating a density function in quadrants. + + Computes the probabilities of finding points in each of the 4 quadrants + defined by a vertical and horizontal line crossing the given `point`. + The probabilities are determined by numerically integrating the 2D density + function `func2D` over each quadrant within the specified limits. + + Parameters + ---------- + func2D : callable + A 2D density function that accepts two arguments (x, y). + point : list or np.ndarray + A 1D array or list with 2 elements representing the center (x, y) + of the 4 quadrants. + xlim : list or np.ndarray + A list or array with 2 elements defining the integration limits for x. + ylim : list or np.ndarray + A list or array with 2 elements defining the integration limits for y. + rounddig : int, optional + Number of decimal digits to round the resulting probabilities to, + by default 4. + + Returns + ------- + tuple[float, float, float, float] + A tuple containing four floats (fpp, fnp, fpn, fnn), representing the + integrated probabilities in each quadrant, normalized by the total integral: + - fpp: Probability in the positive-x, positive-y quadrant. + - fnp: Probability in the negative-x, positive-y quadrant. + - fpn: Probability in the positive-x, negative-y quadrant. + - fnn: Probability in the negative-x, negative-y quadrant. + + Raises + ------ + TypeError + If `func2D` is not a callable function with 2 arguments, or if + `point`, `xlim`, or `ylim` are not list-like or numpy arrays with + exactly 2 elements, or if limits in `xlim` or `ylim` are equal. + + """ + if callable(func2D): + if len(inspect.getfullargspec(func2D)[0]) != 2: + raise TypeError("Input func2D is not a function with 2 arguments") + else: + raise TypeError("Input func2D is not a function") + # If xlim, ylim and point are not lists or ndarray, exit. + if isinstance(point, list): + point = np.asarray(np.ravel(point)) + elif type(point).__module__ + type(point).__name__ == "numpyndarray": + point = np.ravel(point.copy()) + else: + raise TypeError("Input point is not a list or numpyndarray") + if len(point) != 2: + raise TypeError("Input point has not exactly 2 elements") + if isinstance(xlim, list): + xlim = np.asarray(np.sort(np.ravel(xlim))) + elif type(xlim).__module__ + type(xlim).__name__ == "numpyndarray": + xlim = np.sort(np.ravel(xlim.copy())) + else: + raise TypeError("Input xlim is not a list or ndarray") + if len(xlim) != 2: + raise TypeError("Input xlim has not exactly 2 elements") + if xlim[0] == xlim[1]: + raise TypeError("Input xlim[0] should be different to xlim[1]") + if isinstance(ylim, list): + ylim = np.asarray(np.sort(np.ravel(ylim))) + elif type(ylim).__module__ + type(ylim).__name__ == "numpyndarray": + ylim = np.sort(np.ravel(ylim.copy())) + else: + raise TypeError("Input ylim is not a list or ndarray") + if len(ylim) != 2: + raise TypeError("Input ylim has not exactly 2 elements") + if ylim[0] == ylim[1]: + raise TypeError("Input ylim[0] should be different to ylim[1]") + # Numerical integration to find the quadrant probabilities. + totInt = scipy.integrate.dblquad( + func2D, *xlim, lambda x: np.amin(ylim), lambda x: np.amax(ylim) + )[0] + Qpp = scipy.integrate.dblquad( + func2D, point[0], np.amax(xlim), lambda x: point[1], lambda x: np.amax(ylim) + )[0] + Qpn = scipy.integrate.dblquad( + func2D, point[0], np.amax(xlim), lambda x: np.amin(ylim), lambda x: point[1] + )[0] + Qnp = scipy.integrate.dblquad( + func2D, np.amin(xlim), point[0], lambda x: point[1], lambda x: np.amax(ylim) + )[0] + Qnn = scipy.integrate.dblquad( + func2D, np.amin(xlim), point[0], lambda x: np.amin(ylim), lambda x: point[1] + )[0] + fpp = round(Qpp / totInt, rounddig) + fnp = round(Qnp / totInt, rounddig) + fpn = round(Qpn / totInt, rounddig) + fnn = round(Qnn / totInt, rounddig) + return (fpp, fnp, fpn, fnn) + + +def Qks(alam, iter=100, prec=1e-17): + """ + Compute the Kolmogorov-Smirnov probability function Q(lambda). + + Calculates the significance level for a given KS statistic `alam` (D). + This function is based on the approximation given in Numerical Recipes in C, + page 623. It represents the probability that the KS statistic will exceed + the observed value `alam` under the null hypothesis. + + Parameters + ---------- + alam : float + The KS statistic D (or a related value, often D * sqrt(N_eff)). + iter : int, optional + Maximum number of iterations for the series summation, by default 100. + prec : float, optional + Convergence precision. The summation stops if the absolute value of + the term to add is less than `prec`, by default 1e-17. + + Returns + ------- + float + The significance level P(D > observed) associated with `alam`. + Returns 1.0 if the series does not converge within `iter` iterations + or if the result exceeds 1.0. Returns 0.0 if the result is below `prec`. + + Raises + ------ + TypeError + If `alam` is not an integer or float. + + """ + # If j iterations are performed, meaning that toadd + # is still 2 times larger than the precision. + if isinstance(alam, int) | isinstance(alam, float): + pass + else: + raise TypeError("Input alam is neither int nor float") + toadd = [1] + qks = 0.0 + j = 1 + while (j < iter) & (abs(toadd[-1]) > prec * 2): + toadd.append(2.0 * (-1.0) ** (j - 1.0) * np.exp(-2.0 * j**2.0 * alam**2.0)) + qks += toadd[-1] + j += 1 + if (j == iter) | (qks > 1): # If no convergence after j iter, return 1.0 + return 1.0 + if qks < prec: + return 0.0 + return qks + + +def ks2d2s(Arr2D1: np.ndarray, Arr2D2: np.ndarray) -> tuple[float, float]: + """ + Perform the 2-dimensional, 2-sample Kolmogorov-Smirnov test. + + Tests the null hypothesis that two independent 2D samples, `Arr2D1` and + `Arr2D2`, are drawn from the same underlying probability distribution. + This implementation is based on the methods described by Peacock (1983) + and Fasano & Franceschini (1987). + + Parameters + ---------- + Arr2D1 : np.ndarray + First 2D sample array (shape N1 x 2). + Arr2D2 : np.ndarray + Second 2D sample array (shape N2 x 2). + + Returns + ------- + tuple[float, float] + d : float + The 2D KS statistic, representing the maximum difference found + between the cumulative distributions in any of the four quadrants, + evaluated at all data points. + prob : float + The significance level (p-value) of the observed statistic `d`. + A small `prob` indicates that the two samples are significantly + different. + + Raises + ------ + TypeError + If `Arr2D1` or `Arr2D2` are not numpy arrays or are not 2D. + + """ + if not isinstance(Arr2D1, np.ndarray): + raise TypeError("Input Arr2D1 is not a numpyndarray") + if Arr2D1.shape[1] > Arr2D1.shape[0]: + Arr2D1 = Arr2D1.copy().T + if not isinstance(Arr2D2, np.ndarray): + raise TypeError("Input Arr2D2 is not a numpyndarray") + + if Arr2D2.shape[1] > Arr2D2.shape[0]: + Arr2D2 = Arr2D2.copy().T + + if Arr2D1.shape[1] != 2: + raise TypeError("Input Arr2D1 is not 2D") + if Arr2D2.shape[1] != 2: + raise TypeError("Input Arr2D2 is not 2D") + + d1, d2 = 0.0, 0.0 + for point1 in Arr2D1: + fpp1, fmp1, fpm1, fmm1 = CountQuads(Arr2D1, point1) + fpp2, fmp2, fpm2, fmm2 = CountQuads(Arr2D2, point1) + d1 = max(d1, abs(fpp1 - fpp2)) + d1 = max(d1, abs(fpm1 - fpm2)) + d1 = max(d1, abs(fmp1 - fmp2)) + d1 = max(d1, abs(fmm1 - fmm2)) + for point2 in Arr2D2: + fpp1, fmp1, fpm1, fmm1 = CountQuads(Arr2D1, point2) + fpp2, fmp2, fpm2, fmm2 = CountQuads(Arr2D2, point2) + d2 = max(d2, abs(fpp1 - fpp2)) + d2 = max(d2, abs(fpm1 - fpm2)) + d2 = max(d2, abs(fmp1 - fmp2)) + d2 = max(d2, abs(fmm1 - fmm2)) + d = (d1 + d2) / 2.0 + sqen = np.sqrt(len(Arr2D1) * len(Arr2D2) / (len(Arr2D1) + len(Arr2D2))) + R1 = scipy.stats.pearsonr(Arr2D1[:, 0], Arr2D1[:, 1]).correlation + R2 = scipy.stats.pearsonr(Arr2D2[:, 0], Arr2D2[:, 1]).correlation + RR = np.sqrt(1.0 - (R1 * R1 + R2 * R2) / 2.0) + prob = Qks(d * sqen / (1.0 + RR * (0.25 - 0.75 / sqen))) + # Small values of prob show that the two samples are significantly + # different. Prob is the significance level of an observed value of d. + # NOT the same as the significance level that ou set and compare to D. + return d, prob + + +def ks2d1s(Arr2D, func2D, xlim=[], ylim=[]): + """ + Perform the 2-dimensional, 1-sample Kolmogorov-Smirnov test. + + Tests the null hypothesis that a 2D sample `Arr2D` is drawn from a + given 2D probability density distribution `func2D`. + + Parameters + ---------- + Arr2D : np.ndarray + The 2D sample array (shape N x 2). + func2D : callable + The theoretical 2D probability density function func(x, y). + xlim : list or np.ndarray, optional + Integration limits for the x-dimension. If empty, defaults are + calculated based on the range of `Arr2D`. + ylim : list or np.ndarray, optional + Integration limits for the y-dimension. If empty, defaults are + calculated based on the range of `Arr2D`. + + Returns + ------- + tuple[float, float] + d : float + The 2D KS statistic, representing the maximum difference between + the empirical distribution (from `Arr2D`) and the theoretical + distribution (`func2D`) in any of the four quadrants, evaluated + at all data points. + prob : float + The significance level (p-value) of the observed statistic `d`. + A small `prob` indicates that the sample is significantly + different from the theoretical distribution. + + Raises + ------ + TypeError + If `func2D` is not a callable function with 2 arguments, or if + `Arr2D` is not a 2D numpy array. + + """ + if callable(func2D): + if len(inspect.getfullargspec(func2D)[0]) != 2: + raise TypeError("Input func2D is not a function with 2 input arguments") + else: + raise TypeError("Input func2D is not a function") + if type(Arr2D).__module__ + type(Arr2D).__name__ == "numpyndarray": + pass + else: + raise TypeError("Input Arr2D is neither list nor numpyndarray") + print(Arr2D.shape) + if Arr2D.shape[1] > Arr2D.shape[0]: + Arr2D = Arr2D.copy().T + if Arr2D.shape[1] != 2: + raise TypeError("Input Arr2D is not 2D") + if xlim == []: + xlim.append( + np.amin(Arr2D[:, 0]) - abs(np.amin(Arr2D[:, 0]) - np.amax(Arr2D[:, 0])) / 10 + ) + xlim.append( + np.amax(Arr2D[:, 0]) - abs(np.amin(Arr2D[:, 0]) - np.amax(Arr2D[:, 0])) / 10 + ) + if ylim == []: + ylim.append( + np.amin(Arr2D[:, 1]) - abs(np.amin(Arr2D[:, 1]) - np.amax(Arr2D[:, 1])) / 10 + ) + + ylim.append( + np.amax(Arr2D[:, 1]) - abs(np.amin(Arr2D[:, 1]) - np.amax(Arr2D[:, 1])) / 10 + ) + d = 0 + for point in Arr2D: + fpp1, fmp1, fpm1, fmm1 = FuncQuads(func2D, point, xlim, ylim) + fpp2, fmp2, fpm2, fmm2 = CountQuads(Arr2D, point) + d = max(d, abs(fpp1 - fpp2)) + d = max(d, abs(fpm1 - fpm2)) + d = max(d, abs(fmp1 - fmp2)) + d = max(d, abs(fmm1 - fmm2)) + sqen = np.sqrt(len(Arr2D)) + R1 = scipy.stats.pearsonr(Arr2D[:, 0], Arr2D[:, 1])[0] + RR = np.sqrt(1.0 - R1**2) + prob = Qks(d * sqen / (1.0 + RR * (0.25 - 0.75 / sqen))) + return d, prob diff --git a/src/soundscapy/spi/msn.py b/src/soundscapy/spi/msn.py new file mode 100644 index 00000000..f999e8fa --- /dev/null +++ b/src/soundscapy/spi/msn.py @@ -0,0 +1,566 @@ +""" +Module for handling Multi-dimensional Skewed Normal (MSN) distributions. + +Provides classes and functions for defining, fitting, sampling, and analyzing +MSN distributions, often used in soundscape analysis for modeling ISOPleasant +and ISOEventful ratings. +""" + +from typing import Literal + +import numpy as np +import pandas as pd + +from soundscapy import get_logger +from soundscapy.plotting import density_plot +from soundscapy.spi import _rsn_wrapper as rsn +from soundscapy.spi.ks2d import ks2d2s + +logger = get_logger() + + +class DirectParams: + """ + Represents a set of direct parameters for a statistical model. + + Direct parameters are the parameters that are directly used in the model. + They are the parameters that are used to define the distribution of the + data. In the case of a skew normal distribution, the direct parameters + are the xi, omega, and alpha values. + + Parameters + ---------- + xi : np.ndarray + The location of the distribution in 2D space, represented as a 2x1 array + with the x and y coordinates. + omega : np.ndarray + The covariance matrix of the distribution, represented as a 2x2 array. + The covariance matrix represents the measure of the relationship between + different variables. It provides information about how changes in one + variable are associated with changes in other variables. + alpha : np.ndarray + The shape parameters for the x and y dimensions, controlling the shape + (skewness) of the distribution. It is represented as a 2x1 array. + + """ + + def __init__(self, xi: np.ndarray, omega: np.ndarray, alpha: np.ndarray) -> None: + """Initialize DirectParams instance.""" + self.xi = xi + self.omega = omega + self.alpha = alpha + self.validate() + + def __repr__(self) -> str: + """Return a string representation of the DirectParams object.""" + return f"DirectParams(xi={self.xi}, omega={self.omega}, alpha={self.alpha})" + + def __str__(self) -> str: + """Return a user-friendly string representation of the DirectParams object.""" + return ( + f"Direct Parameters:" + f"\nxi: {self.xi.round(3)}" + f"\nomega: {self.omega.round(3)}" + f"\nalpha: {self.alpha.round(3)}" + ) + + def _omega_is_pos_def(self) -> bool: + return bool(np.all(np.linalg.eigvals(self.omega) > 0)) + + def _omega_is_symmetric(self) -> bool: + return np.allclose(self.omega, self.omega.T) + + def _xi_is_in_range(self, xi_range: np.ndarray | tuple[float, float]) -> bool: + if isinstance(xi_range, tuple): + xi_range = np.array([xi_range, xi_range]) + return bool(np.all((xi_range[:, 0] <= self.xi) & (self.xi <= xi_range[:, 1]))) + + def validate(self) -> None: + """ + Validate the direct parameters. + + In a skew normal distribution, the covariance matrix, often denoted as + Ω (Omega), represents the measure of the relationship between different + variables. It provides information about how changes in one variable are + associated with changes in other variables. The covariance matrix must + be positive definite and symmetric. + + Raises + ------ + ValueError + If the direct parameters are not valid. + + Returns + ------- + None + + """ + if not self._omega_is_pos_def(): + msg = "Omega must be positive definite" + raise ValueError(msg) + if not self._omega_is_symmetric(): + msg = "Omega must be symmetric" + raise ValueError(msg) + + +class CentredParams: + """ + Represents the centered parameters of a distribution. + + Parameters + ---------- + mean : float + The mean of the distribution. + sigma : float + The standard deviation of the distribution. + skew : float + The skewness of the distribution. + + Attributes + ---------- + mean : float + The mean of the distribution. + sigma : float + The standard deviation of the distribution. + skew : float + The skewness of the distribution. + + Methods + ------- + from_dp(dp) + Converts DirectParams object to CentredParams object. + + """ + + def __init__(self, mean: np.ndarray, sigma: np.ndarray, skew: np.ndarray) -> None: + """Initialize CentredParams instance.""" + self.mean = mean + self.sigma = sigma + self.skew = skew + + def __repr__(self) -> str: + """Return a string representation of the CentredParams object.""" + return f"CentredParams(mean={self.mean}, sigma={self.sigma}, skew={self.skew})" + + def __str__(self) -> str: + """Return a user-friendly string representation of the CentredParams object.""" + return ( + f"Centred Parameters:" + f"\nmean: {self.mean.round(3)}" + f"\nsigma: {self.sigma.round(3)}" + f"\nskew: {self.skew.round(3)}" + ) + + @classmethod + def from_dp(cls, dp: DirectParams) -> "CentredParams": + """ + Convert a DirectParams object to a CentredParams object. + + Parameters + ---------- + dp : DirectParams + The DirectParams object to convert. + + Returns + ------- + CentredParams + A new CentredParams object with the converted parameters. + + """ + cp = dp2cp(dp) + return cls(cp.mean, cp.sigma, cp.skew) + + +class MultiSkewNorm: + """ + A class representing a multi-dimensional skewed normal distribution. + + Attributes + ---------- + selm_model + The fitted SELM model. + cp : CentredParams + The centred parameters of the fitted model. + dp : DirectParams + The direct parameters of the fitted model. + sample_data : np.ndarray | None + The generated sample data from the fitted model. + data : pd.DataFrame | None + The input data used for fitting the model. + + Methods + ------- + summary() + Prints a summary of the fitted model. + fit(data=None, x=None, y=None) + Fits the model to the provided data. + define_dp(xi, omega, alpha) + Defines the direct parameters of the model. + sample(n=1000, return_sample=False) + Generates a sample from the fitted model. + sspy_plot(color='blue', title=None, n=1000) + Plots the joint distribution of the generated sample. + ks2ds(test) + Computes the two-sample Kolmogorov-Smirnov statistic. + spi(test) + Computes the similarity percentage index. + + """ + + def __init__(self) -> None: + """Initialize the MultiSkewNorm object.""" + self.selm_model = None + self.cp = None + self.dp = None + self.sample_data = None + self.data: pd.DataFrame | None = None + + def __repr__(self) -> str: + """Return a string representation of the MultiSkewNorm object.""" + if self.cp is None and self.dp is None and self.selm_model is None: + return "MultiSkewNorm() (unfitted)" + return f"MultiSkewNorm(dp={self.dp})" + + def summary(self) -> str | None: + """ + Provide a summary of the fitted MultiSkewNorm model. + + Returns + ------- + str or None + A string summarizing the model parameters and data, or a message + indicating the model is not fitted. Returns None if fitted but + summary logic is not fully implemented yet. + + """ + if self.cp is None and self.dp is None and self.selm_model is None: + return "MultiSkewNorm is not fitted." + if self.data is not None: + print(f"Fitted from data. n = {len(self.data)}") # noqa: T201 + else: + print("Fitted from direct parameters.") # noqa: T201 + print(self.dp) # noqa: T201 + print("\n") # noqa: T201 + print(self.cp) # noqa: RET503, T201 + + def fit( + self, + data: pd.DataFrame | np.ndarray | None = None, + x: np.ndarray | pd.Series | None = None, + y: np.ndarray | pd.Series | None = None, + ) -> None: + """ + Fit the multi-dimensional skewed normal model to the provided data. + + Parameters + ---------- + data : pd.DataFrame or np.ndarray, optional + The input data as a pandas DataFrame or numpy array. + x : np.ndarray or pd.Series, optional + The x-values of the input data as a numpy array or pandas Series. + y : np.ndarray or pd.Series, optional + The y-values of the input data as a numpy array or pandas Series. + + Raises + ------ + ValueError + If neither `data` nor both `x` and `y` are provided. + + """ + if data is None and (x is None or y is None): + # Either data or x and y must be provided + msg = "Either data or x and y must be provided" + raise ValueError(msg) + + if data is not None: + # If data is provided, convert it to a pandas DataFrame + if isinstance(data, pd.DataFrame): + # If data is already a DataFrame, no need to convert + data.columns = ["x", "y"] + + elif isinstance(data, np.ndarray): + # If data is a numpy array, convert it to a DataFrame + if data.ndim == 2: # noqa: PLR2004 + # If data is 2D, assume it's two variables + data = pd.DataFrame(data, columns=["x", "y"]) + else: + msg = "Data must be a 2D numpy array or DataFrame" + raise ValueError(msg) + else: + # If data is neither a DataFrame nor a numpy array, raise an error + msg = "Data must be a pandas DataFrame or 2D numpy array." + raise ValueError(msg) + + elif x is not None and y is not None: + # If x and y are provided, convert them to a pandas DataFrame + data = pd.DataFrame({"x": x, "y": y}) + + else: + # This should never happen + msg = "Either data or x and y must be provided" + raise ValueError(msg) + + # Fit the model + m = rsn.selm("x", "y", data) + + # Extract the parameters + cp = rsn.extract_cp(m) + dp = rsn.extract_dp(m) + + self.cp = CentredParams(*cp) + self.dp = DirectParams(*dp) + self.data = data + self.selm_model = m + + def define_dp( + self, xi: np.ndarray, omega: np.ndarray, alpha: np.ndarray + ) -> "MultiSkewNorm": + """ + Initiate a distribution from the direct parameters. + + Parameters + ---------- + xi : np.ndarray + The xi values of the direct parameters. + omega : np.ndarray + The omega values of the direct parameters. + alpha : np.ndarray + The alpha values of the direct parameters. + + Returns + ------- + self + + """ + self.dp = DirectParams(xi, omega, alpha) + self.cp = CentredParams.from_dp(self.dp) + return self + + def sample( + self, n: int = 1000, *, return_sample: bool = False + ) -> None | np.ndarray: + """ + Generate a sample from the fitted model. + + Parameters + ---------- + n : int, optional + The number of samples to generate, by default 1000. + return_sample : bool, optional + Whether to return the generated sample as an np.ndarray, by default False. + + Returns + ------- + None or np.ndarray + The generated sample if `return_sample` is True, otherwise None. + + Raises + ------ + ValueError + If the model is not fitted (i.e., `selm_model` is None) and direct + parameters (`dp`) are also not defined. + + """ + if self.selm_model is not None: + sample = rsn.sample_msn(selm_model=self.selm_model, n=n) + elif self.dp is not None: + sample = rsn.sample_msn( + xi=self.dp.xi, omega=self.dp.omega, alpha=self.dp.alpha, n=n + ) + else: + msg = "Either selm_model or xi, omega, and alpha must be provided." + raise ValueError(msg) + + self.sample_data = sample + + if return_sample: + return sample + return None + + def sample_mtsn( + self, n: int = 1000, a: float = -1, b: float = 1, *, return_sample: bool = False + ) -> None | np.ndarray: + """ + Generate a sample from the multi-dimensional truncated skew-normal distribution. + + Uses rejection sampling to ensure that the samples are within the bounds [a, b] + for both dimensions. + + Parameters + ---------- + n : int, optional + The number of samples to generate, by default 1000. + a : float, optional + Lower truncation bound for both dimensions, by default -1. + b : float, optional + Upper truncation bound for both dimensions, by default 1. + return_sample : bool, optional + Whether to return the generated sample as an np.ndarray, by default False. + + Returns + ------- + None or np.ndarray + The generated sample if `return_sample` is True, otherwise None. + + """ + if self.selm_model is not None: + sample = rsn.sample_mtsn( + selm_model=self.selm_model, + n=n, + a=a, + b=b, + ) + elif self.dp is not None: + sample = rsn.sample_mtsn( + xi=self.dp.xi, + omega=self.dp.omega, + alpha=self.dp.alpha, + n=n, + a=a, + b=b, + ) + else: + msg = "Either selm_model or xi, omega, and alpha must be provided." + raise ValueError(msg) + + # Store the sample data + self.sample_data = sample + + if return_sample: + return sample + return None + + def sspy_plot( + self, color: str = "blue", title: str | None = None, n: int = 1000 + ) -> None: + """ + Plot the joint distribution of the generated sample using soundscapy. + + Parameters + ---------- + color : str, optional + Color for the density plot, by default "blue". + title : str, optional + Title for the plot, by default None. + n : int, optional + Number of samples to generate if `sample_data` is None, by default 1000. + + """ + if self.sample_data is None: + self.sample(n=n) + + data = pd.DataFrame(self.sample_data, columns=["ISOPleasant", "ISOEventful"]) + plot_title = title if title is not None else "Soundscapy Density Plot" + density_plot(data, color=color, title=plot_title) + + def ks2d2s(self, test_data: pd.DataFrame | np.ndarray) -> tuple[float, float]: + """ + Compute the two-sample, two-dimensional Kolmogorov-Smirnov statistic. + + Parameters + ---------- + test : pd.DataFrame or np.ndarray + The test data. + + Returns + ------- + tuple + The KS2D statistic and p-value. + + """ + # Ensure test_data is a numpy array + if isinstance(test_data, pd.DataFrame): + if test_data.shape[1] != 2: # noqa: PLR2004 + msg = "Test data must have two columns." + raise ValueError(msg) + test_data_np = test_data.to_numpy() + elif isinstance(test_data, np.ndarray): + test_data_np = test_data + else: + msg = "test_data must be a pandas DataFrame or numpy array." + raise TypeError(msg) + + # Ensure sample_data exists, generate if needed and possible + if self.sample_data is None: + logger.info("Sample data not found, generating default sample (n=1000).") + self.sample(n=1000, return_sample=False) # Generate sample if missing + if self.sample_data is None: # Check again in case sample failed + msg = ( + "Could not generate sample data. " + "Ensure model is defined (fit or define_dp)." + ) + raise ValueError(msg) + + # Perform the 2-sample KS test using ks2d2s + # Note: ks2d2s expects data1, data2 + ks_statistic, p_value = ks2d2s(self.sample_data, test_data_np) + + return ks_statistic, p_value + + def spi(self, test: pd.DataFrame | np.ndarray) -> int: + """ + Compute the Soundscape Perception Index (SPI). + + Calculates the SPI for the test data against the target distribution + represented by this MultiSkewNorm instance. + + Parameters + ---------- + test : pd.DataFrame or np.ndarray + The test data. + + Returns + ------- + int + The Soundscape Perception Index (SPI), ranging from 0 to 100. + + """ + return int((1 - self.ks2d2s(test)[0]) * 100) + + +def cp2dp( + cp: CentredParams, family: Literal["SN", "ESN", "ST", "SC"] = "SN" +) -> DirectParams: + """ + Convert centred parameters to direct parameters. + + Parameters + ---------- + cp : CentredParams + The centred parameters object. + family : str, optional + The distribution family, by default "SN" (Skew Normal). + + Returns + ------- + DirectParams + The corresponding direct parameters object. + + """ + dp_r = rsn.cp2dp(cp.mean, cp.sigma, cp.skew, family=family) + + return DirectParams(*dp_r) + + +def dp2cp( + dp: DirectParams, family: Literal["SN", "ESN", "ST", "SC"] = "SN" +) -> CentredParams: + """ + Convert direct parameters to centred parameters. + + Parameters + ---------- + dp : DirectParams + The direct parameters object. + family : str, optional + The distribution family, by default "SN" (Skew Normal). + + Returns + ------- + CentredParams + The corresponding centred parameters object. + + """ + cp_r = rsn.dp2cp(dp.xi, dp.omega, dp.alpha, family=family) + + return CentredParams(*cp_r) diff --git a/src/soundscapy/logging.py b/src/soundscapy/sspylogging.py similarity index 82% rename from src/soundscapy/logging.py rename to src/soundscapy/sspylogging.py index 78651b07..87797cd5 100644 --- a/src/soundscapy/logging.py +++ b/src/soundscapy/sspylogging.py @@ -1,21 +1,26 @@ """ Logging configuration for Soundscapy. -This module provides simple functions to configure logging for both users and developers. -By default, Soundscapy logging is disabled to avoid unwanted output. +This module provides simple functions to configure logging for both users and +developers. By default, Soundscapy logging is disabled to avoid unwanted output. Users can enable logging with the setup_logging function. """ +from __future__ import annotations + import sys -from typing import Optional, Union, TextIO -from pathlib import Path +from typing import TYPE_CHECKING +import loguru from loguru import logger +if TYPE_CHECKING: + from pathlib import Path + def setup_logging( level: str = "INFO", - log_file: Optional[Union[str, Path]] = None, + log_file: str | Path | None = None, format_level: str = "basic", ) -> None: """ @@ -27,7 +32,8 @@ def setup_logging( Logging level for console output. Options: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL" log_file : str or Path, optional - Path to a log file. If provided, all messages (including DEBUG) will be logged to this file. + Path to a log file. + If provided, all messages (including DEBUG) will be logged to this file. format_level : str, default="basic" Format complexity level. Options: - "basic": Simple format with timestamp, level, and message @@ -45,6 +51,7 @@ def setup_logging( >>> >>> # Use detailed format for debugging >>> setup_logging(level="DEBUG", format_level="detailed") + """ # Enable soundscapy logging (disabled by default in __init__.py) logger.enable("soundscapy") @@ -55,13 +62,19 @@ def setup_logging( # Format configurations formats = { "basic": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}", - "detailed": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}", - "developer": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}\n{exception}", + "detailed": ( + "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | " + "{name}:{function}:{line} | {message}" + ), + "developer": ( + "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | " + "{name}:{function}:{line} | {message}\n{exception}" + ), } # Use the appropriate format if format_level not in formats: - print(f"Warning: Unknown format_level '{format_level}'. Using 'basic' instead.") + logger.warning(f"Unknown format_level '{format_level}'. Using 'basic' instead.") format_level = "basic" log_format = formats[format_level] @@ -100,6 +113,7 @@ def enable_debug() -> None: >>> from soundscapy import enable_debug >>> enable_debug() >>> # Now all debug messages will be shown + """ setup_logging(level="DEBUG", format_level="detailed") logger.info("Debug logging enabled") @@ -114,6 +128,7 @@ def disable_logging() -> None: >>> from soundscapy import disable_logging >>> disable_logging() >>> # No more logging messages will be shown + """ # First remove all handlers to ensure no output logger.remove() @@ -123,7 +138,7 @@ def disable_logging() -> None: logger.add(sys.stderr, level=100) # Level 100 is higher than any standard level -def get_logger(): +def get_logger() -> loguru.Logger: """ Get the Soundscapy logger instance. @@ -140,6 +155,7 @@ def get_logger(): >>> from soundscapy import get_logger >>> logger = get_logger() >>> logger.debug("Custom debug message") + """ return logger @@ -152,6 +168,7 @@ def is_notebook() -> bool: ------- bool True if running in a Jupyter notebook, False otherwise + """ try: from IPython.core.getipython import get_ipython @@ -159,9 +176,8 @@ def is_notebook() -> bool: shell = get_ipython().__class__.__name__ if shell == "ZMQInteractiveShell": # Jupyter notebook/lab return True - elif shell == "TerminalInteractiveShell": # IPython - return False - else: + if shell == "TerminalInteractiveShell": # IPython return False + return False # noqa: TRY300 except (NameError, ImportError): return False diff --git a/src/soundscapy/surveys/__init__.py b/src/soundscapy/surveys/__init__.py index dbc04349..9c16da59 100644 --- a/src/soundscapy/surveys/__init__.py +++ b/src/soundscapy/surveys/__init__.py @@ -1,4 +1,16 @@ -from soundscapy.surveys.processing import add_iso_coords, return_paqs +""" +Soundscapy Surveys Package. + +This package handles the processing and analysis of soundscape surveys, +including PAQ (Perceived Affective Quality) data and ISO coordinate calculations. +""" + +from soundscapy.surveys import processing, survey_utils +from soundscapy.surveys.processing import ( + add_iso_coords, + calculate_iso_coords, + return_paqs, +) from soundscapy.surveys.survey_utils import ( LANGUAGE_ANGLES, PAQ_IDS, @@ -7,10 +19,13 @@ ) __all__ = [ - "return_paqs", - "add_iso_coords", - "rename_paqs", "LANGUAGE_ANGLES", "PAQ_IDS", "PAQ_LABELS", + "add_iso_coords", + "calculate_iso_coords", + "processing", + "rename_paqs", + "return_paqs", + "survey_utils", ] diff --git a/src/soundscapy/surveys/processing.py b/src/soundscapy/surveys/processing.py index 547c7eca..68be54da 100644 --- a/src/soundscapy/surveys/processing.py +++ b/src/soundscapy/surveys/processing.py @@ -6,20 +6,29 @@ Notes ----- -The functions in this module are designed to be fairly general and can be used with any dataset in a similar format to -the ISD. The key to this is using a simple dataframe/sheet with the following columns: - Index columns: e.g. LocationID, RecordID, GroupID, SessionID - Perceptual attributes: PAQ1, PAQ2, ..., PAQ8 - Independent variables: e.g. Laeq, N5, Sharpness, etc. - -The key functions of this module are designed to clean/validate datasets, calculate ISO coordinate values or SSM metrics, -filter on index columns. Functions and operations which are specific to a particular dataset are located in their own +The functions in this module are designed to be fairly general and can be used with +any dataset in a similar format to the ISD. The key to this is using a simple +dataframe/sheet with the following columns: + + - Index columns: e.g. LocationID, RecordID, GroupID, SessionID + - Perceptual attributes: PAQ1, PAQ2, ..., PAQ8 + - Independent variables: e.g. Laeq, N5, Sharpness, etc. + +The key functions of this module are designed to clean/validate datasets, calculate ISO +coordinate values or SSM metrics, filter on index columns. Functions and operations +which are specific to a particular dataset are located in their own modules under `soundscape.databases`. + """ import warnings from dataclasses import dataclass -from typing import List, Optional, Tuple +from typing import TypedDict + +try: + from typing import Unpack +except ImportError: + from typing_extensions import Unpack import numpy as np import pandas as pd @@ -28,7 +37,7 @@ from soundscapy.surveys.survey_utils import EQUAL_ANGLES, PAQ_IDS, return_paqs -np.set_printoptions(legacy="1.25") +np.set_printoptions(legacy="1.21") @dataclass @@ -50,6 +59,25 @@ class SSMMetrics: r_squared: float def table(self) -> pd.Series: + """ + Generate a pandas Series containing specific attributes of the instance. + + This method collects the values of the instance attributes related to + amplitude, angle, elevation, displacement, and r_squared, and organizes + them into a pandas Series. It is useful for presenting the data in a + structured format suitable for further processing or analysis. + + Returns + ------- + pandas.Series + A pandas Series containing the following key-value pairs: + - "amplitude": instance attribute representing a certain magnitude. + - "angle": instance attribute representing a specific angular measurement. + - "elevation": instance attribute indicating a height or vertical position. + - "displacement": instance attribute defining the movement or shift. + - "r_squared": instance attribute denoting coefficient of determination. + + """ return pd.Series( { "amplitude": self.amplitude, @@ -63,9 +91,9 @@ def table(self) -> pd.Series: def calculate_iso_coords( results_df: pd.DataFrame, - val_range: Tuple[int, int] = (5, 1), - angles: Tuple[int, ...] = EQUAL_ANGLES, -) -> Tuple[pd.Series, pd.Series]: + val_range: tuple[int, int] = (5, 1), + angles: tuple[int, ...] = EQUAL_ANGLES, +) -> tuple[pd.Series, pd.Series]: """ Calculate the projected ISOPleasant and ISOEventful coordinates. @@ -99,6 +127,7 @@ def calculate_iso_coords( 0 -0.28 1 0.18 dtype: float64 + """ scale = max(val_range) - min(val_range) @@ -111,7 +140,7 @@ def calculate_iso_coords( return iso_pleasant, iso_eventful -def _adj_iso_pl(values: pd.Series, angles: Tuple[int, ...], scale: float) -> float: +def _adj_iso_pl(values: pd.Series, angles: tuple[int, ...], scale: float) -> float: """ Calculate the adjusted ISOPleasant value. @@ -130,16 +159,20 @@ def _adj_iso_pl(values: pd.Series, angles: Tuple[int, ...], scale: float) -> flo ------- float Adjusted ISOPleasant value + """ iso_pl = np.sum( - [np.cos(np.deg2rad(angle)) * value for angle, value in zip(angles, values)] + [ + np.cos(np.deg2rad(angle)) * value + for angle, value in zip(angles, values, strict=False) + ] ) return iso_pl / ( scale / 2 * np.sum(np.abs([np.cos(np.deg2rad(angle)) for angle in angles])) ) -def _adj_iso_ev(values: pd.Series, angles: Tuple[int, ...], scale: float) -> float: +def _adj_iso_ev(values: pd.Series, angles: tuple[int, ...], scale: float) -> float: """ Calculate the adjusted ISOEventful value. @@ -158,9 +191,13 @@ def _adj_iso_ev(values: pd.Series, angles: Tuple[int, ...], scale: float) -> flo ------- float Adjusted ISOEventful value + """ iso_ev = np.sum( - [np.sin(np.deg2rad(angle)) * value for angle, value in zip(angles, values)] + [ + np.sin(np.deg2rad(angle)) * value + for angle, value in zip(angles, values, strict=False) + ] ) return iso_ev / ( scale / 2 * np.sum(np.abs([np.sin(np.deg2rad(angle)) for angle in angles])) @@ -169,10 +206,11 @@ def _adj_iso_ev(values: pd.Series, angles: Tuple[int, ...], scale: float) -> flo def add_iso_coords( data: pd.DataFrame, - val_range: Tuple[int, int] = (1, 5), - names: Tuple[str, str] = ("ISOPleasant", "ISOEventful"), + val_range: tuple[int, int] = (1, 5), + names: tuple[str, str] = ("ISOPleasant", "ISOEventful"), + angles: tuple[int, ...] = EQUAL_ANGLES, + *, overwrite: bool = False, - angles: Tuple[int, ...] = EQUAL_ANGLES, ) -> pd.DataFrame: """ Calculate and add ISO coordinates as new columns in the DataFrame. @@ -185,10 +223,11 @@ def add_iso_coords( (min, max) range of original PAQ responses, by default (1, 5) names : Tuple[str, str], optional Names for new coordinate columns, by default ("ISOPleasant", "ISOEventful") - overwrite : bool, optional - Whether to overwrite existing ISO coordinate columns, by default False angles : Tuple[int, ...], optional Angles for each PAQ in degrees, by default EQUAL_ANGLES + * + overwrite : bool, optional + Whether to overwrite existing ISO coordinate columns, by default False Returns ------- @@ -212,15 +251,17 @@ def add_iso_coords( ISOPleasant ISOEventful 0 -0.03 -0.28 1 0.47 0.18 + """ for name in names: if name in data.columns: if overwrite: data = data.drop(name, axis=1) else: - raise Warning( + msg = ( f"{name} already in dataframe. Use `overwrite=True` to replace it." ) + raise Warning(msg) iso_pleasant, iso_eventful = calculate_iso_coords( data, val_range=val_range, angles=angles @@ -232,8 +273,8 @@ def add_iso_coords( def likert_data_quality( - df: pd.DataFrame, allow_na: bool = False, val_range: Tuple[int, int] = (1, 5) -) -> Optional[List[int]]: + df: pd.DataFrame, val_range: tuple[int, int] = (1, 5), *, allow_na: bool = False +) -> list[int] | None: """ Perform basic quality checks on PAQ (Likert scale) data. @@ -248,8 +289,7 @@ def likert_data_quality( Returns ------- - Optional[List[int]] - List of indices to be removed, or None if no issues found + List of indices to be removed, or None if no issues found Examples -------- @@ -262,20 +302,28 @@ def likert_data_quality( ... }) >>> likert_data_quality(df) [0, 1, 2] - >>> likert_data_quality(df, allow_na=True) + >>> likert_data_quality(df,allow_na=True) [1, 2] + """ paqs = return_paqs(df, incl_ids=False) invalid_indices = [] - for i, row in paqs.iterrows(): - if not allow_na and row.isna().any(): - invalid_indices.append(i) - elif row.notna().all(): - if row.min() < min(val_range) or row.max() > max(val_range): - invalid_indices.append(i) - elif row.nunique() == 1 and row.iloc[0] != np.mean(val_range): - invalid_indices.append(i) + for idx, row in paqs.iterrows(): + # Convert the index to int to ensure type compatibility + row_idx = int(idx) if isinstance(idx, str) else idx + row_array = row.to_numpy() + is_constant = row_array.shape[0] > 0 and (row_array[0] == row_array).all() + + if (not allow_na and row.isna().any()) or ( + row.notna().all() + and ( + row.min() < min(val_range) + or row.max() > max(val_range) + or (is_constant and row.iloc[0] != np.mean(val_range)) + ) + ): + invalid_indices.append(row_idx) if invalid_indices: logger.info(f"Found {len(invalid_indices)} samples with data quality issues") @@ -285,11 +333,20 @@ def likert_data_quality( return None +class _AddISOCoordsKwargs( + TypedDict, total=False +): # total=False allows for optional keys + names: tuple[str, str] + angles: tuple[int, ...] + overwrite: bool + + def simulation( n: int = 3000, - val_range: Tuple[int, int] = (1, 5), + val_range: tuple[int, int] = (1, 5), + *, incl_iso_coords: bool = False, - **coord_kwargs, + **coord_kwargs: Unpack[_AddISOCoordsKwargs], ) -> pd.DataFrame: """ Generate random PAQ responses for simulation purposes. @@ -298,12 +355,16 @@ def simulation( ---------- n : int, optional Number of samples to simulate, by default 3000 - val_range : Tuple[int, int], optional + val_range : tuple[int, int], optional Range of values for PAQ responses, by default (1, 5) - add_iso_coords : bool, optional + incl_iso_coords : bool, optional Whether to add calculated ISO coordinates, by default False - **coord_kwargs : dict - Additional keyword arguments to pass to add_iso_coords function + **coord_kwargs : Unpack[_AddISOCoordsKwargs] + Optional keyword arguments passed directly to the `add_iso_coords` function + if `incl_iso_coords` is True. These can include: + - `names` (tuple[str, str]): Names for the new ISO coordinate columns. + - `angles` (tuple[int, ...]): Angles for each PAQ used in calculation. + - `overwrite` (bool): Whether to overwrite existing ISO coordinate columns. Returns ------- @@ -312,31 +373,33 @@ def simulation( Examples -------- - >>> df = simulation(n=5, incl_iso_coords=True) - >>> df.shape + >>> data = simulation(n=5,incl_iso_coords=True) + >>> data.shape (5, 10) - >>> list(df.columns) + >>> list(data.columns) ['PAQ1', 'PAQ2', 'PAQ3', 'PAQ4', 'PAQ5', 'PAQ6', 'PAQ7', 'PAQ8', 'ISOPleasant', 'ISOEventful'] - """ - np.random.seed(42) - df = pd.DataFrame( - np.random.randint(min(val_range), max(val_range) + 1, size=(n, 8)), + + """ # noqa: E501 + data = pd.DataFrame( + np.random.default_rng().integers( + min(val_range), max(val_range) + 1, size=(n, 8) + ), columns=PAQ_IDS, ) if incl_iso_coords: - df = add_iso_coords(df, val_range=val_range, **coord_kwargs) + data = add_iso_coords(data, val_range=val_range, **coord_kwargs) logger.info(f"Generated simulated PAQ data with {n} samples") - return df + return data def ssm_metrics( df: pd.DataFrame, - paq_cols: List[str] = PAQ_IDS, + paq_cols: list[str] = PAQ_IDS, method: str = "cosine", - val_range: Tuple[int, int] = (5, 1), - angles: Tuple[int, ...] = EQUAL_ANGLES, + val_range: tuple[int, int] = (5, 1), + angles: tuple[int, ...] = EQUAL_ANGLES, ) -> pd.DataFrame: """ Calculate the Structural Summary Method (SSM) metrics for each response. @@ -362,35 +425,43 @@ def ssm_metrics( Raises ------ ValueError - If PAQ columns are not present in the DataFrame or if an invalid method is specified + If PAQ columns are not present in the DataFrame + or if an invalid method is specified Examples -------- >>> # xdoctest: +SKIP >>> import pandas as pd - >>> df = pd.DataFrame({ + >>> data = pd.DataFrame({ ... 'PAQ1': [4, 2], 'PAQ2': [3, 5], 'PAQ3': [2, 4], 'PAQ4': [1, 3], ... 'PAQ5': [5, 1], 'PAQ6': [3, 2], 'PAQ7': [4, 3], 'PAQ8': [2, 5] ... }) - >>> ssm_metrics(df).round(2) + >>> ssm_metrics(data).round(2) amplitude angle elevation displacement r_squared 0 0.68 263.82 10.57 -7.57 0.15 1 1.21 20.63 0.01 3.11 0.39 + """ - # TODO: Replace with a call to circumplex package + # TODO(MitchellAcoustics): Replace with a call to circumplex package # noqa: TD003 warnings.warn( - "This function is not yet fully implemented. See https://github.com/MitchellAcoustics/circumplex for a more complete implementation.", + "This function is not yet fully implemented." + "See https://github.com/MitchellAcoustics/circumplex for a " + "more complete implementation.", PendingDeprecationWarning, + stacklevel=2, ) if not set(paq_cols).issubset(df.columns): - raise ValueError("PAQ columns are not present in the DataFrame") + msg = f"PAQ columns {paq_cols} not present in DataFrame" + raise ValueError(msg) if method == "polar": iso_pleasant, iso_eventful = calculate_iso_coords( df[paq_cols], val_range, angles ) - r, theta = _convert_to_polar_coords(iso_pleasant, iso_eventful) + r, theta = _convert_to_polar_coords( + iso_pleasant.to_numpy(), iso_eventful.to_numpy() + ) mean = df[paq_cols].mean(axis=1) mean = mean / (max(val_range) - min(val_range)) if val_range != (0, 1) else mean @@ -403,20 +474,20 @@ def ssm_metrics( "r_squared": 1, # R-squared is always 1 for polar method } ) - elif method == "cosine": + if method == "cosine": return df[paq_cols].apply( lambda y: ssm_cosine_fit(y, angles).table(), axis=1, result_type="expand", ) - else: - raise ValueError("Method must be either 'polar' or 'cosine'") + msg = "Method must be either 'polar' or 'cosine'" + raise ValueError(msg) def ssm_cosine_fit( y: pd.Series, - angles: Tuple[int, ...] = EQUAL_ANGLES, - bounds: Tuple[List[float], List[float]] = ( + angles: tuple[int, ...] | np.ndarray = EQUAL_ANGLES, + bounds: tuple[list[float], list[float]] = ( [0, 0, 0, -np.inf], [np.inf, 360, np.inf, np.inf], ), @@ -428,10 +499,11 @@ def ssm_cosine_fit( ---------- y : pd.Series Series of PAQ values - angles : Tuple[int, ...], optional + angles : tuple[int, ...], optional Angles for each PAQ in degrees, by default EQUAL_ANGLES - bounds : Tuple[List[float], List[float]], optional - Bounds for the optimization parameters, by default ([0, 0, 0, -np.inf], [np.inf, 360, np.inf, np.inf]) + bounds : tuple[list[float], list[float]], optional + Bounds for the optimization parameters, + by default ([0, 0, 0, -np.inf], [np.inf, 360, np.inf, np.inf]) Returns ------- @@ -446,23 +518,30 @@ def ssm_cosine_fit( >>> metrics = ssm_cosine_fit(y) >>> [round(v, 2) if isinstance(v, float) else v for v in metrics.table()] [0.68, 263.82, 10.57, -7.57, 0.15] + """ warnings.warn( - "This function is not yet fully implemented. See https://github.com/MitchellAcoustics/circumplex for a more complete implementation.", + "This function is not yet fully implemented." + "See https://github.com/MitchellAcoustics/circumplex " + "for a more complete implementation.", PendingDeprecationWarning, + stacklevel=2, ) - def cosine_model(theta, amp, delta, elev, dev): + def _cosine_model( + theta: np.ndarray, amp: float, delta: float, elev: float, dev: float + ) -> np.ndarray: return elev + amp * np.cos(np.radians(theta - delta)) + dev param, _ = optimize.curve_fit( - cosine_model, + _cosine_model, xdata=angles, ydata=y, bounds=bounds, ) amp, delta, elev, dev = param - r_squared = _r2_score(y, cosine_model(angles, *param)) + angles = np.array(angles) if isinstance(angles, tuple) else angles + r_squared = _r2_score(y.to_numpy(), _cosine_model(angles, *param)) return SSMMetrics( amplitude=amp, @@ -475,7 +554,7 @@ def cosine_model(theta, amp, delta, elev, dev): def _convert_to_polar_coords( x: float | np.ndarray, y: float | np.ndarray -) -> Tuple[float | np.ndarray, float | np.ndarray]: +) -> tuple[float | np.ndarray, float | np.ndarray]: """ Convert Cartesian coordinates to polar coordinates. @@ -497,6 +576,7 @@ def _convert_to_polar_coords( >>> r, theta = _convert_to_polar_coords(x, y) >>> round(r, 2), round(theta, 2) (5.0, 53.13) + """ r = np.sqrt(x**2 + y**2) theta = np.rad2deg(np.arctan2(y, x)) @@ -525,10 +605,12 @@ def _r2_score(y_true: np.ndarray, y_pred: np.ndarray) -> float: >>> y_pred = np.array([2.5, 4.2, 5.1, 2.2, 1.3]) >>> round(_r2_score(y_true, y_pred), 2) 0.96 + """ ss_total = np.sum((y_true - np.mean(y_true)) ** 2) ss_residual = np.sum((y_true - y_pred) ** 2) - return 1 - (ss_residual / ss_total) + # Ensure the return type matches the annotation + return float(1 - (ss_residual / ss_total)) if __name__ == "__main__": diff --git a/src/soundscapy/surveys/survey_utils.py b/src/soundscapy/surveys/survey_utils.py index 1ebada0e..19941a38 100644 --- a/src/soundscapy/surveys/survey_utils.py +++ b/src/soundscapy/surveys/survey_utils.py @@ -6,7 +6,6 @@ """ from enum import Enum -from typing import Dict, List, Tuple, Union import pandas as pd from loguru import logger @@ -24,7 +23,18 @@ class PAQ(Enum): UNEVENTFUL = ("uneventful", "PAQ7") CALM = ("calm", "PAQ8") - def __init__(self, label: str, id: str): + def __init__(self, label: str, id: str) -> None: # noqa: A002 + """ + Initialize a PAQ enum member. + + Parameters + ---------- + label : str + The descriptive label for the PAQ (e.g., 'pleasant'). + id : str + The standard identifier for the PAQ (e.g., 'PAQ1'). + + """ self.label = label self.id = id @@ -52,7 +62,7 @@ def __init__(self, label: str, id: str): def return_paqs( - df: pd.DataFrame, incl_ids: bool = True, other_cols: List[str] = None + df: pd.DataFrame, other_cols: list[str] | None = None, *, incl_ids: bool = True ) -> pd.DataFrame: """ Return only the PAQ columns from a DataFrame. @@ -61,15 +71,16 @@ def return_paqs( ---------- df : pd.DataFrame Input DataFrame containing PAQ data. - incl_ids : bool, optional - Whether to include ID columns (RecordID, GroupID, etc.), by default True. other_cols : List[str], optional Other columns to include in the output, by default None. + incl_ids : bool, optional + Whether to include ID columns (RecordID, GroupID, etc.), by default True. Returns ------- pd.DataFrame - DataFrame containing only the PAQ columns and optionally ID and other specified columns. + DataFrame containing only the PAQ columns and optionally ID and other specified + columns. Examples -------- @@ -94,6 +105,7 @@ def return_paqs( PAQ1 PAQ2 PAQ3 PAQ4 PAQ5 PAQ6 PAQ7 PAQ8 OtherCol 0 4 2 1 3 5 2 4 1 A 1 3 5 2 4 1 3 5 2 B + """ cols = PAQ_IDS.copy() @@ -113,7 +125,7 @@ def return_paqs( def rename_paqs( - df: pd.DataFrame, paq_aliases: Union[Tuple, Dict] = None + df: pd.DataFrame, paq_aliases: list | tuple | dict | None = None ) -> pd.DataFrame: """ Rename the PAQ columns in a DataFrame to standard PAQ IDs. @@ -157,6 +169,7 @@ def rename_paqs( PAQ1 PAQ2 0 4 2 1 3 5 + """ if paq_aliases is None: if any(paq_id in df.columns for paq_id in PAQ_IDS): @@ -165,12 +178,13 @@ def rename_paqs( if any(paq_name in df.columns for paq_name in PAQ_LABELS): paq_aliases = PAQ_LABELS - if isinstance(paq_aliases, (list, tuple)): - rename_dict = dict(zip(paq_aliases, PAQ_IDS)) + if isinstance(paq_aliases, list | tuple): + rename_dict = dict(zip(paq_aliases, PAQ_IDS, strict=False)) elif isinstance(paq_aliases, dict): rename_dict = paq_aliases else: - raise ValueError("paq_aliases must be a tuple, list, or dictionary.") + msg = "paq_aliases must be a tuple, list, or dictionary." + raise TypeError(msg) logger.debug(f"Renaming PAQs with the following mapping: {rename_dict}") return df.rename(columns=rename_dict) @@ -193,8 +207,8 @@ def mean_responses(df: pd.DataFrame, group: str) -> pd.DataFrame: DataFrame with mean responses for each PAQ group. """ - df = return_paqs(df, incl_ids=False, other_cols=[group]) - return df.groupby(group).mean().reset_index() + data = return_paqs(df, other_cols=[group], incl_ids=False) + return data.groupby(group).mean().reset_index() # Add other utility functions here as needed diff --git a/test/audio/test_analysis_settings.py b/test/audio/test_analysis_settings.py index 12fbf404..e6d40e37 100644 --- a/test/audio/test_analysis_settings.py +++ b/test/audio/test_analysis_settings.py @@ -107,7 +107,7 @@ def test_to_yaml(self, tmp_path, sample_config): assert output_file.exists() # Read back and verify - with open(output_file, "r") as f: + with open(output_file) as f: loaded_config = yaml.safe_load(f) assert loaded_config["version"] == "1.1" assert "LAeq" in loaded_config["AcousticToolbox"] diff --git a/test/audio/test_binaural.py b/test/audio/test_binaural.py index 4f31358d..e7a5734b 100644 --- a/test/audio/test_binaural.py +++ b/test/audio/test_binaural.py @@ -73,7 +73,7 @@ def test_binaural_calibration(test_binaural_signal): assert isinstance(calibrated, Binaural) # Test invalid input - with pytest.raises(ValueError): + with pytest.raises(TypeError): test_binaural_signal.calibrate_to([60, 62, 64]) diff --git a/test/data/Levels.json b/test/data/Levels.json index ffcf3dc1..d81d1697 100644 --- a/test/data/Levels.json +++ b/test/data/Levels.json @@ -1,5810 +1,5810 @@ -{ - "CT101": { - "Left": 79.0, - "Right": 79.72 - }, - "CT102": { - "Left": 79.35, - "Right": 79.88 - }, - "CT103": { - "Left": 76.25, - "Right": 76.41 - }, - "CT104": { - "Left": 79.9, - "Right": 79.93 - }, - "CT107": { - "Left": 78.21, - "Right": 78.47 - }, - "CT108": { - "Left": 79.23, - "Right": 79.51 - }, - "CT109": { - "Left": 79.32, - "Right": 79.37 - }, - "CT110": { - "Left": 78.15, - "Right": 80.09 - }, - "CT112": { - "Left": 79.08, - "Right": 79.77 - }, - "CT113": { - "Left": 81.32, - "Right": 81.67 - }, - "CT114": { - "Left": 76.69, - "Right": 76.94 - }, - "CT116": { - "Left": 77.88, - "Right": 78.82 - }, - "CT117": { - "Left": 80.18, - "Right": 80.54 - }, - "CT119": { - "Left": 76.45, - "Right": 77.0 - }, - "CT120": { - "Left": 79.9, - "Right": 80.27 - }, - "CT121": { - "Left": 78.14, - "Right": 78.51 - }, - "CT122": { - "Left": 80.88, - "Right": 81.3 - }, - "CT123": { - "Left": 77.46, - "Right": 77.9 - }, - "CT124": { - "Left": 78.14, - "Right": 78.49 - }, - "CT201": { - "Left": 76.79, - "Right": 76.81 - }, - "CT202": { - "Left": 82.07, - "Right": 82.74 - }, - "CT203": { - "Left": 77.48, - "Right": 77.47 - }, - "CT301": { - "Left": 75.84, - "Right": 78.33 - }, - "CT305": { - "Left": 83.14, - "Right": 81.74 - }, - "CT306": { - "Left": 77.45, - "Right": 77.96 - }, - "CT308": { - "Left": 87.1, - "Right": 86.2 - }, - "CT309": { - "Left": 78.2, - "Right": 79.3 - }, - "CT310": { - "Left": 80.94, - "Right": 81.73 - }, - "CT311": { - "Left": 79.41, - "Right": 80.08 - }, - "CT312": { - "Left": 79.57, - "Right": 80.11 - }, - "CT313": { - "Left": 80.34, - "Right": 83.0 - }, - "CT314": { - "Left": 79.48, - "Right": 80.06 - }, - "CT315": { - "Left": 77.78, - "Right": 78.18 - }, - "CT317": { - "Left": 82.03, - "Right": 81.59 - }, - "CT318": { - "Left": 78.14, - "Right": 78.74 - }, - "CT319": { - "Left": 79.54, - "Right": 80.82 - }, - "CT320": { - "Left": 79.99, - "Right": 80.0 - }, - "CT321": { - "Left": 76.89, - "Right": 77.45 - }, - "CT322": { - "Left": 81.15, - "Right": 81.53 - }, - "CT323": { - "Left": 81.91, - "Right": 80.14 - }, - "CT324": { - "Left": 81.08, - "Right": 80.1 - }, - "CT325": { - "Left": 74.26, - "Right": 75.38 - }, - "CT326": { - "Left": 84.36, - "Right": 83.76 - }, - "CT327": { - "Left": 77.13, - "Right": 77.39 - }, - "CT328": { - "Left": 77.72, - "Right": 77.98 - }, - "CT329": { - "Left": 79.82, - "Right": 80.27 - }, - "CT401": { - "Left": 96.66, - "Right": 97.26 - }, - "CT402": { - "Left": 86.17, - "Right": 86.29 - }, - "CT403": { - "Left": 84.51, - "Right": 83.94 - }, - "CT404": { - "Left": 83.48, - "Right": 83.61 - }, - "CT405": { - "Left": 78.42, - "Right": 78.91 - }, - "CT406": { - "Left": 82.16, - "Right": 83.97 - }, - "CT407": { - "Left": 80.34, - "Right": 81.52 - }, - "CT408": { - "Left": 82.76, - "Right": 83.06 - }, - "CT411": { - "Left": 85.54, - "Right": 85.65 - }, - "CT412": { - "Left": 86.65, - "Right": 86.29 - }, - "CT413": { - "Left": 88.02, - "Right": 88.57 - }, - "CT414": { - "Left": 88.84, - "Right": 88.06 - }, - "CT415": { - "Left": 87.69, - "Right": 88.97 - }, - "CT501": { - "Left": 72.85, - "Right": 73.85 - }, - "CT502": { - "Left": 77.25, - "Right": 78.19 - }, - "CT503": { - "Left": 83.07, - "Right": 84.22 - }, - "CT504": { - "Left": 72.75, - "Right": 73.25 - }, - "CT505": { - "Left": 76.09, - "Right": 78.99 - }, - "CT506": { - "Left": 71.03, - "Right": 70.72 - }, - "CT507": { - "Left": 79.26, - "Right": 80.19 - }, - "CT508": { - "Left": 70.27, - "Right": 70.98 - }, - "CT509": { - "Left": 73.8, - "Right": 74.22 - }, - "CT510": { - "Left": 74.71, - "Right": 75.26 - }, - "CT511": { - "Left": 77.16, - "Right": 76.59 - }, - "CT512": { - "Left": 76.57, - "Right": 77.09 - }, - "CT513": { - "Left": 83.01, - "Right": 85.52 - }, - "CT514": { - "Left": 79.29, - "Right": 79.45 - }, - "CT515": { - "Left": 75.42, - "Right": 75.65 - }, - "CT516": { - "Left": 78.11, - "Right": 78.18 - }, - "CT517": { - "Left": 76.1, - "Right": 76.55 - }, - "CT518": { - "Left": 75.92, - "Right": 75.95 - }, - "CT519": { - "Left": 83.23, - "Right": 82.75 - }, - "CT520": { - "Left": 79.1, - "Right": 75.49 - }, - "CT521": { - "Left": 81.82, - "Right": 79.89 - }, - "CT522": { - "Left": 75.32, - "Right": 77.07 - }, - "CT523": { - "Left": 81.2, - "Right": 81.94 - }, - "CT524": { - "Left": 75.91, - "Right": 81.02 - }, - "CT525": { - "Left": 80.61, - "Right": 82.03 - }, - "CT526": { - "Left": 78.68, - "Right": 78.78 - }, - "CT527": { - "Left": 83.97, - "Right": 84.09 - }, - "CT528": { - "Left": 78.78, - "Right": 78.89 - }, - "CT529": { - "Left": 71.9, - "Right": 71.29 - }, - "CT530": { - "Left": 79.86, - "Right": 79.89 - }, - "CT531": { - "Left": 84.14, - "Right": 84.85 - }, - "CT532": { - "Left": 73.27, - "Right": 73.33 - }, - "CT533": { - "Left": 77.46, - "Right": 81.46 - }, - "CT534": { - "Left": 72.52, - "Right": 78.82 - }, - "CT535": { - "Left": 75.1, - "Right": 75.19 - }, - "CT536": { - "Left": 78.08, - "Right": 78.47 - }, - "CT537": { - "Left": 78.67, - "Right": 78.83 - }, - "CT538": { - "Left": 77.58, - "Right": 78.1 - }, - "CT539": { - "Left": 77.85, - "Right": 77.83 - }, - "CT540": { - "Left": 76.78, - "Right": 76.88 - }, - "CT541": { - "Left": 78.41, - "Right": 79.39 - }, - "CT542": { - "Left": 80.43, - "Right": 79.19 - }, - "CT543": { - "Left": 79.81, - "Right": 81.01 - }, - "CT544": { - "Left": 78.3, - "Right": 80.23 - }, - "CT545": { - "Left": 79.46, - "Right": 79.53 - }, - "ET101": { - "Left": 77.06, - "Right": 77.3 - }, - "ET102": { - "Left": 79.25, - "Right": 78.97 - }, - "ET103": { - "Left": 79.15, - "Right": 78.9 - }, - "ET104": { - "Left": 77.63, - "Right": 77.96 - }, - "ET105": { - "Left": 74.73, - "Right": 74.9 - }, - "ET106": { - "Left": 74.87, - "Right": 74.93 - }, - "ET107": { - "Left": 80.74, - "Right": 80.88 - }, - "ET108": { - "Left": 79.03, - "Right": 79.5 - }, - "ET109": { - "Left": 77.26, - "Right": 77.34 - }, - "ET110": { - "Left": 76.62, - "Right": 76.4 - }, - "ET111": { - "Left": 77.14, - "Right": 77.37 - }, - "ET112": { - "Left": 85.67, - "Right": 85.78 - }, - "ET113": { - "Left": 78.36, - "Right": 78.11 - }, - "ET114": { - "Left": 78.7, - "Right": 78.52 - }, - "ET115": { - "Left": 77.24, - "Right": 77.13 - }, - "ET116": { - "Left": 79.78, - "Right": 79.94 - }, - "ET117": { - "Left": 78.63, - "Right": 79.02 - }, - "ET118": { - "Left": 74.72, - "Right": 74.66 - }, - "ET119": { - "Left": 75.83, - "Right": 76.01 - }, - "ET120": { - "Left": 79.22, - "Right": 79.11 - }, - "ET121": { - "Left": 79.36, - "Right": 79.85 - }, - "ET122": { - "Left": 78.93, - "Right": 79.27 - }, - "ET123": { - "Left": 76.55, - "Right": 76.5 - }, - "ET124": { - "Left": 77.47, - "Right": 77.29 - }, - "ET125": { - "Left": 82.11, - "Right": 81.67 - }, - "ET201": { - "Left": 76.54, - "Right": 76.75 - }, - "ET202": { - "Left": 78.97, - "Right": 79.75 - }, - "ET203": { - "Left": 74.35, - "Right": 74.42 - }, - "ET204": { - "Left": 78.56, - "Right": 78.99 - }, - "ET205": { - "Left": 77.09, - "Right": 76.43 - }, - "ET206": { - "Left": 76.26, - "Right": 76.33 - }, - "ET207": { - "Left": 74.69, - "Right": 74.41 - }, - "ET210": { - "Left": 76.0, - "Right": 76.36 - }, - "ET211": { - "Left": 79.31, - "Right": 79.09 - }, - "ET212": { - "Left": 78.28, - "Right": 78.45 - }, - "ET213": { - "Left": 77.75, - "Right": 77.69 - }, - "ET214": { - "Left": 75.59, - "Right": 75.51 - }, - "ET215": { - "Left": 76.12, - "Right": 76.26 - }, - "ET216": { - "Left": 78.18, - "Right": 78.25 - }, - "ET218": { - "Left": 77.83, - "Right": 77.9 - }, - "ET219": { - "Left": 80.52, - "Right": 80.41 - }, - "ET220": { - "Left": 77.95, - "Right": 78.39 - }, - "ET221": { - "Left": 77.27, - "Right": 77.78 - }, - "ET222": { - "Left": 80.16, - "Right": 79.75 - }, - "ET301": { - "Left": 78.75, - "Right": 78.96 - }, - "ET302": { - "Left": 77.57, - "Right": 77.78 - }, - "ET303": { - "Left": 78.04, - "Right": 78.44 - }, - "ET304": { - "Left": 77.62, - "Right": 77.39 - }, - "ET305": { - "Left": 78.3, - "Right": 78.27 - }, - "ET306": { - "Left": 76.24, - "Right": 76.1 - }, - "ET307": { - "Left": 78.24, - "Right": 78.3 - }, - "ET308": { - "Left": 78.12, - "Right": 77.72 - }, - "ET309": { - "Left": 78.1, - "Right": 78.52 - }, - "ET310": { - "Left": 75.76, - "Right": 75.17 - }, - "ET311": { - "Left": 78.67, - "Right": 79.51 - }, - "ET312": { - "Left": 75.74, - "Right": 75.84 - }, - "ET313": { - "Left": 77.46, - "Right": 77.75 - }, - "ET314": { - "Left": 75.29, - "Right": 74.94 - }, - "ET401": { - "Left": 76.05, - "Right": 76.81 - }, - "ET402": { - "Left": 68.88, - "Right": 69.54 - }, - "ET403": { - "Left": 74.09, - "Right": 75.73 - }, - "ET404": { - "Left": 70.88, - "Right": 71.28 - }, - "ET405": { - "Left": 74.6, - "Right": 75.0 - }, - "ET406": { - "Left": 77.0, - "Right": 78.62 - }, - "ET407": { - "Left": 76.56, - "Right": 76.83 - }, - "ET408": { - "Left": 69.12, - "Right": 69.38 - }, - "ET409": { - "Left": 78.04, - "Right": 78.07 - }, - "ET410": { - "Left": 76.46, - "Right": 75.85 - }, - "ET411": { - "Left": 73.47, - "Right": 72.66 - }, - "ET412": { - "Left": 73.3, - "Right": 73.58 - }, - "ET413": { - "Left": 73.04, - "Right": 75.11 - }, - "ET414": { - "Left": 71.88, - "Right": 72.81 - }, - "ET415": { - "Left": 77.82, - "Right": 74.54 - }, - "ET416": { - "Left": 79.67, - "Right": 79.5 - }, - "ET417": { - "Left": 80.52, - "Right": 80.47 - }, - "ET418": { - "Left": 73.52, - "Right": 73.53 - }, - "ET419": { - "Left": 82.66, - "Right": 82.71 - }, - "ET420": { - "Left": 75.49, - "Right": 74.95 - }, - "ET421": { - "Left": 76.71, - "Right": 76.48 - }, - "ET422": { - "Left": 76.08, - "Right": 76.81 - }, - "ET423": { - "Left": 72.3, - "Right": 72.32 - }, - "ET424": { - "Left": 75.75, - "Right": 76.14 - }, - "ET425": { - "Left": 76.07, - "Right": 76.49 - }, - "ET426": { - "Left": 76.19, - "Right": 76.75 - }, - "ET427": { - "Left": 78.39, - "Right": 78.54 - }, - "ET428": { - "Left": 73.91, - "Right": 74.3 - }, - "ET429": { - "Left": 78.81, - "Right": 77.94 - }, - "ET430": { - "Left": 72.42, - "Right": 72.69 - }, - "ET431": { - "Left": 80.19, - "Right": 79.91 - }, - "ET432": { - "Left": 76.64, - "Right": 76.93 - }, - "ET433": { - "Left": 77.43, - "Right": 77.24 - }, - "ET434": { - "Left": 78.2, - "Right": 78.81 - }, - "ET435": { - "Left": 70.97, - "Right": 71.5 - }, - "ET436": { - "Left": 74.86, - "Right": 75.11 - }, - "ET437": { - "Left": 75.76, - "Right": 76.33 - }, - "ET438": { - "Left": 78.15, - "Right": 77.91 - }, - "Mc101": { - "Left": 73.23, - "Right": 74.05 - }, - "mc102": { - "Left": 69.57, - "Right": 70.68 - }, - "Mc103": { - "Left": 73.13, - "Right": 74.11 - }, - "Mc104": { - "Left": 67.29, - "Right": 68.67 - }, - "Mc105": { - "Left": 70.44, - "Right": 72.03 - }, - "Mc106": { - "Left": 76.81, - "Right": 76.15 - }, - "Mc108": { - "Left": 69.4, - "Right": 69.58 - }, - "Mc109": { - "Left": 68.43, - "Right": 69.41 - }, - "Mc110": { - "Left": 71.27, - "Right": 71.68 - }, - "Mc111": { - "Left": 70.7, - "Right": 70.93 - }, - "Mc112": { - "Left": 74.05, - "Right": 74.57 - }, - "Mc114": { - "Left": 68.53, - "Right": 69.31 - }, - "Mc115": { - "Left": 70.38, - "Right": 72.7 - }, - "Mc116": { - "Left": 76.66, - "Right": 75.75 - }, - "Mc118": { - "Left": 70.45, - "Right": 70.72 - }, - "Mc119": { - "Left": 71.78, - "Right": 71.4 - }, - "Mc120": { - "Left": 66.86, - "Right": 67.88 - }, - "Mc121": { - "Left": 70.0, - "Right": 71.25 - }, - "Mc122": { - "Left": 73.18, - "Right": 74.29 - }, - "Mc127": { - "Left": 68.91, - "Right": 68.58 - }, - "Mc129": { - "Left": 69.32, - "Right": 69.82 - }, - "Mc203": { - "Left": 76.17, - "Right": 77.08 - }, - "Mc204": { - "Left": 70.35, - "Right": 78.01 - }, - "Mc206": { - "Left": 71.16, - "Right": 73.61 - }, - "Mc207": { - "Left": 68.51, - "Right": 67.82 - }, - "Mc301": { - "Left": 67.25, - "Right": 67.27 - }, - "Mc302": { - "Left": 65.61, - "Right": 66.25 - }, - "Mc303": { - "Left": 67.84, - "Right": 68.42 - }, - "Mc305": { - "Left": 70.48, - "Right": 70.18 - }, - "Mc307": { - "Left": 65.75, - "Right": 66.01 - }, - "Mc308": { - "Left": 68.21, - "Right": 72.22 - }, - "Mc309": { - "Left": 65.98, - "Right": 66.48 - }, - "Mc310": { - "Left": 74.67, - "Right": 70.96 - }, - "Mc311": { - "Left": 72.43, - "Right": 72.93 - }, - "Mc312": { - "Left": 70.21, - "Right": 71.02 - }, - "Mc316": { - "Left": 69.95, - "Right": 73.59 - }, - "Mc317": { - "Left": 67.69, - "Right": 67.14 - }, - "Mc318": { - "Left": 66.18, - "Right": 67.16 - }, - "Mc319": { - "Left": 74.48, - "Right": 75.51 - }, - "Mc320": { - "Left": 66.88, - "Right": 66.97 - }, - "Mc321": { - "Left": 68.4, - "Right": 71.17 - }, - "Mc323": { - "Left": 71.12, - "Right": 72.35 - }, - "Mc324": { - "Left": 69.77, - "Right": 71.41 - }, - "Mc325": { - "Left": 72.71, - "Right": 75.12 - }, - "Mc328": { - "Left": 68.06, - "Right": 68.71 - }, - "Mc329": { - "Left": 71.81, - "Right": 72.59 - }, - "Mc330": { - "Left": 69.25, - "Right": 70.18 - }, - "Mc331": { - "Left": 67.54, - "Right": 68.01 - }, - "Mc332": { - "Left": 67.89, - "Right": 68.51 - }, - "Mc333": { - "Left": 64.26, - "Right": 65.17 - }, - "Mc334": { - "Left": 64.56, - "Right": 66.24 - }, - "Mc335": { - "Left": 70.17, - "Right": 70.18 - }, - "Mc336": { - "Left": 65.94, - "Right": 66.98 - }, - "Mc401": { - "Left": 67.78, - "Right": 68.82 - }, - "Mc402": { - "Left": 77.06, - "Right": 76.99 - }, - "Mc404": { - "Left": 69.46, - "Right": 72.39 - }, - "Mc405": { - "Left": 71.85, - "Right": 72.84 - }, - "Mc406": { - "Left": 72.81, - "Right": 74.47 - }, - "Mc407": { - "Left": 71.63, - "Right": 77.74 - }, - "Mc409": { - "Left": 71.41, - "Right": 74.86 - }, - "Mc410": { - "Left": 73.23, - "Right": 74.21 - }, - "Mc412": { - "Left": 69.78, - "Right": 70.65 - }, - "Mc413": { - "Left": 69.45, - "Right": 70.29 - }, - "Mc414": { - "Left": 74.64, - "Right": 78.62 - }, - "Mc415": { - "Left": 65.14, - "Right": 65.48 - }, - "Mc416": { - "Left": 70.05, - "Right": 70.37 - }, - "Mc417": { - "Left": 63.42, - "Right": 65.02 - }, - "Mc418": { - "Left": 72.65, - "Right": 74.63 - }, - "MC501": { - "Left": 68.14, - "Right": 69.94 - }, - "MC502": { - "Left": 64.31, - "Right": 65.2 - }, - "MC503": { - "Left": 65.59, - "Right": 66.43 - }, - "MC504": { - "Left": 60.85, - "Right": 61.34 - }, - "MC505": { - "Left": 62.36, - "Right": 64.82 - }, - "MC506": { - "Left": 64.76, - "Right": 65.66 - }, - "MC507": { - "Left": 67.43, - "Right": 67.76 - }, - "MC508": { - "Left": 66.59, - "Right": 66.88 - }, - "MC509": { - "Left": 70.3, - "Right": 69.07 - }, - "MC510": { - "Left": 73.23, - "Right": 73.39 - }, - "MC511": { - "Left": 71.9, - "Right": 73.65 - }, - "MC512": { - "Left": 72.43, - "Right": 75.25 - }, - "MC513": { - "Left": 72.43, - "Right": 73.65 - }, - "MC514": { - "Left": 69.96, - "Right": 70.9 - }, - "MC515": { - "Left": 68.61, - "Right": 70.55 - }, - "MC516": { - "Left": 65.45, - "Right": 71.82 - }, - "MC517": { - "Left": 75.98, - "Right": 73.23 - }, - "MC518": { - "Left": 66.83, - "Right": 73.57 - }, - "MC519": { - "Left": 73.85, - "Right": 75.39 - }, - "MC520": { - "Left": 70.33, - "Right": 68.48 - }, - "MC521": { - "Left": 68.33, - "Right": 67.06 - }, - "MC522": { - "Left": 65.76, - "Right": 65.99 - }, - "MC523": { - "Left": 66.56, - "Right": 68.77 - }, - "MC524": { - "Left": 67.21, - "Right": 67.87 - }, - "MC525": { - "Left": 66.13, - "Right": 66.62 - }, - "MC526": { - "Left": 63.22, - "Right": 65.08 - }, - "MC527": { - "Left": 62.69, - "Right": 65.18 - }, - "MC528": { - "Left": 65.84, - "Right": 66.12 - }, - "MC529": { - "Left": 65.42, - "Right": 66.51 - }, - "MC530": { - "Left": 69.33, - "Right": 77.59 - }, - "MC531": { - "Left": 66.67, - "Right": 66.77 - }, - "MC532": { - "Left": 68.63, - "Right": 69.08 - }, - "MC533": { - "Left": 73.47, - "Right": 79.24 - }, - "MC534": { - "Left": 73.24, - "Right": 73.91 - }, - "MC535": { - "Left": 70.47, - "Right": 70.77 - }, - "MC536": { - "Left": 67.77, - "Right": 68.23 - }, - "MC537": { - "Left": 69.84, - "Right": 71.41 - }, - "MC538": { - "Left": 65.12, - "Right": 68.15 - }, - "MC539": { - "Left": 62.91, - "Right": 63.54 - }, - "MC540": { - "Left": 62.53, - "Right": 63.13 - }, - "MC541": { - "Left": 65.16, - "Right": 66.58 - }, - "PL101": { - "Left": 71.21, - "Right": 72.95 - }, - "Pl102": { - "Left": 78.66, - "Right": 80.02 - }, - "Pl103": { - "Left": 71.6, - "Right": 76.37 - }, - "Pl105": { - "Left": 69.07, - "Right": 69.7 - }, - "Pl106": { - "Left": 71.24, - "Right": 74.19 - }, - "PL107": { - "Left": 71.54, - "Right": 74.35 - }, - "PL108": { - "Left": 78.98, - "Right": 79.34 - }, - "Pl109": { - "Left": 72.15, - "Right": 74.69 - }, - "Pl112": { - "Left": 69.85, - "Right": 72.15 - }, - "Pl114": { - "Left": 70.0, - "Right": 73.12 - }, - "PL116": { - "Left": 74.19, - "Right": 81.3 - }, - "Pl119": { - "Left": 78.77, - "Right": 80.57 - }, - "PL120": { - "Left": 74.1, - "Right": 71.27 - }, - "PL201": { - "Left": 72.35, - "Right": 73.5 - }, - "PL202": { - "Left": 70.58, - "Right": 72.01 - }, - "Pl203": { - "Left": 78.15, - "Right": 78.29 - }, - "Pl204": { - "Left": 69.28, - "Right": 69.72 - }, - "Pl205": { - "Left": 71.43, - "Right": 70.7 - }, - "Pl206": { - "Left": 69.69, - "Right": 70.12 - }, - "Pl209": { - "Left": 80.4, - "Right": 76.71 - }, - "Pl210": { - "Left": 67.23, - "Right": 67.72 - }, - "Pl211": { - "Left": 69.37, - "Right": 69.95 - }, - "Pl213": { - "Left": 74.63, - "Right": 75.27 - }, - "Pl215": { - "Left": 78.19, - "Right": 78.1 - }, - "Pl216": { - "Left": 68.25, - "Right": 70.12 - }, - "Pl217": { - "Left": 69.28, - "Right": 70.24 - }, - "Pl218": { - "Left": 69.18, - "Right": 70.0 - }, - "Pl219": { - "Left": 67.58, - "Right": 67.94 - }, - "Pl220": { - "Left": 70.64, - "Right": 70.62 - }, - "Pl221": { - "Left": 73.89, - "Right": 77.45 - }, - "Pl222": { - "Left": 67.31, - "Right": 69.18 - }, - "Pl223": { - "Left": 67.47, - "Right": 71.23 - }, - "Pl224": { - "Left": 71.97, - "Right": 74.81 - }, - "Pl227": { - "Left": 65.45, - "Right": 67.4 - }, - "Pl228": { - "Left": 66.81, - "Right": 66.18 - }, - "Pl230": { - "Left": 72.24, - "Right": 72.82 - }, - "Pl231": { - "Left": 70.48, - "Right": 70.82 - }, - "Pl234": { - "Left": 74.68, - "Right": 72.72 - }, - "PL301": { - "Left": 72.37, - "Right": 74.16 - }, - "PL302": { - "Left": 69.0, - "Right": 70.47 - }, - "PL303": { - "Left": 66.27, - "Right": 71.57 - }, - "PL304": { - "Left": 67.21, - "Right": 70.25 - }, - "PL305": { - "Left": 70.15, - "Right": 75.97 - }, - "PL306": { - "Left": 81.27, - "Right": 77.65 - }, - "PL307": { - "Left": 81.93, - "Right": 76.98 - }, - "PL308": { - "Left": 76.22, - "Right": 77.38 - }, - "PL309": { - "Left": 62.44, - "Right": 62.5 - }, - "PL310": { - "Left": 76.45, - "Right": 74.49 - }, - "PL311": { - "Left": 66.74, - "Right": 69.63 - }, - "PL312": { - "Left": 63.95, - "Right": 63.75 - }, - "PL313": { - "Left": 61.43, - "Right": 61.59 - }, - "PL314": { - "Left": 66.64, - "Right": 67.58 - }, - "PL315": { - "Left": 80.41, - "Right": 80.38 - }, - "PL316": { - "Left": 73.82, - "Right": 70.09 - }, - "PL317": { - "Left": 68.28, - "Right": 69.76 - }, - "PL318": { - "Left": 72.98, - "Right": 74.67 - }, - "PL319": { - "Left": 71.32, - "Right": 71.14 - }, - "PL320": { - "Left": 71.1, - "Right": 71.2 - }, - "PL321": { - "Left": 77.63, - "Right": 77.8 - }, - "PL322": { - "Left": 67.93, - "Right": 72.73 - }, - "PL323": { - "Left": 71.15, - "Right": 73.16 - }, - "PL324": { - "Left": 70.9, - "Right": 72.95 - }, - "PL325": { - "Left": 72.21, - "Right": 78.0 - }, - "PL326": { - "Left": 78.74, - "Right": 78.51 - }, - "PL327": { - "Left": 72.23, - "Right": 79.93 - }, - "PL328": { - "Left": 73.68, - "Right": 71.4 - }, - "PL329": { - "Left": 78.18, - "Right": 73.71 - }, - "PL330": { - "Left": 77.03, - "Right": 77.19 - }, - "PL331": { - "Left": 74.03, - "Right": 74.3 - }, - "PL332": { - "Left": 77.26, - "Right": 78.85 - }, - "PL333": { - "Left": 73.88, - "Right": 72.44 - }, - "PL334": { - "Left": 74.52, - "Right": 71.12 - }, - "PL335": { - "Left": 73.89, - "Right": 70.67 - }, - "PL336": { - "Left": 73.0, - "Right": 73.73 - }, - "PL337": { - "Left": 70.53, - "Right": 72.1 - }, - "PL338": { - "Left": 68.96, - "Right": 67.79 - }, - "PL339": { - "Left": 74.25, - "Right": 74.05 - }, - "PL340": { - "Left": 68.43, - "Right": 68.37 - }, - "PL341": { - "Left": 74.4, - "Right": 72.37 - }, - "PL401": { - "Left": 67.64, - "Right": 72.27 - }, - "PL402": { - "Left": 75.47, - "Right": 74.67 - }, - "PL403": { - "Left": 68.32, - "Right": 72.5 - }, - "PL404": { - "Left": 75.71, - "Right": 78.21 - }, - "PL405": { - "Left": 76.95, - "Right": 77.46 - }, - "PL406": { - "Left": 62.79, - "Right": 66.0 - }, - "PL407": { - "Left": 61.14, - "Right": 62.82 - }, - "PL408": { - "Left": 69.0, - "Right": 75.5 - }, - "PL409": { - "Left": 83.99, - "Right": 78.44 - }, - "PL410": { - "Left": 79.57, - "Right": 82.0 - }, - "PL411": { - "Left": 78.53, - "Right": 78.54 - }, - "PL412": { - "Left": 65.95, - "Right": 64.87 - }, - "PL413": { - "Left": 67.01, - "Right": 68.63 - }, - "PL414": { - "Left": 72.65, - "Right": 72.64 - }, - "PL415": { - "Left": 68.88, - "Right": 71.56 - }, - "PL416": { - "Left": 67.42, - "Right": 67.69 - }, - "PL417": { - "Left": 70.1, - "Right": 71.58 - }, - "PL418": { - "Left": 67.83, - "Right": 67.27 - }, - "PL419": { - "Left": 68.57, - "Right": 68.64 - }, - "PL420": { - "Left": 61.0, - "Right": 61.74 - }, - "PL421": { - "Left": 60.02, - "Right": 60.43 - }, - "PL422": { - "Left": 72.53, - "Right": 65.91 - }, - "PL423": { - "Left": 78.86, - "Right": 75.76 - }, - "PL424": { - "Left": 73.14, - "Right": 69.44 - }, - "PL425": { - "Left": 69.9, - "Right": 72.26 - }, - "PL426": { - "Left": 66.36, - "Right": 67.27 - }, - "PL427": { - "Left": 60.75, - "Right": 60.75 - }, - "PL428": { - "Left": 68.65, - "Right": 70.71 - }, - "PL429": { - "Left": 62.92, - "Right": 65.26 - }, - "PL430": { - "Left": 62.35, - "Right": 62.46 - }, - "PL431": { - "Left": 67.18, - "Right": 68.34 - }, - "PL432": { - "Left": 63.79, - "Right": 64.02 - }, - "PL433": { - "Left": 61.3, - "Right": 61.31 - }, - "PL434": { - "Left": 62.0, - "Right": 63.32 - }, - "PL435": { - "Left": 62.1, - "Right": 62.7 - }, - "PL436": { - "Left": 64.3, - "Right": 64.03 - }, - "PL437": { - "Left": 76.87, - "Right": 76.43 - }, - "PL438": { - "Left": 78.92, - "Right": 78.2 - }, - "PL439": { - "Left": 76.69, - "Right": 76.18 - }, - "Rf101": { - "Left": 62.65, - "Right": 63.0 - }, - "Rf102": { - "Left": 66.26, - "Right": 66.1 - }, - "Rf103": { - "Left": 63.48, - "Right": 66.98 - }, - "Rf104": { - "Left": 62.64, - "Right": 63.09 - }, - "Rf105": { - "Left": 70.64, - "Right": 70.53 - }, - "Rf106": { - "Left": 63.06, - "Right": 64.54 - }, - "Rf108": { - "Left": 64.6, - "Right": 64.62 - }, - "Rf109": { - "Left": 62.7, - "Right": 63.28 - }, - "Rf110": { - "Left": 63.72, - "Right": 62.92 - }, - "Rf111": { - "Left": 64.74, - "Right": 66.86 - }, - "Rf112": { - "Left": 63.68, - "Right": 64.99 - }, - "Rf113": { - "Left": 86.13, - "Right": 87.09 - }, - "Rf114": { - "Left": 75.1, - "Right": 75.94 - }, - "Rf115": { - "Left": 92.54, - "Right": 93.58 - }, - "Rf116": { - "Left": 78.76, - "Right": 79.91 - }, - "Rf117": { - "Left": 75.39, - "Right": 76.34 - }, - "Rf118": { - "Left": 63.73, - "Right": 64.81 - }, - "Rf120": { - "Left": 64.33, - "Right": 73.72 - }, - "Rf121": { - "Left": 68.53, - "Right": 71.51 - }, - "Rf122": { - "Left": 72.45, - "Right": 66.07 - }, - "Rf124": { - "Left": 61.52, - "Right": 61.91 - }, - "Rf125": { - "Left": 66.08, - "Right": 67.39 - }, - "Rf126": { - "Left": 60.62, - "Right": 62.04 - }, - "Rf127": { - "Left": 60.59, - "Right": 60.83 - }, - "Rf128": { - "Left": 64.0, - "Right": 65.18 - }, - "Rf129": { - "Left": 62.12, - "Right": 62.96 - }, - "Rf130": { - "Left": 63.65, - "Right": 63.46 - }, - "Rf131": { - "Left": 62.53, - "Right": 63.78 - }, - "Rf132": { - "Left": 64.62, - "Right": 64.6 - }, - "Rf133": { - "Left": 63.8, - "Right": 64.61 - }, - "Rf134": { - "Left": 67.23, - "Right": 68.55 - }, - "Rf136": { - "Left": 71.33, - "Right": 71.65 - }, - "Rf137": { - "Left": 65.75, - "Right": 66.63 - }, - "Rf139": { - "Left": 69.29, - "Right": 69.27 - }, - "Rf140": { - "Left": 63.46, - "Right": 64.08 - }, - "Rf141": { - "Left": 63.09, - "Right": 64.25 - }, - "Rf143": { - "Left": 72.81, - "Right": 73.81 - }, - "Rf201": { - "Left": 67.04, - "Right": 67.03 - }, - "Rf203": { - "Left": 62.83, - "Right": 63.52 - }, - "Rf204": { - "Left": 68.99, - "Right": 72.62 - }, - "Rf205": { - "Left": 66.84, - "Right": 69.11 - }, - "Rf206": { - "Left": 66.01, - "Right": 66.39 - }, - "Rf207": { - "Left": 69.92, - "Right": 71.33 - }, - "Rf208": { - "Left": 63.11, - "Right": 62.92 - }, - "Rf209": { - "Left": 63.67, - "Right": 63.98 - }, - "Rf210": { - "Left": 65.96, - "Right": 65.05 - }, - "Rf211": { - "Left": 62.44, - "Right": 62.14 - }, - "Rf212": { - "Left": 66.65, - "Right": 64.35 - }, - "Rf213": { - "Left": 70.78, - "Right": 71.13 - }, - "Rf214": { - "Left": 68.89, - "Right": 72.08 - }, - "Rf215": { - "Left": 63.28, - "Right": 63.18 - }, - "Rf216": { - "Left": 64.56, - "Right": 65.91 - }, - "Rf217": { - "Left": 72.23, - "Right": 75.98 - }, - "Rf218": { - "Left": 65.04, - "Right": 66.21 - }, - "Rf219": { - "Left": 64.16, - "Right": 65.43 - }, - "Rf220": { - "Left": 70.25, - "Right": 74.35 - }, - "Rf221": { - "Left": 62.77, - "Right": 62.41 - }, - "Rf222": { - "Left": 70.64, - "Right": 67.68 - }, - "Rf223": { - "Left": 70.71, - "Right": 70.4 - }, - "Rf225": { - "Left": 66.54, - "Right": 64.3 - }, - "Rf226": { - "Left": 65.59, - "Right": 64.95 - }, - "Rf227": { - "Left": 66.9, - "Right": 65.69 - }, - "RF301": { - "Left": 67.9, - "Right": 72.54 - }, - "RF302": { - "Left": 64.36, - "Right": 70.4 - }, - "RF303": { - "Left": 73.23, - "Right": 74.02 - }, - "RF304": { - "Left": 72.73, - "Right": 66.91 - }, - "RF305": { - "Left": 76.58, - "Right": 78.34 - }, - "RF306": { - "Left": 66.68, - "Right": 68.1 - }, - "RF307": { - "Left": 71.71, - "Right": 76.8 - }, - "RF308": { - "Left": 76.31, - "Right": 81.46 - }, - "RF309": { - "Left": 76.87, - "Right": 86.05 - }, - "RF310": { - "Left": 78.3, - "Right": 81.16 - }, - "RF311": { - "Left": 84.7, - "Right": 88.67 - }, - "RF312": { - "Left": 73.57, - "Right": 76.35 - }, - "RF313": { - "Left": 71.16, - "Right": 75.18 - }, - "RF314": { - "Left": 59.35, - "Right": 62.24 - }, - "RF315": { - "Left": 59.17, - "Right": 62.77 - }, - "RF316": { - "Left": 62.8, - "Right": 65.79 - }, - "RF317": { - "Left": 63.6, - "Right": 67.68 - }, - "RF318": { - "Left": 69.06, - "Right": 71.48 - }, - "RF319": { - "Left": 61.13, - "Right": 63.07 - }, - "RF320": { - "Left": 72.2, - "Right": 76.28 - }, - "RF321": { - "Left": 73.74, - "Right": 77.0 - }, - "RF322": { - "Left": 71.23, - "Right": 77.93 - }, - "RF323": { - "Left": 70.99, - "Right": 78.71 - }, - "RF324": { - "Left": 65.31, - "Right": 67.2 - }, - "RF325": { - "Left": 66.57, - "Right": 68.07 - }, - "RF326": { - "Left": 63.46, - "Right": 62.29 - }, - "RF327": { - "Left": 64.78, - "Right": 65.1 - }, - "RF328": { - "Left": 69.32, - "Right": 66.48 - }, - "RF329": { - "Left": 64.85, - "Right": 67.45 - }, - "RF330": { - "Left": 60.43, - "Right": 60.82 - }, - "RF331": { - "Left": 65.2, - "Right": 65.13 - }, - "RF332": { - "Left": 61.26, - "Right": 61.36 - }, - "RF333": { - "Left": 61.3, - "Right": 60.49 - }, - "RF334": { - "Left": 66.16, - "Right": 69.71 - }, - "RF335": { - "Left": 70.68, - "Right": 71.68 - }, - "RF336": { - "Left": 62.41, - "Right": 64.43 - }, - "RF337": { - "Left": 65.51, - "Right": 72.65 - }, - "RF338": { - "Left": 69.39, - "Right": 66.12 - }, - "RF339": { - "Left": 79.3, - "Right": 80.84 - }, - "RF340": { - "Left": 78.14, - "Right": 82.69 - }, - "RF341": { - "Left": 68.04, - "Right": 67.64 - }, - "RF342": { - "Left": 73.66, - "Right": 74.74 - }, - "RF343": { - "Left": 67.81, - "Right": 68.62 - }, - "RF344": { - "Left": 60.39, - "Right": 61.56 - }, - "Rp102": { - "Left": 71.27, - "Right": 69.91 - }, - "Rp103": { - "Left": 72.53, - "Right": 69.72 - }, - "Rp104": { - "Left": 69.71, - "Right": 75.52 - }, - "Rp105": { - "Left": 67.61, - "Right": 67.86 - }, - "Rp108": { - "Left": 70.64, - "Right": 69.38 - }, - "Rp110": { - "Left": 66.84, - "Right": 68.24 - }, - "Rp113": { - "Left": 65.38, - "Right": 66.41 - }, - "Rp114": { - "Left": 67.5, - "Right": 70.91 - }, - "Rp115": { - "Left": 70.76, - "Right": 70.13 - }, - "Rp116": { - "Left": 67.16, - "Right": 69.69 - }, - "Rp117": { - "Left": 71.18, - "Right": 75.39 - }, - "Rp118": { - "Left": 69.05, - "Right": 68.5 - }, - "Rp119": { - "Left": 67.23, - "Right": 69.66 - }, - "Rp120": { - "Left": 71.4, - "Right": 71.24 - }, - "Rp121": { - "Left": 68.73, - "Right": 66.5 - }, - "Rp122": { - "Left": 71.38, - "Right": 69.49 - }, - "Rp123": { - "Left": 72.88, - "Right": 73.81 - }, - "Rp124": { - "Left": 68.41, - "Right": 70.84 - }, - "Rp125": { - "Left": 68.38, - "Right": 69.96 - }, - "Rp126": { - "Left": 71.79, - "Right": 69.08 - }, - "Rp127": { - "Left": 68.45, - "Right": 69.4 - }, - "Rp128": { - "Left": 66.07, - "Right": 69.46 - }, - "Rp129": { - "Left": 71.4, - "Right": 70.89 - }, - "Rp130": { - "Left": 66.64, - "Right": 69.17 - }, - "Rp131": { - "Left": 69.49, - "Right": 71.34 - }, - "Rp132": { - "Left": 69.71, - "Right": 72.31 - }, - "Rp133": { - "Left": 68.82, - "Right": 67.37 - }, - "Rp134": { - "Left": 70.34, - "Right": 74.1 - }, - "Rp135": { - "Left": 68.19, - "Right": 69.29 - }, - "Rp136": { - "Left": 70.91, - "Right": 71.76 - }, - "Rp137": { - "Left": 73.78, - "Right": 75.22 - }, - "Rp138": { - "Left": 73.3, - "Right": 70.54 - }, - "Rp139": { - "Left": 72.16, - "Right": 74.39 - }, - "Rp201": { - "Left": 61.99, - "Right": 62.41 - }, - "Rp203": { - "Left": 62.84, - "Right": 63.31 - }, - "Rp204": { - "Left": 61.66, - "Right": 62.57 - }, - "Rp206": { - "Left": 61.42, - "Right": 62.06 - }, - "Rp207": { - "Left": 62.51, - "Right": 63.25 - }, - "Rp208": { - "Left": 61.63, - "Right": 62.3 - }, - "Rp209": { - "Left": 61.67, - "Right": 62.33 - }, - "Rp210": { - "Left": 60.26, - "Right": 60.97 - }, - "Rp211": { - "Left": 61.31, - "Right": 61.91 - }, - "Rp212": { - "Left": 62.34, - "Right": 63.24 - }, - "Rp213": { - "Left": 61.67, - "Right": 62.27 - }, - "Rp214": { - "Left": 61.69, - "Right": 62.29 - }, - "Rp215": { - "Left": 62.8, - "Right": 63.63 - }, - "Rp217": { - "Left": 63.5, - "Right": 63.81 - }, - "Rp218": { - "Left": 62.09, - "Right": 62.48 - }, - "Rp219": { - "Left": 64.52, - "Right": 64.93 - }, - "Rp220": { - "Left": 65.53, - "Right": 65.54 - }, - "Rp221": { - "Left": 66.47, - "Right": 67.07 - }, - "Rp223": { - "Left": 61.65, - "Right": 64.52 - }, - "Rp224": { - "Left": 62.27, - "Right": 62.96 - }, - "Rp226": { - "Left": 60.71, - "Right": 62.02 - }, - "Rp227": { - "Left": 62.64, - "Right": 63.55 - }, - "Rp228": { - "Left": 60.84, - "Right": 61.02 - }, - "Rp229": { - "Left": 61.62, - "Right": 62.21 - }, - "Rp230": { - "Left": 60.4, - "Right": 61.28 - }, - "Rp232": { - "Left": 62.44, - "Right": 62.74 - }, - "Rp233": { - "Left": 64.74, - "Right": 65.15 - }, - "RP301": { - "Left": 59.56, - "Right": 59.63 - }, - "RP302": { - "Left": 61.04, - "Right": 61.16 - }, - "RP303": { - "Left": 60.33, - "Right": 60.06 - }, - "RP304": { - "Left": 59.26, - "Right": 60.43 - }, - "RP305": { - "Left": 73.87, - "Right": 79.44 - }, - "RP306": { - "Left": 70.1, - "Right": 71.44 - }, - "RP307": { - "Left": 72.36, - "Right": 70.18 - }, - "RP308": { - "Left": 72.76, - "Right": 71.25 - }, - "RP309": { - "Left": 71.69, - "Right": 70.16 - }, - "RP310": { - "Left": 70.76, - "Right": 72.12 - }, - "RP311": { - "Left": 74.84, - "Right": 71.04 - }, - "RP312": { - "Left": 69.65, - "Right": 69.6 - }, - "RP313": { - "Left": 72.58, - "Right": 74.1 - }, - "RP314": { - "Left": 80.09, - "Right": 80.81 - }, - "RP315": { - "Left": 80.3, - "Right": 81.5 - }, - "RP316": { - "Left": 74.82, - "Right": 78.38 - }, - "RP317": { - "Left": 74.04, - "Right": 71.35 - }, - "RP318": { - "Left": 72.92, - "Right": 75.36 - }, - "RP319": { - "Left": 71.14, - "Right": 70.08 - }, - "RP320": { - "Left": 72.54, - "Right": 75.86 - }, - "RP321": { - "Left": 68.69, - "Right": 72.2 - }, - "RP322": { - "Left": 64.61, - "Right": 66.95 - }, - "RP323": { - "Left": 68.88, - "Right": 69.2 - }, - "RP324": { - "Left": 61.38, - "Right": 61.61 - }, - "RP325": { - "Left": 64.97, - "Right": 64.44 - }, - "RP326": { - "Left": 60.6, - "Right": 60.85 - }, - "RP327": { - "Left": 61.05, - "Right": 61.0 - }, - "RP328": { - "Left": 59.98, - "Right": 60.32 - }, - "RP329": { - "Left": 62.62, - "Right": 63.36 - }, - "RP330": { - "Left": 63.02, - "Right": 65.19 - }, - "RP331": { - "Left": 68.94, - "Right": 68.83 - }, - "RP332": { - "Left": 74.27, - "Right": 80.46 - }, - "RP333": { - "Left": 73.21, - "Right": 71.8 - }, - "RP334": { - "Left": 67.06, - "Right": 68.25 - }, - "RP335": { - "Left": 74.1, - "Right": 74.46 - }, - "Rs101": { - "Left": 71.92, - "Right": 72.97 - }, - "Rs102": { - "Left": 75.73, - "Right": 76.22 - }, - "Rs103": { - "Left": 71.04, - "Right": 71.37 - }, - "Rs104": { - "Left": 73.36, - "Right": 73.36 - }, - "Rs105": { - "Left": 71.68, - "Right": 72.33 - }, - "Rs106": { - "Left": 75.4, - "Right": 75.38 - }, - "Rs107": { - "Left": 72.34, - "Right": 72.82 - }, - "Rs108": { - "Left": 74.05, - "Right": 74.51 - }, - "Rs109": { - "Left": 73.95, - "Right": 74.12 - }, - "Rs110": { - "Left": 71.77, - "Right": 72.27 - }, - "Rs111": { - "Left": 73.51, - "Right": 74.31 - }, - "Rs112": { - "Left": 72.58, - "Right": 73.56 - }, - "Rs113": { - "Left": 72.31, - "Right": 72.9 - }, - "Rs114": { - "Left": 72.7, - "Right": 73.12 - }, - "Rs115": { - "Left": 73.36, - "Right": 74.04 - }, - "Rs116": { - "Left": 71.43, - "Right": 72.46 - }, - "Rs117": { - "Left": 72.1, - "Right": 73.09 - }, - "Rs118": { - "Left": 73.06, - "Right": 74.55 - }, - "Rs119": { - "Left": 73.06, - "Right": 74.46 - }, - "Rs120": { - "Left": 71.69, - "Right": 71.44 - }, - "Rs121": { - "Left": 74.25, - "Right": 74.16 - }, - "Rs122": { - "Left": 72.4, - "Right": 72.71 - }, - "Rs123": { - "Left": 73.06, - "Right": 73.34 - }, - "Rs124": { - "Left": 72.43, - "Right": 73.37 - }, - "Rs125": { - "Left": 70.87, - "Right": 71.49 - }, - "Rs126": { - "Left": 71.48, - "Right": 72.52 - }, - "Rs127": { - "Left": 73.34, - "Right": 73.92 - }, - "Rs201": { - "Left": 75.0, - "Right": 75.58 - }, - "Rs202": { - "Left": 72.67, - "Right": 73.19 - }, - "Rs203": { - "Left": 73.23, - "Right": 73.39 - }, - "Rs204": { - "Left": 75.2, - "Right": 75.93 - }, - "Rs205": { - "Left": 73.29, - "Right": 74.34 - }, - "Rs206": { - "Left": 73.94, - "Right": 75.51 - }, - "Rs207": { - "Left": 73.49, - "Right": 74.08 - }, - "Rs208": { - "Left": 73.96, - "Right": 74.76 - }, - "Rs209": { - "Left": 73.61, - "Right": 74.18 - }, - "Rs210": { - "Left": 77.16, - "Right": 77.22 - }, - "Rs211": { - "Left": 74.82, - "Right": 74.96 - }, - "Rs212": { - "Left": 72.71, - "Right": 73.36 - }, - "Rs213": { - "Left": 71.23, - "Right": 74.36 - }, - "Rs214": { - "Left": 75.08, - "Right": 75.62 - }, - "Rs215": { - "Left": 74.53, - "Right": 74.42 - }, - "Rs216": { - "Left": 72.0, - "Right": 72.62 - }, - "Rs217": { - "Left": 73.49, - "Right": 76.04 - }, - "Rs218": { - "Left": 73.15, - "Right": 75.31 - }, - "Rs219": { - "Left": 73.35, - "Right": 72.98 - }, - "Rs220": { - "Left": 73.47, - "Right": 75.27 - }, - "Rs221": { - "Left": 73.61, - "Right": 74.9 - }, - "Rs222": { - "Left": 80.79, - "Right": 80.77 - }, - "Rs223": { - "Left": 79.05, - "Right": 79.34 - }, - "Rs224": { - "Left": 72.98, - "Right": 73.5 - }, - "Rs225": { - "Left": 71.87, - "Right": 72.6 - }, - "Rs226": { - "Left": 73.84, - "Right": 74.61 - }, - "Rs227": { - "Left": 73.81, - "Right": 76.51 - }, - "Rs228": { - "Left": 72.99, - "Right": 73.6 - }, - "Rs229": { - "Left": 72.5, - "Right": 73.41 - }, - "Rs230": { - "Left": 74.6, - "Right": 75.05 - }, - "Rs231": { - "Left": 73.06, - "Right": 73.81 - }, - "Rs232": { - "Left": 72.83, - "Right": 73.54 - }, - "Rs303": { - "Left": 74.09, - "Right": 74.77 - }, - "Rs304": { - "Left": 76.1, - "Right": 76.67 - }, - "Rs305": { - "Left": 74.38, - "Right": 74.43 - }, - "Rs306": { - "Left": 73.57, - "Right": 73.83 - }, - "Rs308": { - "Left": 72.42, - "Right": 72.78 - }, - "Rs309": { - "Left": 72.28, - "Right": 74.88 - }, - "Rs310": { - "Left": 75.27, - "Right": 75.73 - }, - "Rs311": { - "Left": 82.94, - "Right": 83.79 - }, - "Rs312": { - "Left": 77.42, - "Right": 78.51 - }, - "Rs313": { - "Left": 74.52, - "Right": 75.06 - }, - "Rs314": { - "Left": 72.75, - "Right": 73.04 - }, - "Rs315": { - "Left": 72.31, - "Right": 73.19 - }, - "Rs317": { - "Left": 73.39, - "Right": 73.96 - }, - "Rs318": { - "Left": 74.19, - "Right": 74.67 - }, - "Rs319": { - "Left": 76.86, - "Right": 77.88 - }, - "Rs320": { - "Left": 77.14, - "Right": 77.77 - }, - "Rs321": { - "Left": 74.05, - "Right": 74.36 - }, - "Rs322": { - "Left": 76.35, - "Right": 76.92 - }, - "Rs323": { - "Left": 72.15, - "Right": 72.65 - }, - "Rs324": { - "Left": 72.87, - "Right": 73.39 - }, - "Rs325": { - "Left": 73.76, - "Right": 74.25 - }, - "Rs326": { - "Left": 74.33, - "Right": 75.15 - }, - "Rs327": { - "Left": 74.27, - "Right": 74.91 - }, - "Rs328": { - "Left": 73.58, - "Right": 80.69 - }, - "Rs329": { - "Left": 74.94, - "Right": 75.3 - }, - "Rs330": { - "Left": 72.07, - "Right": 73.44 - }, - "Rs331": { - "Left": 73.04, - "Right": 73.36 - }, - "RS401": { - "Left": 64.48, - "Right": 66.03 - }, - "RS402": { - "Left": 72.7, - "Right": 74.25 - }, - "RS403": { - "Left": 77.36, - "Right": 82.98 - }, - "RS404": { - "Left": 69.55, - "Right": 71.39 - }, - "RS405": { - "Left": 76.12, - "Right": 76.49 - }, - "RS406": { - "Left": 75.37, - "Right": 76.11 - }, - "RS407": { - "Left": 74.24, - "Right": 73.45 - }, - "RS408": { - "Left": 70.78, - "Right": 70.79 - }, - "RS409": { - "Left": 67.26, - "Right": 68.25 - }, - "RS410": { - "Left": 72.3, - "Right": 75.28 - }, - "RS411": { - "Left": 71.53, - "Right": 73.93 - }, - "RS412": { - "Left": 68.79, - "Right": 69.0 - }, - "RS413": { - "Left": 68.53, - "Right": 68.75 - }, - "RS414": { - "Left": 67.26, - "Right": 69.1 - }, - "RS415": { - "Left": 72.4, - "Right": 72.73 - }, - "RS416": { - "Left": 68.05, - "Right": 68.13 - }, - "RS417": { - "Left": 76.38, - "Right": 74.73 - }, - "RS418": { - "Left": 79.79, - "Right": 80.59 - }, - "RS419": { - "Left": 72.04, - "Right": 71.29 - }, - "RS420": { - "Left": 67.74, - "Right": 68.24 - }, - "RS421": { - "Left": 71.31, - "Right": 75.1 - }, - "RS422": { - "Left": 71.72, - "Right": 73.23 - }, - "RS423": { - "Left": 69.05, - "Right": 69.25 - }, - "RS424": { - "Left": 65.62, - "Right": 65.75 - }, - "RS425": { - "Left": 61.9, - "Right": 62.15 - }, - "RS426": { - "Left": 65.99, - "Right": 68.39 - }, - "RS427": { - "Left": 68.3, - "Right": 69.54 - }, - "RS428": { - "Left": 69.0, - "Right": 70.17 - }, - "RS429": { - "Left": 65.61, - "Right": 64.99 - }, - "RS430": { - "Left": 65.31, - "Right": 65.18 - }, - "RS431": { - "Left": 66.45, - "Right": 66.49 - }, - "RS432": { - "Left": 73.37, - "Right": 74.86 - }, - "RS433": { - "Left": 65.96, - "Right": 66.83 - }, - "RS434": { - "Left": 68.13, - "Right": 68.41 - }, - "RS435": { - "Left": 64.55, - "Right": 64.84 - }, - "RS436": { - "Left": 68.93, - "Right": 69.31 - }, - "RS437": { - "Left": 67.71, - "Right": 67.68 - }, - "RS438": { - "Left": 69.87, - "Right": 70.13 - }, - "RS439": { - "Left": 73.37, - "Right": 71.96 - }, - "RS440": { - "Left": 70.9, - "Right": 71.17 - }, - "sc101": { - "Left": 69.92, - "Right": 68.56 - }, - "sc102": { - "Left": 73.36, - "Right": 70.21 - }, - "sc103": { - "Left": 68.38, - "Right": 68.43 - }, - "sc104": { - "Left": 69.62, - "Right": 68.77 - }, - "sc105": { - "Left": 69.6, - "Right": 69.53 - }, - "sc106": { - "Left": 67.85, - "Right": 67.91 - }, - "sc107": { - "Left": 68.15, - "Right": 68.24 - }, - "sc108": { - "Left": 68.46, - "Right": 68.4 - }, - "sc109": { - "Left": 66.66, - "Right": 66.97 - }, - "sc110": { - "Left": 67.01, - "Right": 67.21 - }, - "sc111": { - "Left": 67.55, - "Right": 68.41 - }, - "sc112": { - "Left": 70.29, - "Right": 68.72 - }, - "sc113": { - "Left": 68.01, - "Right": 68.34 - }, - "sc114": { - "Left": 68.08, - "Right": 67.41 - }, - "sc115": { - "Left": 66.76, - "Right": 67.05 - }, - "sc116": { - "Left": 75.21, - "Right": 73.42 - }, - "sc117": { - "Left": 65.91, - "Right": 66.68 - }, - "sc118": { - "Left": 68.29, - "Right": 68.33 - }, - "sc119": { - "Left": 68.14, - "Right": 68.35 - }, - "sc120": { - "Left": 69.69, - "Right": 70.09 - }, - "sc121": { - "Left": 69.67, - "Right": 69.8 - }, - "sc122": { - "Left": 69.65, - "Right": 70.96 - }, - "sc123": { - "Left": 69.45, - "Right": 69.85 - }, - "sc124": { - "Left": 67.45, - "Right": 68.2 - }, - "sc125": { - "Left": 69.03, - "Right": 70.34 - }, - "sc126": { - "Left": 67.81, - "Right": 68.54 - }, - "sc127": { - "Left": 68.09, - "Right": 67.63 - }, - "sc128": { - "Left": 68.99, - "Right": 68.54 - }, - "sc129": { - "Left": 68.14, - "Right": 68.18 - }, - "sc130": { - "Left": 68.06, - "Right": 67.91 - }, - "sc131": { - "Left": 70.13, - "Right": 70.21 - }, - "sc134": { - "Left": 67.67, - "Right": 67.84 - }, - "sc135": { - "Left": 66.29, - "Right": 67.22 - }, - "sc136": { - "Left": 65.51, - "Right": 66.08 - }, - "sc137": { - "Left": 68.9, - "Right": 69.23 - }, - "sc138": { - "Left": 68.38, - "Right": 68.6 - }, - "sc139": { - "Left": 65.99, - "Right": 65.31 - }, - "sc140": { - "Left": 71.0, - "Right": 71.25 - }, - "sc141": { - "Left": 69.13, - "Right": 69.33 - }, - "sc142": { - "Left": 68.57, - "Right": 68.85 - }, - "sc143": { - "Left": 68.56, - "Right": 68.6 - }, - "sc144": { - "Left": 68.78, - "Right": 68.46 - }, - "SC201": { - "Left": 66.92, - "Right": 66.99 - }, - "SC202": { - "Left": 68.46, - "Right": 68.26 - }, - "SC203": { - "Left": 68.71, - "Right": 71.38 - }, - "SC204": { - "Left": 65.73, - "Right": 65.91 - }, - "SC205": { - "Left": 71.62, - "Right": 72.14 - }, - "SC206": { - "Left": 72.1, - "Right": 71.73 - }, - "SC207": { - "Left": 71.28, - "Right": 71.53 - }, - "SC208": { - "Left": 65.88, - "Right": 65.33 - }, - "SC209": { - "Left": 72.36, - "Right": 71.48 - }, - "SC210": { - "Left": 67.87, - "Right": 68.31 - }, - "SC211": { - "Left": 76.11, - "Right": 75.59 - }, - "SC212": { - "Left": 69.48, - "Right": 72.56 - }, - "SC213": { - "Left": 69.38, - "Right": 70.59 - }, - "SC214": { - "Left": 66.47, - "Right": 68.19 - }, - "SC215": { - "Left": 65.61, - "Right": 66.01 - }, - "SC216": { - "Left": 68.91, - "Right": 70.75 - }, - "SC217": { - "Left": 66.84, - "Right": 72.42 - }, - "SC218": { - "Left": 68.94, - "Right": 69.08 - }, - "SC219": { - "Left": 67.59, - "Right": 68.11 - }, - "SC220": { - "Left": 69.99, - "Right": 68.31 - }, - "SC221": { - "Left": 66.29, - "Right": 68.01 - }, - "SC222": { - "Left": 68.2, - "Right": 69.53 - }, - "SC223": { - "Left": 69.13, - "Right": 67.29 - }, - "SC224": { - "Left": 82.94, - "Right": 80.42 - }, - "SC225": { - "Left": 78.91, - "Right": 79.84 - }, - "SC226": { - "Left": 77.02, - "Right": 79.99 - }, - "SC227": { - "Left": 76.16, - "Right": 74.46 - }, - "sr101": { - "Left": 78.66, - "Right": 82.27 - }, - "sr103": { - "Left": 71.92, - "Right": 72.36 - }, - "sr105": { - "Left": 75.27, - "Right": 75.43 - }, - "sr107": { - "Left": 70.54, - "Right": 71.3 - }, - "sr108": { - "Left": 78.43, - "Right": 77.86 - }, - "sr109": { - "Left": 78.11, - "Right": 78.51 - }, - "sr110": { - "Left": 76.62, - "Right": 81.19 - }, - "sr111": { - "Left": 74.99, - "Right": 74.25 - }, - "sr112": { - "Left": 73.16, - "Right": 74.84 - }, - "sr114": { - "Left": 72.29, - "Right": 75.03 - }, - "sr115": { - "Left": 72.73, - "Right": 74.72 - }, - "sr116": { - "Left": 72.37, - "Right": 73.29 - }, - "sr117": { - "Left": 73.7, - "Right": 74.29 - }, - "sr118": { - "Left": 75.39, - "Right": 75.84 - }, - "sr119": { - "Left": 74.51, - "Right": 77.38 - }, - "sr120": { - "Left": 71.64, - "Right": 73.88 - }, - "sr121": { - "Left": 77.78, - "Right": 78.38 - }, - "sr123": { - "Left": 74.93, - "Right": 73.58 - }, - "sr124": { - "Left": 74.15, - "Right": 74.45 - }, - "sr125": { - "Left": 70.41, - "Right": 71.14 - }, - "sr126": { - "Left": 75.07, - "Right": 75.66 - }, - "sr128": { - "Left": 76.42, - "Right": 78.04 - }, - "sr129": { - "Left": 75.57, - "Right": 73.88 - }, - "sr130": { - "Left": 79.55, - "Right": 81.5 - }, - "sr131": { - "Left": 73.79, - "Right": 74.41 - }, - "sr132": { - "Left": 72.73, - "Right": 76.27 - }, - "sr133": { - "Left": 74.42, - "Right": 76.54 - }, - "sr134": { - "Left": 71.43, - "Right": 73.63 - }, - "sr137": { - "Left": 73.26, - "Right": 75.3 - }, - "sr139": { - "Left": 76.79, - "Right": 76.35 - }, - "sr140": { - "Left": 71.0, - "Right": 72.43 - }, - "sr144": { - "Left": 78.93, - "Right": 80.96 - }, - "sr145": { - "Left": 74.86, - "Right": 75.82 - }, - "sr146": { - "Left": 69.89, - "Right": 71.23 - }, - "sr147": { - "Left": 70.42, - "Right": 72.0 - }, - "st104": { - "Left": 70.75, - "Right": 72.9 - }, - "st106": { - "Left": 73.45, - "Right": 72.8 - }, - "SR201": { - "Left": 67.24, - "Right": 67.65 - }, - "SR202": { - "Left": 73.24, - "Right": 73.68 - }, - "SR203": { - "Left": 65.92, - "Right": 70.88 - }, - "SR204": { - "Left": 69.37, - "Right": 70.36 - }, - "SR205": { - "Left": 69.55, - "Right": 69.55 - }, - "SR206": { - "Left": 68.0, - "Right": 70.55 - }, - "SR207": { - "Left": 70.25, - "Right": 71.3 - }, - "SR208": { - "Left": 76.83, - "Right": 72.31 - }, - "SR209": { - "Left": 75.76, - "Right": 75.57 - }, - "SR210": { - "Left": 75.45, - "Right": 74.72 - }, - "SR211": { - "Left": 72.62, - "Right": 78.99 - }, - "SR212": { - "Left": 72.92, - "Right": 71.08 - }, - "SR213": { - "Left": 72.43, - "Right": 72.6 - }, - "SR214": { - "Left": 74.14, - "Right": 75.05 - }, - "SR215": { - "Left": 69.67, - "Right": 72.73 - }, - "SR216": { - "Left": 71.27, - "Right": 68.25 - }, - "SR217": { - "Left": 64.35, - "Right": 64.03 - }, - "SR218": { - "Left": 65.7, - "Right": 66.26 - }, - "SR219": { - "Left": 66.27, - "Right": 69.76 - }, - "SR220": { - "Left": 67.21, - "Right": 67.46 - }, - "SR221": { - "Left": 67.69, - "Right": 66.57 - }, - "SR222": { - "Left": 67.12, - "Right": 67.18 - }, - "SR223": { - "Left": 77.17, - "Right": 78.36 - }, - "SR224": { - "Left": 70.74, - "Right": 68.58 - }, - "SR225": { - "Left": 67.67, - "Right": 67.16 - }, - "SR226": { - "Left": 68.72, - "Right": 68.85 - }, - "SR227": { - "Left": 67.64, - "Right": 67.44 - }, - "SR228": { - "Left": 68.97, - "Right": 72.16 - }, - "SR229": { - "Left": 69.9, - "Right": 70.98 - }, - "SR230": { - "Left": 63.7, - "Right": 63.59 - }, - "SR231": { - "Left": 66.05, - "Right": 66.51 - }, - "SR232": { - "Left": 68.69, - "Right": 67.93 - }, - "SR233": { - "Left": 69.09, - "Right": 69.34 - }, - "SR234": { - "Left": 65.41, - "Right": 65.36 - }, - "SR235": { - "Left": 69.68, - "Right": 70.97 - }, - "SR236": { - "Left": 68.33, - "Right": 68.88 - }, - "SR237": { - "Left": 64.48, - "Right": 65.42 - }, - "SR238": { - "Left": 67.29, - "Right": 67.41 - }, - "SR239": { - "Left": 66.57, - "Right": 66.57 - }, - "SR240": { - "Left": 67.57, - "Right": 70.03 - }, - "SR241": { - "Left": 65.9, - "Right": 65.97 - }, - "SR242": { - "Left": 62.05, - "Right": 62.64 - }, - "SR243": { - "Left": 64.89, - "Right": 65.48 - }, - "SR244": { - "Left": 67.96, - "Right": 70.78 - }, - "SR245": { - "Left": 70.51, - "Right": 74.8 - }, - "SR246": { - "Left": 66.51, - "Right": 65.69 - }, - "SR247": { - "Left": 66.64, - "Right": 66.71 - }, - "SR248": { - "Left": 65.23, - "Right": 65.42 - }, - "tm101": { - "Left": 74.82, - "Right": 75.43 - }, - "tm102": { - "Left": 77.73, - "Right": 77.63 - }, - "tm103": { - "Left": 76.42, - "Right": 76.76 - }, - "tm104": { - "Left": 83.75, - "Right": 81.65 - }, - "tm105": { - "Left": 76.1, - "Right": 78.81 - }, - "tm106": { - "Left": 76.4, - "Right": 75.65 - }, - "tm107": { - "Left": 73.13, - "Right": 73.06 - }, - "tm108": { - "Left": 76.4, - "Right": 76.0 - }, - "tm109": { - "Left": 75.12, - "Right": 74.81 - }, - "tm110": { - "Left": 77.47, - "Right": 77.06 - }, - "tm111": { - "Left": 74.94, - "Right": 76.4 - }, - "tm112": { - "Left": 73.3, - "Right": 74.73 - }, - "tm113": { - "Left": 72.36, - "Right": 72.43 - }, - "tm115": { - "Left": 76.67, - "Right": 77.24 - }, - "tm116": { - "Left": 72.65, - "Right": 73.47 - }, - "tm117": { - "Left": 73.58, - "Right": 73.15 - }, - "tm118": { - "Left": 73.06, - "Right": 74.62 - }, - "tm119": { - "Left": 70.69, - "Right": 71.06 - }, - "tm120": { - "Left": 75.66, - "Right": 76.84 - }, - "tm121": { - "Left": 77.86, - "Right": 80.9 - }, - "tm122": { - "Left": 73.3, - "Right": 73.4 - }, - "tm123": { - "Left": 73.36, - "Right": 73.06 - }, - "tm124": { - "Left": 69.53, - "Right": 70.03 - }, - "tm126": { - "Left": 73.79, - "Right": 75.27 - }, - "tm127": { - "Left": 76.03, - "Right": 77.32 - }, - "tm128": { - "Left": 74.11, - "Right": 74.1 - }, - "tm129": { - "Left": 68.55, - "Right": 70.82 - }, - "tm130": { - "Left": 80.73, - "Right": 81.58 - }, - "tm133": { - "Left": 78.07, - "Right": 78.29 - }, - "tm201": { - "Left": 67.33, - "Right": 69.71 - }, - "tm202": { - "Left": 74.84, - "Right": 75.71 - }, - "tm203": { - "Left": 71.15, - "Right": 72.94 - }, - "tm204": { - "Left": 81.59, - "Right": 82.25 - }, - "tm205": { - "Left": 81.19, - "Right": 83.01 - }, - "tm206": { - "Left": 72.38, - "Right": 73.85 - }, - "tm207": { - "Left": 73.97, - "Right": 75.41 - }, - "tm208": { - "Left": 73.32, - "Right": 76.03 - }, - "tm209": { - "Left": 70.72, - "Right": 72.93 - }, - "tm211": { - "Left": 74.97, - "Right": 76.43 - }, - "tm212": { - "Left": 72.95, - "Right": 73.88 - }, - "tm213": { - "Left": 74.64, - "Right": 75.75 - }, - "tm214": { - "Left": 76.62, - "Right": 77.24 - }, - "tm215": { - "Left": 76.83, - "Right": 79.42 - }, - "tm216": { - "Left": 70.43, - "Right": 71.66 - }, - "tm217": { - "Left": 73.62, - "Right": 76.03 - }, - "tm218": { - "Left": 71.32, - "Right": 72.66 - }, - "tm219": { - "Left": 70.04, - "Right": 71.24 - }, - "tm220": { - "Left": 73.73, - "Right": 74.6 - }, - "tm221": { - "Left": 76.75, - "Right": 84.61 - }, - "tm222": { - "Left": 70.15, - "Right": 72.61 - }, - "tm223": { - "Left": 77.37, - "Right": 84.42 - }, - "tm226": { - "Left": 72.03, - "Right": 73.92 - }, - "tm229": { - "Left": 71.36, - "Right": 72.27 - }, - "tm230": { - "Left": 70.44, - "Right": 70.93 - }, - "tm231": { - "Left": 82.29, - "Right": 81.36 - }, - "tm232": { - "Left": 71.77, - "Right": 72.81 - }, - "tm301": { - "Left": 73.31, - "Right": 73.32 - }, - "tm303": { - "Left": 69.09, - "Right": 70.97 - }, - "tm304": { - "Left": 76.62, - "Right": 76.73 - }, - "tm306": { - "Left": 73.64, - "Right": 75.1 - }, - "tm307": { - "Left": 71.78, - "Right": 72.49 - }, - "tm308": { - "Left": 73.01, - "Right": 73.26 - }, - "tm309": { - "Left": 76.08, - "Right": 77.05 - }, - "tm310": { - "Left": 76.37, - "Right": 79.41 - }, - "tm311": { - "Left": 71.05, - "Right": 71.93 - }, - "tm312": { - "Left": 73.63, - "Right": 73.4 - }, - "tm316": { - "Left": 77.5, - "Right": 79.24 - }, - "tm318": { - "Left": 72.85, - "Right": 72.32 - }, - "tm319": { - "Left": 78.41, - "Right": 77.8 - }, - "tm320": { - "Left": 72.26, - "Right": 72.34 - }, - "tm321": { - "Left": 77.92, - "Right": 78.71 - }, - "tm325": { - "Left": 77.68, - "Right": 78.86 - }, - "tm326": { - "Left": 72.89, - "Right": 73.47 - }, - "tm327": { - "Left": 76.71, - "Right": 77.28 - }, - "tm328": { - "Left": 72.02, - "Right": 73.24 - }, - "tm329": { - "Left": 69.99, - "Right": 70.58 - }, - "tm330": { - "Left": 69.97, - "Right": 70.49 - }, - "tm331": { - "Left": 69.5, - "Right": 69.97 - }, - "tm332": { - "Left": 72.19, - "Right": 72.59 - }, - "tm333": { - "Left": 73.84, - "Right": 74.29 - }, - "tm334": { - "Left": 70.34, - "Right": 70.87 - }, - "tm335": { - "Left": 71.2, - "Right": 74.06 - }, - "tm336": { - "Left": 72.43, - "Right": 72.51 - }, - "TM401": { - "Left": 69.9, - "Right": 68.96 - }, - "TM402": { - "Left": 79.28, - "Right": 76.13 - }, - "TM403": { - "Left": 73.44, - "Right": 79.86 - }, - "TM404": { - "Left": 69.5, - "Right": 70.21 - }, - "TM405": { - "Left": 68.8, - "Right": 68.53 - }, - "TM406": { - "Left": 68.54, - "Right": 68.02 - }, - "TM407": { - "Left": 69.46, - "Right": 70.38 - }, - "TM408": { - "Left": 65.2, - "Right": 65.21 - }, - "TM409": { - "Left": 68.13, - "Right": 71.14 - }, - "TM410": { - "Left": 63.47, - "Right": 64.09 - }, - "TM411": { - "Left": 69.36, - "Right": 75.2 - }, - "TM412": { - "Left": 67.46, - "Right": 68.87 - }, - "TM413": { - "Left": 68.81, - "Right": 72.9 - }, - "TM414": { - "Left": 69.62, - "Right": 71.07 - }, - "TM415": { - "Left": 68.22, - "Right": 72.19 - }, - "TM416": { - "Left": 65.55, - "Right": 66.89 - }, - "TM417": { - "Left": 64.71, - "Right": 65.28 - }, - "TM418": { - "Left": 70.47, - "Right": 73.03 - }, - "TM419": { - "Left": 69.07, - "Right": 68.39 - }, - "TM420": { - "Left": 72.64, - "Right": 71.33 - }, - "TM421": { - "Left": 72.49, - "Right": 73.22 - }, - "TM422": { - "Left": 69.89, - "Right": 76.58 - }, - "TM423": { - "Left": 72.42, - "Right": 73.2 - }, - "TM424": { - "Left": 72.32, - "Right": 72.42 - }, - "TM425": { - "Left": 67.93, - "Right": 68.32 - }, - "TM426": { - "Left": 68.16, - "Right": 68.5 - }, - "TM427": { - "Left": 66.67, - "Right": 66.49 - }, - "TM428": { - "Left": 65.99, - "Right": 67.38 - }, - "TM429": { - "Left": 68.38, - "Right": 69.57 - }, - "TM430": { - "Left": 66.43, - "Right": 67.49 - }, - "TM431": { - "Left": 67.08, - "Right": 67.91 - }, - "TM432": { - "Left": 70.1, - "Right": 72.73 - }, - "TM433": { - "Left": 68.01, - "Right": 70.02 - }, - "TM434": { - "Left": 70.79, - "Right": 71.09 - }, - "TM435": { - "Left": 67.81, - "Right": 70.07 - }, - "TM436": { - "Left": 70.66, - "Right": 71.26 - }, - "TM437": { - "Left": 73.09, - "Right": 69.48 - }, - "TM438": { - "Left": 73.61, - "Right": 74.12 - }, - "TM439": { - "Left": 67.41, - "Right": 66.94 - }, - "TM440": { - "Left": 70.95, - "Right": 73.26 - }, - "TM441": { - "Left": 67.44, - "Right": 67.87 - }, - "TM442": { - "Left": 74.36, - "Right": 75.24 - }, - "ts202": { - "Left": 75.23, - "Right": 73.42 - }, - "ts203": { - "Left": 68.99, - "Right": 71.29 - }, - "ts204": { - "Left": 66.72, - "Right": 66.83 - }, - "ts205": { - "Left": 65.01, - "Right": 65.71 - }, - "ts206": { - "Left": 67.12, - "Right": 66.81 - }, - "ts207": { - "Left": 71.07, - "Right": 71.13 - }, - "ts208": { - "Left": 69.02, - "Right": 70.73 - }, - "ts209": { - "Left": 74.85, - "Right": 72.94 - }, - "ts210": { - "Left": 67.62, - "Right": 67.93 - }, - "ts211": { - "Left": 69.05, - "Right": 68.66 - }, - "ts212": { - "Left": 66.47, - "Right": 67.31 - }, - "ts213": { - "Left": 70.71, - "Right": 71.08 - }, - "ts214": { - "Left": 75.69, - "Right": 74.66 - }, - "ts215": { - "Left": 73.19, - "Right": 73.72 - }, - "ts216": { - "Left": 70.17, - "Right": 70.13 - }, - "ts217": { - "Left": 72.26, - "Right": 73.6 - }, - "ts218": { - "Left": 72.9, - "Right": 72.43 - }, - "ts219": { - "Left": 69.81, - "Right": 69.84 - }, - "ts220": { - "Left": 69.69, - "Right": 68.54 - }, - "ts221": { - "Left": 67.59, - "Right": 68.39 - }, - "ts223": { - "Left": 72.19, - "Right": 72.94 - }, - "ts224": { - "Left": 74.98, - "Right": 78.1 - }, - "ts225": { - "Left": 66.98, - "Right": 68.95 - }, - "ts226": { - "Left": 76.47, - "Right": 77.9 - }, - "ts229": { - "Left": 69.8, - "Right": 69.91 - }, - "ts230": { - "Left": 64.29, - "Right": 64.42 - }, - "ts231": { - "Left": 62.69, - "Right": 63.82 - }, - "ts232": { - "Left": 70.57, - "Right": 72.8 - }, - "ts233": { - "Left": 68.89, - "Right": 68.75 - }, - "ts234": { - "Left": 73.82, - "Right": 72.72 - }, - "ts235": { - "Left": 75.93, - "Right": 74.81 - }, - "ts236": { - "Left": 67.44, - "Right": 70.87 - }, - "ts237": { - "Left": 72.67, - "Right": 73.0 - }, - "ts239": { - "Left": 70.57, - "Right": 70.46 - }, - "ts302": { - "Left": 66.55, - "Right": 67.03 - }, - "ts303": { - "Left": 70.76, - "Right": 71.46 - }, - "ts304": { - "Left": 67.09, - "Right": 66.19 - }, - "ts305": { - "Left": 70.72, - "Right": 70.16 - }, - "ts306": { - "Left": 65.29, - "Right": 65.09 - }, - "ts307": { - "Left": 70.34, - "Right": 69.97 - }, - "ts308": { - "Left": 68.15, - "Right": 68.67 - }, - "ts309": { - "Left": 68.18, - "Right": 67.72 - }, - "ts310": { - "Left": 65.93, - "Right": 65.95 - }, - "ts311": { - "Left": 69.44, - "Right": 69.63 - }, - "ts312": { - "Left": 71.6, - "Right": 71.63 - }, - "ts313": { - "Left": 67.39, - "Right": 67.78 - }, - "ts314": { - "Left": 71.59, - "Right": 71.94 - }, - "ts315": { - "Left": 69.34, - "Right": 69.35 - }, - "ts316": { - "Left": 64.41, - "Right": 65.31 - }, - "ts317": { - "Left": 70.07, - "Right": 71.19 - }, - "ts318": { - "Left": 67.97, - "Right": 68.13 - }, - "ts319": { - "Left": 66.63, - "Right": 67.27 - }, - "ts320": { - "Left": 69.13, - "Right": 68.48 - }, - "ts321": { - "Left": 69.23, - "Right": 69.05 - }, - "ts322": { - "Left": 67.43, - "Right": 67.3 - }, - "ts323": { - "Left": 66.3, - "Right": 66.5 - }, - "Ts401": { - "Left": 73.89, - "Right": 73.31 - }, - "Ts405": { - "Left": 71.17, - "Right": 71.3 - }, - "Ts406": { - "Left": 68.44, - "Right": 68.37 - }, - "Ts407": { - "Left": 69.38, - "Right": 70.04 - }, - "Ts408": { - "Left": 70.38, - "Right": 70.01 - }, - "Ts411": { - "Left": 73.85, - "Right": 74.74 - }, - "Ts412": { - "Left": 74.11, - "Right": 75.79 - }, - "Ts413": { - "Left": 75.46, - "Right": 74.95 - }, - "Ts414": { - "Left": 70.04, - "Right": 70.03 - }, - "Ts415": { - "Left": 72.55, - "Right": 72.9 - }, - "Ts416": { - "Left": 71.34, - "Right": 72.56 - }, - "Ts417": { - "Left": 73.38, - "Right": 72.64 - }, - "Ts418": { - "Left": 71.37, - "Right": 71.15 - }, - "Ts419": { - "Left": 73.8, - "Right": 74.43 - }, - "Ts420": { - "Left": 70.31, - "Right": 71.07 - }, - "Ts421": { - "Left": 76.73, - "Right": 75.56 - }, - "Ts422": { - "Left": 67.37, - "Right": 67.49 - }, - "Ts423": { - "Left": 72.35, - "Right": 72.54 - }, - "Ts424": { - "Left": 69.42, - "Right": 69.4 - }, - "Ts425": { - "Left": 70.29, - "Right": 70.61 - }, - "Ts426": { - "Left": 73.14, - "Right": 73.44 - }, - "TS501": { - "Left": 73.52, - "Right": 76.67 - }, - "TS502": { - "Left": 63.9, - "Right": 63.95 - }, - "TS503": { - "Left": 71.31, - "Right": 72.65 - }, - "TS504": { - "Left": 79.4, - "Right": 74.99 - }, - "TS505": { - "Left": 83.52, - "Right": 83.28 - }, - "TS506": { - "Left": 81.73, - "Right": 83.06 - }, - "TS507": { - "Left": 87.14, - "Right": 86.01 - }, - "TS508": { - "Left": 80.33, - "Right": 81.37 - }, - "TS509": { - "Left": 66.57, - "Right": 66.56 - }, - "TS510": { - "Left": 81.41, - "Right": 83.11 - }, - "TS511": { - "Left": 80.1, - "Right": 74.34 - }, - "TS512": { - "Left": 69.89, - "Right": 65.32 - }, - "TS513": { - "Left": 74.62, - "Right": 78.65 - }, - "TS514": { - "Left": 67.96, - "Right": 73.03 - }, - "TS515": { - "Left": 67.4, - "Right": 74.47 - }, - "TS516": { - "Left": 74.08, - "Right": 73.8 - }, - "TS517": { - "Left": 78.38, - "Right": 74.92 - }, - "TS518": { - "Left": 84.38, - "Right": 81.74 - }, - "TS519": { - "Left": 75.03, - "Right": 74.17 - }, - "TS520": { - "Left": 68.01, - "Right": 65.1 - }, - "TS521": { - "Left": 75.59, - "Right": 72.54 - }, - "TS522": { - "Left": 78.27, - "Right": 75.42 - }, - "TS523": { - "Left": 80.55, - "Right": 76.8 - }, - "TS524": { - "Left": 76.28, - "Right": 77.54 - }, - "TS525": { - "Left": 77.95, - "Right": 78.15 - }, - "TS526": { - "Left": 69.79, - "Right": 67.35 - }, - "TS527": { - "Left": 71.51, - "Right": 71.28 - }, - "TS528": { - "Left": 65.21, - "Right": 65.34 - }, - "TS529": { - "Left": 65.93, - "Right": 63.43 - }, - "TS530": { - "Left": 70.78, - "Right": 69.29 - }, - "TS531": { - "Left": 70.49, - "Right": 71.2 - }, - "TS532": { - "Left": 65.82, - "Right": 66.8 - }, - "TS533": { - "Left": 69.95, - "Right": 70.33 - }, - "TS534": { - "Left": 68.29, - "Right": 66.79 - }, - "TS535": { - "Left": 70.93, - "Right": 71.15 - }, - "TS536": { - "Left": 79.07, - "Right": 77.41 - }, - "TS537": { - "Left": 71.03, - "Right": 70.52 - }, - "TS538": { - "Left": 68.73, - "Right": 66.87 - }, - "TS539": { - "Left": 79.25, - "Right": 78.69 - }, - "TS540": { - "Left": 65.1, - "Right": 67.16 - }, - "TS541": { - "Left": 75.87, - "Right": 77.88 - }, - "NP101": { - "Left": 62.57, - "Right": 61.12 - }, - "NP102": { - "Left": 71.91, - "Right": 71.91 - }, - "NP106": { - "Left": 71.59, - "Right": 72.11 - }, - "NP107": { - "Left": 57.09, - "Right": 58.11 - }, - "NP108": { - "Left": 63.86, - "Right": 65.56 - }, - "NP109": { - "Left": 61.09, - "Right": 62.33 - }, - "NP110": { - "Left": 70.89, - "Right": 72.02 - }, - "NP111": { - "Left": 59.44, - "Right": 61.75 - }, - "NP112": { - "Left": 63.68, - "Right": 62.82 - }, - "NP113": { - "Left": 65.56, - "Right": 65.96 - }, - "NP114": { - "Left": 66.16, - "Right": 67.46 - }, - "NP116": { - "Left": 65.47, - "Right": 65.64 - }, - "NP117": { - "Left": 65.18, - "Right": 65.05 - }, - "NP118": { - "Left": 65.77, - "Right": 66.08 - }, - "NP119": { - "Left": 75.69, - "Right": 76.11 - }, - "NP120": { - "Left": 72.97, - "Right": 73.53 - }, - "NP123": { - "Left": 70.59, - "Right": 70.3 - }, - "NP125": { - "Left": 65.53, - "Right": 63.53 - }, - "NP126": { - "Left": 62.59, - "Right": 63.44 - }, - "NP127": { - "Left": 56.49, - "Right": 57.02 - }, - "NP128": { - "Left": 70.1, - "Right": 71.76 - }, - "NP129": { - "Left": 63.92, - "Right": 64.41 - }, - "NP131": { - "Left": 63.57, - "Right": 64.05 - }, - "NP132": { - "Left": 60.65, - "Right": 62.17 - }, - "NP133": { - "Left": 64.06, - "Right": 64.58 - }, - "NP134": { - "Left": 67.39, - "Right": 66.55 - }, - "NP135": { - "Left": 66.76, - "Right": 68.34 - }, - "NP136": { - "Left": 69.36, - "Right": 70.2 - }, - "NP138": { - "Left": 69.07, - "Right": 69.44 - }, - "NP139": { - "Left": 61.51, - "Right": 61.66 - }, - "NP140": { - "Left": 62.44, - "Right": 63.58 - }, - "NP141": { - "Left": 74.38, - "Right": 75.3 - }, - "NP142": { - "Left": 67.22, - "Right": 67.88 - }, - "NP143": { - "Left": 67.27, - "Right": 68.31 - }, - "NP144": { - "Left": 64.72, - "Right": 67.98 - }, - "NP146": { - "Left": 61.97, - "Right": 62.68 - }, - "NP147": { - "Left": 62.41, - "Right": 61.82 - }, - "NP148": { - "Left": 72.19, - "Right": 73.64 - }, - "NP149": { - "Left": 69.5, - "Right": 71.19 - }, - "NP150": { - "Left": 62.57, - "Right": 62.51 - }, - "NP151": { - "Left": 63.52, - "Right": 64.4 - }, - "NP152": { - "Left": 59.81, - "Right": 60.3 - }, - "NP153": { - "Left": 64.29, - "Right": 64.08 - }, - "NP154": { - "Left": 61.47, - "Right": 62.16 - }, - "NP155": { - "Left": 56.22, - "Right": 57.98 - }, - "NP156": { - "Left": 65.27, - "Right": 66.52 - }, - "NP157": { - "Left": 71.63, - "Right": 71.67 - }, - "NP158": { - "Left": 66.97, - "Right": 65.61 - }, - "NP160": { - "Left": 70.48, - "Right": 70.97 - }, - "NP161": { - "Left": 66.76, - "Right": 67.59 - }, - "NP163": { - "Left": 66.67, - "Right": 66.31 - }, - "cp00": { - "Left": 67.97, - "Right": 68.81 - }, - "cp0": { - "Left": 67.48, - "Right": 66.29 - }, - "cp1": { - "Left": 61.65, - "Right": 62.25 - }, - "cp2": { - "Left": 61.46, - "Right": 61.93 - }, - "cp3": { - "Left": 61.79, - "Right": 61.89 - }, - "cp4": { - "Left": 60.02, - "Right": 61.18 - }, - "cp5": { - "Left": 63.64, - "Right": 64.74 - }, - "cp6": { - "Left": 61.53, - "Right": 62.0 - }, - "cp7": { - "Left": 64.82, - "Right": 65.5 - }, - "cp8": { - "Left": 64.81, - "Right": 66.31 - }, - "cp9": { - "Left": 62.05, - "Right": 62.58 - }, - "cp11": { - "Left": 62.35, - "Right": 62.84 - }, - "cp12": { - "Left": 65.25, - "Right": 65.84 - }, - "cp13": { - "Left": 68.58, - "Right": 69.6 - }, - "cp14": { - "Left": 64.43, - "Right": 64.3 - }, - "cp16": { - "Left": 68.59, - "Right": 69.06 - }, - "cp17": { - "Left": 67.53, - "Right": 67.36 - }, - "cp18": { - "Left": 72.06, - "Right": 70.27 - }, - "cp19": { - "Left": 68.03, - "Right": 67.61 - }, - "cp20": { - "Left": 69.57, - "Right": 74.23 - }, - "cp21": { - "Left": 70.79, - "Right": 71.54 - }, - "cv12": { - "Left": 70.91, - "Right": 70.18 - }, - "cv14": { - "Left": 68.76, - "Right": 69.31 - }, - "cv15": { - "Left": 58.36, - "Right": 55.72 - }, - "cv16": { - "Left": 52.46, - "Right": 53.0 - }, - "cv22": { - "Left": 71.7, - "Right": 74.18 - }, - "cv23": { - "Left": 67.84, - "Right": 74.22 - }, - "cv27": { - "Left": 60.42, - "Right": 62.4 - }, - "cv28": { - "Left": 65.79, - "Right": 68.26 - }, - "cv31": { - "Left": 59.25, - "Right": 59.87 - }, - "cv33": { - "Left": 71.33, - "Right": 77.46 - }, - "cv35": { - "Left": 64.54, - "Right": 74.44 - }, - "cv37": { - "Left": 68.05, - "Right": 77.84 - }, - "cv38": { - "Left": 68.89, - "Right": 73.26 - }, - "cv39": { - "Left": 71.95, - "Right": 74.29 - }, - "cv41": { - "Left": 57.31, - "Right": 62.06 - }, - "cv42": { - "Left": 60.29, - "Right": 61.43 - }, - "cv43": { - "Left": 73.48, - "Right": 80.8 - }, - "cv45": { - "Left": 62.1, - "Right": 64.14 - }, - "cv210": { - "Left": 72.04, - "Right": 72.05 - }, - "cv212": { - "Left": 56.7, - "Right": 57.0 - }, - "cv213": { - "Left": 60.58, - "Right": 60.58 - }, - "cv214": { - "Left": 62.01, - "Right": 62.43 - }, - "2cv11": { - "Left": 63.43, - "Right": 66.77 - }, - "2cv12": { - "Left": 65.68, - "Right": 65.36 - }, - "2cv13": { - "Left": 69.4, - "Right": 75.52 - }, - "2cv14": { - "Left": 60.94, - "Right": 60.72 - }, - "2cv15": { - "Left": 64.31, - "Right": 66.93 - }, - "2cv21": { - "Left": 68.49, - "Right": 68.85 - }, - "2cv22": { - "Left": 65.08, - "Right": 72.91 - }, - "2cv23": { - "Left": 68.49, - "Right": 68.88 - }, - "2cv25": { - "Left": 64.09, - "Right": 68.72 - }, - "2cv26": { - "Left": 63.24, - "Right": 65.41 - }, - "2cv31": { - "Left": 63.06, - "Right": 64.92 - }, - "2cv32": { - "Left": 65.24, - "Right": 65.12 - }, - "2cv33": { - "Left": 69.48, - "Right": 69.2 - }, - "2cv34": { - "Left": 70.0, - "Right": 68.34 - }, - "2cv41": { - "Left": 65.82, - "Right": 63.64 - }, - "2cv51": { - "Left": 68.96, - "Right": 71.86 - }, - "2cv61": { - "Left": 68.1, - "Right": 71.26 - }, - "2cv62": { - "Left": 74.29, - "Right": 76.46 - }, - "m11": { - "Left": 71.02, - "Right": 72.06 - }, - "m12": { - "Left": 73.9, - "Right": 74.89 - }, - "m13": { - "Left": 78.11, - "Right": 75.08 - }, - "m14": { - "Left": 71.17, - "Right": 73.52 - }, - "m15": { - "Left": 76.8, - "Right": 76.56 - }, - "m16": { - "Left": 71.35, - "Right": 73.08 - }, - "m17": { - "Left": 63.49, - "Right": 64.38 - }, - "m18": { - "Left": 74.66, - "Right": 76.63 - }, - "m21": { - "Left": 68.44, - "Right": 67.34 - }, - "m22": { - "Left": 73.48, - "Right": 73.03 - }, - "m23": { - "Left": 70.75, - "Right": 71.75 - }, - "m24": { - "Left": 75.96, - "Right": 76.24 - }, - "m25": { - "Left": 66.24, - "Right": 67.47 - }, - "m26": { - "Left": 67.02, - "Right": 66.04 - }, - "m27": { - "Left": 65.76, - "Right": 65.45 - }, - "m28": { - "Left": 78.16, - "Right": 81.02 - }, - "m29": { - "Left": 75.87, - "Right": 81.2 - }, - "m110": { - "Left": 78.57, - "Right": 77.42 - }, - "m111": { - "Left": 69.47, - "Right": 70.04 - }, - "m112": { - "Left": 68.67, - "Right": 74.14 - }, - "m113": { - "Left": 69.75, - "Right": 70.4 - }, - "m210": { - "Left": 78.16, - "Right": 77.72 - }, - "m211": { - "Left": 74.1, - "Right": 76.01 - }, - "m212": { - "Left": 73.16, - "Right": 75.06 - }, - "m213": { - "Left": 67.22, - "Right": 68.85 - }, - "bib1": { - "Left": 69.86, - "Right": 70.57 - }, - "bib2": { - "Left": 66.58, - "Right": 66.81 - }, - "bib3": { - "Left": 68.75, - "Right": 69.87 - }, - "bib4": { - "Left": 68.71, - "Right": 67.68 - }, - "bib5": { - "Left": 67.79, - "Right": 68.24 - }, - "bib6": { - "Left": 67.54, - "Right": 68.67 - }, - "bib8": { - "Left": 65.67, - "Right": 66.81 - }, - "bib9": { - "Left": 68.76, - "Right": 70.86 - }, - "bib10": { - "Left": 68.96, - "Right": 67.73 - }, - "bib11": { - "Left": 67.51, - "Right": 68.89 - }, - "bib12": { - "Left": 67.32, - "Right": 68.07 - }, - "bib13": { - "Left": 66.01, - "Right": 66.41 - }, - "bib14": { - "Left": 70.11, - "Right": 68.61 - }, - "bib15": { - "Left": 67.35, - "Right": 67.16 - }, - "bib16": { - "Left": 68.76, - "Right": 69.57 - }, - "MG101": { - "Left": 60.81, - "Right": 60.55 - }, - "MG102": { - "Left": 62.27, - "Right": 61.78 - }, - "MG103": { - "Left": 59.55, - "Right": 59.34 - }, - "MG104": { - "Left": 56.24, - "Right": 56.08 - }, - "MG105": { - "Left": 58.36, - "Right": 58.0 - }, - "MG106": { - "Left": 63.31, - "Right": 62.07 - }, - "MG108": { - "Left": 61.29, - "Right": 61.19 - }, - "MG109": { - "Left": 65.99, - "Right": 68.68 - }, - "MG110": { - "Left": 59.57, - "Right": 58.73 - }, - "MG111": { - "Left": 66.96, - "Right": 65.82 - }, - "MG114": { - "Left": 60.89, - "Right": 62.0 - }, - "MG116": { - "Left": 62.0, - "Right": 59.62 - }, - "MG118": { - "Left": 61.76, - "Right": 63.53 - }, - "MG119": { - "Left": 56.82, - "Right": 56.26 - }, - "MG201": { - "Left": 76.07, - "Right": 75.64 - }, - "MG202": { - "Left": 75.64, - "Right": 75.4 - }, - "MG203": { - "Left": 76.27, - "Right": 76.15 - }, - "MG204": { - "Left": 75.75, - "Right": 75.66 - }, - "MG205": { - "Left": 75.41, - "Right": 75.42 - }, - "MG206": { - "Left": 75.49, - "Right": 75.66 - }, - "MG207": { - "Left": 74.52, - "Right": 74.89 - }, - "MG208": { - "Left": 74.99, - "Right": 75.24 - }, - "MG209": { - "Left": 83.24, - "Right": 82.35 - }, - "MG210": { - "Left": 84.15, - "Right": 81.64 - }, - "MG211": { - "Left": 82.37, - "Right": 81.09 - }, - "MG212": { - "Left": 81.67, - "Right": 82.69 - }, - "MG213": { - "Left": 80.92, - "Right": 81.59 - }, - "MG214": { - "Left": 80.04, - "Right": 80.15 - }, - "MG215": { - "Left": 76.15, - "Right": 76.13 - }, - "MG216": { - "Left": 79.81, - "Right": 79.21 - }, - "MG217": { - "Left": 79.24, - "Right": 78.4 - }, - "MG218": { - "Left": 78.34, - "Right": 79.27 - }, - "MG219": { - "Left": 81.84, - "Right": 83.18 - }, - "MG220": { - "Left": 81.17, - "Right": 82.53 - }, - "MG301": { - "Left": 65.72, - "Right": 61.34 - }, - "MG302": { - "Left": 55.31, - "Right": 53.69 - }, - "MG303": { - "Left": 55.28, - "Right": 57.92 - }, - "MG304": { - "Left": 55.52, - "Right": 54.79 - }, - "MG305": { - "Left": 57.93, - "Right": 56.82 - }, - "MG306": { - "Left": 53.2, - "Right": 52.62 - }, - "MG307": { - "Left": 65.82, - "Right": 62.45 - }, - "MG308": { - "Left": 64.98, - "Right": 62.05 - }, - "MG309": { - "Left": 56.38, - "Right": 59.62 - }, - "MG310": { - "Left": 65.01, - "Right": 61.03 - }, - "MG311": { - "Left": 61.85, - "Right": 59.44 - }, - "MG312": { - "Left": 57.91, - "Right": 57.31 - }, - "MG313": { - "Left": 60.97, - "Right": 60.88 - }, - "MG314": { - "Left": 53.65, - "Right": 53.66 - }, - "MG315": { - "Left": 63.13, - "Right": 61.39 - }, - "MG316": { - "Left": 56.42, - "Right": 56.59 - }, - "MG317": { - "Left": 52.73, - "Right": 52.51 - }, - "MG318": { - "Left": 55.54, - "Right": 55.31 - }, - "MG319": { - "Left": 53.36, - "Right": 52.85 - }, - "MG320": { - "Left": 56.43, - "Right": 55.9 - }, - "MG321": { - "Left": 65.8, - "Right": 64.73 - }, - "MG322": { - "Left": 62.2, - "Right": 61.56 - }, - "MG323": { - "Left": 65.15, - "Right": 67.37 - }, - "MG324": { - "Left": 66.13, - "Right": 67.45 - }, - "MG325": { - "Left": 59.99, - "Right": 65.75 - }, - "MG326": { - "Left": 76.14, - "Right": 76.96 - }, - "MG327": { - "Left": 58.83, - "Right": 58.23 - }, - "MG328": { - "Left": 60.38, - "Right": 60.4 - }, - "MG329": { - "Left": 60.86, - "Right": 59.88 - }, - "MG330": { - "Left": 61.23, - "Right": 60.62 - }, - "MG331": { - "Left": 56.93, - "Right": 57.68 - }, - "MG332": { - "Left": 56.8, - "Right": 55.43 - }, - "MG333": { - "Left": 56.82, - "Right": 56.29 - }, - "MG334": { - "Left": 59.82, - "Right": 59.27 - }, - "AM02": { - "Left": 71.73, - "Right": 72.47 - }, - "AM06": { - "Left": 72.74, - "Right": 72.6 - }, - "AM07": { - "Left": 70.9, - "Right": 69.95 - }, - "ML101": { - "Left": 74.35, - "Right": 73.84 - }, - "ML103": { - "Left": 72.43, - "Right": 71.73 - }, - "ML104": { - "Left": 72.91, - "Right": 72.21 - }, - "ML105": { - "Left": 71.81, - "Right": 71.42 - }, - "ML106": { - "Left": 69.97, - "Right": 70.41 - }, - "ML111": { - "Left": 68.43, - "Right": 68.86 - }, - "ML112": { - "Left": 71.4, - "Right": 70.61 - }, - "ML130": { - "Left": 70.15, - "Right": 70.34 - }, - "ML131": { - "Left": 71.94, - "Right": 72.0 - }, - "ML132": { - "Left": 72.12, - "Right": 72.43 - }, - "ML133": { - "Left": 72.64, - "Right": 72.33 - }, - "ML202": { - "Left": 82.27, - "Right": 82.05 - }, - "ML203": { - "Left": 70.78, - "Right": 70.67 - }, - "ML204": { - "Left": 71.03, - "Right": 72.16 - }, - "ML205": { - "Left": 70.94, - "Right": 71.6 - }, - "ML206": { - "Left": 70.27, - "Right": 69.73 - }, - "ML207": { - "Left": 71.85, - "Right": 72.88 - }, - "ML208": { - "Left": 67.8, - "Right": 67.63 - }, - "ML209": { - "Left": 73.29, - "Right": 72.05 - }, - "ML210": { - "Left": 71.68, - "Right": 72.68 - }, - "ML211": { - "Left": 74.08, - "Right": 74.64 - }, - "ML212": { - "Left": 73.7, - "Right": 74.29 - }, - "ML213": { - "Left": 72.33, - "Right": 73.29 - }, - "ML214": { - "Left": 79.1, - "Right": 77.09 - }, - "SM301": { - "Left": 74.26, - "Right": 74.5 - }, - "SM302": { - "Left": 76.62, - "Right": 75.87 - }, - "SM303": { - "Left": 78.13, - "Right": 78.11 - }, - "SM304": { - "Left": 76.74, - "Right": 76.15 - }, - "SM305": { - "Left": 75.78, - "Right": 75.67 - }, - "SM306": { - "Left": 76.65, - "Right": 77.91 - }, - "SM307": { - "Left": 76.28, - "Right": 76.83 - }, - "SM308": { - "Left": 76.32, - "Right": 75.91 - }, - "SM309": { - "Left": 75.4, - "Right": 75.42 - }, - "SM310": { - "Left": 75.89, - "Right": 75.32 - }, - "SM311": { - "Left": 75.76, - "Right": 75.76 - }, - "SM312": { - "Left": 75.12, - "Right": 75.35 - }, - "SM313": { - "Left": 75.67, - "Right": 75.92 - }, - "SM314": { - "Left": 79.08, - "Right": 78.59 - }, - "SM401": { - "Left": 71.98, - "Right": 71.19 - }, - "SM402": { - "Left": 66.27, - "Right": 72.63 - }, - "SM403": { - "Left": 68.33, - "Right": 67.97 - }, - "SM404": { - "Left": 69.48, - "Right": 75.89 - }, - "SM405": { - "Left": 68.01, - "Right": 69.51 - }, - "SM406": { - "Left": 70.92, - "Right": 75.87 - }, - "SM407": { - "Left": 73.73, - "Right": 74.14 - }, - "SM408": { - "Left": 72.03, - "Right": 77.73 - }, - "SM409": { - "Left": 69.73, - "Right": 72.76 - }, - "SM410": { - "Left": 74.6, - "Right": 78.2 - }, - "SM411": { - "Left": 72.91, - "Right": 71.23 - }, - "SM412": { - "Left": 73.81, - "Right": 75.5 - }, - "SM413": { - "Left": 72.86, - "Right": 70.77 - }, - "SM414": { - "Left": 70.78, - "Right": 72.41 - }, - "SM415": { - "Left": 68.57, - "Right": 71.5 - }, - "SM416": { - "Left": 69.84, - "Right": 72.03 - }, - "SM417": { - "Left": 69.4, - "Right": 70.09 - }, - "SM418": { - "Left": 71.05, - "Right": 72.24 - }, - "SM419": { - "Left": 69.18, - "Right": 68.79 - }, - "SM420": { - "Left": 67.0, - "Right": 67.14 - }, - "SM501": { - "Left": 56.87, - "Right": 56.82 - }, - "SM502": { - "Left": 56.51, - "Right": 56.42 - }, - "SM503": { - "Left": 59.47, - "Right": 58.94 - }, - "SM504": { - "Left": 69.55, - "Right": 73.89 - }, - "SM505": { - "Left": 69.22, - "Right": 69.09 - }, - "SM506": { - "Left": 71.3, - "Right": 71.38 - }, - "SM507": { - "Left": 67.04, - "Right": 68.1 - }, - "SM508": { - "Left": 69.84, - "Right": 69.39 - }, - "SM509": { - "Left": 62.62, - "Right": 66.17 - }, - "SM510": { - "Left": 61.84, - "Right": 61.77 - }, - "SM511": { - "Left": 62.69, - "Right": 62.88 - }, - "SM512": { - "Left": 61.92, - "Right": 59.41 - }, - "SM513": { - "Left": 65.66, - "Right": 67.05 - }, - "SM514": { - "Left": 56.41, - "Right": 58.5 - }, - "SM515": { - "Left": 70.94, - "Right": 71.01 - }, - "SM516": { - "Left": 68.6, - "Right": 67.22 - }, - "SM517": { - "Left": 68.27, - "Right": 67.01 - }, - "SM518": { - "Left": 67.46, - "Right": 67.91 - }, - "SM519": { - "Left": 64.91, - "Right": 64.28 - }, - "SM520": { - "Left": 68.53, - "Right": 69.3 - } -} \ No newline at end of file +{ + "CT101": { + "Left": 79.0, + "Right": 79.72 + }, + "CT102": { + "Left": 79.35, + "Right": 79.88 + }, + "CT103": { + "Left": 76.25, + "Right": 76.41 + }, + "CT104": { + "Left": 79.9, + "Right": 79.93 + }, + "CT107": { + "Left": 78.21, + "Right": 78.47 + }, + "CT108": { + "Left": 79.23, + "Right": 79.51 + }, + "CT109": { + "Left": 79.32, + "Right": 79.37 + }, + "CT110": { + "Left": 78.15, + "Right": 80.09 + }, + "CT112": { + "Left": 79.08, + "Right": 79.77 + }, + "CT113": { + "Left": 81.32, + "Right": 81.67 + }, + "CT114": { + "Left": 76.69, + "Right": 76.94 + }, + "CT116": { + "Left": 77.88, + "Right": 78.82 + }, + "CT117": { + "Left": 80.18, + "Right": 80.54 + }, + "CT119": { + "Left": 76.45, + "Right": 77.0 + }, + "CT120": { + "Left": 79.9, + "Right": 80.27 + }, + "CT121": { + "Left": 78.14, + "Right": 78.51 + }, + "CT122": { + "Left": 80.88, + "Right": 81.3 + }, + "CT123": { + "Left": 77.46, + "Right": 77.9 + }, + "CT124": { + "Left": 78.14, + "Right": 78.49 + }, + "CT201": { + "Left": 76.79, + "Right": 76.81 + }, + "CT202": { + "Left": 82.07, + "Right": 82.74 + }, + "CT203": { + "Left": 77.48, + "Right": 77.47 + }, + "CT301": { + "Left": 75.84, + "Right": 78.33 + }, + "CT305": { + "Left": 83.14, + "Right": 81.74 + }, + "CT306": { + "Left": 77.45, + "Right": 77.96 + }, + "CT308": { + "Left": 87.1, + "Right": 86.2 + }, + "CT309": { + "Left": 78.2, + "Right": 79.3 + }, + "CT310": { + "Left": 80.94, + "Right": 81.73 + }, + "CT311": { + "Left": 79.41, + "Right": 80.08 + }, + "CT312": { + "Left": 79.57, + "Right": 80.11 + }, + "CT313": { + "Left": 80.34, + "Right": 83.0 + }, + "CT314": { + "Left": 79.48, + "Right": 80.06 + }, + "CT315": { + "Left": 77.78, + "Right": 78.18 + }, + "CT317": { + "Left": 82.03, + "Right": 81.59 + }, + "CT318": { + "Left": 78.14, + "Right": 78.74 + }, + "CT319": { + "Left": 79.54, + "Right": 80.82 + }, + "CT320": { + "Left": 79.99, + "Right": 80.0 + }, + "CT321": { + "Left": 76.89, + "Right": 77.45 + }, + "CT322": { + "Left": 81.15, + "Right": 81.53 + }, + "CT323": { + "Left": 81.91, + "Right": 80.14 + }, + "CT324": { + "Left": 81.08, + "Right": 80.1 + }, + "CT325": { + "Left": 74.26, + "Right": 75.38 + }, + "CT326": { + "Left": 84.36, + "Right": 83.76 + }, + "CT327": { + "Left": 77.13, + "Right": 77.39 + }, + "CT328": { + "Left": 77.72, + "Right": 77.98 + }, + "CT329": { + "Left": 79.82, + "Right": 80.27 + }, + "CT401": { + "Left": 96.66, + "Right": 97.26 + }, + "CT402": { + "Left": 86.17, + "Right": 86.29 + }, + "CT403": { + "Left": 84.51, + "Right": 83.94 + }, + "CT404": { + "Left": 83.48, + "Right": 83.61 + }, + "CT405": { + "Left": 78.42, + "Right": 78.91 + }, + "CT406": { + "Left": 82.16, + "Right": 83.97 + }, + "CT407": { + "Left": 80.34, + "Right": 81.52 + }, + "CT408": { + "Left": 82.76, + "Right": 83.06 + }, + "CT411": { + "Left": 85.54, + "Right": 85.65 + }, + "CT412": { + "Left": 86.65, + "Right": 86.29 + }, + "CT413": { + "Left": 88.02, + "Right": 88.57 + }, + "CT414": { + "Left": 88.84, + "Right": 88.06 + }, + "CT415": { + "Left": 87.69, + "Right": 88.97 + }, + "CT501": { + "Left": 72.85, + "Right": 73.85 + }, + "CT502": { + "Left": 77.25, + "Right": 78.19 + }, + "CT503": { + "Left": 83.07, + "Right": 84.22 + }, + "CT504": { + "Left": 72.75, + "Right": 73.25 + }, + "CT505": { + "Left": 76.09, + "Right": 78.99 + }, + "CT506": { + "Left": 71.03, + "Right": 70.72 + }, + "CT507": { + "Left": 79.26, + "Right": 80.19 + }, + "CT508": { + "Left": 70.27, + "Right": 70.98 + }, + "CT509": { + "Left": 73.8, + "Right": 74.22 + }, + "CT510": { + "Left": 74.71, + "Right": 75.26 + }, + "CT511": { + "Left": 77.16, + "Right": 76.59 + }, + "CT512": { + "Left": 76.57, + "Right": 77.09 + }, + "CT513": { + "Left": 83.01, + "Right": 85.52 + }, + "CT514": { + "Left": 79.29, + "Right": 79.45 + }, + "CT515": { + "Left": 75.42, + "Right": 75.65 + }, + "CT516": { + "Left": 78.11, + "Right": 78.18 + }, + "CT517": { + "Left": 76.1, + "Right": 76.55 + }, + "CT518": { + "Left": 75.92, + "Right": 75.95 + }, + "CT519": { + "Left": 83.23, + "Right": 82.75 + }, + "CT520": { + "Left": 79.1, + "Right": 75.49 + }, + "CT521": { + "Left": 81.82, + "Right": 79.89 + }, + "CT522": { + "Left": 75.32, + "Right": 77.07 + }, + "CT523": { + "Left": 81.2, + "Right": 81.94 + }, + "CT524": { + "Left": 75.91, + "Right": 81.02 + }, + "CT525": { + "Left": 80.61, + "Right": 82.03 + }, + "CT526": { + "Left": 78.68, + "Right": 78.78 + }, + "CT527": { + "Left": 83.97, + "Right": 84.09 + }, + "CT528": { + "Left": 78.78, + "Right": 78.89 + }, + "CT529": { + "Left": 71.9, + "Right": 71.29 + }, + "CT530": { + "Left": 79.86, + "Right": 79.89 + }, + "CT531": { + "Left": 84.14, + "Right": 84.85 + }, + "CT532": { + "Left": 73.27, + "Right": 73.33 + }, + "CT533": { + "Left": 77.46, + "Right": 81.46 + }, + "CT534": { + "Left": 72.52, + "Right": 78.82 + }, + "CT535": { + "Left": 75.1, + "Right": 75.19 + }, + "CT536": { + "Left": 78.08, + "Right": 78.47 + }, + "CT537": { + "Left": 78.67, + "Right": 78.83 + }, + "CT538": { + "Left": 77.58, + "Right": 78.1 + }, + "CT539": { + "Left": 77.85, + "Right": 77.83 + }, + "CT540": { + "Left": 76.78, + "Right": 76.88 + }, + "CT541": { + "Left": 78.41, + "Right": 79.39 + }, + "CT542": { + "Left": 80.43, + "Right": 79.19 + }, + "CT543": { + "Left": 79.81, + "Right": 81.01 + }, + "CT544": { + "Left": 78.3, + "Right": 80.23 + }, + "CT545": { + "Left": 79.46, + "Right": 79.53 + }, + "ET101": { + "Left": 77.06, + "Right": 77.3 + }, + "ET102": { + "Left": 79.25, + "Right": 78.97 + }, + "ET103": { + "Left": 79.15, + "Right": 78.9 + }, + "ET104": { + "Left": 77.63, + "Right": 77.96 + }, + "ET105": { + "Left": 74.73, + "Right": 74.9 + }, + "ET106": { + "Left": 74.87, + "Right": 74.93 + }, + "ET107": { + "Left": 80.74, + "Right": 80.88 + }, + "ET108": { + "Left": 79.03, + "Right": 79.5 + }, + "ET109": { + "Left": 77.26, + "Right": 77.34 + }, + "ET110": { + "Left": 76.62, + "Right": 76.4 + }, + "ET111": { + "Left": 77.14, + "Right": 77.37 + }, + "ET112": { + "Left": 85.67, + "Right": 85.78 + }, + "ET113": { + "Left": 78.36, + "Right": 78.11 + }, + "ET114": { + "Left": 78.7, + "Right": 78.52 + }, + "ET115": { + "Left": 77.24, + "Right": 77.13 + }, + "ET116": { + "Left": 79.78, + "Right": 79.94 + }, + "ET117": { + "Left": 78.63, + "Right": 79.02 + }, + "ET118": { + "Left": 74.72, + "Right": 74.66 + }, + "ET119": { + "Left": 75.83, + "Right": 76.01 + }, + "ET120": { + "Left": 79.22, + "Right": 79.11 + }, + "ET121": { + "Left": 79.36, + "Right": 79.85 + }, + "ET122": { + "Left": 78.93, + "Right": 79.27 + }, + "ET123": { + "Left": 76.55, + "Right": 76.5 + }, + "ET124": { + "Left": 77.47, + "Right": 77.29 + }, + "ET125": { + "Left": 82.11, + "Right": 81.67 + }, + "ET201": { + "Left": 76.54, + "Right": 76.75 + }, + "ET202": { + "Left": 78.97, + "Right": 79.75 + }, + "ET203": { + "Left": 74.35, + "Right": 74.42 + }, + "ET204": { + "Left": 78.56, + "Right": 78.99 + }, + "ET205": { + "Left": 77.09, + "Right": 76.43 + }, + "ET206": { + "Left": 76.26, + "Right": 76.33 + }, + "ET207": { + "Left": 74.69, + "Right": 74.41 + }, + "ET210": { + "Left": 76.0, + "Right": 76.36 + }, + "ET211": { + "Left": 79.31, + "Right": 79.09 + }, + "ET212": { + "Left": 78.28, + "Right": 78.45 + }, + "ET213": { + "Left": 77.75, + "Right": 77.69 + }, + "ET214": { + "Left": 75.59, + "Right": 75.51 + }, + "ET215": { + "Left": 76.12, + "Right": 76.26 + }, + "ET216": { + "Left": 78.18, + "Right": 78.25 + }, + "ET218": { + "Left": 77.83, + "Right": 77.9 + }, + "ET219": { + "Left": 80.52, + "Right": 80.41 + }, + "ET220": { + "Left": 77.95, + "Right": 78.39 + }, + "ET221": { + "Left": 77.27, + "Right": 77.78 + }, + "ET222": { + "Left": 80.16, + "Right": 79.75 + }, + "ET301": { + "Left": 78.75, + "Right": 78.96 + }, + "ET302": { + "Left": 77.57, + "Right": 77.78 + }, + "ET303": { + "Left": 78.04, + "Right": 78.44 + }, + "ET304": { + "Left": 77.62, + "Right": 77.39 + }, + "ET305": { + "Left": 78.3, + "Right": 78.27 + }, + "ET306": { + "Left": 76.24, + "Right": 76.1 + }, + "ET307": { + "Left": 78.24, + "Right": 78.3 + }, + "ET308": { + "Left": 78.12, + "Right": 77.72 + }, + "ET309": { + "Left": 78.1, + "Right": 78.52 + }, + "ET310": { + "Left": 75.76, + "Right": 75.17 + }, + "ET311": { + "Left": 78.67, + "Right": 79.51 + }, + "ET312": { + "Left": 75.74, + "Right": 75.84 + }, + "ET313": { + "Left": 77.46, + "Right": 77.75 + }, + "ET314": { + "Left": 75.29, + "Right": 74.94 + }, + "ET401": { + "Left": 76.05, + "Right": 76.81 + }, + "ET402": { + "Left": 68.88, + "Right": 69.54 + }, + "ET403": { + "Left": 74.09, + "Right": 75.73 + }, + "ET404": { + "Left": 70.88, + "Right": 71.28 + }, + "ET405": { + "Left": 74.6, + "Right": 75.0 + }, + "ET406": { + "Left": 77.0, + "Right": 78.62 + }, + "ET407": { + "Left": 76.56, + "Right": 76.83 + }, + "ET408": { + "Left": 69.12, + "Right": 69.38 + }, + "ET409": { + "Left": 78.04, + "Right": 78.07 + }, + "ET410": { + "Left": 76.46, + "Right": 75.85 + }, + "ET411": { + "Left": 73.47, + "Right": 72.66 + }, + "ET412": { + "Left": 73.3, + "Right": 73.58 + }, + "ET413": { + "Left": 73.04, + "Right": 75.11 + }, + "ET414": { + "Left": 71.88, + "Right": 72.81 + }, + "ET415": { + "Left": 77.82, + "Right": 74.54 + }, + "ET416": { + "Left": 79.67, + "Right": 79.5 + }, + "ET417": { + "Left": 80.52, + "Right": 80.47 + }, + "ET418": { + "Left": 73.52, + "Right": 73.53 + }, + "ET419": { + "Left": 82.66, + "Right": 82.71 + }, + "ET420": { + "Left": 75.49, + "Right": 74.95 + }, + "ET421": { + "Left": 76.71, + "Right": 76.48 + }, + "ET422": { + "Left": 76.08, + "Right": 76.81 + }, + "ET423": { + "Left": 72.3, + "Right": 72.32 + }, + "ET424": { + "Left": 75.75, + "Right": 76.14 + }, + "ET425": { + "Left": 76.07, + "Right": 76.49 + }, + "ET426": { + "Left": 76.19, + "Right": 76.75 + }, + "ET427": { + "Left": 78.39, + "Right": 78.54 + }, + "ET428": { + "Left": 73.91, + "Right": 74.3 + }, + "ET429": { + "Left": 78.81, + "Right": 77.94 + }, + "ET430": { + "Left": 72.42, + "Right": 72.69 + }, + "ET431": { + "Left": 80.19, + "Right": 79.91 + }, + "ET432": { + "Left": 76.64, + "Right": 76.93 + }, + "ET433": { + "Left": 77.43, + "Right": 77.24 + }, + "ET434": { + "Left": 78.2, + "Right": 78.81 + }, + "ET435": { + "Left": 70.97, + "Right": 71.5 + }, + "ET436": { + "Left": 74.86, + "Right": 75.11 + }, + "ET437": { + "Left": 75.76, + "Right": 76.33 + }, + "ET438": { + "Left": 78.15, + "Right": 77.91 + }, + "Mc101": { + "Left": 73.23, + "Right": 74.05 + }, + "mc102": { + "Left": 69.57, + "Right": 70.68 + }, + "Mc103": { + "Left": 73.13, + "Right": 74.11 + }, + "Mc104": { + "Left": 67.29, + "Right": 68.67 + }, + "Mc105": { + "Left": 70.44, + "Right": 72.03 + }, + "Mc106": { + "Left": 76.81, + "Right": 76.15 + }, + "Mc108": { + "Left": 69.4, + "Right": 69.58 + }, + "Mc109": { + "Left": 68.43, + "Right": 69.41 + }, + "Mc110": { + "Left": 71.27, + "Right": 71.68 + }, + "Mc111": { + "Left": 70.7, + "Right": 70.93 + }, + "Mc112": { + "Left": 74.05, + "Right": 74.57 + }, + "Mc114": { + "Left": 68.53, + "Right": 69.31 + }, + "Mc115": { + "Left": 70.38, + "Right": 72.7 + }, + "Mc116": { + "Left": 76.66, + "Right": 75.75 + }, + "Mc118": { + "Left": 70.45, + "Right": 70.72 + }, + "Mc119": { + "Left": 71.78, + "Right": 71.4 + }, + "Mc120": { + "Left": 66.86, + "Right": 67.88 + }, + "Mc121": { + "Left": 70.0, + "Right": 71.25 + }, + "Mc122": { + "Left": 73.18, + "Right": 74.29 + }, + "Mc127": { + "Left": 68.91, + "Right": 68.58 + }, + "Mc129": { + "Left": 69.32, + "Right": 69.82 + }, + "Mc203": { + "Left": 76.17, + "Right": 77.08 + }, + "Mc204": { + "Left": 70.35, + "Right": 78.01 + }, + "Mc206": { + "Left": 71.16, + "Right": 73.61 + }, + "Mc207": { + "Left": 68.51, + "Right": 67.82 + }, + "Mc301": { + "Left": 67.25, + "Right": 67.27 + }, + "Mc302": { + "Left": 65.61, + "Right": 66.25 + }, + "Mc303": { + "Left": 67.84, + "Right": 68.42 + }, + "Mc305": { + "Left": 70.48, + "Right": 70.18 + }, + "Mc307": { + "Left": 65.75, + "Right": 66.01 + }, + "Mc308": { + "Left": 68.21, + "Right": 72.22 + }, + "Mc309": { + "Left": 65.98, + "Right": 66.48 + }, + "Mc310": { + "Left": 74.67, + "Right": 70.96 + }, + "Mc311": { + "Left": 72.43, + "Right": 72.93 + }, + "Mc312": { + "Left": 70.21, + "Right": 71.02 + }, + "Mc316": { + "Left": 69.95, + "Right": 73.59 + }, + "Mc317": { + "Left": 67.69, + "Right": 67.14 + }, + "Mc318": { + "Left": 66.18, + "Right": 67.16 + }, + "Mc319": { + "Left": 74.48, + "Right": 75.51 + }, + "Mc320": { + "Left": 66.88, + "Right": 66.97 + }, + "Mc321": { + "Left": 68.4, + "Right": 71.17 + }, + "Mc323": { + "Left": 71.12, + "Right": 72.35 + }, + "Mc324": { + "Left": 69.77, + "Right": 71.41 + }, + "Mc325": { + "Left": 72.71, + "Right": 75.12 + }, + "Mc328": { + "Left": 68.06, + "Right": 68.71 + }, + "Mc329": { + "Left": 71.81, + "Right": 72.59 + }, + "Mc330": { + "Left": 69.25, + "Right": 70.18 + }, + "Mc331": { + "Left": 67.54, + "Right": 68.01 + }, + "Mc332": { + "Left": 67.89, + "Right": 68.51 + }, + "Mc333": { + "Left": 64.26, + "Right": 65.17 + }, + "Mc334": { + "Left": 64.56, + "Right": 66.24 + }, + "Mc335": { + "Left": 70.17, + "Right": 70.18 + }, + "Mc336": { + "Left": 65.94, + "Right": 66.98 + }, + "Mc401": { + "Left": 67.78, + "Right": 68.82 + }, + "Mc402": { + "Left": 77.06, + "Right": 76.99 + }, + "Mc404": { + "Left": 69.46, + "Right": 72.39 + }, + "Mc405": { + "Left": 71.85, + "Right": 72.84 + }, + "Mc406": { + "Left": 72.81, + "Right": 74.47 + }, + "Mc407": { + "Left": 71.63, + "Right": 77.74 + }, + "Mc409": { + "Left": 71.41, + "Right": 74.86 + }, + "Mc410": { + "Left": 73.23, + "Right": 74.21 + }, + "Mc412": { + "Left": 69.78, + "Right": 70.65 + }, + "Mc413": { + "Left": 69.45, + "Right": 70.29 + }, + "Mc414": { + "Left": 74.64, + "Right": 78.62 + }, + "Mc415": { + "Left": 65.14, + "Right": 65.48 + }, + "Mc416": { + "Left": 70.05, + "Right": 70.37 + }, + "Mc417": { + "Left": 63.42, + "Right": 65.02 + }, + "Mc418": { + "Left": 72.65, + "Right": 74.63 + }, + "MC501": { + "Left": 68.14, + "Right": 69.94 + }, + "MC502": { + "Left": 64.31, + "Right": 65.2 + }, + "MC503": { + "Left": 65.59, + "Right": 66.43 + }, + "MC504": { + "Left": 60.85, + "Right": 61.34 + }, + "MC505": { + "Left": 62.36, + "Right": 64.82 + }, + "MC506": { + "Left": 64.76, + "Right": 65.66 + }, + "MC507": { + "Left": 67.43, + "Right": 67.76 + }, + "MC508": { + "Left": 66.59, + "Right": 66.88 + }, + "MC509": { + "Left": 70.3, + "Right": 69.07 + }, + "MC510": { + "Left": 73.23, + "Right": 73.39 + }, + "MC511": { + "Left": 71.9, + "Right": 73.65 + }, + "MC512": { + "Left": 72.43, + "Right": 75.25 + }, + "MC513": { + "Left": 72.43, + "Right": 73.65 + }, + "MC514": { + "Left": 69.96, + "Right": 70.9 + }, + "MC515": { + "Left": 68.61, + "Right": 70.55 + }, + "MC516": { + "Left": 65.45, + "Right": 71.82 + }, + "MC517": { + "Left": 75.98, + "Right": 73.23 + }, + "MC518": { + "Left": 66.83, + "Right": 73.57 + }, + "MC519": { + "Left": 73.85, + "Right": 75.39 + }, + "MC520": { + "Left": 70.33, + "Right": 68.48 + }, + "MC521": { + "Left": 68.33, + "Right": 67.06 + }, + "MC522": { + "Left": 65.76, + "Right": 65.99 + }, + "MC523": { + "Left": 66.56, + "Right": 68.77 + }, + "MC524": { + "Left": 67.21, + "Right": 67.87 + }, + "MC525": { + "Left": 66.13, + "Right": 66.62 + }, + "MC526": { + "Left": 63.22, + "Right": 65.08 + }, + "MC527": { + "Left": 62.69, + "Right": 65.18 + }, + "MC528": { + "Left": 65.84, + "Right": 66.12 + }, + "MC529": { + "Left": 65.42, + "Right": 66.51 + }, + "MC530": { + "Left": 69.33, + "Right": 77.59 + }, + "MC531": { + "Left": 66.67, + "Right": 66.77 + }, + "MC532": { + "Left": 68.63, + "Right": 69.08 + }, + "MC533": { + "Left": 73.47, + "Right": 79.24 + }, + "MC534": { + "Left": 73.24, + "Right": 73.91 + }, + "MC535": { + "Left": 70.47, + "Right": 70.77 + }, + "MC536": { + "Left": 67.77, + "Right": 68.23 + }, + "MC537": { + "Left": 69.84, + "Right": 71.41 + }, + "MC538": { + "Left": 65.12, + "Right": 68.15 + }, + "MC539": { + "Left": 62.91, + "Right": 63.54 + }, + "MC540": { + "Left": 62.53, + "Right": 63.13 + }, + "MC541": { + "Left": 65.16, + "Right": 66.58 + }, + "PL101": { + "Left": 71.21, + "Right": 72.95 + }, + "Pl102": { + "Left": 78.66, + "Right": 80.02 + }, + "Pl103": { + "Left": 71.6, + "Right": 76.37 + }, + "Pl105": { + "Left": 69.07, + "Right": 69.7 + }, + "Pl106": { + "Left": 71.24, + "Right": 74.19 + }, + "PL107": { + "Left": 71.54, + "Right": 74.35 + }, + "PL108": { + "Left": 78.98, + "Right": 79.34 + }, + "Pl109": { + "Left": 72.15, + "Right": 74.69 + }, + "Pl112": { + "Left": 69.85, + "Right": 72.15 + }, + "Pl114": { + "Left": 70.0, + "Right": 73.12 + }, + "PL116": { + "Left": 74.19, + "Right": 81.3 + }, + "Pl119": { + "Left": 78.77, + "Right": 80.57 + }, + "PL120": { + "Left": 74.1, + "Right": 71.27 + }, + "PL201": { + "Left": 72.35, + "Right": 73.5 + }, + "PL202": { + "Left": 70.58, + "Right": 72.01 + }, + "Pl203": { + "Left": 78.15, + "Right": 78.29 + }, + "Pl204": { + "Left": 69.28, + "Right": 69.72 + }, + "Pl205": { + "Left": 71.43, + "Right": 70.7 + }, + "Pl206": { + "Left": 69.69, + "Right": 70.12 + }, + "Pl209": { + "Left": 80.4, + "Right": 76.71 + }, + "Pl210": { + "Left": 67.23, + "Right": 67.72 + }, + "Pl211": { + "Left": 69.37, + "Right": 69.95 + }, + "Pl213": { + "Left": 74.63, + "Right": 75.27 + }, + "Pl215": { + "Left": 78.19, + "Right": 78.1 + }, + "Pl216": { + "Left": 68.25, + "Right": 70.12 + }, + "Pl217": { + "Left": 69.28, + "Right": 70.24 + }, + "Pl218": { + "Left": 69.18, + "Right": 70.0 + }, + "Pl219": { + "Left": 67.58, + "Right": 67.94 + }, + "Pl220": { + "Left": 70.64, + "Right": 70.62 + }, + "Pl221": { + "Left": 73.89, + "Right": 77.45 + }, + "Pl222": { + "Left": 67.31, + "Right": 69.18 + }, + "Pl223": { + "Left": 67.47, + "Right": 71.23 + }, + "Pl224": { + "Left": 71.97, + "Right": 74.81 + }, + "Pl227": { + "Left": 65.45, + "Right": 67.4 + }, + "Pl228": { + "Left": 66.81, + "Right": 66.18 + }, + "Pl230": { + "Left": 72.24, + "Right": 72.82 + }, + "Pl231": { + "Left": 70.48, + "Right": 70.82 + }, + "Pl234": { + "Left": 74.68, + "Right": 72.72 + }, + "PL301": { + "Left": 72.37, + "Right": 74.16 + }, + "PL302": { + "Left": 69.0, + "Right": 70.47 + }, + "PL303": { + "Left": 66.27, + "Right": 71.57 + }, + "PL304": { + "Left": 67.21, + "Right": 70.25 + }, + "PL305": { + "Left": 70.15, + "Right": 75.97 + }, + "PL306": { + "Left": 81.27, + "Right": 77.65 + }, + "PL307": { + "Left": 81.93, + "Right": 76.98 + }, + "PL308": { + "Left": 76.22, + "Right": 77.38 + }, + "PL309": { + "Left": 62.44, + "Right": 62.5 + }, + "PL310": { + "Left": 76.45, + "Right": 74.49 + }, + "PL311": { + "Left": 66.74, + "Right": 69.63 + }, + "PL312": { + "Left": 63.95, + "Right": 63.75 + }, + "PL313": { + "Left": 61.43, + "Right": 61.59 + }, + "PL314": { + "Left": 66.64, + "Right": 67.58 + }, + "PL315": { + "Left": 80.41, + "Right": 80.38 + }, + "PL316": { + "Left": 73.82, + "Right": 70.09 + }, + "PL317": { + "Left": 68.28, + "Right": 69.76 + }, + "PL318": { + "Left": 72.98, + "Right": 74.67 + }, + "PL319": { + "Left": 71.32, + "Right": 71.14 + }, + "PL320": { + "Left": 71.1, + "Right": 71.2 + }, + "PL321": { + "Left": 77.63, + "Right": 77.8 + }, + "PL322": { + "Left": 67.93, + "Right": 72.73 + }, + "PL323": { + "Left": 71.15, + "Right": 73.16 + }, + "PL324": { + "Left": 70.9, + "Right": 72.95 + }, + "PL325": { + "Left": 72.21, + "Right": 78.0 + }, + "PL326": { + "Left": 78.74, + "Right": 78.51 + }, + "PL327": { + "Left": 72.23, + "Right": 79.93 + }, + "PL328": { + "Left": 73.68, + "Right": 71.4 + }, + "PL329": { + "Left": 78.18, + "Right": 73.71 + }, + "PL330": { + "Left": 77.03, + "Right": 77.19 + }, + "PL331": { + "Left": 74.03, + "Right": 74.3 + }, + "PL332": { + "Left": 77.26, + "Right": 78.85 + }, + "PL333": { + "Left": 73.88, + "Right": 72.44 + }, + "PL334": { + "Left": 74.52, + "Right": 71.12 + }, + "PL335": { + "Left": 73.89, + "Right": 70.67 + }, + "PL336": { + "Left": 73.0, + "Right": 73.73 + }, + "PL337": { + "Left": 70.53, + "Right": 72.1 + }, + "PL338": { + "Left": 68.96, + "Right": 67.79 + }, + "PL339": { + "Left": 74.25, + "Right": 74.05 + }, + "PL340": { + "Left": 68.43, + "Right": 68.37 + }, + "PL341": { + "Left": 74.4, + "Right": 72.37 + }, + "PL401": { + "Left": 67.64, + "Right": 72.27 + }, + "PL402": { + "Left": 75.47, + "Right": 74.67 + }, + "PL403": { + "Left": 68.32, + "Right": 72.5 + }, + "PL404": { + "Left": 75.71, + "Right": 78.21 + }, + "PL405": { + "Left": 76.95, + "Right": 77.46 + }, + "PL406": { + "Left": 62.79, + "Right": 66.0 + }, + "PL407": { + "Left": 61.14, + "Right": 62.82 + }, + "PL408": { + "Left": 69.0, + "Right": 75.5 + }, + "PL409": { + "Left": 83.99, + "Right": 78.44 + }, + "PL410": { + "Left": 79.57, + "Right": 82.0 + }, + "PL411": { + "Left": 78.53, + "Right": 78.54 + }, + "PL412": { + "Left": 65.95, + "Right": 64.87 + }, + "PL413": { + "Left": 67.01, + "Right": 68.63 + }, + "PL414": { + "Left": 72.65, + "Right": 72.64 + }, + "PL415": { + "Left": 68.88, + "Right": 71.56 + }, + "PL416": { + "Left": 67.42, + "Right": 67.69 + }, + "PL417": { + "Left": 70.1, + "Right": 71.58 + }, + "PL418": { + "Left": 67.83, + "Right": 67.27 + }, + "PL419": { + "Left": 68.57, + "Right": 68.64 + }, + "PL420": { + "Left": 61.0, + "Right": 61.74 + }, + "PL421": { + "Left": 60.02, + "Right": 60.43 + }, + "PL422": { + "Left": 72.53, + "Right": 65.91 + }, + "PL423": { + "Left": 78.86, + "Right": 75.76 + }, + "PL424": { + "Left": 73.14, + "Right": 69.44 + }, + "PL425": { + "Left": 69.9, + "Right": 72.26 + }, + "PL426": { + "Left": 66.36, + "Right": 67.27 + }, + "PL427": { + "Left": 60.75, + "Right": 60.75 + }, + "PL428": { + "Left": 68.65, + "Right": 70.71 + }, + "PL429": { + "Left": 62.92, + "Right": 65.26 + }, + "PL430": { + "Left": 62.35, + "Right": 62.46 + }, + "PL431": { + "Left": 67.18, + "Right": 68.34 + }, + "PL432": { + "Left": 63.79, + "Right": 64.02 + }, + "PL433": { + "Left": 61.3, + "Right": 61.31 + }, + "PL434": { + "Left": 62.0, + "Right": 63.32 + }, + "PL435": { + "Left": 62.1, + "Right": 62.7 + }, + "PL436": { + "Left": 64.3, + "Right": 64.03 + }, + "PL437": { + "Left": 76.87, + "Right": 76.43 + }, + "PL438": { + "Left": 78.92, + "Right": 78.2 + }, + "PL439": { + "Left": 76.69, + "Right": 76.18 + }, + "Rf101": { + "Left": 62.65, + "Right": 63.0 + }, + "Rf102": { + "Left": 66.26, + "Right": 66.1 + }, + "Rf103": { + "Left": 63.48, + "Right": 66.98 + }, + "Rf104": { + "Left": 62.64, + "Right": 63.09 + }, + "Rf105": { + "Left": 70.64, + "Right": 70.53 + }, + "Rf106": { + "Left": 63.06, + "Right": 64.54 + }, + "Rf108": { + "Left": 64.6, + "Right": 64.62 + }, + "Rf109": { + "Left": 62.7, + "Right": 63.28 + }, + "Rf110": { + "Left": 63.72, + "Right": 62.92 + }, + "Rf111": { + "Left": 64.74, + "Right": 66.86 + }, + "Rf112": { + "Left": 63.68, + "Right": 64.99 + }, + "Rf113": { + "Left": 86.13, + "Right": 87.09 + }, + "Rf114": { + "Left": 75.1, + "Right": 75.94 + }, + "Rf115": { + "Left": 92.54, + "Right": 93.58 + }, + "Rf116": { + "Left": 78.76, + "Right": 79.91 + }, + "Rf117": { + "Left": 75.39, + "Right": 76.34 + }, + "Rf118": { + "Left": 63.73, + "Right": 64.81 + }, + "Rf120": { + "Left": 64.33, + "Right": 73.72 + }, + "Rf121": { + "Left": 68.53, + "Right": 71.51 + }, + "Rf122": { + "Left": 72.45, + "Right": 66.07 + }, + "Rf124": { + "Left": 61.52, + "Right": 61.91 + }, + "Rf125": { + "Left": 66.08, + "Right": 67.39 + }, + "Rf126": { + "Left": 60.62, + "Right": 62.04 + }, + "Rf127": { + "Left": 60.59, + "Right": 60.83 + }, + "Rf128": { + "Left": 64.0, + "Right": 65.18 + }, + "Rf129": { + "Left": 62.12, + "Right": 62.96 + }, + "Rf130": { + "Left": 63.65, + "Right": 63.46 + }, + "Rf131": { + "Left": 62.53, + "Right": 63.78 + }, + "Rf132": { + "Left": 64.62, + "Right": 64.6 + }, + "Rf133": { + "Left": 63.8, + "Right": 64.61 + }, + "Rf134": { + "Left": 67.23, + "Right": 68.55 + }, + "Rf136": { + "Left": 71.33, + "Right": 71.65 + }, + "Rf137": { + "Left": 65.75, + "Right": 66.63 + }, + "Rf139": { + "Left": 69.29, + "Right": 69.27 + }, + "Rf140": { + "Left": 63.46, + "Right": 64.08 + }, + "Rf141": { + "Left": 63.09, + "Right": 64.25 + }, + "Rf143": { + "Left": 72.81, + "Right": 73.81 + }, + "Rf201": { + "Left": 67.04, + "Right": 67.03 + }, + "Rf203": { + "Left": 62.83, + "Right": 63.52 + }, + "Rf204": { + "Left": 68.99, + "Right": 72.62 + }, + "Rf205": { + "Left": 66.84, + "Right": 69.11 + }, + "Rf206": { + "Left": 66.01, + "Right": 66.39 + }, + "Rf207": { + "Left": 69.92, + "Right": 71.33 + }, + "Rf208": { + "Left": 63.11, + "Right": 62.92 + }, + "Rf209": { + "Left": 63.67, + "Right": 63.98 + }, + "Rf210": { + "Left": 65.96, + "Right": 65.05 + }, + "Rf211": { + "Left": 62.44, + "Right": 62.14 + }, + "Rf212": { + "Left": 66.65, + "Right": 64.35 + }, + "Rf213": { + "Left": 70.78, + "Right": 71.13 + }, + "Rf214": { + "Left": 68.89, + "Right": 72.08 + }, + "Rf215": { + "Left": 63.28, + "Right": 63.18 + }, + "Rf216": { + "Left": 64.56, + "Right": 65.91 + }, + "Rf217": { + "Left": 72.23, + "Right": 75.98 + }, + "Rf218": { + "Left": 65.04, + "Right": 66.21 + }, + "Rf219": { + "Left": 64.16, + "Right": 65.43 + }, + "Rf220": { + "Left": 70.25, + "Right": 74.35 + }, + "Rf221": { + "Left": 62.77, + "Right": 62.41 + }, + "Rf222": { + "Left": 70.64, + "Right": 67.68 + }, + "Rf223": { + "Left": 70.71, + "Right": 70.4 + }, + "Rf225": { + "Left": 66.54, + "Right": 64.3 + }, + "Rf226": { + "Left": 65.59, + "Right": 64.95 + }, + "Rf227": { + "Left": 66.9, + "Right": 65.69 + }, + "RF301": { + "Left": 67.9, + "Right": 72.54 + }, + "RF302": { + "Left": 64.36, + "Right": 70.4 + }, + "RF303": { + "Left": 73.23, + "Right": 74.02 + }, + "RF304": { + "Left": 72.73, + "Right": 66.91 + }, + "RF305": { + "Left": 76.58, + "Right": 78.34 + }, + "RF306": { + "Left": 66.68, + "Right": 68.1 + }, + "RF307": { + "Left": 71.71, + "Right": 76.8 + }, + "RF308": { + "Left": 76.31, + "Right": 81.46 + }, + "RF309": { + "Left": 76.87, + "Right": 86.05 + }, + "RF310": { + "Left": 78.3, + "Right": 81.16 + }, + "RF311": { + "Left": 84.7, + "Right": 88.67 + }, + "RF312": { + "Left": 73.57, + "Right": 76.35 + }, + "RF313": { + "Left": 71.16, + "Right": 75.18 + }, + "RF314": { + "Left": 59.35, + "Right": 62.24 + }, + "RF315": { + "Left": 59.17, + "Right": 62.77 + }, + "RF316": { + "Left": 62.8, + "Right": 65.79 + }, + "RF317": { + "Left": 63.6, + "Right": 67.68 + }, + "RF318": { + "Left": 69.06, + "Right": 71.48 + }, + "RF319": { + "Left": 61.13, + "Right": 63.07 + }, + "RF320": { + "Left": 72.2, + "Right": 76.28 + }, + "RF321": { + "Left": 73.74, + "Right": 77.0 + }, + "RF322": { + "Left": 71.23, + "Right": 77.93 + }, + "RF323": { + "Left": 70.99, + "Right": 78.71 + }, + "RF324": { + "Left": 65.31, + "Right": 67.2 + }, + "RF325": { + "Left": 66.57, + "Right": 68.07 + }, + "RF326": { + "Left": 63.46, + "Right": 62.29 + }, + "RF327": { + "Left": 64.78, + "Right": 65.1 + }, + "RF328": { + "Left": 69.32, + "Right": 66.48 + }, + "RF329": { + "Left": 64.85, + "Right": 67.45 + }, + "RF330": { + "Left": 60.43, + "Right": 60.82 + }, + "RF331": { + "Left": 65.2, + "Right": 65.13 + }, + "RF332": { + "Left": 61.26, + "Right": 61.36 + }, + "RF333": { + "Left": 61.3, + "Right": 60.49 + }, + "RF334": { + "Left": 66.16, + "Right": 69.71 + }, + "RF335": { + "Left": 70.68, + "Right": 71.68 + }, + "RF336": { + "Left": 62.41, + "Right": 64.43 + }, + "RF337": { + "Left": 65.51, + "Right": 72.65 + }, + "RF338": { + "Left": 69.39, + "Right": 66.12 + }, + "RF339": { + "Left": 79.3, + "Right": 80.84 + }, + "RF340": { + "Left": 78.14, + "Right": 82.69 + }, + "RF341": { + "Left": 68.04, + "Right": 67.64 + }, + "RF342": { + "Left": 73.66, + "Right": 74.74 + }, + "RF343": { + "Left": 67.81, + "Right": 68.62 + }, + "RF344": { + "Left": 60.39, + "Right": 61.56 + }, + "Rp102": { + "Left": 71.27, + "Right": 69.91 + }, + "Rp103": { + "Left": 72.53, + "Right": 69.72 + }, + "Rp104": { + "Left": 69.71, + "Right": 75.52 + }, + "Rp105": { + "Left": 67.61, + "Right": 67.86 + }, + "Rp108": { + "Left": 70.64, + "Right": 69.38 + }, + "Rp110": { + "Left": 66.84, + "Right": 68.24 + }, + "Rp113": { + "Left": 65.38, + "Right": 66.41 + }, + "Rp114": { + "Left": 67.5, + "Right": 70.91 + }, + "Rp115": { + "Left": 70.76, + "Right": 70.13 + }, + "Rp116": { + "Left": 67.16, + "Right": 69.69 + }, + "Rp117": { + "Left": 71.18, + "Right": 75.39 + }, + "Rp118": { + "Left": 69.05, + "Right": 68.5 + }, + "Rp119": { + "Left": 67.23, + "Right": 69.66 + }, + "Rp120": { + "Left": 71.4, + "Right": 71.24 + }, + "Rp121": { + "Left": 68.73, + "Right": 66.5 + }, + "Rp122": { + "Left": 71.38, + "Right": 69.49 + }, + "Rp123": { + "Left": 72.88, + "Right": 73.81 + }, + "Rp124": { + "Left": 68.41, + "Right": 70.84 + }, + "Rp125": { + "Left": 68.38, + "Right": 69.96 + }, + "Rp126": { + "Left": 71.79, + "Right": 69.08 + }, + "Rp127": { + "Left": 68.45, + "Right": 69.4 + }, + "Rp128": { + "Left": 66.07, + "Right": 69.46 + }, + "Rp129": { + "Left": 71.4, + "Right": 70.89 + }, + "Rp130": { + "Left": 66.64, + "Right": 69.17 + }, + "Rp131": { + "Left": 69.49, + "Right": 71.34 + }, + "Rp132": { + "Left": 69.71, + "Right": 72.31 + }, + "Rp133": { + "Left": 68.82, + "Right": 67.37 + }, + "Rp134": { + "Left": 70.34, + "Right": 74.1 + }, + "Rp135": { + "Left": 68.19, + "Right": 69.29 + }, + "Rp136": { + "Left": 70.91, + "Right": 71.76 + }, + "Rp137": { + "Left": 73.78, + "Right": 75.22 + }, + "Rp138": { + "Left": 73.3, + "Right": 70.54 + }, + "Rp139": { + "Left": 72.16, + "Right": 74.39 + }, + "Rp201": { + "Left": 61.99, + "Right": 62.41 + }, + "Rp203": { + "Left": 62.84, + "Right": 63.31 + }, + "Rp204": { + "Left": 61.66, + "Right": 62.57 + }, + "Rp206": { + "Left": 61.42, + "Right": 62.06 + }, + "Rp207": { + "Left": 62.51, + "Right": 63.25 + }, + "Rp208": { + "Left": 61.63, + "Right": 62.3 + }, + "Rp209": { + "Left": 61.67, + "Right": 62.33 + }, + "Rp210": { + "Left": 60.26, + "Right": 60.97 + }, + "Rp211": { + "Left": 61.31, + "Right": 61.91 + }, + "Rp212": { + "Left": 62.34, + "Right": 63.24 + }, + "Rp213": { + "Left": 61.67, + "Right": 62.27 + }, + "Rp214": { + "Left": 61.69, + "Right": 62.29 + }, + "Rp215": { + "Left": 62.8, + "Right": 63.63 + }, + "Rp217": { + "Left": 63.5, + "Right": 63.81 + }, + "Rp218": { + "Left": 62.09, + "Right": 62.48 + }, + "Rp219": { + "Left": 64.52, + "Right": 64.93 + }, + "Rp220": { + "Left": 65.53, + "Right": 65.54 + }, + "Rp221": { + "Left": 66.47, + "Right": 67.07 + }, + "Rp223": { + "Left": 61.65, + "Right": 64.52 + }, + "Rp224": { + "Left": 62.27, + "Right": 62.96 + }, + "Rp226": { + "Left": 60.71, + "Right": 62.02 + }, + "Rp227": { + "Left": 62.64, + "Right": 63.55 + }, + "Rp228": { + "Left": 60.84, + "Right": 61.02 + }, + "Rp229": { + "Left": 61.62, + "Right": 62.21 + }, + "Rp230": { + "Left": 60.4, + "Right": 61.28 + }, + "Rp232": { + "Left": 62.44, + "Right": 62.74 + }, + "Rp233": { + "Left": 64.74, + "Right": 65.15 + }, + "RP301": { + "Left": 59.56, + "Right": 59.63 + }, + "RP302": { + "Left": 61.04, + "Right": 61.16 + }, + "RP303": { + "Left": 60.33, + "Right": 60.06 + }, + "RP304": { + "Left": 59.26, + "Right": 60.43 + }, + "RP305": { + "Left": 73.87, + "Right": 79.44 + }, + "RP306": { + "Left": 70.1, + "Right": 71.44 + }, + "RP307": { + "Left": 72.36, + "Right": 70.18 + }, + "RP308": { + "Left": 72.76, + "Right": 71.25 + }, + "RP309": { + "Left": 71.69, + "Right": 70.16 + }, + "RP310": { + "Left": 70.76, + "Right": 72.12 + }, + "RP311": { + "Left": 74.84, + "Right": 71.04 + }, + "RP312": { + "Left": 69.65, + "Right": 69.6 + }, + "RP313": { + "Left": 72.58, + "Right": 74.1 + }, + "RP314": { + "Left": 80.09, + "Right": 80.81 + }, + "RP315": { + "Left": 80.3, + "Right": 81.5 + }, + "RP316": { + "Left": 74.82, + "Right": 78.38 + }, + "RP317": { + "Left": 74.04, + "Right": 71.35 + }, + "RP318": { + "Left": 72.92, + "Right": 75.36 + }, + "RP319": { + "Left": 71.14, + "Right": 70.08 + }, + "RP320": { + "Left": 72.54, + "Right": 75.86 + }, + "RP321": { + "Left": 68.69, + "Right": 72.2 + }, + "RP322": { + "Left": 64.61, + "Right": 66.95 + }, + "RP323": { + "Left": 68.88, + "Right": 69.2 + }, + "RP324": { + "Left": 61.38, + "Right": 61.61 + }, + "RP325": { + "Left": 64.97, + "Right": 64.44 + }, + "RP326": { + "Left": 60.6, + "Right": 60.85 + }, + "RP327": { + "Left": 61.05, + "Right": 61.0 + }, + "RP328": { + "Left": 59.98, + "Right": 60.32 + }, + "RP329": { + "Left": 62.62, + "Right": 63.36 + }, + "RP330": { + "Left": 63.02, + "Right": 65.19 + }, + "RP331": { + "Left": 68.94, + "Right": 68.83 + }, + "RP332": { + "Left": 74.27, + "Right": 80.46 + }, + "RP333": { + "Left": 73.21, + "Right": 71.8 + }, + "RP334": { + "Left": 67.06, + "Right": 68.25 + }, + "RP335": { + "Left": 74.1, + "Right": 74.46 + }, + "Rs101": { + "Left": 71.92, + "Right": 72.97 + }, + "Rs102": { + "Left": 75.73, + "Right": 76.22 + }, + "Rs103": { + "Left": 71.04, + "Right": 71.37 + }, + "Rs104": { + "Left": 73.36, + "Right": 73.36 + }, + "Rs105": { + "Left": 71.68, + "Right": 72.33 + }, + "Rs106": { + "Left": 75.4, + "Right": 75.38 + }, + "Rs107": { + "Left": 72.34, + "Right": 72.82 + }, + "Rs108": { + "Left": 74.05, + "Right": 74.51 + }, + "Rs109": { + "Left": 73.95, + "Right": 74.12 + }, + "Rs110": { + "Left": 71.77, + "Right": 72.27 + }, + "Rs111": { + "Left": 73.51, + "Right": 74.31 + }, + "Rs112": { + "Left": 72.58, + "Right": 73.56 + }, + "Rs113": { + "Left": 72.31, + "Right": 72.9 + }, + "Rs114": { + "Left": 72.7, + "Right": 73.12 + }, + "Rs115": { + "Left": 73.36, + "Right": 74.04 + }, + "Rs116": { + "Left": 71.43, + "Right": 72.46 + }, + "Rs117": { + "Left": 72.1, + "Right": 73.09 + }, + "Rs118": { + "Left": 73.06, + "Right": 74.55 + }, + "Rs119": { + "Left": 73.06, + "Right": 74.46 + }, + "Rs120": { + "Left": 71.69, + "Right": 71.44 + }, + "Rs121": { + "Left": 74.25, + "Right": 74.16 + }, + "Rs122": { + "Left": 72.4, + "Right": 72.71 + }, + "Rs123": { + "Left": 73.06, + "Right": 73.34 + }, + "Rs124": { + "Left": 72.43, + "Right": 73.37 + }, + "Rs125": { + "Left": 70.87, + "Right": 71.49 + }, + "Rs126": { + "Left": 71.48, + "Right": 72.52 + }, + "Rs127": { + "Left": 73.34, + "Right": 73.92 + }, + "Rs201": { + "Left": 75.0, + "Right": 75.58 + }, + "Rs202": { + "Left": 72.67, + "Right": 73.19 + }, + "Rs203": { + "Left": 73.23, + "Right": 73.39 + }, + "Rs204": { + "Left": 75.2, + "Right": 75.93 + }, + "Rs205": { + "Left": 73.29, + "Right": 74.34 + }, + "Rs206": { + "Left": 73.94, + "Right": 75.51 + }, + "Rs207": { + "Left": 73.49, + "Right": 74.08 + }, + "Rs208": { + "Left": 73.96, + "Right": 74.76 + }, + "Rs209": { + "Left": 73.61, + "Right": 74.18 + }, + "Rs210": { + "Left": 77.16, + "Right": 77.22 + }, + "Rs211": { + "Left": 74.82, + "Right": 74.96 + }, + "Rs212": { + "Left": 72.71, + "Right": 73.36 + }, + "Rs213": { + "Left": 71.23, + "Right": 74.36 + }, + "Rs214": { + "Left": 75.08, + "Right": 75.62 + }, + "Rs215": { + "Left": 74.53, + "Right": 74.42 + }, + "Rs216": { + "Left": 72.0, + "Right": 72.62 + }, + "Rs217": { + "Left": 73.49, + "Right": 76.04 + }, + "Rs218": { + "Left": 73.15, + "Right": 75.31 + }, + "Rs219": { + "Left": 73.35, + "Right": 72.98 + }, + "Rs220": { + "Left": 73.47, + "Right": 75.27 + }, + "Rs221": { + "Left": 73.61, + "Right": 74.9 + }, + "Rs222": { + "Left": 80.79, + "Right": 80.77 + }, + "Rs223": { + "Left": 79.05, + "Right": 79.34 + }, + "Rs224": { + "Left": 72.98, + "Right": 73.5 + }, + "Rs225": { + "Left": 71.87, + "Right": 72.6 + }, + "Rs226": { + "Left": 73.84, + "Right": 74.61 + }, + "Rs227": { + "Left": 73.81, + "Right": 76.51 + }, + "Rs228": { + "Left": 72.99, + "Right": 73.6 + }, + "Rs229": { + "Left": 72.5, + "Right": 73.41 + }, + "Rs230": { + "Left": 74.6, + "Right": 75.05 + }, + "Rs231": { + "Left": 73.06, + "Right": 73.81 + }, + "Rs232": { + "Left": 72.83, + "Right": 73.54 + }, + "Rs303": { + "Left": 74.09, + "Right": 74.77 + }, + "Rs304": { + "Left": 76.1, + "Right": 76.67 + }, + "Rs305": { + "Left": 74.38, + "Right": 74.43 + }, + "Rs306": { + "Left": 73.57, + "Right": 73.83 + }, + "Rs308": { + "Left": 72.42, + "Right": 72.78 + }, + "Rs309": { + "Left": 72.28, + "Right": 74.88 + }, + "Rs310": { + "Left": 75.27, + "Right": 75.73 + }, + "Rs311": { + "Left": 82.94, + "Right": 83.79 + }, + "Rs312": { + "Left": 77.42, + "Right": 78.51 + }, + "Rs313": { + "Left": 74.52, + "Right": 75.06 + }, + "Rs314": { + "Left": 72.75, + "Right": 73.04 + }, + "Rs315": { + "Left": 72.31, + "Right": 73.19 + }, + "Rs317": { + "Left": 73.39, + "Right": 73.96 + }, + "Rs318": { + "Left": 74.19, + "Right": 74.67 + }, + "Rs319": { + "Left": 76.86, + "Right": 77.88 + }, + "Rs320": { + "Left": 77.14, + "Right": 77.77 + }, + "Rs321": { + "Left": 74.05, + "Right": 74.36 + }, + "Rs322": { + "Left": 76.35, + "Right": 76.92 + }, + "Rs323": { + "Left": 72.15, + "Right": 72.65 + }, + "Rs324": { + "Left": 72.87, + "Right": 73.39 + }, + "Rs325": { + "Left": 73.76, + "Right": 74.25 + }, + "Rs326": { + "Left": 74.33, + "Right": 75.15 + }, + "Rs327": { + "Left": 74.27, + "Right": 74.91 + }, + "Rs328": { + "Left": 73.58, + "Right": 80.69 + }, + "Rs329": { + "Left": 74.94, + "Right": 75.3 + }, + "Rs330": { + "Left": 72.07, + "Right": 73.44 + }, + "Rs331": { + "Left": 73.04, + "Right": 73.36 + }, + "RS401": { + "Left": 64.48, + "Right": 66.03 + }, + "RS402": { + "Left": 72.7, + "Right": 74.25 + }, + "RS403": { + "Left": 77.36, + "Right": 82.98 + }, + "RS404": { + "Left": 69.55, + "Right": 71.39 + }, + "RS405": { + "Left": 76.12, + "Right": 76.49 + }, + "RS406": { + "Left": 75.37, + "Right": 76.11 + }, + "RS407": { + "Left": 74.24, + "Right": 73.45 + }, + "RS408": { + "Left": 70.78, + "Right": 70.79 + }, + "RS409": { + "Left": 67.26, + "Right": 68.25 + }, + "RS410": { + "Left": 72.3, + "Right": 75.28 + }, + "RS411": { + "Left": 71.53, + "Right": 73.93 + }, + "RS412": { + "Left": 68.79, + "Right": 69.0 + }, + "RS413": { + "Left": 68.53, + "Right": 68.75 + }, + "RS414": { + "Left": 67.26, + "Right": 69.1 + }, + "RS415": { + "Left": 72.4, + "Right": 72.73 + }, + "RS416": { + "Left": 68.05, + "Right": 68.13 + }, + "RS417": { + "Left": 76.38, + "Right": 74.73 + }, + "RS418": { + "Left": 79.79, + "Right": 80.59 + }, + "RS419": { + "Left": 72.04, + "Right": 71.29 + }, + "RS420": { + "Left": 67.74, + "Right": 68.24 + }, + "RS421": { + "Left": 71.31, + "Right": 75.1 + }, + "RS422": { + "Left": 71.72, + "Right": 73.23 + }, + "RS423": { + "Left": 69.05, + "Right": 69.25 + }, + "RS424": { + "Left": 65.62, + "Right": 65.75 + }, + "RS425": { + "Left": 61.9, + "Right": 62.15 + }, + "RS426": { + "Left": 65.99, + "Right": 68.39 + }, + "RS427": { + "Left": 68.3, + "Right": 69.54 + }, + "RS428": { + "Left": 69.0, + "Right": 70.17 + }, + "RS429": { + "Left": 65.61, + "Right": 64.99 + }, + "RS430": { + "Left": 65.31, + "Right": 65.18 + }, + "RS431": { + "Left": 66.45, + "Right": 66.49 + }, + "RS432": { + "Left": 73.37, + "Right": 74.86 + }, + "RS433": { + "Left": 65.96, + "Right": 66.83 + }, + "RS434": { + "Left": 68.13, + "Right": 68.41 + }, + "RS435": { + "Left": 64.55, + "Right": 64.84 + }, + "RS436": { + "Left": 68.93, + "Right": 69.31 + }, + "RS437": { + "Left": 67.71, + "Right": 67.68 + }, + "RS438": { + "Left": 69.87, + "Right": 70.13 + }, + "RS439": { + "Left": 73.37, + "Right": 71.96 + }, + "RS440": { + "Left": 70.9, + "Right": 71.17 + }, + "sc101": { + "Left": 69.92, + "Right": 68.56 + }, + "sc102": { + "Left": 73.36, + "Right": 70.21 + }, + "sc103": { + "Left": 68.38, + "Right": 68.43 + }, + "sc104": { + "Left": 69.62, + "Right": 68.77 + }, + "sc105": { + "Left": 69.6, + "Right": 69.53 + }, + "sc106": { + "Left": 67.85, + "Right": 67.91 + }, + "sc107": { + "Left": 68.15, + "Right": 68.24 + }, + "sc108": { + "Left": 68.46, + "Right": 68.4 + }, + "sc109": { + "Left": 66.66, + "Right": 66.97 + }, + "sc110": { + "Left": 67.01, + "Right": 67.21 + }, + "sc111": { + "Left": 67.55, + "Right": 68.41 + }, + "sc112": { + "Left": 70.29, + "Right": 68.72 + }, + "sc113": { + "Left": 68.01, + "Right": 68.34 + }, + "sc114": { + "Left": 68.08, + "Right": 67.41 + }, + "sc115": { + "Left": 66.76, + "Right": 67.05 + }, + "sc116": { + "Left": 75.21, + "Right": 73.42 + }, + "sc117": { + "Left": 65.91, + "Right": 66.68 + }, + "sc118": { + "Left": 68.29, + "Right": 68.33 + }, + "sc119": { + "Left": 68.14, + "Right": 68.35 + }, + "sc120": { + "Left": 69.69, + "Right": 70.09 + }, + "sc121": { + "Left": 69.67, + "Right": 69.8 + }, + "sc122": { + "Left": 69.65, + "Right": 70.96 + }, + "sc123": { + "Left": 69.45, + "Right": 69.85 + }, + "sc124": { + "Left": 67.45, + "Right": 68.2 + }, + "sc125": { + "Left": 69.03, + "Right": 70.34 + }, + "sc126": { + "Left": 67.81, + "Right": 68.54 + }, + "sc127": { + "Left": 68.09, + "Right": 67.63 + }, + "sc128": { + "Left": 68.99, + "Right": 68.54 + }, + "sc129": { + "Left": 68.14, + "Right": 68.18 + }, + "sc130": { + "Left": 68.06, + "Right": 67.91 + }, + "sc131": { + "Left": 70.13, + "Right": 70.21 + }, + "sc134": { + "Left": 67.67, + "Right": 67.84 + }, + "sc135": { + "Left": 66.29, + "Right": 67.22 + }, + "sc136": { + "Left": 65.51, + "Right": 66.08 + }, + "sc137": { + "Left": 68.9, + "Right": 69.23 + }, + "sc138": { + "Left": 68.38, + "Right": 68.6 + }, + "sc139": { + "Left": 65.99, + "Right": 65.31 + }, + "sc140": { + "Left": 71.0, + "Right": 71.25 + }, + "sc141": { + "Left": 69.13, + "Right": 69.33 + }, + "sc142": { + "Left": 68.57, + "Right": 68.85 + }, + "sc143": { + "Left": 68.56, + "Right": 68.6 + }, + "sc144": { + "Left": 68.78, + "Right": 68.46 + }, + "SC201": { + "Left": 66.92, + "Right": 66.99 + }, + "SC202": { + "Left": 68.46, + "Right": 68.26 + }, + "SC203": { + "Left": 68.71, + "Right": 71.38 + }, + "SC204": { + "Left": 65.73, + "Right": 65.91 + }, + "SC205": { + "Left": 71.62, + "Right": 72.14 + }, + "SC206": { + "Left": 72.1, + "Right": 71.73 + }, + "SC207": { + "Left": 71.28, + "Right": 71.53 + }, + "SC208": { + "Left": 65.88, + "Right": 65.33 + }, + "SC209": { + "Left": 72.36, + "Right": 71.48 + }, + "SC210": { + "Left": 67.87, + "Right": 68.31 + }, + "SC211": { + "Left": 76.11, + "Right": 75.59 + }, + "SC212": { + "Left": 69.48, + "Right": 72.56 + }, + "SC213": { + "Left": 69.38, + "Right": 70.59 + }, + "SC214": { + "Left": 66.47, + "Right": 68.19 + }, + "SC215": { + "Left": 65.61, + "Right": 66.01 + }, + "SC216": { + "Left": 68.91, + "Right": 70.75 + }, + "SC217": { + "Left": 66.84, + "Right": 72.42 + }, + "SC218": { + "Left": 68.94, + "Right": 69.08 + }, + "SC219": { + "Left": 67.59, + "Right": 68.11 + }, + "SC220": { + "Left": 69.99, + "Right": 68.31 + }, + "SC221": { + "Left": 66.29, + "Right": 68.01 + }, + "SC222": { + "Left": 68.2, + "Right": 69.53 + }, + "SC223": { + "Left": 69.13, + "Right": 67.29 + }, + "SC224": { + "Left": 82.94, + "Right": 80.42 + }, + "SC225": { + "Left": 78.91, + "Right": 79.84 + }, + "SC226": { + "Left": 77.02, + "Right": 79.99 + }, + "SC227": { + "Left": 76.16, + "Right": 74.46 + }, + "sr101": { + "Left": 78.66, + "Right": 82.27 + }, + "sr103": { + "Left": 71.92, + "Right": 72.36 + }, + "sr105": { + "Left": 75.27, + "Right": 75.43 + }, + "sr107": { + "Left": 70.54, + "Right": 71.3 + }, + "sr108": { + "Left": 78.43, + "Right": 77.86 + }, + "sr109": { + "Left": 78.11, + "Right": 78.51 + }, + "sr110": { + "Left": 76.62, + "Right": 81.19 + }, + "sr111": { + "Left": 74.99, + "Right": 74.25 + }, + "sr112": { + "Left": 73.16, + "Right": 74.84 + }, + "sr114": { + "Left": 72.29, + "Right": 75.03 + }, + "sr115": { + "Left": 72.73, + "Right": 74.72 + }, + "sr116": { + "Left": 72.37, + "Right": 73.29 + }, + "sr117": { + "Left": 73.7, + "Right": 74.29 + }, + "sr118": { + "Left": 75.39, + "Right": 75.84 + }, + "sr119": { + "Left": 74.51, + "Right": 77.38 + }, + "sr120": { + "Left": 71.64, + "Right": 73.88 + }, + "sr121": { + "Left": 77.78, + "Right": 78.38 + }, + "sr123": { + "Left": 74.93, + "Right": 73.58 + }, + "sr124": { + "Left": 74.15, + "Right": 74.45 + }, + "sr125": { + "Left": 70.41, + "Right": 71.14 + }, + "sr126": { + "Left": 75.07, + "Right": 75.66 + }, + "sr128": { + "Left": 76.42, + "Right": 78.04 + }, + "sr129": { + "Left": 75.57, + "Right": 73.88 + }, + "sr130": { + "Left": 79.55, + "Right": 81.5 + }, + "sr131": { + "Left": 73.79, + "Right": 74.41 + }, + "sr132": { + "Left": 72.73, + "Right": 76.27 + }, + "sr133": { + "Left": 74.42, + "Right": 76.54 + }, + "sr134": { + "Left": 71.43, + "Right": 73.63 + }, + "sr137": { + "Left": 73.26, + "Right": 75.3 + }, + "sr139": { + "Left": 76.79, + "Right": 76.35 + }, + "sr140": { + "Left": 71.0, + "Right": 72.43 + }, + "sr144": { + "Left": 78.93, + "Right": 80.96 + }, + "sr145": { + "Left": 74.86, + "Right": 75.82 + }, + "sr146": { + "Left": 69.89, + "Right": 71.23 + }, + "sr147": { + "Left": 70.42, + "Right": 72.0 + }, + "st104": { + "Left": 70.75, + "Right": 72.9 + }, + "st106": { + "Left": 73.45, + "Right": 72.8 + }, + "SR201": { + "Left": 67.24, + "Right": 67.65 + }, + "SR202": { + "Left": 73.24, + "Right": 73.68 + }, + "SR203": { + "Left": 65.92, + "Right": 70.88 + }, + "SR204": { + "Left": 69.37, + "Right": 70.36 + }, + "SR205": { + "Left": 69.55, + "Right": 69.55 + }, + "SR206": { + "Left": 68.0, + "Right": 70.55 + }, + "SR207": { + "Left": 70.25, + "Right": 71.3 + }, + "SR208": { + "Left": 76.83, + "Right": 72.31 + }, + "SR209": { + "Left": 75.76, + "Right": 75.57 + }, + "SR210": { + "Left": 75.45, + "Right": 74.72 + }, + "SR211": { + "Left": 72.62, + "Right": 78.99 + }, + "SR212": { + "Left": 72.92, + "Right": 71.08 + }, + "SR213": { + "Left": 72.43, + "Right": 72.6 + }, + "SR214": { + "Left": 74.14, + "Right": 75.05 + }, + "SR215": { + "Left": 69.67, + "Right": 72.73 + }, + "SR216": { + "Left": 71.27, + "Right": 68.25 + }, + "SR217": { + "Left": 64.35, + "Right": 64.03 + }, + "SR218": { + "Left": 65.7, + "Right": 66.26 + }, + "SR219": { + "Left": 66.27, + "Right": 69.76 + }, + "SR220": { + "Left": 67.21, + "Right": 67.46 + }, + "SR221": { + "Left": 67.69, + "Right": 66.57 + }, + "SR222": { + "Left": 67.12, + "Right": 67.18 + }, + "SR223": { + "Left": 77.17, + "Right": 78.36 + }, + "SR224": { + "Left": 70.74, + "Right": 68.58 + }, + "SR225": { + "Left": 67.67, + "Right": 67.16 + }, + "SR226": { + "Left": 68.72, + "Right": 68.85 + }, + "SR227": { + "Left": 67.64, + "Right": 67.44 + }, + "SR228": { + "Left": 68.97, + "Right": 72.16 + }, + "SR229": { + "Left": 69.9, + "Right": 70.98 + }, + "SR230": { + "Left": 63.7, + "Right": 63.59 + }, + "SR231": { + "Left": 66.05, + "Right": 66.51 + }, + "SR232": { + "Left": 68.69, + "Right": 67.93 + }, + "SR233": { + "Left": 69.09, + "Right": 69.34 + }, + "SR234": { + "Left": 65.41, + "Right": 65.36 + }, + "SR235": { + "Left": 69.68, + "Right": 70.97 + }, + "SR236": { + "Left": 68.33, + "Right": 68.88 + }, + "SR237": { + "Left": 64.48, + "Right": 65.42 + }, + "SR238": { + "Left": 67.29, + "Right": 67.41 + }, + "SR239": { + "Left": 66.57, + "Right": 66.57 + }, + "SR240": { + "Left": 67.57, + "Right": 70.03 + }, + "SR241": { + "Left": 65.9, + "Right": 65.97 + }, + "SR242": { + "Left": 62.05, + "Right": 62.64 + }, + "SR243": { + "Left": 64.89, + "Right": 65.48 + }, + "SR244": { + "Left": 67.96, + "Right": 70.78 + }, + "SR245": { + "Left": 70.51, + "Right": 74.8 + }, + "SR246": { + "Left": 66.51, + "Right": 65.69 + }, + "SR247": { + "Left": 66.64, + "Right": 66.71 + }, + "SR248": { + "Left": 65.23, + "Right": 65.42 + }, + "tm101": { + "Left": 74.82, + "Right": 75.43 + }, + "tm102": { + "Left": 77.73, + "Right": 77.63 + }, + "tm103": { + "Left": 76.42, + "Right": 76.76 + }, + "tm104": { + "Left": 83.75, + "Right": 81.65 + }, + "tm105": { + "Left": 76.1, + "Right": 78.81 + }, + "tm106": { + "Left": 76.4, + "Right": 75.65 + }, + "tm107": { + "Left": 73.13, + "Right": 73.06 + }, + "tm108": { + "Left": 76.4, + "Right": 76.0 + }, + "tm109": { + "Left": 75.12, + "Right": 74.81 + }, + "tm110": { + "Left": 77.47, + "Right": 77.06 + }, + "tm111": { + "Left": 74.94, + "Right": 76.4 + }, + "tm112": { + "Left": 73.3, + "Right": 74.73 + }, + "tm113": { + "Left": 72.36, + "Right": 72.43 + }, + "tm115": { + "Left": 76.67, + "Right": 77.24 + }, + "tm116": { + "Left": 72.65, + "Right": 73.47 + }, + "tm117": { + "Left": 73.58, + "Right": 73.15 + }, + "tm118": { + "Left": 73.06, + "Right": 74.62 + }, + "tm119": { + "Left": 70.69, + "Right": 71.06 + }, + "tm120": { + "Left": 75.66, + "Right": 76.84 + }, + "tm121": { + "Left": 77.86, + "Right": 80.9 + }, + "tm122": { + "Left": 73.3, + "Right": 73.4 + }, + "tm123": { + "Left": 73.36, + "Right": 73.06 + }, + "tm124": { + "Left": 69.53, + "Right": 70.03 + }, + "tm126": { + "Left": 73.79, + "Right": 75.27 + }, + "tm127": { + "Left": 76.03, + "Right": 77.32 + }, + "tm128": { + "Left": 74.11, + "Right": 74.1 + }, + "tm129": { + "Left": 68.55, + "Right": 70.82 + }, + "tm130": { + "Left": 80.73, + "Right": 81.58 + }, + "tm133": { + "Left": 78.07, + "Right": 78.29 + }, + "tm201": { + "Left": 67.33, + "Right": 69.71 + }, + "tm202": { + "Left": 74.84, + "Right": 75.71 + }, + "tm203": { + "Left": 71.15, + "Right": 72.94 + }, + "tm204": { + "Left": 81.59, + "Right": 82.25 + }, + "tm205": { + "Left": 81.19, + "Right": 83.01 + }, + "tm206": { + "Left": 72.38, + "Right": 73.85 + }, + "tm207": { + "Left": 73.97, + "Right": 75.41 + }, + "tm208": { + "Left": 73.32, + "Right": 76.03 + }, + "tm209": { + "Left": 70.72, + "Right": 72.93 + }, + "tm211": { + "Left": 74.97, + "Right": 76.43 + }, + "tm212": { + "Left": 72.95, + "Right": 73.88 + }, + "tm213": { + "Left": 74.64, + "Right": 75.75 + }, + "tm214": { + "Left": 76.62, + "Right": 77.24 + }, + "tm215": { + "Left": 76.83, + "Right": 79.42 + }, + "tm216": { + "Left": 70.43, + "Right": 71.66 + }, + "tm217": { + "Left": 73.62, + "Right": 76.03 + }, + "tm218": { + "Left": 71.32, + "Right": 72.66 + }, + "tm219": { + "Left": 70.04, + "Right": 71.24 + }, + "tm220": { + "Left": 73.73, + "Right": 74.6 + }, + "tm221": { + "Left": 76.75, + "Right": 84.61 + }, + "tm222": { + "Left": 70.15, + "Right": 72.61 + }, + "tm223": { + "Left": 77.37, + "Right": 84.42 + }, + "tm226": { + "Left": 72.03, + "Right": 73.92 + }, + "tm229": { + "Left": 71.36, + "Right": 72.27 + }, + "tm230": { + "Left": 70.44, + "Right": 70.93 + }, + "tm231": { + "Left": 82.29, + "Right": 81.36 + }, + "tm232": { + "Left": 71.77, + "Right": 72.81 + }, + "tm301": { + "Left": 73.31, + "Right": 73.32 + }, + "tm303": { + "Left": 69.09, + "Right": 70.97 + }, + "tm304": { + "Left": 76.62, + "Right": 76.73 + }, + "tm306": { + "Left": 73.64, + "Right": 75.1 + }, + "tm307": { + "Left": 71.78, + "Right": 72.49 + }, + "tm308": { + "Left": 73.01, + "Right": 73.26 + }, + "tm309": { + "Left": 76.08, + "Right": 77.05 + }, + "tm310": { + "Left": 76.37, + "Right": 79.41 + }, + "tm311": { + "Left": 71.05, + "Right": 71.93 + }, + "tm312": { + "Left": 73.63, + "Right": 73.4 + }, + "tm316": { + "Left": 77.5, + "Right": 79.24 + }, + "tm318": { + "Left": 72.85, + "Right": 72.32 + }, + "tm319": { + "Left": 78.41, + "Right": 77.8 + }, + "tm320": { + "Left": 72.26, + "Right": 72.34 + }, + "tm321": { + "Left": 77.92, + "Right": 78.71 + }, + "tm325": { + "Left": 77.68, + "Right": 78.86 + }, + "tm326": { + "Left": 72.89, + "Right": 73.47 + }, + "tm327": { + "Left": 76.71, + "Right": 77.28 + }, + "tm328": { + "Left": 72.02, + "Right": 73.24 + }, + "tm329": { + "Left": 69.99, + "Right": 70.58 + }, + "tm330": { + "Left": 69.97, + "Right": 70.49 + }, + "tm331": { + "Left": 69.5, + "Right": 69.97 + }, + "tm332": { + "Left": 72.19, + "Right": 72.59 + }, + "tm333": { + "Left": 73.84, + "Right": 74.29 + }, + "tm334": { + "Left": 70.34, + "Right": 70.87 + }, + "tm335": { + "Left": 71.2, + "Right": 74.06 + }, + "tm336": { + "Left": 72.43, + "Right": 72.51 + }, + "TM401": { + "Left": 69.9, + "Right": 68.96 + }, + "TM402": { + "Left": 79.28, + "Right": 76.13 + }, + "TM403": { + "Left": 73.44, + "Right": 79.86 + }, + "TM404": { + "Left": 69.5, + "Right": 70.21 + }, + "TM405": { + "Left": 68.8, + "Right": 68.53 + }, + "TM406": { + "Left": 68.54, + "Right": 68.02 + }, + "TM407": { + "Left": 69.46, + "Right": 70.38 + }, + "TM408": { + "Left": 65.2, + "Right": 65.21 + }, + "TM409": { + "Left": 68.13, + "Right": 71.14 + }, + "TM410": { + "Left": 63.47, + "Right": 64.09 + }, + "TM411": { + "Left": 69.36, + "Right": 75.2 + }, + "TM412": { + "Left": 67.46, + "Right": 68.87 + }, + "TM413": { + "Left": 68.81, + "Right": 72.9 + }, + "TM414": { + "Left": 69.62, + "Right": 71.07 + }, + "TM415": { + "Left": 68.22, + "Right": 72.19 + }, + "TM416": { + "Left": 65.55, + "Right": 66.89 + }, + "TM417": { + "Left": 64.71, + "Right": 65.28 + }, + "TM418": { + "Left": 70.47, + "Right": 73.03 + }, + "TM419": { + "Left": 69.07, + "Right": 68.39 + }, + "TM420": { + "Left": 72.64, + "Right": 71.33 + }, + "TM421": { + "Left": 72.49, + "Right": 73.22 + }, + "TM422": { + "Left": 69.89, + "Right": 76.58 + }, + "TM423": { + "Left": 72.42, + "Right": 73.2 + }, + "TM424": { + "Left": 72.32, + "Right": 72.42 + }, + "TM425": { + "Left": 67.93, + "Right": 68.32 + }, + "TM426": { + "Left": 68.16, + "Right": 68.5 + }, + "TM427": { + "Left": 66.67, + "Right": 66.49 + }, + "TM428": { + "Left": 65.99, + "Right": 67.38 + }, + "TM429": { + "Left": 68.38, + "Right": 69.57 + }, + "TM430": { + "Left": 66.43, + "Right": 67.49 + }, + "TM431": { + "Left": 67.08, + "Right": 67.91 + }, + "TM432": { + "Left": 70.1, + "Right": 72.73 + }, + "TM433": { + "Left": 68.01, + "Right": 70.02 + }, + "TM434": { + "Left": 70.79, + "Right": 71.09 + }, + "TM435": { + "Left": 67.81, + "Right": 70.07 + }, + "TM436": { + "Left": 70.66, + "Right": 71.26 + }, + "TM437": { + "Left": 73.09, + "Right": 69.48 + }, + "TM438": { + "Left": 73.61, + "Right": 74.12 + }, + "TM439": { + "Left": 67.41, + "Right": 66.94 + }, + "TM440": { + "Left": 70.95, + "Right": 73.26 + }, + "TM441": { + "Left": 67.44, + "Right": 67.87 + }, + "TM442": { + "Left": 74.36, + "Right": 75.24 + }, + "ts202": { + "Left": 75.23, + "Right": 73.42 + }, + "ts203": { + "Left": 68.99, + "Right": 71.29 + }, + "ts204": { + "Left": 66.72, + "Right": 66.83 + }, + "ts205": { + "Left": 65.01, + "Right": 65.71 + }, + "ts206": { + "Left": 67.12, + "Right": 66.81 + }, + "ts207": { + "Left": 71.07, + "Right": 71.13 + }, + "ts208": { + "Left": 69.02, + "Right": 70.73 + }, + "ts209": { + "Left": 74.85, + "Right": 72.94 + }, + "ts210": { + "Left": 67.62, + "Right": 67.93 + }, + "ts211": { + "Left": 69.05, + "Right": 68.66 + }, + "ts212": { + "Left": 66.47, + "Right": 67.31 + }, + "ts213": { + "Left": 70.71, + "Right": 71.08 + }, + "ts214": { + "Left": 75.69, + "Right": 74.66 + }, + "ts215": { + "Left": 73.19, + "Right": 73.72 + }, + "ts216": { + "Left": 70.17, + "Right": 70.13 + }, + "ts217": { + "Left": 72.26, + "Right": 73.6 + }, + "ts218": { + "Left": 72.9, + "Right": 72.43 + }, + "ts219": { + "Left": 69.81, + "Right": 69.84 + }, + "ts220": { + "Left": 69.69, + "Right": 68.54 + }, + "ts221": { + "Left": 67.59, + "Right": 68.39 + }, + "ts223": { + "Left": 72.19, + "Right": 72.94 + }, + "ts224": { + "Left": 74.98, + "Right": 78.1 + }, + "ts225": { + "Left": 66.98, + "Right": 68.95 + }, + "ts226": { + "Left": 76.47, + "Right": 77.9 + }, + "ts229": { + "Left": 69.8, + "Right": 69.91 + }, + "ts230": { + "Left": 64.29, + "Right": 64.42 + }, + "ts231": { + "Left": 62.69, + "Right": 63.82 + }, + "ts232": { + "Left": 70.57, + "Right": 72.8 + }, + "ts233": { + "Left": 68.89, + "Right": 68.75 + }, + "ts234": { + "Left": 73.82, + "Right": 72.72 + }, + "ts235": { + "Left": 75.93, + "Right": 74.81 + }, + "ts236": { + "Left": 67.44, + "Right": 70.87 + }, + "ts237": { + "Left": 72.67, + "Right": 73.0 + }, + "ts239": { + "Left": 70.57, + "Right": 70.46 + }, + "ts302": { + "Left": 66.55, + "Right": 67.03 + }, + "ts303": { + "Left": 70.76, + "Right": 71.46 + }, + "ts304": { + "Left": 67.09, + "Right": 66.19 + }, + "ts305": { + "Left": 70.72, + "Right": 70.16 + }, + "ts306": { + "Left": 65.29, + "Right": 65.09 + }, + "ts307": { + "Left": 70.34, + "Right": 69.97 + }, + "ts308": { + "Left": 68.15, + "Right": 68.67 + }, + "ts309": { + "Left": 68.18, + "Right": 67.72 + }, + "ts310": { + "Left": 65.93, + "Right": 65.95 + }, + "ts311": { + "Left": 69.44, + "Right": 69.63 + }, + "ts312": { + "Left": 71.6, + "Right": 71.63 + }, + "ts313": { + "Left": 67.39, + "Right": 67.78 + }, + "ts314": { + "Left": 71.59, + "Right": 71.94 + }, + "ts315": { + "Left": 69.34, + "Right": 69.35 + }, + "ts316": { + "Left": 64.41, + "Right": 65.31 + }, + "ts317": { + "Left": 70.07, + "Right": 71.19 + }, + "ts318": { + "Left": 67.97, + "Right": 68.13 + }, + "ts319": { + "Left": 66.63, + "Right": 67.27 + }, + "ts320": { + "Left": 69.13, + "Right": 68.48 + }, + "ts321": { + "Left": 69.23, + "Right": 69.05 + }, + "ts322": { + "Left": 67.43, + "Right": 67.3 + }, + "ts323": { + "Left": 66.3, + "Right": 66.5 + }, + "Ts401": { + "Left": 73.89, + "Right": 73.31 + }, + "Ts405": { + "Left": 71.17, + "Right": 71.3 + }, + "Ts406": { + "Left": 68.44, + "Right": 68.37 + }, + "Ts407": { + "Left": 69.38, + "Right": 70.04 + }, + "Ts408": { + "Left": 70.38, + "Right": 70.01 + }, + "Ts411": { + "Left": 73.85, + "Right": 74.74 + }, + "Ts412": { + "Left": 74.11, + "Right": 75.79 + }, + "Ts413": { + "Left": 75.46, + "Right": 74.95 + }, + "Ts414": { + "Left": 70.04, + "Right": 70.03 + }, + "Ts415": { + "Left": 72.55, + "Right": 72.9 + }, + "Ts416": { + "Left": 71.34, + "Right": 72.56 + }, + "Ts417": { + "Left": 73.38, + "Right": 72.64 + }, + "Ts418": { + "Left": 71.37, + "Right": 71.15 + }, + "Ts419": { + "Left": 73.8, + "Right": 74.43 + }, + "Ts420": { + "Left": 70.31, + "Right": 71.07 + }, + "Ts421": { + "Left": 76.73, + "Right": 75.56 + }, + "Ts422": { + "Left": 67.37, + "Right": 67.49 + }, + "Ts423": { + "Left": 72.35, + "Right": 72.54 + }, + "Ts424": { + "Left": 69.42, + "Right": 69.4 + }, + "Ts425": { + "Left": 70.29, + "Right": 70.61 + }, + "Ts426": { + "Left": 73.14, + "Right": 73.44 + }, + "TS501": { + "Left": 73.52, + "Right": 76.67 + }, + "TS502": { + "Left": 63.9, + "Right": 63.95 + }, + "TS503": { + "Left": 71.31, + "Right": 72.65 + }, + "TS504": { + "Left": 79.4, + "Right": 74.99 + }, + "TS505": { + "Left": 83.52, + "Right": 83.28 + }, + "TS506": { + "Left": 81.73, + "Right": 83.06 + }, + "TS507": { + "Left": 87.14, + "Right": 86.01 + }, + "TS508": { + "Left": 80.33, + "Right": 81.37 + }, + "TS509": { + "Left": 66.57, + "Right": 66.56 + }, + "TS510": { + "Left": 81.41, + "Right": 83.11 + }, + "TS511": { + "Left": 80.1, + "Right": 74.34 + }, + "TS512": { + "Left": 69.89, + "Right": 65.32 + }, + "TS513": { + "Left": 74.62, + "Right": 78.65 + }, + "TS514": { + "Left": 67.96, + "Right": 73.03 + }, + "TS515": { + "Left": 67.4, + "Right": 74.47 + }, + "TS516": { + "Left": 74.08, + "Right": 73.8 + }, + "TS517": { + "Left": 78.38, + "Right": 74.92 + }, + "TS518": { + "Left": 84.38, + "Right": 81.74 + }, + "TS519": { + "Left": 75.03, + "Right": 74.17 + }, + "TS520": { + "Left": 68.01, + "Right": 65.1 + }, + "TS521": { + "Left": 75.59, + "Right": 72.54 + }, + "TS522": { + "Left": 78.27, + "Right": 75.42 + }, + "TS523": { + "Left": 80.55, + "Right": 76.8 + }, + "TS524": { + "Left": 76.28, + "Right": 77.54 + }, + "TS525": { + "Left": 77.95, + "Right": 78.15 + }, + "TS526": { + "Left": 69.79, + "Right": 67.35 + }, + "TS527": { + "Left": 71.51, + "Right": 71.28 + }, + "TS528": { + "Left": 65.21, + "Right": 65.34 + }, + "TS529": { + "Left": 65.93, + "Right": 63.43 + }, + "TS530": { + "Left": 70.78, + "Right": 69.29 + }, + "TS531": { + "Left": 70.49, + "Right": 71.2 + }, + "TS532": { + "Left": 65.82, + "Right": 66.8 + }, + "TS533": { + "Left": 69.95, + "Right": 70.33 + }, + "TS534": { + "Left": 68.29, + "Right": 66.79 + }, + "TS535": { + "Left": 70.93, + "Right": 71.15 + }, + "TS536": { + "Left": 79.07, + "Right": 77.41 + }, + "TS537": { + "Left": 71.03, + "Right": 70.52 + }, + "TS538": { + "Left": 68.73, + "Right": 66.87 + }, + "TS539": { + "Left": 79.25, + "Right": 78.69 + }, + "TS540": { + "Left": 65.1, + "Right": 67.16 + }, + "TS541": { + "Left": 75.87, + "Right": 77.88 + }, + "NP101": { + "Left": 62.57, + "Right": 61.12 + }, + "NP102": { + "Left": 71.91, + "Right": 71.91 + }, + "NP106": { + "Left": 71.59, + "Right": 72.11 + }, + "NP107": { + "Left": 57.09, + "Right": 58.11 + }, + "NP108": { + "Left": 63.86, + "Right": 65.56 + }, + "NP109": { + "Left": 61.09, + "Right": 62.33 + }, + "NP110": { + "Left": 70.89, + "Right": 72.02 + }, + "NP111": { + "Left": 59.44, + "Right": 61.75 + }, + "NP112": { + "Left": 63.68, + "Right": 62.82 + }, + "NP113": { + "Left": 65.56, + "Right": 65.96 + }, + "NP114": { + "Left": 66.16, + "Right": 67.46 + }, + "NP116": { + "Left": 65.47, + "Right": 65.64 + }, + "NP117": { + "Left": 65.18, + "Right": 65.05 + }, + "NP118": { + "Left": 65.77, + "Right": 66.08 + }, + "NP119": { + "Left": 75.69, + "Right": 76.11 + }, + "NP120": { + "Left": 72.97, + "Right": 73.53 + }, + "NP123": { + "Left": 70.59, + "Right": 70.3 + }, + "NP125": { + "Left": 65.53, + "Right": 63.53 + }, + "NP126": { + "Left": 62.59, + "Right": 63.44 + }, + "NP127": { + "Left": 56.49, + "Right": 57.02 + }, + "NP128": { + "Left": 70.1, + "Right": 71.76 + }, + "NP129": { + "Left": 63.92, + "Right": 64.41 + }, + "NP131": { + "Left": 63.57, + "Right": 64.05 + }, + "NP132": { + "Left": 60.65, + "Right": 62.17 + }, + "NP133": { + "Left": 64.06, + "Right": 64.58 + }, + "NP134": { + "Left": 67.39, + "Right": 66.55 + }, + "NP135": { + "Left": 66.76, + "Right": 68.34 + }, + "NP136": { + "Left": 69.36, + "Right": 70.2 + }, + "NP138": { + "Left": 69.07, + "Right": 69.44 + }, + "NP139": { + "Left": 61.51, + "Right": 61.66 + }, + "NP140": { + "Left": 62.44, + "Right": 63.58 + }, + "NP141": { + "Left": 74.38, + "Right": 75.3 + }, + "NP142": { + "Left": 67.22, + "Right": 67.88 + }, + "NP143": { + "Left": 67.27, + "Right": 68.31 + }, + "NP144": { + "Left": 64.72, + "Right": 67.98 + }, + "NP146": { + "Left": 61.97, + "Right": 62.68 + }, + "NP147": { + "Left": 62.41, + "Right": 61.82 + }, + "NP148": { + "Left": 72.19, + "Right": 73.64 + }, + "NP149": { + "Left": 69.5, + "Right": 71.19 + }, + "NP150": { + "Left": 62.57, + "Right": 62.51 + }, + "NP151": { + "Left": 63.52, + "Right": 64.4 + }, + "NP152": { + "Left": 59.81, + "Right": 60.3 + }, + "NP153": { + "Left": 64.29, + "Right": 64.08 + }, + "NP154": { + "Left": 61.47, + "Right": 62.16 + }, + "NP155": { + "Left": 56.22, + "Right": 57.98 + }, + "NP156": { + "Left": 65.27, + "Right": 66.52 + }, + "NP157": { + "Left": 71.63, + "Right": 71.67 + }, + "NP158": { + "Left": 66.97, + "Right": 65.61 + }, + "NP160": { + "Left": 70.48, + "Right": 70.97 + }, + "NP161": { + "Left": 66.76, + "Right": 67.59 + }, + "NP163": { + "Left": 66.67, + "Right": 66.31 + }, + "cp00": { + "Left": 67.97, + "Right": 68.81 + }, + "cp0": { + "Left": 67.48, + "Right": 66.29 + }, + "cp1": { + "Left": 61.65, + "Right": 62.25 + }, + "cp2": { + "Left": 61.46, + "Right": 61.93 + }, + "cp3": { + "Left": 61.79, + "Right": 61.89 + }, + "cp4": { + "Left": 60.02, + "Right": 61.18 + }, + "cp5": { + "Left": 63.64, + "Right": 64.74 + }, + "cp6": { + "Left": 61.53, + "Right": 62.0 + }, + "cp7": { + "Left": 64.82, + "Right": 65.5 + }, + "cp8": { + "Left": 64.81, + "Right": 66.31 + }, + "cp9": { + "Left": 62.05, + "Right": 62.58 + }, + "cp11": { + "Left": 62.35, + "Right": 62.84 + }, + "cp12": { + "Left": 65.25, + "Right": 65.84 + }, + "cp13": { + "Left": 68.58, + "Right": 69.6 + }, + "cp14": { + "Left": 64.43, + "Right": 64.3 + }, + "cp16": { + "Left": 68.59, + "Right": 69.06 + }, + "cp17": { + "Left": 67.53, + "Right": 67.36 + }, + "cp18": { + "Left": 72.06, + "Right": 70.27 + }, + "cp19": { + "Left": 68.03, + "Right": 67.61 + }, + "cp20": { + "Left": 69.57, + "Right": 74.23 + }, + "cp21": { + "Left": 70.79, + "Right": 71.54 + }, + "cv12": { + "Left": 70.91, + "Right": 70.18 + }, + "cv14": { + "Left": 68.76, + "Right": 69.31 + }, + "cv15": { + "Left": 58.36, + "Right": 55.72 + }, + "cv16": { + "Left": 52.46, + "Right": 53.0 + }, + "cv22": { + "Left": 71.7, + "Right": 74.18 + }, + "cv23": { + "Left": 67.84, + "Right": 74.22 + }, + "cv27": { + "Left": 60.42, + "Right": 62.4 + }, + "cv28": { + "Left": 65.79, + "Right": 68.26 + }, + "cv31": { + "Left": 59.25, + "Right": 59.87 + }, + "cv33": { + "Left": 71.33, + "Right": 77.46 + }, + "cv35": { + "Left": 64.54, + "Right": 74.44 + }, + "cv37": { + "Left": 68.05, + "Right": 77.84 + }, + "cv38": { + "Left": 68.89, + "Right": 73.26 + }, + "cv39": { + "Left": 71.95, + "Right": 74.29 + }, + "cv41": { + "Left": 57.31, + "Right": 62.06 + }, + "cv42": { + "Left": 60.29, + "Right": 61.43 + }, + "cv43": { + "Left": 73.48, + "Right": 80.8 + }, + "cv45": { + "Left": 62.1, + "Right": 64.14 + }, + "cv210": { + "Left": 72.04, + "Right": 72.05 + }, + "cv212": { + "Left": 56.7, + "Right": 57.0 + }, + "cv213": { + "Left": 60.58, + "Right": 60.58 + }, + "cv214": { + "Left": 62.01, + "Right": 62.43 + }, + "2cv11": { + "Left": 63.43, + "Right": 66.77 + }, + "2cv12": { + "Left": 65.68, + "Right": 65.36 + }, + "2cv13": { + "Left": 69.4, + "Right": 75.52 + }, + "2cv14": { + "Left": 60.94, + "Right": 60.72 + }, + "2cv15": { + "Left": 64.31, + "Right": 66.93 + }, + "2cv21": { + "Left": 68.49, + "Right": 68.85 + }, + "2cv22": { + "Left": 65.08, + "Right": 72.91 + }, + "2cv23": { + "Left": 68.49, + "Right": 68.88 + }, + "2cv25": { + "Left": 64.09, + "Right": 68.72 + }, + "2cv26": { + "Left": 63.24, + "Right": 65.41 + }, + "2cv31": { + "Left": 63.06, + "Right": 64.92 + }, + "2cv32": { + "Left": 65.24, + "Right": 65.12 + }, + "2cv33": { + "Left": 69.48, + "Right": 69.2 + }, + "2cv34": { + "Left": 70.0, + "Right": 68.34 + }, + "2cv41": { + "Left": 65.82, + "Right": 63.64 + }, + "2cv51": { + "Left": 68.96, + "Right": 71.86 + }, + "2cv61": { + "Left": 68.1, + "Right": 71.26 + }, + "2cv62": { + "Left": 74.29, + "Right": 76.46 + }, + "m11": { + "Left": 71.02, + "Right": 72.06 + }, + "m12": { + "Left": 73.9, + "Right": 74.89 + }, + "m13": { + "Left": 78.11, + "Right": 75.08 + }, + "m14": { + "Left": 71.17, + "Right": 73.52 + }, + "m15": { + "Left": 76.8, + "Right": 76.56 + }, + "m16": { + "Left": 71.35, + "Right": 73.08 + }, + "m17": { + "Left": 63.49, + "Right": 64.38 + }, + "m18": { + "Left": 74.66, + "Right": 76.63 + }, + "m21": { + "Left": 68.44, + "Right": 67.34 + }, + "m22": { + "Left": 73.48, + "Right": 73.03 + }, + "m23": { + "Left": 70.75, + "Right": 71.75 + }, + "m24": { + "Left": 75.96, + "Right": 76.24 + }, + "m25": { + "Left": 66.24, + "Right": 67.47 + }, + "m26": { + "Left": 67.02, + "Right": 66.04 + }, + "m27": { + "Left": 65.76, + "Right": 65.45 + }, + "m28": { + "Left": 78.16, + "Right": 81.02 + }, + "m29": { + "Left": 75.87, + "Right": 81.2 + }, + "m110": { + "Left": 78.57, + "Right": 77.42 + }, + "m111": { + "Left": 69.47, + "Right": 70.04 + }, + "m112": { + "Left": 68.67, + "Right": 74.14 + }, + "m113": { + "Left": 69.75, + "Right": 70.4 + }, + "m210": { + "Left": 78.16, + "Right": 77.72 + }, + "m211": { + "Left": 74.1, + "Right": 76.01 + }, + "m212": { + "Left": 73.16, + "Right": 75.06 + }, + "m213": { + "Left": 67.22, + "Right": 68.85 + }, + "bib1": { + "Left": 69.86, + "Right": 70.57 + }, + "bib2": { + "Left": 66.58, + "Right": 66.81 + }, + "bib3": { + "Left": 68.75, + "Right": 69.87 + }, + "bib4": { + "Left": 68.71, + "Right": 67.68 + }, + "bib5": { + "Left": 67.79, + "Right": 68.24 + }, + "bib6": { + "Left": 67.54, + "Right": 68.67 + }, + "bib8": { + "Left": 65.67, + "Right": 66.81 + }, + "bib9": { + "Left": 68.76, + "Right": 70.86 + }, + "bib10": { + "Left": 68.96, + "Right": 67.73 + }, + "bib11": { + "Left": 67.51, + "Right": 68.89 + }, + "bib12": { + "Left": 67.32, + "Right": 68.07 + }, + "bib13": { + "Left": 66.01, + "Right": 66.41 + }, + "bib14": { + "Left": 70.11, + "Right": 68.61 + }, + "bib15": { + "Left": 67.35, + "Right": 67.16 + }, + "bib16": { + "Left": 68.76, + "Right": 69.57 + }, + "MG101": { + "Left": 60.81, + "Right": 60.55 + }, + "MG102": { + "Left": 62.27, + "Right": 61.78 + }, + "MG103": { + "Left": 59.55, + "Right": 59.34 + }, + "MG104": { + "Left": 56.24, + "Right": 56.08 + }, + "MG105": { + "Left": 58.36, + "Right": 58.0 + }, + "MG106": { + "Left": 63.31, + "Right": 62.07 + }, + "MG108": { + "Left": 61.29, + "Right": 61.19 + }, + "MG109": { + "Left": 65.99, + "Right": 68.68 + }, + "MG110": { + "Left": 59.57, + "Right": 58.73 + }, + "MG111": { + "Left": 66.96, + "Right": 65.82 + }, + "MG114": { + "Left": 60.89, + "Right": 62.0 + }, + "MG116": { + "Left": 62.0, + "Right": 59.62 + }, + "MG118": { + "Left": 61.76, + "Right": 63.53 + }, + "MG119": { + "Left": 56.82, + "Right": 56.26 + }, + "MG201": { + "Left": 76.07, + "Right": 75.64 + }, + "MG202": { + "Left": 75.64, + "Right": 75.4 + }, + "MG203": { + "Left": 76.27, + "Right": 76.15 + }, + "MG204": { + "Left": 75.75, + "Right": 75.66 + }, + "MG205": { + "Left": 75.41, + "Right": 75.42 + }, + "MG206": { + "Left": 75.49, + "Right": 75.66 + }, + "MG207": { + "Left": 74.52, + "Right": 74.89 + }, + "MG208": { + "Left": 74.99, + "Right": 75.24 + }, + "MG209": { + "Left": 83.24, + "Right": 82.35 + }, + "MG210": { + "Left": 84.15, + "Right": 81.64 + }, + "MG211": { + "Left": 82.37, + "Right": 81.09 + }, + "MG212": { + "Left": 81.67, + "Right": 82.69 + }, + "MG213": { + "Left": 80.92, + "Right": 81.59 + }, + "MG214": { + "Left": 80.04, + "Right": 80.15 + }, + "MG215": { + "Left": 76.15, + "Right": 76.13 + }, + "MG216": { + "Left": 79.81, + "Right": 79.21 + }, + "MG217": { + "Left": 79.24, + "Right": 78.4 + }, + "MG218": { + "Left": 78.34, + "Right": 79.27 + }, + "MG219": { + "Left": 81.84, + "Right": 83.18 + }, + "MG220": { + "Left": 81.17, + "Right": 82.53 + }, + "MG301": { + "Left": 65.72, + "Right": 61.34 + }, + "MG302": { + "Left": 55.31, + "Right": 53.69 + }, + "MG303": { + "Left": 55.28, + "Right": 57.92 + }, + "MG304": { + "Left": 55.52, + "Right": 54.79 + }, + "MG305": { + "Left": 57.93, + "Right": 56.82 + }, + "MG306": { + "Left": 53.2, + "Right": 52.62 + }, + "MG307": { + "Left": 65.82, + "Right": 62.45 + }, + "MG308": { + "Left": 64.98, + "Right": 62.05 + }, + "MG309": { + "Left": 56.38, + "Right": 59.62 + }, + "MG310": { + "Left": 65.01, + "Right": 61.03 + }, + "MG311": { + "Left": 61.85, + "Right": 59.44 + }, + "MG312": { + "Left": 57.91, + "Right": 57.31 + }, + "MG313": { + "Left": 60.97, + "Right": 60.88 + }, + "MG314": { + "Left": 53.65, + "Right": 53.66 + }, + "MG315": { + "Left": 63.13, + "Right": 61.39 + }, + "MG316": { + "Left": 56.42, + "Right": 56.59 + }, + "MG317": { + "Left": 52.73, + "Right": 52.51 + }, + "MG318": { + "Left": 55.54, + "Right": 55.31 + }, + "MG319": { + "Left": 53.36, + "Right": 52.85 + }, + "MG320": { + "Left": 56.43, + "Right": 55.9 + }, + "MG321": { + "Left": 65.8, + "Right": 64.73 + }, + "MG322": { + "Left": 62.2, + "Right": 61.56 + }, + "MG323": { + "Left": 65.15, + "Right": 67.37 + }, + "MG324": { + "Left": 66.13, + "Right": 67.45 + }, + "MG325": { + "Left": 59.99, + "Right": 65.75 + }, + "MG326": { + "Left": 76.14, + "Right": 76.96 + }, + "MG327": { + "Left": 58.83, + "Right": 58.23 + }, + "MG328": { + "Left": 60.38, + "Right": 60.4 + }, + "MG329": { + "Left": 60.86, + "Right": 59.88 + }, + "MG330": { + "Left": 61.23, + "Right": 60.62 + }, + "MG331": { + "Left": 56.93, + "Right": 57.68 + }, + "MG332": { + "Left": 56.8, + "Right": 55.43 + }, + "MG333": { + "Left": 56.82, + "Right": 56.29 + }, + "MG334": { + "Left": 59.82, + "Right": 59.27 + }, + "AM02": { + "Left": 71.73, + "Right": 72.47 + }, + "AM06": { + "Left": 72.74, + "Right": 72.6 + }, + "AM07": { + "Left": 70.9, + "Right": 69.95 + }, + "ML101": { + "Left": 74.35, + "Right": 73.84 + }, + "ML103": { + "Left": 72.43, + "Right": 71.73 + }, + "ML104": { + "Left": 72.91, + "Right": 72.21 + }, + "ML105": { + "Left": 71.81, + "Right": 71.42 + }, + "ML106": { + "Left": 69.97, + "Right": 70.41 + }, + "ML111": { + "Left": 68.43, + "Right": 68.86 + }, + "ML112": { + "Left": 71.4, + "Right": 70.61 + }, + "ML130": { + "Left": 70.15, + "Right": 70.34 + }, + "ML131": { + "Left": 71.94, + "Right": 72.0 + }, + "ML132": { + "Left": 72.12, + "Right": 72.43 + }, + "ML133": { + "Left": 72.64, + "Right": 72.33 + }, + "ML202": { + "Left": 82.27, + "Right": 82.05 + }, + "ML203": { + "Left": 70.78, + "Right": 70.67 + }, + "ML204": { + "Left": 71.03, + "Right": 72.16 + }, + "ML205": { + "Left": 70.94, + "Right": 71.6 + }, + "ML206": { + "Left": 70.27, + "Right": 69.73 + }, + "ML207": { + "Left": 71.85, + "Right": 72.88 + }, + "ML208": { + "Left": 67.8, + "Right": 67.63 + }, + "ML209": { + "Left": 73.29, + "Right": 72.05 + }, + "ML210": { + "Left": 71.68, + "Right": 72.68 + }, + "ML211": { + "Left": 74.08, + "Right": 74.64 + }, + "ML212": { + "Left": 73.7, + "Right": 74.29 + }, + "ML213": { + "Left": 72.33, + "Right": 73.29 + }, + "ML214": { + "Left": 79.1, + "Right": 77.09 + }, + "SM301": { + "Left": 74.26, + "Right": 74.5 + }, + "SM302": { + "Left": 76.62, + "Right": 75.87 + }, + "SM303": { + "Left": 78.13, + "Right": 78.11 + }, + "SM304": { + "Left": 76.74, + "Right": 76.15 + }, + "SM305": { + "Left": 75.78, + "Right": 75.67 + }, + "SM306": { + "Left": 76.65, + "Right": 77.91 + }, + "SM307": { + "Left": 76.28, + "Right": 76.83 + }, + "SM308": { + "Left": 76.32, + "Right": 75.91 + }, + "SM309": { + "Left": 75.4, + "Right": 75.42 + }, + "SM310": { + "Left": 75.89, + "Right": 75.32 + }, + "SM311": { + "Left": 75.76, + "Right": 75.76 + }, + "SM312": { + "Left": 75.12, + "Right": 75.35 + }, + "SM313": { + "Left": 75.67, + "Right": 75.92 + }, + "SM314": { + "Left": 79.08, + "Right": 78.59 + }, + "SM401": { + "Left": 71.98, + "Right": 71.19 + }, + "SM402": { + "Left": 66.27, + "Right": 72.63 + }, + "SM403": { + "Left": 68.33, + "Right": 67.97 + }, + "SM404": { + "Left": 69.48, + "Right": 75.89 + }, + "SM405": { + "Left": 68.01, + "Right": 69.51 + }, + "SM406": { + "Left": 70.92, + "Right": 75.87 + }, + "SM407": { + "Left": 73.73, + "Right": 74.14 + }, + "SM408": { + "Left": 72.03, + "Right": 77.73 + }, + "SM409": { + "Left": 69.73, + "Right": 72.76 + }, + "SM410": { + "Left": 74.6, + "Right": 78.2 + }, + "SM411": { + "Left": 72.91, + "Right": 71.23 + }, + "SM412": { + "Left": 73.81, + "Right": 75.5 + }, + "SM413": { + "Left": 72.86, + "Right": 70.77 + }, + "SM414": { + "Left": 70.78, + "Right": 72.41 + }, + "SM415": { + "Left": 68.57, + "Right": 71.5 + }, + "SM416": { + "Left": 69.84, + "Right": 72.03 + }, + "SM417": { + "Left": 69.4, + "Right": 70.09 + }, + "SM418": { + "Left": 71.05, + "Right": 72.24 + }, + "SM419": { + "Left": 69.18, + "Right": 68.79 + }, + "SM420": { + "Left": 67.0, + "Right": 67.14 + }, + "SM501": { + "Left": 56.87, + "Right": 56.82 + }, + "SM502": { + "Left": 56.51, + "Right": 56.42 + }, + "SM503": { + "Left": 59.47, + "Right": 58.94 + }, + "SM504": { + "Left": 69.55, + "Right": 73.89 + }, + "SM505": { + "Left": 69.22, + "Right": 69.09 + }, + "SM506": { + "Left": 71.3, + "Right": 71.38 + }, + "SM507": { + "Left": 67.04, + "Right": 68.1 + }, + "SM508": { + "Left": 69.84, + "Right": 69.39 + }, + "SM509": { + "Left": 62.62, + "Right": 66.17 + }, + "SM510": { + "Left": 61.84, + "Right": 61.77 + }, + "SM511": { + "Left": 62.69, + "Right": 62.88 + }, + "SM512": { + "Left": 61.92, + "Right": 59.41 + }, + "SM513": { + "Left": 65.66, + "Right": 67.05 + }, + "SM514": { + "Left": 56.41, + "Right": 58.5 + }, + "SM515": { + "Left": 70.94, + "Right": 71.01 + }, + "SM516": { + "Left": 68.6, + "Right": 67.22 + }, + "SM517": { + "Left": 68.27, + "Right": 67.01 + }, + "SM518": { + "Left": 67.46, + "Right": 67.91 + }, + "SM519": { + "Left": 64.91, + "Right": 64.28 + }, + "SM520": { + "Left": 68.53, + "Right": 69.3 + } +} diff --git a/test/databases/test_isd.py b/test/databases/test_isd.py index f3506142..dc2d8b52 100644 --- a/test/databases/test_isd.py +++ b/test/databases/test_isd.py @@ -3,7 +3,7 @@ import pytest from pytest import raises -import soundscapy.databases.isd as isd +from soundscapy.databases import isd from soundscapy.surveys.processing import add_iso_coords diff --git a/test/generate_baselines.py b/test/generate_baselines.py index 2acba1c9..9b1f71ba 100644 --- a/test/generate_baselines.py +++ b/test/generate_baselines.py @@ -1,6 +1,4 @@ -""" -Script to generate baseline images for pytest-mpl comparisons. -""" +"""Script to generate baseline images for pytest-mpl comparisons.""" import os diff --git a/test/plotting/test_plotting.py b/test/plotting/test_plotting.py index da35f0ea..73ca2980 100644 --- a/test/plotting/test_plotting.py +++ b/test/plotting/test_plotting.py @@ -4,12 +4,10 @@ import matplotlib.pyplot as plt import numpy as np -import pandas as pd -import plotly.graph_objs as go import pytest +import seaborn.objects as so from soundscapy.plotting import ( - Backend, CircumplexPlot, PlotType, create_circumplex_subplots, @@ -31,7 +29,7 @@ def sample_data(): ) def test_scatter_plot_image(sample_data): """Test scatter plot image comparison.""" - ax = scatter_plot(sample_data, backend=Backend.SEABORN) + ax = scatter_plot(sample_data) return ax.figure @@ -40,14 +38,19 @@ def test_scatter_plot_image(sample_data): ) def test_density_plot_image(sample_data): """Test density plot image comparison.""" - ax = density_plot(sample_data, backend=Backend.SEABORN) + ax = density_plot(sample_data) return ax.figure -def test_scatter_plot_seaborn(sample_data): - """Test scatter plot with Seaborn backend.""" - ax = scatter_plot(sample_data, backend=Backend.SEABORN) - assert isinstance(ax, plt.Axes) +def test_scatter_plot(sample_data): + """Test scatter plot functionality.""" + # Create a figure and axis for the test + fig, ax = plt.subplots() + # Plot directly on the provided axis + result_ax = scatter_plot(sample_data, ax=ax) + # It should return the same axis + assert result_ax is ax + # Check axis properties assert ax.get_xlabel() == "ISOPleasant" assert ax.get_ylabel() == "ISOEventful" assert ax.get_xlim() == (-1, 1) @@ -55,17 +58,21 @@ def test_scatter_plot_seaborn(sample_data): assert ax.get_title() == "Soundscape Scatter Plot" -@pytest.mark.filterwarnings("ignore::UserWarning") -def test_scatter_plot_plotly(sample_data): - """Test scatter plot with Plotly backend.""" - fig = scatter_plot(sample_data, backend=Backend.PLOTLY) - assert isinstance(fig, go.Figure) +def test_scatter_plot_as_objects(sample_data): + """Test scatter plot with objects return type.""" + plot = scatter_plot(sample_data, as_objects=True) + assert isinstance(plot, so.Plot) -def test_density_plot_seaborn(sample_data): - """Test density plot with Seaborn backend.""" - ax = density_plot(sample_data, backend=Backend.SEABORN) - assert isinstance(ax, plt.Axes) +def test_density_plot(sample_data): + """Test density plot functionality.""" + # Create a figure and axis for the test + fig, ax = plt.subplots() + # Plot directly on the provided axis + result_ax = density_plot(sample_data, ax=ax) + # It should return the same axis + assert result_ax is ax + # Check axis properties assert ax.get_xlabel() == "ISOPleasant" assert ax.get_ylabel() == "ISOEventful" assert ax.get_xlim() == (-1, 1) @@ -73,13 +80,10 @@ def test_density_plot_seaborn(sample_data): assert ax.get_title() == "Soundscape Density Plot" -@pytest.mark.filterwarnings("ignore::UserWarning") -def test_density_plot_plotly(sample_data): - """Test density plot with Plotly backend.""" - # Update when density plots are implemented for Plotly - with pytest.raises(NotImplementedError): - fig = density_plot(sample_data, backend=Backend.PLOTLY) - assert isinstance(fig, go.Figure) +def test_density_plot_as_objects(sample_data): + """Test density plot with objects return type.""" + plot = density_plot(sample_data, as_objects=True) + assert isinstance(plot, so.Plot) def test_create_circumplex_subplots(sample_data): @@ -93,74 +97,68 @@ def test_create_circumplex_subplots(sample_data): def test_simple_density_plot_type(sample_data): fig = create_circumplex_subplots( - [sample_data, sample_data], plot_type=PlotType.SIMPLE_DENSITY, nrows=1, ncols=2 + [sample_data, sample_data], plot_type="simple_density", nrows=1, ncols=2 ) assert isinstance(fig, plt.Figure) assert len(fig.axes) == 2 -def test_circumplex_plot_seaborn(sample_data): - """Test CircumplexPlot with Seaborn backend.""" - plot = CircumplexPlot(sample_data, backend=Backend.SEABORN) +def test_circumplex_plot_methods(sample_data): + """Test CircumplexPlot methods.""" + # Test scatter method + plot = CircumplexPlot(sample_data) plot.scatter() assert isinstance(plot.get_axes(), plt.Axes) + + # Test density method + plot = CircumplexPlot(sample_data) plot.density() assert isinstance(plot.get_axes(), plt.Axes) + + # Test jointplot method + plot = CircumplexPlot(sample_data) plot.jointplot() assert isinstance(plot.get_axes(), plt.Axes) -@pytest.mark.filterwarnings("ignore::UserWarning") -def test_circumplex_plot_plotly(sample_data): - """Test CircumplexPlot with Plotly backend.""" - plot = CircumplexPlot(sample_data, backend=Backend.PLOTLY) +def test_plot_size(sample_data): + """Test customizing plot size.""" + plot = CircumplexPlot(sample_data) plot.scatter() - assert isinstance(plot.get_figure(), go.Figure) - with pytest.raises(NotImplementedError): - # Update when density plots are implemented for Plotly - plot.density() - assert isinstance(plot.get_figure(), go.Figure) + fig, _ = plot.build(as_objects=False) + # Default size should be 6x6 + assert np.array_equal(fig.get_size_inches(), np.array((6, 6))) -def test_style_options(sample_data): - """Test updating style options.""" - plot = CircumplexPlot(sample_data, backend=Backend.SEABORN) - plot.update_style_options(figsize=(8, 8)) - plot.scatter() - fig = plot.get_figure()[0] - assert np.array_equal(fig.get_size_inches(), np.array((8, 8))) - +def test_builder_pattern(sample_data): + """Test the builder pattern API.""" + plot = CircumplexPlot(sample_data).add_scatter().add_grid().add_title("Test Title") -def test_invalid_backend(): - """Test invalid backend raises ValueError.""" - with pytest.raises(ValueError): - CircumplexPlot(pd.DataFrame(), backend="invalid_backend") + # Verify the plot was built correctly + assert plot.has_scatter is True + assert plot.has_grid is True def test_invalid_plot_type(sample_data): - """Test invalid plot type raises ValueError.""" - with pytest.raises(KeyError): - create_circumplex_subplots([sample_data], plot_type="invalid_plot_type") + """Test invalid plot type gets treated as default.""" + # Invalid types no longer raise errors - they just fall back to default behavior + fig = create_circumplex_subplots([sample_data], plot_type="invalid_plot_type") + assert isinstance(fig, plt.Figure) def test_simple_density(sample_data): - plot = CircumplexPlot(sample_data, backend=Backend.SEABORN) - plot.simple_density() - assert isinstance(plot.get_axes(), plt.Axes) - - -def test_simple_density_with_custom_params(sample_data): - from soundscapy.plotting.circumplex_plot import CircumplexPlotParams - - params = CircumplexPlotParams(fill=False) - plot = CircumplexPlot(sample_data, backend=Backend.SEABORN, params=params) + """Test simple density plot functionality.""" + plot = CircumplexPlot(sample_data) plot.simple_density() assert isinstance(plot.get_axes(), plt.Axes) -def test_simple_density_with_custom_axes(sample_data): +def test_annotations(sample_data): + """Test annotation functionality.""" plot = CircumplexPlot(sample_data) - fig, ax = plt.subplots() - plot.simple_density(ax=ax) - fig = plot.get_figure() - assert isinstance(fig[0], plt.Figure) + plot.add_scatter() + plot.add_annotation(0) + plot.add_grid() + + # Just testing that it doesn't error since we can't easily check annotation + assert plot.has_scatter is True diff --git a/test/spi/test_MSN.py b/test/spi/test_MSN.py new file mode 100644 index 00000000..612183aa --- /dev/null +++ b/test/spi/test_MSN.py @@ -0,0 +1,551 @@ +from unittest.mock import patch # Keep patch for plotting + +import numpy as np +import pandas as pd +import pytest + +from soundscapy.spi.msn import CentredParams, DirectParams, MultiSkewNorm, cp2dp, dp2cp + +# Check for R and 'sn' package availability +try: + # import rpy2.robjects as ro # No longer needed directly + from rpy2.rinterface_lib.embedded import RRuntimeError + from rpy2.robjects.packages import importr + + # Try importing the 'sn' package in R + try: + importr("sn") + r_sn_available = True + except RRuntimeError: + r_sn_available = False +except ImportError: + r_sn_available = False + +needs_r_sn = pytest.mark.skipif( + not r_sn_available, + reason="Requires R, rpy2, and the R 'sn' package to be installed.", +) + + +class TestDirectParams: + def test_direct_params_init_valid(self): + """Test initialization with valid parameters.""" + xi = np.array([0.1, 0.2]) + omega = np.array([[1.0, 0.5], [0.5, 1.0]]) # Symmetric and positive definite + alpha = np.array([0.3, 0.4]) + + dp = DirectParams(xi, omega, alpha) + + assert np.array_equal(dp.xi, xi) + assert np.array_equal(dp.omega, omega) + assert np.array_equal(dp.alpha, alpha) + + def test_direct_params_init_not_pos_def(self): + """Test initialization with omega that is not positive definite.""" + xi = np.array([0.1, 0.2]) + # Not positive definite matrix + omega = np.array([[1.0, 2.0], [2.0, 1.0]]) + alpha = np.array([0.3, 0.4]) + + with pytest.raises(ValueError, match="Omega must be positive definite"): + DirectParams(xi, omega, alpha) + + def test_direct_params_init_not_symmetric(self): + """Test initialization with omega that is not symmetric.""" + xi = np.array([0.1, 0.2]) + # Not symmetric matrix + omega = np.array([[1.0, 0.5], [0.6, 1.0]]) + alpha = np.array([0.3, 0.4]) + + with pytest.raises(ValueError, match="Omega must be symmetric"): + DirectParams(xi, omega, alpha) + + def test_direct_params_repr(self): + """Test the __repr__ method.""" + xi = np.array([0.1, 0.2]) + omega = np.array([[1.0, 0.5], [0.5, 1.0]]) + alpha = np.array([0.3, 0.4]) + + dp = DirectParams(xi, omega, alpha) + + assert repr(dp) == f"DirectParams(xi={xi}, omega={omega}, alpha={alpha})" + + def test_direct_params_str(self): + """Test the __str__ method.""" + xi = np.array([0.1, 0.2]) + omega = np.array([[1.0, 0.5], [0.5, 1.0]]) + alpha = np.array([0.3, 0.4]) + + dp = DirectParams(xi, omega, alpha) + + expected_str = ( + f"Direct Parameters:" + f"\nxi: {xi.round(3)}" + f"\nomega: {omega.round(3)}" + f"\nalpha: {alpha.round(3)}" + ) + + assert str(dp) == expected_str + + def test_xi_is_in_range(self): + """Test the _xi_is_in_range method with tuple input.""" + xi = np.array([0.1, 0.2]) + omega = np.array([[1.0, 0.5], [0.5, 1.0]]) + alpha = np.array([0.3, 0.4]) + + dp = DirectParams(xi, omega, alpha) + + # Test with tuple input + assert dp._xi_is_in_range((-1.0, 1.0)) + assert not dp._xi_is_in_range((0.2, 1.0)) + + +class TestCentredParams: + def test_centred_params_init(self): + """Test initialization of CentredParams.""" + mean = np.array([0.5, 0.6]) + sigma = np.array([1.0, 1.2]) + skew = np.array([0.1, -0.1]) + + cp = CentredParams(mean, sigma, skew) + + assert np.array_equal(cp.mean, mean) + assert np.array_equal(cp.sigma, sigma) + assert np.array_equal(cp.skew, skew) + + def test_centred_params_repr(self): + """Test the __repr__ method.""" + mean = np.array([0.5, 0.6]) + sigma = np.array([1.0, 1.2]) + skew = np.array([0.1, -0.1]) + + cp = CentredParams(mean, sigma, skew) + + expected_repr = f"CentredParams(mean={mean}, sigma={sigma}, skew={skew})" + assert repr(cp) == expected_repr + + def test_centred_params_str(self): + """Test the __str__ method.""" + mean = np.array([0.5123, 0.6789]) + sigma = np.array([1.0456, 1.2789]) + skew = np.array([0.1111, -0.1999]) + + cp = CentredParams(mean, sigma, skew) + + expected_str = ( + f"Centred Parameters:" + f"\nmean: {mean.round(3)}" + f"\nsigma: {sigma.round(3)}" + f"\nskew: {skew.round(3)}" + ) + assert str(cp) == expected_str + + @needs_r_sn + def test_centred_params_from_dp(self): + """Test the from_dp class method.""" + # Create a dummy DirectParams object + dp_xi = np.array([0.1, 0.2]) + dp_omega = np.array([[1.0, 0.5], [0.5, 1.0]]) + dp_alpha = np.array([0.3, 0.4]) + dp = DirectParams(dp_xi, dp_omega, dp_alpha) + + # Expected values calculated from a known R execution or previous test + expected_cp = CentredParams( + mean=np.array([0.44083939, 0.57492333]), + sigma=np.array([[0.88382851, 0.37221136], [0.37221136, 0.8594325]]), + skew=np.array([0.02045318, 0.02839051]), + ) + + # Call the class method (which internally calls dp2cp) + cp_from_dp = CentredParams.from_dp(dp) + + # Assert that the returned object is an instance of CentredParams + assert isinstance(cp_from_dp, CentredParams) + + # Assert that the attributes match the expected values + np.testing.assert_allclose(cp_from_dp.mean, expected_cp.mean, atol=1e-5) + # Assuming sigma in CentredParams now holds the covariance matrix from dp2cp + np.testing.assert_allclose(cp_from_dp.sigma, expected_cp.sigma, atol=1e-5) + np.testing.assert_allclose(cp_from_dp.skew, expected_cp.skew, atol=1e-5) + + +# Mock data and parameters for testing +# Use values consistent with TestCentredParams.test_centred_params_from_dp +MOCK_XI = np.array([0.1, 0.2]) +MOCK_OMEGA = np.array([[1.0, 0.5], [0.5, 1.0]]) +MOCK_ALPHA = np.array([0.3, 0.4]) +# Corresponding CP values (mean, covariance, skew) from dp2cp +EXPECTED_MEAN = np.array([0.44083939, 0.57492333]) +EXPECTED_SIGMA_COV = np.array([[0.88382851, 0.37221136], [0.37221136, 0.8594325]]) +EXPECTED_SKEW = np.array([0.02045318, 0.02839051]) + +# Sample data for fitting tests +MOCK_DF = pd.DataFrame( + np.random.rand(50, 2) * 0.5 + 0.1, columns=["x", "y"] +) # Smaller N for faster fit +MOCK_X = MOCK_DF["x"].values +MOCK_Y = MOCK_DF["y"].values +MOCK_SAMPLE_SIZE = 100 + + +@needs_r_sn +class TestMultiSkewNorm: + def test_init(self): + """Test initialization of MultiSkewNorm.""" + msn = MultiSkewNorm() + assert msn.selm_model is None + assert msn.cp is None + assert msn.dp is None + assert msn.sample_data is None + assert msn.data is None + + def test_repr_unfitted(self): + """Test __repr__ when the model is not fitted.""" + msn = MultiSkewNorm() + assert repr(msn) == "MultiSkewNorm() (unfitted)" + + def test_repr_fitted(self): + """Test __repr__ when the model is fitted.""" + msn = MultiSkewNorm() + # Define DP, which implicitly calculates CP via dp2cp + msn.define_dp(MOCK_XI, MOCK_OMEGA, MOCK_ALPHA) + # The repr should now show the DP + assert repr(msn) == f"MultiSkewNorm(dp={msn.dp})" + + def test_summary_unfitted(self): # No mock needed + """Test summary when the model is not fitted.""" + msn = MultiSkewNorm() + assert msn.summary() == "MultiSkewNorm is not fitted." + + @needs_r_sn # Needs fit -> R + def test_summary_fitted_from_data(self, capsys): + """Test summary when the model is fitted from data.""" + msn = MultiSkewNorm() + msn.fit(data=MOCK_DF.copy()) + msn.summary() + captured = capsys.readouterr() + assert f"Fitted from data. n = {len(MOCK_DF)}" in captured.out + assert "Direct Parameters:" in captured.out + assert "Centred Parameters:" in captured.out + assert "xi:" in captured.out + assert "mean:" in captured.out + + def test_summary_fitted_from_dp(self, capsys): + """Test summary when the model is fitted from direct parameters.""" + msn = MultiSkewNorm() + msn.define_dp(MOCK_XI, MOCK_OMEGA, MOCK_ALPHA) # This calculates CP + msn.summary() + captured = capsys.readouterr() + assert "Fitted from direct parameters." in captured.out + assert str(msn.dp) in captured.out + assert str(msn.cp) in captured.out + + def test_fit_with_dataframe(self): + """Test fit method with pandas DataFrame.""" + msn = MultiSkewNorm() + msn.fit(data=MOCK_DF.copy()) + + assert msn.selm_model is not None # Check R model object exists + assert isinstance(msn.cp, CentredParams) + assert isinstance(msn.dp, DirectParams) + assert msn.data is not None # Add assertion for type checker + pd.testing.assert_frame_equal(msn.data, MOCK_DF) + # Check dimensions of parameters + assert msn.cp.mean.shape == (2,) + assert msn.dp.xi.shape == (2,) + assert msn.dp.omega.shape == (2, 2) + assert msn.dp.alpha.shape == (2,) + + def test_fit_with_numpy_array(self): + """Test fit method with numpy array.""" + msn = MultiSkewNorm() + numpy_data = MOCK_DF.values + msn.fit(data=numpy_data) + + expected_df = pd.DataFrame(numpy_data, columns=["x", "y"]) + + assert msn.selm_model is not None + assert isinstance(msn.cp, CentredParams) + assert isinstance(msn.dp, DirectParams) + assert msn.data is not None # Add assertion for type checker + pd.testing.assert_frame_equal(msn.data, expected_df) + assert msn.cp.mean.shape == (2,) + assert msn.dp.xi.shape == (2,) + + def test_fit_with_1d_numpy_array(self): + """Test fit method raises error on 1D numpy array.""" + msn = MultiSkewNorm() + one_d_array = np.array([0.1, 0.2, 0.3]) + + with pytest.raises( + ValueError, match="Data must be a 2D numpy array or DataFrame" + ): + msn.fit(data=one_d_array) + + def test_fit_with_x_y(self): + """Test fit method with x and y arrays.""" + msn = MultiSkewNorm() + msn.fit(x=MOCK_X, y=MOCK_Y) + + expected_df = pd.DataFrame({"x": MOCK_X, "y": MOCK_Y}) + + assert msn.selm_model is not None + assert isinstance(msn.cp, CentredParams) + assert isinstance(msn.dp, DirectParams) + assert msn.data is not None # Add assertion for type checker + pd.testing.assert_frame_equal(msn.data, expected_df) + assert msn.cp.mean.shape == (2,) + assert msn.dp.xi.shape == (2,) + + def test_fit_no_data(self): + """Test fit method raises ValueError when no data is provided.""" + msn = MultiSkewNorm() + with pytest.raises(ValueError, match="Either data or x and y must be provided"): + msn.fit() + + def test_define_dp(self): + """Test define_dp method.""" + msn = MultiSkewNorm() + result = msn.define_dp(MOCK_XI, MOCK_OMEGA, MOCK_ALPHA) + + assert isinstance(msn.dp, DirectParams) + np.testing.assert_array_equal(msn.dp.xi, MOCK_XI) + np.testing.assert_array_equal(msn.dp.omega, MOCK_OMEGA) + np.testing.assert_array_equal(msn.dp.alpha, MOCK_ALPHA) + # Check CP was also calculated + assert isinstance(msn.cp, CentredParams) + np.testing.assert_allclose(msn.cp.mean, EXPECTED_MEAN, atol=1e-5) + # Assuming CentredParams.sigma holds covariance matrix after dp2cp + np.testing.assert_allclose(msn.cp.sigma, EXPECTED_SIGMA_COV, atol=1e-5) + np.testing.assert_allclose(msn.cp.skew, EXPECTED_SKEW, atol=1e-5) + assert result is msn # Check if it returns self for chaining + + def test_sample_after_fit(self): + """Test sample method after fitting the model.""" + msn = MultiSkewNorm() + msn.fit(data=MOCK_DF) + result = msn.sample(n=MOCK_SAMPLE_SIZE, return_sample=False) + + assert result is None + assert isinstance(msn.sample_data, np.ndarray) + assert msn.sample_data.shape == (MOCK_SAMPLE_SIZE, 2) + + def test_sample_after_define_dp(self): + """Test sample method after defining direct parameters.""" + msn = MultiSkewNorm() + msn.define_dp(MOCK_XI, MOCK_OMEGA, MOCK_ALPHA) + result = msn.sample(n=MOCK_SAMPLE_SIZE, return_sample=False) + + assert result is None + assert isinstance(msn.sample_data, np.ndarray) + assert msn.sample_data.shape == (MOCK_SAMPLE_SIZE, 2) + + def test_sample_return_sample_true(self): + """Test sample method with return_sample=True.""" + msn = MultiSkewNorm() + msn.define_dp(MOCK_XI, MOCK_OMEGA, MOCK_ALPHA) + sample = msn.sample(n=MOCK_SAMPLE_SIZE, return_sample=True) + + assert isinstance(sample, np.ndarray) + assert sample.shape == (MOCK_SAMPLE_SIZE, 2) + # Ensure sample_data is also set + assert isinstance(msn.sample_data, np.ndarray) + np.testing.assert_array_equal(msn.sample_data, sample) + + def test_sample_not_fitted_or_defined(self): # No mock needed + """Test sample method raises ValueError when not fitted or defined.""" + msn = MultiSkewNorm() + with pytest.raises( + ValueError, + match="Either selm_model or xi, omega, and alpha must be provided.", + ): + msn.sample() + + @patch("soundscapy.spi.msn.density_plot") # Keep mocking the plotting call + def test_sspy_plot_calls_sample_if_needed(self, mock_density_plot): + """Test sspy_plot calls sample if sample_data is None.""" + msn = MultiSkewNorm() + msn.define_dp(MOCK_XI, MOCK_OMEGA, MOCK_ALPHA) # Define DP so sample can run + + assert msn.sample_data is None + msn.sspy_plot( + n=MOCK_SAMPLE_SIZE, color="red", title="Test Plot" + ) # Pass n to sample + + # Check sample was called implicitly and data was generated + assert isinstance(msn.sample_data, np.ndarray) + assert msn.sample_data.shape == (MOCK_SAMPLE_SIZE, 2) + + # Check plot was called with the sampled data + mock_density_plot.assert_called_once() + call_args = mock_density_plot.call_args[0] + call_kwargs = mock_density_plot.call_args[1] + assert isinstance(call_args[0], pd.DataFrame) + # Check the dataframe passed to plot matches the generated sample data + expected_plot_df = pd.DataFrame( + msn.sample_data, columns=["ISOPleasant", "ISOEventful"] + ) + pd.testing.assert_frame_equal(call_args[0], expected_plot_df) + assert call_kwargs["color"] == "red" + assert call_kwargs["title"] == "Test Plot" + + @patch("soundscapy.spi.msn.density_plot") # Keep mocking the plotting call + def test_sspy_plot_uses_existing_sample(self, mock_density_plot): + """Test sspy_plot uses existing sample_data if available.""" + msn = MultiSkewNorm() + # Create some dummy sample data + existing_sample = np.random.rand(50, 2) + msn.sample_data = existing_sample + + # Store original sample_data reference to check it wasn't re-generated + sample_data_before_plot = msn.sample_data + + msn.sspy_plot() # Should use existing sample_data + + # Check sample was NOT called again (sample_data should be unchanged) + assert msn.sample_data is sample_data_before_plot + np.testing.assert_array_equal(msn.sample_data, existing_sample) + + # Check plot was called with the existing data + mock_density_plot.assert_called_once() + call_args = mock_density_plot.call_args[0] + assert isinstance(call_args[0], pd.DataFrame) + expected_plot_df = pd.DataFrame( + existing_sample, columns=["ISOPleasant", "ISOEventful"] + ) + pd.testing.assert_frame_equal(call_args[0], expected_plot_df) + + def test_ks2d2s_calls_sample_if_needed(self): + """Test ks2d2s calls sample if sample_data is None and calls ks2d2s.""" + msn = MultiSkewNorm() + msn.define_dp(MOCK_XI, MOCK_OMEGA, MOCK_ALPHA) # Define DP so sample can run + + assert msn.sample_data is None + test_data_df = pd.DataFrame(np.random.rand(40, 2), columns=["col1", "col2"]) + + result = msn.ks2d2s(test_data_df) + # TODO: still need to implement check for actual result values + + # Check sample was called implicitly and data was generated + assert isinstance(msn.sample_data, np.ndarray) + assert ( + msn.sample_data.shape[1] == 2 # noqa: PLR2004 + ) # Check sample data has 2 columns + + assert isinstance(result, tuple) + assert isinstance(result[0], float) + assert isinstance(result[1], float) + + def test_ks2d2s(self): + """Test ks2d2s converts DataFrame input to numpy array.""" + msn = MultiSkewNorm() + msn.define_dp(MOCK_XI, MOCK_OMEGA, MOCK_ALPHA) + msn.sample(n=50) # Generate sample data beforehand + + test_data_df = pd.DataFrame(np.random.rand(40, 2), columns=["col1", "col2"]) + test_data_np = test_data_df.to_numpy() + + df_result = msn.ks2d2s(test_data_df) + np_result = msn.ks2d2s(test_data_np) + # TODO: still need to implement check for actual result values + + assert df_result == np_result, ( + "Results from DataFrame and numpy array should match." + ) + + assert isinstance(df_result, tuple) + assert isinstance(df_result[0], float) + assert isinstance(df_result[1], float) + + assert isinstance(np_result, tuple) + assert isinstance(np_result[0], float) + assert isinstance(np_result[1], float) + + def test_ks2d2s_dataframe_wrong_shape(self): + """Test ks2d2s raises ValueError for DataFrame with wrong shape.""" + msn = MultiSkewNorm() + msn.define_dp(MOCK_XI, MOCK_OMEGA, MOCK_ALPHA) + msn.sample(n=50) + + test_data_df_wrong = pd.DataFrame(np.random.rand(40, 3)) # 3 columns + + with pytest.raises(ValueError, match="Test data must have two columns."): + msn.ks2d2s(test_data_df_wrong) + + @patch.object(MultiSkewNorm, "ks2d2s") + def test_spi(self, mock_ks2d2s): + """Test spi method calculation.""" + # Mock ks2ds to return a specific KS statistic and p-value + mock_ks_statistic = 0.15 + mock_ks2d2s.return_value = (mock_ks_statistic, 0.04) + + msn = MultiSkewNorm() + # No need to fit or define dp as we are mocking ks2ds + test_data = np.random.rand(50, 2) + + spi_value = msn.spi(test_data) + + # Check ks2ds was called with the test data + mock_ks2d2s.assert_called_once_with(test_data) + + # Check the SPI calculation + expected_spi = int((1 - mock_ks_statistic) * 100) + assert spi_value == expected_spi + assert isinstance(spi_value, int) + + @patch.object(MultiSkewNorm, "ks2d2s") + def test_spi_with_dataframe(self, mock_ks2d2s): + """Test spi method with DataFrame input.""" + mock_ks_statistic = 0.25 + mock_ks2d2s.return_value = (mock_ks_statistic, 0.01) + + msn = MultiSkewNorm() + test_data_df = pd.DataFrame(np.random.rand(60, 2), columns=["a", "b"]) + + spi_value = msn.spi(test_data_df) + + # Check ks2d2s was called (the mock captures the call) + mock_ks2d2s.assert_called_once() + # Check the argument passed to the mock was the DataFrame + call_args = mock_ks2d2s.call_args[0] + pd.testing.assert_frame_equal(call_args[0], test_data_df) + + # Check the SPI calculation + expected_spi = int((1 - mock_ks_statistic) * 100) + assert spi_value == expected_spi + assert isinstance(spi_value, int) + + +@needs_r_sn +@pytest.mark.skip( + reason="Cannot directly convert cp to dp. Need to come up with a reasonable test." +) +def test_cp2dp(): + """Test cp2dp function.""" + cp_input = CentredParams(EXPECTED_MEAN, EXPECTED_SIGMA_COV, EXPECTED_SKEW) + + # Perform the conversion + dp_output = cp2dp(cp_input) + + assert isinstance(dp_output, DirectParams) + # Check if the output DP matches the original MOCK_DP used to generate the CPs + np.testing.assert_allclose(dp_output.xi, MOCK_XI, atol=1e-5) + np.testing.assert_allclose(dp_output.omega, MOCK_OMEGA, atol=1e-5) + np.testing.assert_allclose(dp_output.alpha, MOCK_ALPHA, atol=1e-5) + + +@needs_r_sn # Needs R for conversion +def test_dp2cp(): + """Test dp2cp function.""" + # Use the known DP values + dp_input = DirectParams(MOCK_XI, MOCK_OMEGA, MOCK_ALPHA) + + # Perform the conversion + cp_output = dp2cp(dp_input) + + assert isinstance(cp_output, CentredParams) + # Check if the output CP matches the expected values + np.testing.assert_allclose(cp_output.mean, EXPECTED_MEAN, atol=1e-5) + # Assuming CentredParams.sigma holds covariance matrix from dp2cp + np.testing.assert_allclose(cp_output.sigma, EXPECTED_SIGMA_COV, atol=1e-5) + np.testing.assert_allclose(cp_output.skew, EXPECTED_SKEW, atol=1e-5) diff --git a/test/spi/test_r_wrapper.py b/test/spi/test_r_wrapper.py new file mode 100644 index 00000000..4020384a --- /dev/null +++ b/test/spi/test_r_wrapper.py @@ -0,0 +1,92 @@ +""" +Tests for the R integration wrapper. + +These tests check the R session management and data conversion functions. +They are skipped if rpy2 is not installed. +""" + +import os + +import pytest + + +def test_initialize_r_session_fails(): + """Test that R session initialization fails if R is not available.""" + # Skip if dependencies are actually installed + if os.environ.get("SPI_DEPS") == "1": + pytest.skip("SPI dependencies are installed") + + from soundscapy.spi._r_wrapper import initialize_r_session + + # Simulate R not being available + with pytest.raises(ImportError) as excinfo: + initialize_r_session() + + # Check for helpful error message + assert "R installation" in str(excinfo.value) + assert "install.packages('R')" in str(excinfo.value) + + +@pytest.mark.optional_deps("spi") +class TestRWrapper: + """Test the R wrapper functionality.""" + + def test_module_structure(self): + """Test that the module structure exists.""" + import soundscapy.spi._r_wrapper + + # Module should exist but functions will be implemented later + assert soundscapy.spi._r_wrapper is not None + + def test_initialize_r_session(self): + """Test R session initialization.""" + from soundscapy.spi._r_wrapper import initialize_r_session + + # This should not raise if R is available + res = initialize_r_session() + + assert res is not None, "R session should be initialized successfully" + assert res["r_session"] == "active", "R session should be active" + + def test_shutdown_r_session(self): + """Test R session cleanup.""" + from soundscapy.spi._r_wrapper import shutdown_r_session + + # This should not raise if R session is active + res = shutdown_r_session() + + assert res, "R session should be shut down successfully" + + def test_r_session_reinitialization(self): + """Test that the R session can be reinitialized after shutdown.""" + from soundscapy.spi._r_wrapper import initialize_r_session, shutdown_r_session + + # First initialize the R session + res = initialize_r_session() + assert res is not None, "R session should be initialized successfully" + + # Now shut it down + shutdown_res = shutdown_r_session() + assert shutdown_res, "R session should be shut down successfully" + + # Reinitialize the R session + reinit_res = initialize_r_session() + assert reinit_res is not None, "R session should be reinitialized successfully" + + def test_check_sn_package(self): + """Test that the R 'sn' package availability is checked.""" + # Skip if dependencies are actually installed + + if os.environ.get("SPI_DEPS") == "1": + from soundscapy.spi import _r_wrapper + + _r_wrapper.check_sn_package() + + else: + with pytest.raises(ImportError) as excinfo: + from soundscapy.spi import _r_wrapper + + _r_wrapper.check_sn_package() + + assert "R package 'sn'" in str(excinfo.value) + assert "install.packages('sn')" in str(excinfo.value) diff --git a/test/test__optionals.py b/test/test__optionals.py deleted file mode 100644 index b9324433..00000000 --- a/test/test__optionals.py +++ /dev/null @@ -1,94 +0,0 @@ -import os - -import pytest - - -def test_optional_dependency_groups_defined(): - """Test that OPTIONAL_DEPENDENCIES has expected structure.""" - from soundscapy._optionals import OPTIONAL_DEPENDENCIES - - assert "audio" in OPTIONAL_DEPENDENCIES - - group = OPTIONAL_DEPENDENCIES["audio"] - assert "packages" in group - assert "install" in group - assert "description" in group - - assert isinstance(group["packages"], tuple) - assert len(group["packages"]) > 0 - assert all(isinstance(pkg, str) for pkg in group["packages"]) - - assert group["install"] == "soundscapy[audio]" - assert isinstance(group["description"], str) - - -@pytest.mark.skipif( - os.environ.get("AUDIO_DEPS") == "1", - reason="Test requires audio dependencies to be missing", -) -def test_require_dependencies_missing(): - """Test behavior when dependencies are missing.""" - from soundscapy._optionals import require_dependencies - - with pytest.raises(ImportError) as exc_info: - require_dependencies("audio") - - assert "Install with:" in str(exc_info.value) - assert "pip install soundscapy[audio]" in str(exc_info.value) - - -@pytest.mark.optional_deps("audio") -def test_require_dependencies_present(): - """Test behavior when dependencies are present.""" - from soundscapy._optionals import require_dependencies - - packages = require_dependencies("audio") - assert isinstance(packages, dict) - assert all(pkg in packages for pkg in ["mosqito", "maad", "acoustic_toolbox"]) - assert all(packages[pkg] is not None for pkg in packages) - - -def test_optional_import_behavior(): - """Test top-level optional component import behavior.""" - import importlib - import soundscapy - from soundscapy._optionals import OPTIONAL_IMPORTS - - # Test that optional components are listed in __all__ - for name in OPTIONAL_IMPORTS: - assert name in soundscapy.__all__ - - # Test import behavior - def has_audio_deps(): - """Check if audio dependencies are available.""" - try: - importlib.import_module("mosqito") - importlib.import_module("maad") - importlib.import_module("acoustic_toolbox") - return True - except ImportError: - return False - - # Try importing Binaural as an example component - if has_audio_deps(): - # Should succeed when dependencies are available - from soundscapy import Binaural - - assert hasattr(Binaural, "__module__") - else: - # Should raise helpful ImportError when dependencies missing - import pytest - - with pytest.raises(ImportError) as exc_info: - from soundscapy import Binaural - assert "audio analysis functionality" in str(exc_info.value) - assert "pip install soundscapy[audio]" in str(exc_info.value) - - -def test_invalid_group(): - """Test behavior with invalid dependency group.""" - from soundscapy._optionals import require_dependencies - - with pytest.raises(KeyError) as exc_info: - require_dependencies("nonexistent_group") - assert "Unknown dependency group" in str(exc_info.value) diff --git a/test/test_basic.py b/test/test_basic.py index cb891dbf..af95e0b9 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -1,6 +1,9 @@ -import soundscapy +import os + import pytest +import soundscapy + def test_soundscapy_import(): assert soundscapy.__version__ is not None, "Soundscapy version should be defined" @@ -15,3 +18,37 @@ def test_core_soundscapy_modules(): @pytest.mark.optional_deps("audio") def test_soundscapy_audio_module(): assert hasattr(soundscapy, "audio"), "Soundscapy should have an audio module" + # Test that the key classes are available + assert hasattr(soundscapy, "Binaural") + assert hasattr(soundscapy, "AudioAnalysis") + assert hasattr(soundscapy, "AnalysisSettings") + assert hasattr(soundscapy, "ConfigManager") + + +@pytest.mark.optional_deps("spi") +def test_soundscapy_spi_module(): + """Test that the SPI module can be imported when dependencies are available.""" + assert hasattr(soundscapy, "spi"), "Soundscapy should have an spi module" + # Test top-level imports + assert hasattr(soundscapy, "MultiSkewNorm"), "MultiSkewNorm should be available" + assert hasattr(soundscapy, "dp2cp"), "dp2cp should be available" + # assert hasattr(soundscapy, "calculate_spi"), "calculate_spi should be available" + # assert hasattr(soundscapy, "calculate_spi_from_data"), ( + # "calculate_spi_from_data should be available" + # ) + + +def test_spi_import_error(): + """Test that helpful error message is shown when SPI dependencies are missing.""" + # Skip if dependencies are actually installed + if os.environ.get("SPI_DEPS") == "1": + pytest.skip("SPI dependencies are installed") + + # Since direct imports are now used instead of __getattr__, we need to test + # through direct access to the module which would trigger ImportError + with pytest.raises(ImportError) as excinfo: + import soundscapy.spi # noqa: F401 + + # Check error message contains helpful instructions + assert "SPI functionality requires" in str(excinfo.value) + assert "soundscapy[spi]" in str(excinfo.value) diff --git a/test/test_logging.py b/test/test_sspylogging.py similarity index 94% rename from test/test_logging.py rename to test/test_sspylogging.py index f720e8e2..d911fbd3 100644 --- a/test/test_logging.py +++ b/test/test_sspylogging.py @@ -2,15 +2,14 @@ import tempfile from pathlib import Path -import pytest from loguru import logger -from soundscapy.logging import ( - setup_logging, - enable_debug, +from soundscapy.sspylogging import ( disable_logging, + enable_debug, get_logger, is_notebook, + setup_logging, ) @@ -53,34 +52,34 @@ def test_enable_debug(): def test_disable_logging(): """Test disabling logging.""" import io - + # Create a test output buffer test_output = io.StringIO() - + # Set up logging with our test output logger.remove() logger.enable("soundscapy") - handler_id = logger.add(test_output, level="CRITICAL") - + logger.add(test_output, level="CRITICAL") + # Try with logging enabled logger.critical("This should be logged") logger.complete() assert "This should be logged" in test_output.getvalue() - + # Now disable logging disable_logging() - + # Clear the output buffer test_output.seek(0) test_output.truncate(0) - + # Try to log at CRITICAL level again logger.critical("This should NOT be logged") logger.complete() - + # Verify nothing was logged after disabling assert test_output.getvalue() == "" - + # Reset for other tests setup_logging("INFO") diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..e687abe6 --- /dev/null +++ b/tox.ini @@ -0,0 +1,93 @@ +# tox.ini +[tox] +env_list = + docs, + py{310,311,312}-core, + py{310,311,312}-tutorials, + py{310,311,312}-audio, + py{310,311,312}-spi, + py{310,311,312}-all +isolated_build = True +requires = + tox-uv>=1.0.0 + +[testenv] +# Common configuration for all environments +runner = uv-venv-lock-runner +dependency_groups = test +set_env = + PYTHONPATH = {toxinidir} + PY_IGNORE_IMPORTMISMATCH = 1 +commands_pre = + python -c "import sys; print(f'Python {sys.version}')" + +[testenv:docs] +# Documentation build environment +dependency_groups = docs +allowlist_externals = + mkdocs +commands = + ; mkdocs build --strict + mkdocs build + +[testenv:py{310,311,312}-tutorials] +# Tutorials build environment +dependency_groups = test, docs +extras = spi +allowlist_externals = + R + Rscript +commands_pre = + {[testenv]commands_pre} + # Ensure R 'sn' package is available + Rscript -e "if(!require('sn')) { pak::local_install_deps() }" +commands = + # Build the tutorials + pytest --nbmake -n=auto docs --ignore=docs/tutorials/BinauralAnalysis.ipynb --no-cov # BinauralAnalysis is too slow + +[testenv:py{310,311,312}-core] +# Core-only installation - no optional dependencies +dependency_groups = test +commands = + # Run core tests only (excluding any optional dependency tests) + # Skip SPI module doctest collection + pytest --cov --cov-report=xml -k "not optional_deps" --ignore=src/soundscapy/spi/ + +[testenv:py{310,311,312}-audio] +# Install with audio extras +dependency_groups = test +extras = audio +commands = + # Run core tests and audio-specific tests + # Skip SPI module doctest collection + pytest --cov --cov-report=xml -k "not optional_deps or optional_deps and audio" --ignore=src/soundscapy/spi/ + +[testenv:py{310,311,312}-spi] +# Install with spi extras +dependency_groups = test +extras = spi +allowlist_externals = + R + Rscript +commands_pre = + {[testenv]commands_pre} + # Ensure R 'sn' package is available + Rscript -e "if(!require('sn')) { pak::local_install_deps() }" +commands = + # Run core tests and SPI-specific tests + pytest --cov --cov-report=xml -k "not optional_deps or optional_deps and spi or skip_if_deps and spi" + +[testenv:py{310,311,312}-all] +# Full installation with all extras +dependency_groups = test +extras = all +allowlist_externals = + R + Rscript +commands_pre = + {[testenv]commands_pre} + # Ensure R 'sn' package is available + Rscript -e "if(!require('sn')) { pak::local_install_deps() }" +commands = + # Run all tests, including SPI tests which are skipped with pytestmark + pytest --cov --cov-report=xml diff --git a/uv.lock b/uv.lock index eb38e8e1..ba1ae80a 100644 --- a/uv.lock +++ b/uv.lock @@ -152,6 +152,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, +] + [[package]] name = "beautifulsoup4" version = "4.13.3" @@ -192,18 +201,37 @@ wheels = [ ] [[package]] -name = "bumpver" -version = "2024.1130" +name = "bracex" +version = "2.5.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/02/8db98cdc1a58e0abd6716d5e63244658e6e63513c65f469f34b6f1053fd0/bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6", size = 11558 }, +] + +[[package]] +name = "build" +version = "1.2.2.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "colorama" }, - { name = "lexid" }, - { name = "toml" }, + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/a9/becf78cc86211bd2287114c4f990a3bed450816696f14810cc59d7815bb5/bumpver-2024.1130.tar.gz", hash = "sha256:74f7ebc294b2240f346e99748cc6f238e57b050999d7428db75d76baf2bf1437", size = 115102 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950 }, +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/34/57d038ae30374976ce4ec57db9dea95bf55d1b5543b35e77aa9ce3543198/bumpver-2024.1130-py2.py3-none-any.whl", hash = "sha256:8e54220aefe7db25148622f45959f7beb6b8513af0b0429b38b9072566665a49", size = 65273 }, + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, ] [[package]] @@ -272,6 +300,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, +] + [[package]] name = "charset-normalizer" version = "3.4.1" @@ -495,6 +541,43 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cryptography" +version = "44.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 }, + { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 }, + { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 }, + { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 }, + { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 }, + { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 }, + { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 }, + { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 }, + { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 }, + { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 }, + { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 }, + { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 }, + { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 }, + { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 }, + { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 }, + { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 }, + { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 }, + { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 }, + { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387 }, + { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922 }, + { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715 }, + { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513 }, + { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432 }, + { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421 }, + { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081 }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -597,6 +680,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, ] +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + [[package]] name = "et-xmlfile" version = "2.0.0" @@ -642,6 +743,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924 }, ] +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + [[package]] name = "flask" version = "3.0.3" @@ -769,6 +879,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] +[[package]] +name = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611 }, +] + +[[package]] +name = "identify" +version = "2.6.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101 }, +] + [[package]] name = "idna" version = "3.10" @@ -904,6 +1035,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, +] + [[package]] name = "jedi" version = "0.19.2" @@ -916,6 +1083,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, ] +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 }, +] + [[package]] name = "jinja2" version = "3.1.5" @@ -1222,6 +1398,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/4c/3d7cfac5b8351f649ce41a1007a769baacae8d5d29e481a93d799a209c3f/jupytext-1.16.7-py3-none-any.whl", hash = "sha256:912f9d9af7bd3f15470105e5c5dddf1669b2d8c17f0c55772687fc5a4a73fe69", size = 154154 }, ] +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085 }, +] + [[package]] name = "kiwisolver" version = "1.4.8" @@ -1321,15 +1515,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097 }, ] -[[package]] -name = "lexid" -version = "2021.1006" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/60/0b/28a3f9abc75abbf1fa996eb2dd77e1e33a5d1aac62566e3f60a8ec8b8a22/lexid-2021.1006.tar.gz", hash = "sha256:509a3a4cc926d3dbf22b203b18a4c66c25e6473fb7c0e0d30374533ac28bafe5", size = 11525 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/e3/35764404a4b7e2021be1f88f42264c2e92e0c4720273559a62461ce64a47/lexid-2021.1006-py2.py3-none-any.whl", hash = "sha256:5526bb5606fd74c7add23320da5f02805bddd7c77916f2dc1943e6bada8605ed", size = 7587 }, -] - [[package]] name = "llvmlite" version = "0.44.0" @@ -1608,6 +1793,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, ] +[[package]] +name = "mkdocs-include-markdown-plugin" +version = "7.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/34/ece85384e3ab29f05c2e12e50bde95449aa6dbb47b471923bba8fcf1596c/mkdocs_include_markdown_plugin-7.1.5.tar.gz", hash = "sha256:a986967594da6789226798e3c41c70bc17130fadb92b4313f42bd3defdac0adc", size = 23329 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/eb/472c1bbe93f26fe97647af0b613e9710916a2cf555b64fc969b91e24cf2c/mkdocs_include_markdown_plugin-7.1.5-py3-none-any.whl", hash = "sha256:d0b96edee45e7fda5eb189e63331cfaf1bf1fbdbebbd08371f1daa77045d3ae9", size = 27114 }, +] + [[package]] name = "mkdocs-jupyter" version = "0.25.1" @@ -1694,6 +1892,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/f7/433201c48d4b59208dcbae6e1481febdf732ae20ecb2aee84a4ea142f043/mkdocstrings_python-1.16.1-py3-none-any.whl", hash = "sha256:b88ff6fc6a293cee9cb42313f1cba37a2c5cdf37bcc60b241ec7ab66b5d41b58", size = 449139 }, ] +[[package]] +name = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278 }, +] + [[package]] name = "mosqito" version = "1.2.1" @@ -1708,6 +1915,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4c/6c/3443f0ed85e1091bfad146afb426c30109b765ce15101a6fdc7f9df9e727/mosqito-1.2.1-py3-none-any.whl", hash = "sha256:c5370a729b12cb986ff39c71499e51201551b445623b5ca86aed66539d3cfe83", size = 158692 }, ] +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 }, + { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 }, + { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 }, + { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 }, + { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 }, + { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 }, + { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, + { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, + { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, + { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 }, + { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 }, + { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + [[package]] name = "narwhals" version = "1.27.1" @@ -1806,6 +2060,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, ] +[[package]] +name = "nh3" +version = "0.2.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678 }, + { url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774 }, + { url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012 }, + { url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619 }, + { url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384 }, + { url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908 }, + { url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180 }, + { url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747 }, + { url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908 }, + { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133 }, + { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328 }, + { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020 }, + { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878 }, + { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460 }, + { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369 }, + { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036 }, + { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712 }, + { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559 }, + { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591 }, + { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670 }, + { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093 }, + { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623 }, + { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + [[package]] name = "notebook" version = "7.3.2" @@ -1949,6 +2243,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, ] +[[package]] +name = "optype" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/3c/9d59b0167458b839273ad0c4fc5f62f787058d8f5aed7f71294963a99471/optype-0.9.3.tar.gz", hash = "sha256:5f09d74127d316053b26971ce441a4df01f3a01943601d3712dd6f34cdfbaf48", size = 96143 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/d8/ac50e2982bdc2d3595dc2bfe3c7e5a0574b5e407ad82d70b5f3707009671/optype-0.9.3-py3-none-any.whl", hash = "sha256:2935c033265938d66cc4198b0aca865572e635094e60e6e79522852f029d9e8d", size = 84357 }, +] + [[package]] name = "overrides" version = "7.7.0" @@ -2034,6 +2340,19 @@ excel = [ { name = "xlsxwriter" }, ] +[[package]] +name = "pandas-stubs" +version = "2.2.3.250308" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "types-pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/5a/261f5c67a73e46df2d5984fe7129d66a3ed4864fd7aa9d8721abb3fc802e/pandas_stubs-2.2.3.250308.tar.gz", hash = "sha256:3a6e9daf161f00b85c83772ed3d5cff9522028f07a94817472c07b91f46710fd", size = 103986 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/64/ab61d9ca06ff66c07eb804ec27dec1a2be1978b3c3767caaa91e363438cc/pandas_stubs-2.2.3.250308-py3-none-any.whl", hash = "sha256:a377edff3b61f8b268c82499fdbe7c00fdeed13235b8b71d6a1dc347aeddc74d", size = 158053 }, +] + [[package]] name = "pandocfilters" version = "1.5.1" @@ -2171,6 +2490,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, +] + [[package]] name = "prometheus-client" version = "0.21.1" @@ -2363,6 +2698,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 }, ] +[[package]] +name = "pyproject-api" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/66/fdc17e94486836eda4ba7113c0db9ac7e2f4eea1b968ee09de2fe75e391b/pyproject_api-1.9.0.tar.gz", hash = "sha256:7e8a9854b2dfb49454fae421cb86af43efbb2b2454e5646ffb7623540321ae6e", size = 22714 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/1d/92b7c765df46f454889d9610292b0ccab15362be3119b9a624458455e8d5/pyproject_api-1.9.0-py3-none-any.whl", hash = "sha256:326df9d68dea22d9d98b5243c46e3ca3161b07a1b9b18e213d1e24fd0e605766", size = 13131 }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216 }, +] + [[package]] name = "pysoundfile" version = "0.9.0.post1" @@ -2568,6 +2925,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + [[package]] name = "pywinpty" version = "2.0.15" @@ -2719,6 +3085,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/fe/72e7e166bda3885810bee7b23049133e142f7c80c295bae02c562caeea16/pyzmq-26.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bd8fdee945b877aa3bffc6a5a8816deb048dab0544f9df3731ecd0e54d8c84c9", size = 556563 }, ] +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310 }, +] + [[package]] name = "referencing" version = "0.36.2" @@ -2817,6 +3197,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, +] + [[package]] name = "resampy" version = "0.4.3" @@ -2854,6 +3246,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490 }, ] +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 }, +] + [[package]] name = "rfc3986-validator" version = "0.1.1" @@ -2863,6 +3264,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242 }, ] +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, +] + [[package]] name = "rpds-py" version = "0.23.1" @@ -2948,6 +3363,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/bb/e45f51c4e1327dea3c72b846c6de129eebacb7a6cb309af7af35d0578c80/rpds_py-0.23.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:75307599f0d25bf6937248e5ac4e3bde5ea72ae6618623b86146ccc7845ed00b", size = 233827 }, ] +[[package]] +name = "rpy2" +version = "3.5.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "jinja2" }, + { name = "packaging", marker = "sys_platform == 'win32'" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/85/f3bf48052106fd93e74bea512a43d3de48617953cba7e6dd40e8626f27f5/rpy2-3.5.17.tar.gz", hash = "sha256:dbff08c30f3d79161922623858a5b3b68a3fba8ee1747d6af41bc4ba68f3d582", size = 220963 } + [[package]] name = "ruff" version = "0.9.7" @@ -3085,6 +3512,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705 }, ] +[[package]] +name = "scipy-stubs" +version = "1.15.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "optype" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/ee/0c3e93545b53d3b22e662fbe251c3e61c2b14742f296f36708eff4a2898c/scipy_stubs-1.15.2.2.tar.gz", hash = "sha256:0137d907d75381d2eda4f6af5b1d3211759cb193a0eadf5195716fb0b01ca3cb", size = 275755 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/1a/3eba813584e398d589e1d4e0dac0cf822ce9e25b28cb2d1f0012d137c968/scipy_stubs-1.15.2.2-py3-none-any.whl", hash = "sha256:f02fe66124b58bce5f0897ecd48d0e79226a999cc4e6984a9472520c20b8e4b6", size = 459133 }, +] + [[package]] name = "seaborn" version = "0.13.2" @@ -3099,6 +3538,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914 }, ] +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, +] + [[package]] name = "send2trash" version = "1.8.3" @@ -3117,6 +3569,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, ] +[[package]] +name = "setuptools-scm" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/19/7ae64b70b2429c48c3a7a4ed36f50f94687d3bfcd0ae2f152367b6410dff/setuptools_scm-8.3.1.tar.gz", hash = "sha256:3d555e92b75dacd037d32bafdf94f97af51ea29ae8c7b234cf94b7a5bd242a63", size = 78088 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/ac/8f96ba9b4cfe3e4ea201f23f4f97165862395e9331a424ed325ae37024a8/setuptools_scm-8.3.1-py3-none-any.whl", hash = "sha256:332ca0d43791b818b841213e76b1971b7711a960761c5bea5fc5cdb5196fbce3", size = 43935 }, +] + [[package]] name = "six" version = "1.17.0" @@ -3137,7 +3603,6 @@ wheels = [ [[package]] name = "soundscapy" -version = "0.7.9.dev0" source = { editable = "." } dependencies = [ { name = "loguru" }, @@ -3155,6 +3620,7 @@ all = [ { name = "acoustic-toolbox" }, { name = "mosqito" }, { name = "numba" }, + { name = "rpy2" }, { name = "scikit-maad" }, { name = "tqdm" }, ] @@ -3165,17 +3631,29 @@ audio = [ { name = "scikit-maad" }, { name = "tqdm" }, ] +spi = [ + { name = "rpy2" }, +] [package.dev-dependencies] dev = [ - { name = "bumpver" }, + { name = "build" }, + { name = "mypy" }, + { name = "pandas-stubs" }, + { name = "pre-commit" }, { name = "ruff" }, + { name = "scipy-stubs" }, + { name = "setuptools-scm" }, + { name = "tox" }, + { name = "twine" }, + { name = "types-pyyaml" }, ] docs = [ { name = "ipywidgets" }, { name = "jupyter" }, { name = "jupyter-dash" }, { name = "mkdocs" }, + { name = "mkdocs-include-markdown-plugin" }, { name = "mkdocs-jupyter" }, { name = "mkdocs-material" }, { name = "mkdocstrings", extra = ["python"] }, @@ -3202,24 +3680,35 @@ requires-dist = [ { name = "plotly", specifier = ">=5.23.0" }, { name = "pydantic", specifier = ">=2.8.2" }, { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "rpy2", marker = "extra == 'spi'", specifier = ">=3.5.0" }, { name = "scikit-maad", marker = "extra == 'audio'", specifier = ">=1.4.3" }, { name = "scipy", specifier = ">=1.14.1" }, { name = "seaborn", specifier = ">=0.13.2" }, { name = "soundscapy", extras = ["audio"], marker = "extra == 'all'" }, + { name = "soundscapy", extras = ["spi"], marker = "extra == 'all'" }, { name = "tqdm", marker = "extra == 'audio'", specifier = ">=4.66.5" }, ] -provides-extras = ["all", "audio"] +provides-extras = ["all", "audio", "spi"] [package.metadata.requires-dev] dev = [ - { name = "bumpver", specifier = ">=2023.1129" }, + { name = "build", specifier = ">=1.2.2.post1" }, + { name = "mypy", specifier = ">=1.15.0" }, + { name = "pandas-stubs", specifier = ">=2.2.3.250308" }, + { name = "pre-commit", specifier = ">=4.2.0" }, { name = "ruff", specifier = ">=0.7.2" }, + { name = "scipy-stubs", specifier = ">=1.15.2.2" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, + { name = "tox", specifier = ">=4.25.0" }, + { name = "twine", specifier = ">=6.1.0" }, + { name = "types-pyyaml", specifier = ">=6.0.12.20250402" }, ] docs = [ { name = "ipywidgets", specifier = ">=8.1.3" }, { name = "jupyter", specifier = ">=1.1.1" }, { name = "jupyter-dash", specifier = ">=0.4.2" }, { name = "mkdocs", specifier = ">=1.6.0" }, + { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.5" }, { name = "mkdocs-jupyter", specifier = ">=0.24.8" }, { name = "mkdocs-material", specifier = ">=9.5.31" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.25.2" }, @@ -3305,15 +3794,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610 }, ] -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, -] - [[package]] name = "tomli" version = "2.2.1" @@ -3371,6 +3851,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907 }, ] +[[package]] +name = "tox" +version = "4.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "chardet" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/87/692478f0a194f1cad64803692642bd88c12c5b64eee16bf178e4a32e979c/tox-4.25.0.tar.gz", hash = "sha256:dd67f030317b80722cf52b246ff42aafd3ed27ddf331c415612d084304cf5e52", size = 196255 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/38/33348de6fc4b1afb3d76d8485c8aecbdabcfb3af8da53d40c792332e2b37/tox-4.25.0-py3-none-any.whl", hash = "sha256:4dfdc7ba2cc6fdc6688dde1b21e7b46ff6c41795fb54586c91a3533317b5255c", size = 172420 }, +] + [[package]] name = "tqdm" version = "4.67.1" @@ -3392,6 +3894,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, ] +[[package]] +name = "twine" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791 }, +] + [[package]] name = "types-python-dateutil" version = "2.9.0.20241206" @@ -3401,6 +3923,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/b3/ca41df24db5eb99b00d97f89d7674a90cb6b3134c52fb8121b6d8d30f15c/types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53", size = 14384 }, ] +[[package]] +name = "types-pytz" +version = "2025.2.0.20250326" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/66/38c89861242f2c61c8315ddbcc7d7bbf64979f4b0bdc48db0ba62aeec330/types_pytz-2025.2.0.20250326.tar.gz", hash = "sha256:deda02de24f527066fc8d6a19e284ab3f3ae716a42b4adb6b40e75e408c08d36", size = 10595 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/e0/17f3a6670db5c95dc195f346e2e7290f22ba8327c188133959389b578cbd/types_pytz-2025.2.0.20250326-py3-none-any.whl", hash = "sha256:3c397fd1b845cd2b3adc9398607764ced9e578a98a5d1fbb4a9bc9253edfb162", size = 10222 }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250402" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -3419,6 +3959,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, ] +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 }, +] + [[package]] name = "uri-template" version = "1.3.0" @@ -3437,6 +3989,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, ] +[[package]] +name = "virtualenv" +version = "20.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461 }, +] + [[package]] name = "watchdog" version = "6.0.0" @@ -3469,6 +4035,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, ] +[[package]] +name = "wcmatch" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/ab/b3a52228538ccb983653c446c1656eddf1d5303b9cb8b9aef6a91299f862/wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a", size = 115578 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/df/4ee467ab39cc1de4b852c212c1ed3becfec2e486a51ac1ce0091f85f38d7/wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a", size = 39347 }, +] + [[package]] name = "wcwidth" version = "0.2.13"