pax_global_header00006660000000000000000000000064152170321270014512gustar00rootroot0000000000000052 comment=f6919cc882e752d788e815cb35285ca3126d5260 astropy-regions-f4139aa/000077500000000000000000000000001521703212700152715ustar00rootroot00000000000000astropy-regions-f4139aa/.flake8000066400000000000000000000000571521703212700164460ustar00rootroot00000000000000[flake8] max-line-length = 79 exclude = extern astropy-regions-f4139aa/.gitattributes000066400000000000000000000000161521703212700201610ustar00rootroot00000000000000*.fits binary astropy-regions-f4139aa/.github/000077500000000000000000000000001521703212700166315ustar00rootroot00000000000000astropy-regions-f4139aa/.github/dependabot.yml000066400000000000000000000010461521703212700214620ustar00rootroot00000000000000# Keep dependencies updated with Dependabot version updates # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: ".github/workflows/" schedule: interval: "monthly" cooldown: default-days: 7 groups: actions: patterns: - "*" astropy-regions-f4139aa/.github/workflows/000077500000000000000000000000001521703212700206665ustar00rootroot00000000000000astropy-regions-f4139aa/.github/workflows/ci_cron_daily.yml000066400000000000000000000050601521703212700242100ustar00rootroot00000000000000name: Daily Cron Tests on: schedule: # Run at 6am UTC on Tue-Fri (complete tests are run every Monday) - cron: '0 6 * * 2-5' pull_request: # We also want this workflow triggered if the 'Daily CI' label is added # or present when PR is updated types: - synchronize - labeled push: tags: - '*' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read env: TOXARGS: '-v' jobs: tests: if: | github.repository == 'astropy/regions' && ( github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'Daily CI') ) name: ${{ matrix.prefix }} ${{ matrix.os }}, ${{ matrix.tox_env }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.allow_failure }} strategy: matrix: include: - os: ubuntu-latest python: '3.x' tox_env: 'linkcheck' allow_failure: false prefix: '' - os: ubuntu-latest python: '3.14' tox_env: 'py314-test-devdeps' toxposargs: --remote-data=any allow_failure: true prefix: '(Allowed failure)' steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false fetch-depth: 0 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python }} allow-prereleases: true - name: Install base dependencies run: python -m pip install --upgrade pip setuptools tox - name: Print Python, pip, setuptools, and tox versions run: | python -c "import sys; print(f'Python {sys.version}')" python -c "import pip; print(f'pip {pip.__version__}')" python -c "import setuptools; print(f'setuptools {setuptools.__version__}')" python -c "import tox; print(f'tox {tox.__version__}')" - name: Run tests run: python -m tox -e ${{ matrix.tox_env }} -- ${{ matrix.toxposargs }} - name: Upload coverage to codecov if: ${{ contains(matrix.tox_env, '-cov') }} uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: files: ./coverage.xml verbose: true astropy-regions-f4139aa/.github/workflows/ci_cron_weekly.yml000066400000000000000000000110371521703212700244070ustar00rootroot00000000000000name: Weekly Cron Tests on: schedule: # Run every Monday at 5am UTC - cron: '0 5 * * 1' pull_request: # We also want this workflow triggered if the 'Weekly CI' label is added # or present when PR is updated types: - synchronize - labeled push: tags: - '*' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read env: TOXARGS: '-v' IS_CRON: 'true' jobs: tests: if: | github.repository == 'astropy/regions' && ( github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'Weekly CI') ) name: ${{ matrix.os }}, ${{ matrix.tox_env }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.allow_failure }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest python: '3.14' tox_env: 'py314-test-alldeps-devinfra' allow_failure: false steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false fetch-depth: 0 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python }} - name: Install base dependencies run: python -m pip install --upgrade pip setuptools tox - name: Print Python, pip, setuptools, and tox versions run: | python -c "import sys; print(f'Python {sys.version}')" python -c "import pip; print(f'pip {pip.__version__}')" python -c "import setuptools; print(f'setuptools {setuptools.__version__}')" python -c "import tox; print(f'tox {tox.__version__}')" - name: Run tests run: python -m tox -e ${{ matrix.tox_env }} -- ${{ matrix.toxposargs }} test_more_architectures: # The following architectures are emulated and are therefore slow, so # we include them just in the weekly cron. These also serve as a test # of using system libraries and using pytest directly. runs-on: ubuntu-latest name: More architectures if: | github.repository == 'astropy/regions' && ( github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'Arch CI') ) env: ARCH_ON_CI: ${{ matrix.arch }} strategy: fail-fast: false matrix: include: - arch: s390x - arch: ppc64le - arch: armv7 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false fetch-depth: 0 - uses: uraimo/run-on-arch-action@f9b26e3a1a408d5fd530d20c17b9f3f4428ff8d9 # v3.1.0 name: Run tests id: build with: arch: ${{ matrix.arch }} distro: ubuntu_rolling shell: /bin/bash env: | ARCH_ON_CI: ${{ env.ARCH_ON_CI }} IS_CRON: ${{ env.IS_CRON }} install: | apt-get update -q -y apt-get install -q -y --no-install-recommends \ git \ g++ \ pkg-config \ python3 \ python3-astropy \ python3-erfa \ python3-extension-helpers \ python3-numpy \ python3-pytest-astropy \ python3-setuptools-scm \ python3-scipy \ python3-venv \ python3-wheel \ wcslib-dev run: | uname -a echo "LONG_BIT="$(getconf LONG_BIT) python3 -m venv --system-site-packages tests source tests/bin/activate # Update older versions pip install -U cython setuptools packaging pip install -U --no-build-isolation pyerfa ASTROPY_USE_SYSTEM_ALL=1 pip3 install -v --no-build-isolation -e .[test] pip3 list python3 -m pytest astropy-regions-f4139aa/.github/workflows/ci_test_predeps.yml000066400000000000000000000032641521703212700245720ustar00rootroot00000000000000name: Test Pre-release Dependencies on: workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: tests: name: ${{ matrix.os }}, ${{ matrix.tox_env }} runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: include: - os: ubuntu-latest python: '3.14' tox_env: 'py314-test-alldeps-predeps' toxposargs: --remote-data - os: macos-latest python: '3.14' tox_env: 'py314-test-alldeps-predeps' - os: windows-latest python: '3.14' tox_env: 'py314-test-alldeps-predeps' steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false fetch-depth: 0 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python }} allow-prereleases: true - name: Install base dependencies run: python -m pip install --upgrade pip setuptools tox - name: Print Python, pip, setuptools, and tox versions run: | python -c "import sys; print(f'Python {sys.version}')" python -c "import pip; print(f'pip {pip.__version__}')" python -c "import setuptools; print(f'setuptools {setuptools.__version__}')" python -c "import tox; print(f'tox {tox.__version__}')" - name: Run tests run: python -m tox -e ${{ matrix.tox_env }} -- -n=2 ${{ matrix.toxposargs }} astropy-regions-f4139aa/.github/workflows/ci_tests.yml000066400000000000000000000075771521703212700232460ustar00rootroot00000000000000name: CI Tests on: push: branches: - main tags: - '*' pull_request: schedule: # Run every Monday at 6am UTC - cron: '0 6 * * 1' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read env: TOXARGS: '-v' jobs: tests: name: ${{ matrix.prefix }} ${{ matrix.os }}, ${{ matrix.tox_env }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.allow_failure }} strategy: matrix: include: - os: ubuntu-latest python: '3.11' tox_env: 'py311-test-oldestdeps' allow_failure: false prefix: '' - os: ubuntu-latest python: '3.11' tox_env: 'py311-test-alldeps' allow_failure: false prefix: '' - os: ubuntu-latest python: '3.12' tox_env: 'py312-test-alldeps' allow_failure: false prefix: '' - os: ubuntu-latest python: '3.13' tox_env: 'py313-test-alldeps' allow_failure: false prefix: '' - os: ubuntu-latest python: '3.14' tox_env: 'py314-test-alldeps-cov' toxposargs: --remote-data=any allow_failure: false prefix: '' # Test without all dependencies - os: ubuntu-latest python: '3.14' tox_env: 'py314-test' toxposargs: --remote-data=any allow_failure: false prefix: '' - os: ubuntu-24.04-arm python: '3.14' tox_env: 'py314-test' allow_failure: false prefix: '' - os: macos-latest python: '3.14' tox_env: 'py314-test-alldeps' allow_failure: false prefix: '' - os: windows-latest python: '3.14' tox_env: 'py314-test-alldeps' allow_failure: false prefix: '' - os: ubuntu-latest python: '3.x' tox_env: 'codestyle' allow_failure: false prefix: '' - os: ubuntu-latest python: '3.x' tox_env: 'pep517' allow_failure: false prefix: '' - os: ubuntu-latest python: '3.x' tox_env: 'bandit' allow_failure: false prefix: '' - os: ubuntu-latest python: '3.14' tox_env: 'py314-test-devdeps' toxposargs: --remote-data=any allow_failure: true prefix: '(Allowed failure)' steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false fetch-depth: 0 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python }} allow-prereleases: true - name: Install base dependencies run: python -m pip install --upgrade pip setuptools tox - name: Print Python, pip, setuptools, and tox versions run: | python -c "import sys; print(f'Python {sys.version}')" python -c "import pip; print(f'pip {pip.__version__}')" python -c "import setuptools; print(f'setuptools {setuptools.__version__}')" python -c "import tox; print(f'tox {tox.__version__}')" - name: Run tests run: python -m tox -e ${{ matrix.tox_env }} -- -n=2 ${{ matrix.toxposargs }} - name: Upload coverage to codecov if: ${{ contains(matrix.tox_env, '-cov') }} uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: files: ./coverage.xml verbose: true astropy-regions-f4139aa/.github/workflows/codeql.yml000066400000000000000000000063171521703212700226670ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ "main" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] schedule: - cron: '33 1 * * 0' permissions: contents: read jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Use only 'java' to analyze code written in Java, Kotlin or both # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false fetch-depth: 0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0 with: category: "/language:${{matrix.language}}" astropy-regions-f4139aa/.github/workflows/publish.yml000066400000000000000000000055131521703212700230630ustar00rootroot00000000000000name: Wheel building on: schedule: # Run every day at 4:30am UTC - cron: '30 4 * * *' pull_request: # We also want this workflow triggered if the 'Build all wheels' # label is added or present when PR is updated types: - synchronize - labeled push: branches: - 'main' tags: - '*' - '!*dev*' - '!*pre*' - '!*post*' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: build_and_publish: permissions: contents: none uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish.yml@2835f0cacddf3f8de198db9afdb5354a5cebe0ef # v2.6.3 if: | github.repository == 'astropy/regions' && ( github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'Build all wheels') ) with: upload_to_pypi: false save_artifacts: true test_extras: test test_command: pytest -p no:warnings --pyargs regions targets: | # Linux wheels - cp3*-manylinux_x86_64 - target: cp3*-manylinux_aarch64 runs-on: ubuntu-24.04-arm # MacOS X wheels - cp3*-macosx_x86_64 - cp3*-macosx_arm64 # Windows wheels - cp3*-win_amd64 - cp3*-win_arm64 # Developer wheels upload_to_anaconda: >- ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} anaconda_user: astropy anaconda_package: regions anaconda_keep_n_latest: 10 secrets: anaconda_token: ${{ secrets.anaconda_token }} upload: # This job publishes the built wheels to PyPI for all tags, except # those ending in ".dev". For PRs with the "Build all wheels" label, # wheels are built, but are not uploaded to PyPI. permissions: id-token: write environment: name: pypi url: https://pypi.org/project/regions # We upload to PyPI for all tag pushes, except tags ending in .dev if: | needs.build_and_publish.result == 'success' && startsWith(github.ref, 'refs/tags/') && !endsWith(github.ref, '.dev') && ( github.event_name == 'push' || github.event_name == 'workflow_dispatch' ) name: Upload release to PyPI runs-on: ubuntu-latest needs: [build_and_publish] steps: - name: Download artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: merge-multiple: true pattern: dist-* path: dist - name: Upload to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 astropy-regions-f4139aa/.github/workflows/zizmor.yml000066400000000000000000000013661521703212700227510ustar00rootroot00000000000000name: zizmor GHA Security Analysis on: pull_request: branches: ['main'] paths: - '.github/workflows/**' push: branches: ['main'] paths: - '.github/workflows/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: {} jobs: zizmor: runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Run zizmor 🌈 uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 with: advanced-security: false annotations: true astropy-regions-f4139aa/.gitignore000066400000000000000000000015561521703212700172700ustar00rootroot00000000000000# Compiled files *.py[cod] *.a *.o *.so *.whl __pycache__ # Ignore .c files by default to avoid including generated code. If you want to # add a non-generated .c extension, use `git add -f filename.c`. *.c # Other generated files */version.py */cython_version.py MANIFEST *oldest_deps*.txt requirements-min.txt htmlcov .coverage* .ipynb_checkpoints .pytest_cache # Sphinx docs/_build docs/api # Packages/installer info *.egg *.egg-info dist build eggs .eggs parts bin var sdist develop-eggs .installed.cfg distribute-*.tar.gz pip-wheel-metadata # Virtual environments .venv # uv (Python package manager) uv.lock # pipenv Pipfile.lock Pipfile # poetry poetry.lock # Other .cache .tox .tmp .*.sw[op] *~ # Eclipse editor project files .project .pydevproject .settings # PyCharm editor project files .idea # Visual Studio Code project files .vscode # Mac OSX .DS_Store astropy-regions-f4139aa/.pre-commit-config.yaml000066400000000000000000000102641521703212700215550ustar00rootroot00000000000000ci: autofix_prs: false autoupdate_schedule: 'monthly' repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-added-large-files # Prevent giant files from being committed. - id: check-ast # Simply check whether files parse as valid python. - id: check-case-conflict # Check for files with names that would conflict on a case-insensitive # filesystem like MacOS HFS+ or Windows FAT. - id: check-json # Attempts to load all json files to verify syntax. - id: check-merge-conflict # Check for files that contain merge conflict strings. - id: check-symlinks # Checks for symlinks which do not point to anything. - id: check-toml # Attempts to load all TOML files to verify syntax. - id: check-xml # Attempts to load all xml files to verify syntax. - id: check-yaml # Attempts to load all yaml files to verify syntax. - id: debug-statements # Check for debugger imports and py37+ breakpoint() calls in python # source. - id: detect-private-key # Checks for the existence of private keys. - id: double-quote-string-fixer # Replace double-quoted strings with single-quoted strings. - id: end-of-file-fixer # Makes sure files end in a newline and only a newline. exclude: '.*(svg.*|extern.*|pkl.*|reg.*)$' - id: trailing-whitespace # Trims trailing whitespace. exclude: '.*(data.*|extern.*)$' - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: python-check-mock-methods # Prevent common mistakes of assert mck.not_called(), assert # mck.called_once_with(...) and mck.assert_called. - id: rst-directive-colons # Detect mistake of rst directive not ending with double colon. - id: rst-inline-touching-normal # Detect mistake of inline code touching normal text in rst. - id: text-unicode-replacement-char # Forbid files which have a UTF-8 Unicode replacement character. - id: python-check-blanket-noqa # Enforce that all noqa annotations always occur with specific codes. - repo: https://github.com/astral-sh/ruff-pre-commit rev: 'v0.15.15' hooks: - id: ruff-check args: ['--fix', '--show-fixes'] - repo: https://github.com/scientific-python/cookie rev: 2026.04.04 hooks: - id: sp-repo-review - repo: https://github.com/pycqa/isort rev: 9.0.0a3 hooks: - id: isort name: isort (python) additional_dependencies: [toml] - id: isort name: isort (cython) types: [cython] additional_dependencies: [toml] - repo: https://github.com/PyCQA/flake8 rev: 7.3.0 hooks: - id: flake8 args: ['--ignore', 'E501,W503'] - repo: https://github.com/MarcoGorelli/cython-lint rev: v0.19.0 hooks: - id: cython-lint - repo: https://github.com/woodruffw/zizmor-pre-commit rev: v1.25.2 hooks: - id: zizmor - repo: https://github.com/rbubley/mirrors-prettier rev: v3.8.3 hooks: - id: prettier types_or: [css, rst, json, yaml, toml, markdown] args: ['--single-quote'] # Exclude codeql.yml or dependabot.yml exclude: ^(.*\/)?(codeql\.yml|dependabot\.yml)$ - repo: https://github.com/codespell-project/codespell rev: v2.4.2 hooks: - id: codespell args: ['--write-changes'] additional_dependencies: - tomli - repo: https://github.com/numpy/numpydoc rev: v1.10.0 hooks: - id: numpydoc-validation exclude: 'extern/' # TMP: disabled until new release (Python 3.14 support and without # requiring untokenize) and various regressions are fixed # Formats docstrings to follow PEP 257 # - repo: https://github.com/PyCQA/docformatter # rev: v1.7.7 # hooks: # - id: docformatter # args: [--in-place, --config, ./pyproject.toml] # exclude: "extern/" - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v1.0.2 hooks: - id: sphinx-lint exclude: '(docs/_build/|regions/extern/)' astropy-regions-f4139aa/.pycodestyle000066400000000000000000000000641521703212700176360ustar00rootroot00000000000000[pycodestyle] max-line-length = 79 exclude = extern astropy-regions-f4139aa/.readthedocs.yaml000066400000000000000000000007021521703212700205170ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 apt_packages: - graphviz tools: python: '3.14' jobs: post_checkout: - git fetch --shallow-since=2022-10-01 || true pre_install: - git update-index --assume-unchanged docs/conf.py sphinx: builder: html configuration: docs/conf.py fail_on_warning: true python: install: - method: pip path: . extra_requirements: - docs - all formats: [] astropy-regions-f4139aa/CHANGES.rst000066400000000000000000000465571521703212700171140ustar00rootroot000000000000000.12 (2026-06-24) ================= General ------- - The minimum required NumPy is now 2.0. [#654] - The minimum required Astropy is now 6.1.7. [#654, #690] - The minimum required Matplotlib is now 3.9. [#654] New Features ------------ - Added a ``to_polygon`` method to all region classes that returns a ``PolygonPixelRegion`` or ``PolygonSkyRegion`` approximation of the region. [#649] - Improved pixel–sky coordinate conversions for non-distorted WCS objects, providing more accurate local pixel scales and North angle estimates. [#650, #651] - Improved pixel–sky coordinate conversions for distorted WCS objects (e.g., SIP) and non-astropy WCS implementations (e.g., GWCS), enabling more accurate transformations for directed regions such as ellipses and rectangles. [#650, #651] - Added an ``as_ellipse`` keyword to the ``to_pixel`` and ``to_sky`` methods of circular and circular annulus region classes that allows the user to specify whether to return an elliptical region instead of a circular region when converting between pixel and sky coordinates. This is useful for cases where the WCS is distorted or has different pixel scales along different axes and the circular region would be significantly distorted in pixel or sky coordinates. [#652] - Improved the string representations of all ``Regions`` and ``Region`` objects. [#653] - Regions now includes ``SphericalSkyRegion`` ("region-on-celestial-sphere"), complementing the implicitly planar ``SkyRegion`` ("region-on-images"). ``SphericalSkyRegion`` does not require a WCS to determine whether points are contained within the region or not (unlike ``SkyRegion``). Additionally, ``SphericalSkyRegion`` classes include the method ``transform_to`` to transform the regions between different celestial coordinate reference frames. It is also possible to transform between spherical and planar (sky or pixel) regions (with ``to_sky``, ``to_pixel``, and ``to_spherical_sky``, as appropriate), with the option to capture boundary distortions due to WCS projection effects between spherical and planar geometries or to ignore them (e.g., assuming a circle stays a perfect circle). Current spherical shapes supported include: ``CircleSphericalSkyRegion``, ``CircleAnnulusSphericalSkyRegion``, ``PolygonSphericalSkyRegion`` (currently only supports convex polygons), ``RangeSphericalSkyRegion`` (i.e., bounded by ranges of longitude and/or latitude), and ``LuneSphericalSkyRegion`` (a slice between two great circles, such as between two lines of longitude). Support for additional spherical shapes, and for all cases of planar <-> spherical transformation (where well defined) may be added at a future date. [#618] - Added a ``covers`` method to all pixel region classes (``CirclePixelRegion``, ``EllipsePixelRegion``, ``RectanglePixelRegion``, ``PolygonPixelRegion``, ``LinePixelRegion``, ``PointPixelRegion``, annulus pixel regions, and ``CompoundPixelRegion``) and to the corresponding sky region classes (``SkyRegion.covers``, ``SphericalSkyRegion.covers``, and all spherical sky region subclasses including ``LuneSphericalSkyRegion``, ``RangeSphericalSkyRegion``, ``WholeSphericalSkyRegion``, ``PolygonSphericalSkyRegion``, and ``CompoundSphericalSkyRegion``). This method checks whether points fall inside or on the boundary of a region, consistent with Shapely's ``covers`` function and DE-9IM semantics. [#680] - The ``'exact'`` masking mode (i.e., the ``to_mask`` method) for rectangular and polygonal regions has been implemented. [#685] Bug Fixes --------- - Fixed reading FITS region files whose ``SHAPE`` column values contain leading or trailing whitespace (e.g. from fixed-width character-column padding or cross-platform encoding quirks), which previously raised a spurious ``FITSParserError``. [#674] - Fixed the ``to_sky`` and ``to_pixel`` conversions for elliptical and rectangular regions (and their annulus variants) so that the orientation is correct for a WCS with a flipped parity (e.g., North down, East left). Previously, such regions were mirrored about the x-axis. [#676] - Fixed the local WCS Jacobian computation used by the ``to_sky`` and ``to_pixel`` methods so that it is well-conditioned at the celestial poles and across the RA = 0/360 degree wraparound. [#676] - Fixed rectangular regions to use the SVD-based ellipse ``to_sky`` and ``to_pixel`` conversions so that they round-trip correctly with a sheared WCS. [#676] - Fixed ``Region`` comparison behavior so that numeric parameters (``PixCoord``, ``astropy.units.Quantity``, and ``astropy.coordinates.SkyCoord``) are compared using ``np.allclose`` rather than exact equality, making equality checks more robust to floating-point precision differences. [#677] API Changes ----------- - The ``LinePixelRegion`` ``as_artist`` method now returns a ``matplotlib.lines.Line2D`` object instead of a ``matplotlib.patches.Arrow`` object. [#668] - The ``contains`` method now consistently excludes the region boundary for all pixel regions, matching Shapely's ``contains`` function and DE-9IM semantics. Previously, ``EllipsePixelRegion.contains`` included the boundary and ``PolygonPixelRegion.contains`` treated boundary points inconsistently. Use the new ``covers`` method to include the boundary. [#680] - The ``contains`` methods of all regions now ignore the ``'include'`` metadata attribute (which was previously used to invert evaluation results), making containment and covering behavior depend solely on the geometric shape of the region. [#680] - The ``make_example_dataset`` function is deprecated and will be removed in a future release. [#682] 0.11 (2025-11-05) ================= General ------- - The minimum required Python is now 3.11. [#598] - The minimum required NumPy is now 1.25. [#598] - The minimum required Astropy is now 6.0. [#598] - The minimum required Matplotlib is now 3.8. [#598] - The minimum required Shapely is now 2.0. [#601] Bug Fixes --------- - No longer overwrites input ``meta`` and ``visible`` values when initializing a ``CompoundSkyRegion``. [#628] 0.10 (2024-09-27) ================= General ------- - Added support for NumPy 2.1. API Changes ----------- - The ``regions.test`` function has been removed. Instead use the ``pytest --pyarg regions`` command. [#549] 0.9 (2024-04-12) ================ General ------- - Added support for NumPy 2.0. - The minimum required Python is now 3.10. [#547] - The minimum required NumPy is now 1.23. [#547] - The minimum required Astropy is now 5.1. [#547] - The minimum required pytest-astropy is now 0.11. [#547] - The minimum required sphinx-astropy is now 1.9. [#547] Bug Fixes --------- - Fixed a bug where the text string of ``TextPixelRegion`` and ``TextSkyRegion`` was dropped when serializing or writing to a DS9 file. [#548] 0.8 (2023-11-16) ================ General ------- - The minimum required Python is now 3.9. [#517] - The minimum required NumPy is now 1.22. [#517] - The minimum required Matplotlib is now 3.5. [#517] New Features ------------ - The ``Regions`` class can now be initialized without any arguments. [#527] - The ``Regions`` ``extend`` method now can accept another ``Regions`` object as input. [#527] Bug Fixes --------- API Changes ----------- - The ``Regions`` class and its ``append`` and ``extend`` methods now raise a ``TypeError`` for invalid inputs. [#527] 0.7 (2022-10-27) ================ New Features ------------ - Region meta and visual metadata can be now input as a ``dict``. [#462] Bug Fixes --------- - Fixed an issue with the CRTF for certain malformed input. [#448] - Fixed a bug where DS9 serialization of an empty list raised an error. [#449] - Added validation to ``RegionMeta`` and ``RegionVisual`` ``update`` and ``setdefault`` methods. [#463] - Fixed an issue reading CRTF file with labelcolor defined. [#473] - Fixed a bug parsing CRTF files with ":" and "." coordinate separators. [#407] API Changes ----------- - Removed the deprecated ``BoundingBox`` class. Use ``RegionBoundingBox`` instead. [#456] 0.6 (2022-03-21) ================ New Features ------------ - Added the DS9 'boxcircle' point symbol. [#387] - Added the ability to add and subtract ``PixCoord`` objects. [#396] - Added an ``origin`` keyword to ``PolygonPixelRegion`` to allow specifying the vertices relative to an origin pixel. [#397] - Added a ``RegularPolygonPixelRegion`` class. [#398] - Added the ability to compare ``Region`` objects for equality. [#421] - Added a ``copy`` method to ``RegionMeta`` and ``RegionVisual``. [#424] - Added the ability to handle DS9 elliptical and rectangular annuli and multi-annuli regions. [#436] - Added the ability to handle DS9 composite regions. [#436] - Added the ability to serialize sky regions in the DS9 file format using the coordinate frame of the ``SkyCoord`` object. [#436] - Added the ability to serialize multiple regions in the DS9 file format that have different coordinate frames. [#436] - Added the ability to parse FITS region ``rectangle`` and ``rotrectangle`` shapes. [#444] Bug Fixes --------- - Fixed the DS9 default point symbol to use 'boxcircle'. [#387] - Point symbol markers are no longer filled for consistency with DS9. [#387] - Fixed an issue where plotting elliptical regions were incorrectly filled by default. [#389] - Fixed an issue where compound region colors were being set correctly. [#389] - Fixed an issue where sky/pixel conversions did not preserve ``meta`` and ``visual`` data for some regions. [#420, #424] - Fixed an issue with elliptical and rectangular annulus regions where the outer width/height could be smaller than the inner width/height. [#425] - Fixed an issue converting elliptical and rectangular annulus regions between pixel and sky regions. [#425] - Fixed the string representations of ``TextPixelRegion`` and ``TextSkyRegion`` to include quotes around the text parameter value. [#429] - Fixed many issues with the DS9 parser and serializer in not consistently handling or preserving the region coordinate frame or region parameter units. [#436] - Fixed handling of FITS shapes that are preceded by an exclamation mark. [#444] - Fixed a bug where written FITS region files could not be read back in. [#444] API Changes ----------- - Removed the following deprecated I/O classes and functions: ``crtf_objects_to_string``, ``ds9_objects_to_string``, ``fits_region_object_to_table``, ``read_crtf``, ``read_ds9``, ``read_fits``, ``read_fits_region``, ``write_crtf``, ``write_ds9``, ``write_fits``, ``write_fits_region`` ``CRTFParser``, ``DS9Parser``, ``FITSRegionParser``, ``ShapeList``, and ``Shape``. The ``Regions`` and ``Region`` objects now support this functionality via a unified I/O interface. [#386] - Removed the deprecated ``BoundingBox`` ``slices`` attribute. [#386] - The default matplotlib keywords that are used when plotting now depend on the value of ``self.visual['default_style']``. This keyword is currently set (to a value of 'ds9') only when reading DS9 region files. If set to 'ds9', DS9 plotting defaults are used. If not set or set to 'mpl' or None, then the matplotlib defaults will be used, with the exception that fill is turned off for Patch and Line2D artists. - Renamed the ``BoundingBox`` class to ``RegionBoundingBox``. The old name is deprecated. [#427] - A ``ValueError`` is raised if the radius, width, or height region parameters are not strictly positive (> 0). [#430] - Added a ``precision`` keyword to the DS9 serializer and writer to specify the number of decimal places in output numbers. [#436] - The ``errors`` keyword was removed from the DS9 parser and reader and the ``coordsys``, ``radunit``, and ``fmt`` keywords were removed from the DS9 serializer and writer. The new ``precision`` keyword can be used when serializing and writing DS9 regions. [#436] - The ``PixelRegion.plot()`` method now returns a ``matplotlib.artist.Artist`` object, which can be used in plot legends. [#441] - FITS region files are now always parsed and serialized as ``PixelRegion`` objects. They can be converted to ``SkyRegion`` objects using a WCS object. [#444] 0.5 (2021-07-20) ================ General ------- - The infrastructure of the package has been updated in line with the APE 17 guidelines. The main changes are that the ``python setup.py test`` and ``python setup.py build_docs`` commands will no longer work. The easiest way to replicate these commands is to install the tox package and run ``tox -e test`` and ``tox -e build_docs``. It is also possible to run pytest and sphinx directly. Other significant changes include switching to setuptools_scm to manage the version number, and adding a ``pyproject.toml`` to opt in to isolated builds as described in PEP 517/518. [#315] - Bump the minimum required version of Astropy to 3.2. New Features ------------ - Added a ``as_mpl_selector`` method to the rectangular and ellipse pixel-based regions. This method returns an interactive Matplotlib selector widget. [#317] - Added a ``get_overlap_slices`` method to ``BoundingBox``. [#348] - Added a ``center`` attribute to ``BoundingBox``. [#348] - Added ``get_overlap_slices`` method to ``RegionMask``. [#350] - Added ``get_values`` method to ``RegionMask``. [#351, #353] - Added a ``Regions`` class with a unified I/O interface for reading, writing, parsing, and serializing regions. [#378] - Added ``serialize`` and ``write`` methods to all ``Region`` subclasses. [#378] Bug Fixes --------- - Fixed an issue where ``RegionMask.multiply`` ``fill_value`` was not applied to pixels outside of the mask, but within the region bounding box. [#346] - Fixed an issue where ``RegionMask.cutout`` would raise an error if ``fill_value`` was non-finite and the input array was integer type. [#346] - A ``ValueError`` is now raised when calling ``BoundingBox.slices`` when ``ixmin`` or ``iymin`` is negative. [#347] - Fixed an issue in the DS9 parser where uppercase coordinate frames would fail. [#237] - Fixed an issue where the CRTF file parser would fail if the CRTF version number was included on the first line. [#240] - Fixed an issue where the CRTF file parser would fail if there was a space after the region name. [#271] - Fixed an issue where the CRTF file parser was too restrictive about requiring the last and first polynomial coordinates to be the same. [#359, #362] - Fixed a bug where an ``EllipsePixelRegion`` with zero height and/or width would raise a ``ValueError`` when creating a ``RegionMask``. [#363] - Fixed parsing CRTF regions files that do not have a comma after the region. [#364] - Fixed parsing CRTF regions files that contain a ``symthick`` value. [#365] - Fixed an issue where ``PointPixelRegion`` objects would not plot. [#366] - Fixed an issue where DS9 annulus regions with more than one annulus would not be parsed correctly. Such regions are skipped for now. [#371] - Fixed an issue where ``Angle`` values for ``SkyRegion`` shape parameters could be incorrectly serialized. [#380] - Fixed an issue where a semicolon in the DS9 text field would raise an error. [#381,#383] - Fixed an issue where DS9 regions without metadata would not be parsed correctly. [#382] - Fixed an issue parsing spaces in DS9 region metadata. [#384] API Changes ----------- - Deprecated the ``BoundingBox`` ``slices`` attribute. [#348] - The ``RegionMeta`` and ``RegionVisual`` classes have been moved to the ``regions.core.metadata`` module. [#371] - Deprecated the ``read_fits_region`` and ``write_fits_region`` functions. Instead, use the ``read_fits`` and ``write_fits`` functions. Note that the ``write_fits`` function is called as ``write_fits(regions, filename)`` for consistency with the other functions that write files. [#376] - The following helper functions were removed from the public API: ``to_shape_list``, ``to_crtf_meta``, ``to_ds9_meta``, ``CRTFRegionParser``, ``DS9RegionParser``, ``CoordinateParser``, and ``FITSRegionRowParser``. [#375] - Deprecated the following I/O classes and functions: ``crtf_objects_to_string``, ``ds9_objects_to_string``, ``fits_region_object_to_table``, ``read_crtf``, ``read_ds9``, ``read_fits``, ``write_crtf``, ``write_ds9``, ``write_fits``, ``CRTFParser``, ``DS9Parser``, ``FITSRegionParser``, ``ShapeList``, and ``Shape``. The ``Regions`` and ``Region`` objects now support this functionality via a unified I/O interface. [#378] - Existing ``ds9`` and ``crtf`` region files will not be overwritten by default with the ``write`` functions. Set ``overwrite=True`` to overwrite existing files. [#378] 0.4 (2019-06-17) ================ New Features ------------ - Add region copy methods [#269] - Add pixel region rotate method [#265] - Added ``union`` and ``intersection`` methods to the ``BoundingBox`` class. [#277] - Add support for BOX in FITS regions [#255] - Add PixCoord.xy [#247] Bug Fixes --------- - Fixed a corner-case issue where ``RegionMask.multiply()`` would not set non-finite data values outside of the mask but within the bounding box to zero. [#278] - Fix 'text' renamed to 'label' [#234] Other ----- - Remove astropy-healpix dependency [#258] - Use standalone six to avoid deprecation warnings [#235] - Change CRTF writer to match CASA implementation [#226] - Simplify annulus regions [#279] 0.3 (2018-09-09) ================ New Features ------------ - Changed ``as_patch`` to ``as_artist`` to accommodate non-patch artists [#218] - Implemented ``to_pixel`` for ``regions.CompoundSkyRegions``, ``to_mask`` for ``regions.CompoundPixelRegion`` and ``to_pixel`` for ``regions.CircleSkyRegion``. [#137] - Handling dimension and broadcast of ``x`` and ``y`` in ``regions.PixCoord``. [#172] - Deserialization of ``CRTF`` file format is possible. [#173] - Added ``regions.TextPixelRegion`` and ``regions.TextSkyRegion``. [#177] - Added ``Shape`` layer in the serialization of ``DS9`` format. Also, implemented ``RegionMeta`` and ``RegionVisual`` to validate the meta parameters. [#179] - Serialization of ``regions.Region`` object to ``CRTF`` format is possible. [#186] - Fix mask bug for regions with negative indices. [#190] - Improved the ``plot`` methods for several regions. Added ``as_patch`` for annulus regions. Now, uses the parameters in the ``visual`` attributes of regions in the matplotlib plotting. Also, added ``mpl_properties_default`` method in ``regions.PixelRegion`` to set the visual parameters to that of ``DS9`` by default. [#194] - Now, ``to_mask`` in ``regions.CompoundPixelRegion`` handles negative bounding box. [#195] - Added ``regions.RectangleAnnulusPixelRegion``, ``regions.RectangleAnnulusSkyRegion``, ``regions.EllipseAnnulusPixelRegion`` and ``regions.RectangleAnnulusSkyRegion``. Also, implemented custom descriptor classes for attribute validation. [#196] - Implemented FITS Region Binary Table reader and writer. [#198] - Renamed ``Mask`` class to ``RegionMask`` and added ``origin`` arg to ``as_patch`` and ``plot`` methods in ``regions.Region`` class. [#203] - Support for explicit formatting directives in ``DS9``. [#204] 0.2 (2017-02-16) ================ The changelog was not populated. `Regions v0.2 merged pull requests `__. 0.1 (2016-07-26) ================ The changelog was not populated. `Regions v0.1 merged pull requests `__. astropy-regions-f4139aa/CITATION.cff000066400000000000000000000070411521703212700171650ustar00rootroot00000000000000cff-version: 1.2.0 title: Regions message: 'If you use this software, please cite it as below.' type: software authors: - family-names: Bradley given-names: Larry orcid: 'https://orcid.org/0000-0002-7908-9284' affiliation: >- Space Telescope Science Institute, 3700 San Martin Drive, Baltimore, MD 21218, USA - family-names: Deil given-names: Christoph orcid: 'https://orcid.org/0000-0002-4198-4005' - family-names: Ginsburg given-names: Adam orcid: 'https://orcid.org/0000-0001-6431-9633' - family-names: Patra given-names: Sushobhana - family-names: Robitaille given-names: T. P. orcid: 'https://orcid.org/0000-0002-8642-1329' affiliation: >- Aperio Software Ltd., Headingley Enterprise and Arts Centre, Bennett Road, Leeds, LS6 3HN, United Kingdom - family-names: Sipőcz given-names: Brigitta M. orcid: 'https://orcid.org/0000-0002-3713-6337' affiliation: >- IPAC, MC 100-22, Caltech, 1200E. California Blvd. Pasadena, CA 91125 - family-names: King given-names: Johannes - family-names: Lim given-names: P. L. orcid: 'https://orcid.org/0000-0003-0079-4114' affiliation: >- Space Telescope Science Institute, 3700 San Martin Dr., Baltimore, MD 21218, USA - family-names: Homeier given-names: Derek orcid: https://orcid.org/0000-0002-8546-9128 affiliation: Aperio Software Ltd., Insight House, Riverside Business Park, Stoney Common Road, Stansted, Essex, CM24 8PL, UK - family-names: Jenness given-names: Tim orcid: https://orcid.org/0000-0001-5982-167X affiliation: Vera C. Rubin Observatory, 950 N. Cherry Ave, Tucson, AZ 85719 - family-names: Val-Borro given-names: Miguel de orcid: https://orcid.org/0000-0002-0455-9384 affiliation: Planetary Science Institute, 1700 East Fort Lowell, Tucson, AZ 85719 - family-names: Singer given-names: Leo orcid: https://orcid.org/0000-0001-9898-5597 affiliation: Astroparticle Physics Laboratory, NASA Goddard Space Flight Center, 8800 Greenbelt Road, Greenbelt, MD 20771, USA - family-names: Baumann given-names: Matthieu - family-names: O'Steen given-names: Richard orcid: https://orcid.org/0000-0002-2432-8946 affiliation: Space Telescope Science Institute, 3700 San Martin Drive, Baltimore, MD 21218, USA - family-names: Gondhalekar given-names: Yash orcid: https://orcid.org/0000-0002-6646-4225 affiliation: BITS Pilani/Computer Science, Sancoale, Goa 403726, India - family-names: Donath given-names: Axel orcid: 'https://orcid.org/0000-0003-4568-7005' affiliation: >- Harvard-Smithsonian Center for Astrophysics, 60 Garden St., Cambridge, MA, 02138, USA - family-names: Robert given-names: Clément - family-names: Tollerud given-names: E. J. orcid: 'https://orcid.org/0000-0002-9599-310X' affiliation: >- Space Telescope Science Institute, 3700 San Martin Dr., Baltimore, MD 21218, USA - family-names: Lee given-names: Jae-Joon - family-names: Leinweber given-names: Katrin - family-names: Vinícius given-names: Zé identifiers: - type: doi value: 10.5281/zenodo.5826358 description: Zenodo - all versions repository-code: 'https://github.com/astropy/regions' url: 'https://astropy-regions.readthedocs.io/en/latest/' abstract: Astropy package for region handling keywords: - Astronomy software - Astronomy data analysis - Photometry - Image Data Analysis - Open source software license: BSD-3-Clause version: 0.11 date-released: '2025-11-05' astropy-regions-f4139aa/LICENSE.rst000066400000000000000000000027451521703212700171150ustar00rootroot00000000000000Copyright (c) 2015-2026, Regions Developers All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 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. astropy-regions-f4139aa/README.rst000066400000000000000000000051011521703212700167550ustar00rootroot00000000000000======= Regions ======= |PyPI Version| |Conda Version| |Astropy| |CI Status| |Codecov Status| |Latest RTD Status| Regions is an `Astropy`_ package for region handling. Please see the `online documentation `_ for `installation instructions `_ and usage information. Citing Regions -------------- |Zenodo| If you use Regions for a project that leads to a publication, whether directly or as a dependency of another package, please include the following acknowledgment:: This research made use of Regions, an Astropy package for region handling (Bradley et al. ). where (Bradley et al. ) is a citation to the `Zenodo record `_ of the Regions version that was used. We also encourage citations in the main text wherever appropriate. License ------- Regions is licensed under a BSD 3-clause license. Please see the `LICENSE `_ file for details. .. |PyPI Version| image:: https://img.shields.io/pypi/v/regions.svg?logo=pypi&logoColor=white&label=PyPI :target: https://pypi.org/project/regions/ :alt: PyPI Latest Release .. |Conda Version| image:: https://img.shields.io/conda/vn/conda-forge/regions?label=conda%20package :target: https://anaconda.org/conda-forge/regions :alt: Conda Latest Release .. |Astropy| image:: https://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat :target: https://www.astropy.org/ :alt: Powered by Astropy .. |Zenodo| image:: https://zenodo.org/badge/35690635.svg :target: https://zenodo.org/badge/latestdoi/35690635 .. |CI Status| image:: https://github.com/astropy/regions/workflows/CI%20Tests/badge.svg# :target: https://github.com/astropy/regions/actions :alt: CI Status .. |Codecov Status| image:: https://img.shields.io/codecov/c/github/astropy/regions?logo=codecov :target: https://codecov.io/gh/astropy/regions :alt: Coverage Status .. |Stable RTD Status| image:: https://img.shields.io/readthedocs/astropy-regions/latest.svg?logo=read%20the%20docs&logoColor=white&label=Docs&version=stable :target: https://astropy-regions.readthedocs.io/en/stable/ :alt: Stable Documentation Status .. |Latest RTD Status| image:: https://img.shields.io/readthedocs/astropy-regions/latest.svg?logo=read%20the%20docs&logoColor=white&label=Docs&version=latest :target: https://astropy-regions.readthedocs.io/en/latest/ :alt: Latest Documentation Status .. _Astropy: https://www.astropy.org/ astropy-regions-f4139aa/codecov.yml000066400000000000000000000003151521703212700174350ustar00rootroot00000000000000comment: off codecov: branch: main coverage: status: project: default: target: auto # this allows a small drop from the previous base commit coverage threshold: 0.05% astropy-regions-f4139aa/docs/000077500000000000000000000000001521703212700162215ustar00rootroot00000000000000astropy-regions-f4139aa/docs/Makefile000066400000000000000000000012511521703212700176600ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile clean clean: rm -rf $(BUILDDIR) rm -rf api # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) astropy-regions-f4139aa/docs/_static/000077500000000000000000000000001521703212700176475ustar00rootroot00000000000000astropy-regions-f4139aa/docs/_static/custom.css000066400000000000000000000121271521703212700216760ustar00rootroot00000000000000/* ========================================================================= CSS Variables ========================================================================= */ :root { --pst-font-size-h1: 2rem; --pst-font-size-h2: 1.6rem; --pst-font-size-h3: 1.35rem; --pst-font-size-h4: 1.15rem; --pst-font-size-h5: 1rem; --pst-font-size-h6: 0.9rem; --sst-accent-color-bright: #ff9a6c; --sst-accent-color-muted: #ff9a6c; } html[data-theme='light'] { --pst-color-surface: #f3f4f5; --pst-color-border: #d1d5da; --pst-color-warning-bg: #f5e4d4; /* Steel blue link/card colors */ --pst-color-link: #2e6da4; --pst-color-link-hover: #1f5080; --pst-color-primary: #2e6da4; --pst-color-primary-highlight: #1f5080; --sd-color-primary-highlight: #1f5080; /* Orange secondary accent */ --pst-color-secondary: #d07530; --pst-color-secondary-highlight: #b85f1f; --pst-color-secondary-text: #ffffff; --pst-color-secondary-highlight-text: #ffffff; --pst-color-inline-code: #912583; --pst-color-inline-code-links: #2e6da4; --pst-color-link-higher-contrast: #2e6da4; --pst-color-target: #f3cf95; --sst-footer-background-color: #f6f6f6; } html[data-theme='dark'] { /* Steel blue link/card colors */ --pst-color-link: #5b9fd6; --pst-color-link-hover: #4a85bc; --pst-color-primary: #5b9fd6; --pst-color-primary-highlight: #4a85bc; --sd-color-primary-highlight: #4a85bc; /* Orange secondary accent */ --pst-color-secondary: #e08a42; --pst-color-secondary-highlight: #d07530; --pst-color-secondary--text: #14181e; --pst-color-secondary-highlight-text: #14181e; --pst-color-inline-code: #f3c7ee; --pst-color-inline-code-links: #5b9fd6; --pst-color-link-higher-contrast: #5b9fd6; --pst-color-target: #5a3c19; --sst-footer-background-color: #282828; } /* ========================================================================= Header / Navigation ========================================================================= */ .bd-header a.navbar-brand { color: #ff8457 !important; } .bd-header ul.navbar-nav > li.nav-item > .nav-link:hover { color: var(--sst-accent-color-bright) !important; } .bd-header ul.navbar-nav > li.nav-item > .nav-link:hover:before { border-bottom-color: var(--sst-accent-color-bright) !important; } /* Theme toggle and GitHub icon hover color */ html .pst-navbar-icon:hover { color: #ff9a6c !important; } /* Selected theme in the light/dark mode dropdown */ html[data-theme='light'] .theme-switch-button + .dropdown-menu { --bs-dropdown-link-active-color: #5b9fd6; } /* Vertical sidebar/content separator */ html[data-theme='light'] .bd-sidebar-primary { border-right-width: thin; border-right-color: #c4c4c4; } html[data-theme='dark'] .bd-sidebar-primary { border-right-width: thin; border-right-color: #666666; } /* ========================================================================= Layout ========================================================================= */ .bd-article { padding-top: 0.75rem; } .section { margin-bottom: 1.6rem; } p { margin-bottom: 0.7rem; } div.sidebar-primary-item > a.logo { display: flex; flex-direction: column; } img.index-logo { margin-bottom: 1rem; } /* ========================================================================= Footer ========================================================================= */ html[data-theme='light'] .bd-footer { border-top: 1px solid #c4c4c4; } html[data-theme='dark'] .bd-footer { border-top: 1px solid #666666; } /* ========================================================================= Headings ========================================================================= */ .bd-content h1 { margin-top: 0; } .bd-content h2 { border-bottom: 1px solid var(--pst-color-border); padding-bottom: 0.3rem; } /* ========================================================================= Admonitions (dark mode) ========================================================================= */ /* Muted slate blue for general admonitions (note, important, etc.) */ html[data-theme='dark'] .admonition { border-color: #2e6da4; } html[data-theme='dark'] .admonition > .admonition-title { background-color: #1c4378; } html[data-theme='dark'] .admonition > .admonition-title:after { color: #5b9fd6; } /* Orange theming for warning admonitions */ html[data-theme='dark'] .admonition.warning { border-color: #c07030; } html[data-theme='dark'] .admonition.warning > .admonition-title { background-color: #6d4225; } html[data-theme='dark'] .admonition.warning > .admonition-title:after { color: #e08a42; } /* ========================================================================= Typography / Code ========================================================================= */ .field-list ul { padding-left: 2em; } code.literal { background-color: var(--pst-color-surface); border: 1px solid var(--pst-color-border); border-radius: 0.25rem; padding: 0.1rem 0.25rem; } /* Prevent unnecessary vertical scrollbars for MathJax equations */ mjx-container { overflow: visible !important; padding-bottom: 0 !important; } astropy-regions-f4139aa/docs/_static/regions_logo.ico000066400000000000000000000404661521703212700230430ustar00rootroot00000000000000@? A(@~ ?c:c9d9c9c9c9c9b9b8b8a8a8a8`7`7_7_7_7`9_7^6c9c9d9c9c9c9c9c9c9c9b9b8b8a8a8a8`7`7`7_8^6^6^6]6[5]6]6c8c9c9c9c9b8c9c9c9c9c92c9Ub9qb8b8a8a8a8|`7d`7F`7$^8^6^6^6^2\5\5]3]4\5S1c9c9c9c9c9c9c9c9c91c9|c9c9c9b9b8b8a8a8a8`7`7`7_7_7^6^6n^6+^3\5\5[4[4_5c6Z4T.c9c9c9c9c:c9c9c9ec9c9c9c9b9b9b8a8a8a8`7`7`7_7_7_6^6^6^6]6]5\5t\5[4[4Z4Y4Z5[5Y3c9c9c9c9c9c9c9c9hc9c9c9c9b9b8b8a7a8a8`8`7`7_7_7_7^6^6^6]6]6]5\5\5\5[5[4-Z4Z4Y3X3X3X3X2b;c9c9c9c9c9c90c9c9c9c9c9b9b8a8b9d<`7`7`7_7_7_7^6^6^6]6]6]5\5\5\5[5[4[4Z4Z4Z4!Y3V4W2X3X2W2J+c9c9c9_0c9c9mc9c9c9c9b9b8b8b9`7nH)pX]3c;_7_7_6^6^6^6]6]5]5\5\5\5[5[4[4Z4Z4Z4Y3Y3g W2W2U2V2V2T4c9c9c9c:c8c9c9c9c9b9b8b8a8a8^4M sO2IX. ]5^6^6^6]6]6]5\6\7\7[7[6[6Z6Z6Z6Y5Y3Y3X3X3W22W2V1U1U1U1U1c9c9c9W4d9c9c9c9b9b8b8a8a8a8_6tP3iƼmUe?]5^6]6]5\5\6\0\+\, [+ [+[+Z+Z+Z*Y, X4X3X2W2W2V2pV1T1U1U1T1S0c9a9e9b9c9c9c9c9b9b8b8a8a8a8`8_6oJ,lR²r`Ec=\5]6\5\5\7\, WcTRSRRRRRRTyrX+ W4W2W2V2V2V1X3 T1T0T0S0S/b8b9b9c9c9qc9b9b8b8a8a8a8`7`7`7_7]4M!oK.HX/ \4]5\5\5\6[3[8PNONNNNNNOVL7W, V4V2V1U1U1U1T1 S0S0S0R0Q.b8b8b9c9c96b9b8b8a8a8a8`7`7`7_7_7_7_7]5iD&fMZ2_8\5\5\5[5[7\(UNONOOOOOOOQW* V3V1U1U1U1T0T0S0S0S0R/R/M,a8c9f:^6b8b8a8a8a8`8`7`7_7_7_7^6^6^6]6]6^6\5\5\5[5[4[5[/YH.OPLuTNOOOOOOTaTV'U3U1T1T0T0S0S0R/ R/R/Q/Q.  a8a8b8b8b8sb8a8a8a8`7`7`7_7_7_6^6^6^6]6]5]5\5\5[5[4[4Z4Z6Z*SPSL`NQOOOOOPU1T0T1T0S0S0S0R/R/O/R/Q/Q.P.`7a8a8a7a8a8a8`7`7`7_7_7_6_6_7_8_8_7^6]6\5\5[5[4[4Z4Z6Z+W[IMH@Cg8GLOOOOORysT$S3S0S0R/R/R/Q/Q/Q/O/R/K.O-Y8_7a8a8a8sa8`7`7`7_7_7_7_7`7\5X1U/T.T.U/X2\5]6[4Z4Z4Z4Y3Y/PSr{ZNOOOOOS=$S,S1R/R/R/Q/Q/P.P.{P.O.M-N-P/_7`8`:f1`8`7`7_7_7_7_7`7Y2U/b:xM&[2b6`5U-lC X1R-Z4[5Y3Y5Y)UsjMV`MOOOOOPR%R1R/Q/Q/Q.P.P.P.O.>O.O.N-M-^7`7`7`7=`7`7_7_7^6_7]5U/rH#vE˔\١fޥhߥiޥhܣgԝbR^3Z4S.[5X5Y+ UaRONF>Hm7ELOOOOOOQ~{R%Q1Q/Q.P.P.P.O.O-O-L-M-N-L+L*c?`7`7`7_7_7_6^6_7[3]6pA؟dޥh٠d՝bԝbԜbԜb՝b֞cܣgߦiÎWvL&P,Z4X4X'ROQSLbNRPOOOOOOQ.Q.Q/P.P.P.O.O.O-N-N-N-N-N-L+L,_7^6_7_7_7^6^6_7Z3c;Rj֞cԜa֟dעi֞b֞c֞c֞c֞c՝bԜaۢfڢf]3O+Y5W- UF/OOPKVMOOOOOOOP`TQ%Q1P.P.P.O.O-O-N-N-M-M,M,-M,M,Q0L-_6_7_7^6^6^6^7\5_7Tߥiӛa֞c՜_٦p㼔ԙ[֟d֞c֞c֞c֞c֞cԝb֞cޥhZ1N+X6V%ROOOQOOOOOOOOPQ& P0P.P.O.O.O.N-N-N-M,M,M,L,L,L,V3J+^6^6^6:^6^6]6^7V/xGiӛa֞c֟dԚ]ඊ՜_֟e֞c֞c֞c֞c֞c֞c՝bןdןdlD"P-V1U6POOOOOOOOOOOPJ8P' P0O.O.O-L(L)N-N.M-M,L,L,L,K+:K+K+Q< ^6^6^6[]6]6^6V/}Q*ܤgԝb֞d֝aڧqȦḍ֟e֞c֞c֞c֞c֞c֞cԜbݤgPP-U4U&Rj`OOOOOOOOOOOP"O1O.O.L(bXhdV?CL+J&L* M-L-K+K+K+K+K+L0I*]6]6]6u]5\5]6W1Oܤgԝb֞c֞c֟d՛^᷋՜`ןe֞c֞c֞c֞c֞c֞c֞c֞cԝbܣgvM(M*U2S+ POOOOOOOOOO8 O+O.O-N,P2qxvtlm]NpO3%I&I'K*J+J+J+%J+J+G*\5\5\5\5]6W1oE!֞c֞c֞c֞c֞c֞c՛_٦p㽔ԙ[֟d֞c֞c֞c֞c֞c֝a֞c֞cԜbߥhuFK)U3S+ RG2QytPslPtnPtnPsnPsnPrlOyuOUFO' O/N-O.K'\Lhvt|pzpxe_T?GK-J*J*I*I*I*G,\5\5\5\5]6R-_4ޥhԜb֞c֞c֞c֞c֞cןeآi֞b֞c֞c֞c֞c֝aآiܫx՜`֞d՝bڡeŏXU2R/R0R+R#Q$Q#P#P"P#O#O!O& N.N-N-N-L* jktoy^jbostR;>H&I+I*Y'>,H*F)[4[5[5[4]6R-rBߥiԜb֞c֞c֞c֞c֞c֞c֝b֞c֞c֞c֞c֞d՛^ܬzʩԙZ֟e֞cןdӛab=N,R0Q0Q1P1P1P1O1O1O0N0N/N-M-M-K(TV2V1W2N+yHiӛ`՛^֞cдԙ\՜_֞b֞c֞c֞c֞c֝bנg٦pԜaןdؠdhC!H(O.M,M,L,L,L,L,N.H'v^KY<%I)K-F$XIduq|r}r}r}r}q|ub]B"F)E(D'D'D'C'C'C&~C&C&W2V1V1W1U1U1U2R/X3Vߥhӛa֟eݯ~֞c֞d֞c֞c֞c֞c֞c֞c֞cԜ`֞ciZ2E% O.M,M,L,L,L,K+K+K+J+K,J+I*I+G' hitr|r}r}r}r}r|tP=GC$E(D'D'C'C'C&B&B&B&B&[7W/U1U1U1T1T0U1O-\8Tj՝bӛ`՞c֞c֞c֞c֞c֞c֞c՝bӜbۢfܣg]4E% O.M,L,L,L,K+K+K+J+J+J*I*I*I+F&P9;s~r}r}r}r}r}r|tjmD&D'D'D'C'C'C&B&B&B&yB&B&W1T0T0T0/T0T0S0U1O,T0qB١eߦiؠd՝bԜbԜbԜbԝb֞cۣfiŐYqJ'D% N-M,L,L,K+K+K+J+J+J*I*I*I*H*I+E#`Yur|q|q|r}r}q|uXLqA!D(C'C'C&B&B&B&A&A%hA%A%U1S/S0S0S0S0S0R/S0Q.I(lE#vF̖]ۢfߦhiiޥh֞cT^5P/E&O.L,L,L+K+K+J+J+J+I*I*I*H*H*H)G(I-nvtq{{q|r}s~pyH/%B% C'C&B&B&B&A&A%A%@%O@%@%S.S0S0S0ER/R/R/Q/R/S0J)H'X5qJ'Z2a7_5{S-c?K+C$ L+N.L,L,K+K+K+J+J+J*I*I*I*H*H)H)H*D#WHcuq{`mcpq{ua^@!D(B&B&B&A&A&A%@%@%@%/@%@%T0P,R0R0R/Q/Q/Q/P.P.Q/Q0M,H'E% D% D% E& H(M-O/M.L-K,K,K+J+J+J*I*I*I*H*H)H)G)G)G*E% ght~zsO$R/R0P.P.P.P.P.O.O-O-N-N-N-M,M,M-K&L.PB9QIFPGCN>3J0T5K&G!I( I,H+H)G)G)G)F)F(G)B!^Xuq|p{]i_jtWMr> B'A%A%@%@%@%?$?$?$>$~>$>$$>$>$3>$>$?$Q.P.O-O-O-IN-N-N-M-M,M,L,L,L,K*`ebbceVZcZORWG,DE% F*E)E)B#P>Lsusq|q|r}q|ua^> A&@%@%@%?$?$?$>$>$>#>$>%>#>$Q+P.Q.O-N-N-M-M,M,L,L,L,K+K,J& \cbb_^eX`bd^TiH4%CD% E)D'D'J30[Slruur}q{tM;I> A&@%?$?$?$>$>$>#=#=#g=#=#>$ O.?-M-P+N,M,M,L,L,L,K+K+K-I%OHEcaf|`cdaUqF2#BC'C)A#? C(QAVdcq{xjo?$ @%?%?$?$>$>$>$=#=#=#>$=#=#>$N-M-M,L,M,L,L,K+K+K+J+J+J,H!S_ldchrsoebbbbc`QdwC%A C)C(A%> >!G0-]XSGe<@%?$>$>$>$=#=#=#<#<#g<#<#>#=#O-M-M,L,L+L+K+K+J+J+J*I*I*I,G!R\hca_PRaabbbbnbd\H@"<>" ?$>$>$>$>#=#=#=#<#<#<#?$;">#M,L,L,K+K+K+J+J*I*I*I*H*H*H+F MGE_g[z^cbbcdgdddbO_p?@%@&@%@%@%?$>$>$>$=#=#=#<#<#<"<"-;"<"<#;#K,K+K+K+K+J*I*I*I*H*H)H)G)G+E"G-Wx`tg`bbb^Sb[U]beSq>?%?$?$>$>$>$=#=#=#<#<#<";";"g;";"<"<#L+K+J+J*J*L. H*H)H)G)G)G)F(F*E&CLFCYcdbbazxabeRm=?&>$>$=#=#=#<#<#<";";";";";"4# ="<"J+J*J*I*J*H+G)yG)G)F)F(F(E(E)E*C!C" MNS[cdbttbbbdHJO<>%=#=#=#<#<"<";";";";":";";";"H*I*I*I*G+G)G)9F(F(E(E(E(D'D'D)D(AB"JFEW}`aVaZV^bbadW;=%=#<#<#<";";";";";"> ;";";"H*H)H)I)G)G)H%E(E(D'D'D'C'C'C'C(B'@?D1$NZhYdffdcccgSw;=%<#<";";";";";"e;";#;";";"9# G)F)F(G)G(E(D'&D'D'C'C'C&B&B&B&A'A'@" >>B/ QPRPf~VZ\\ZPi>(! ;<<= =$=$;:%?&>&>%=$=#<#<#<$;";";";";"b;";"<";";";";"D%D'S&7' C'C'B&B&?)@%.@%r@%?$?$>$>$>$=#=#=#<#<#<#;";";";"z;"0;";";";";";";":"@'B&A'E*B&B&?,@%@%@%@#>$&>$H>$f=#}=#=#<#<#<"q;"T;"2;";";";";";";";";";"A%A%C%B%@%@%@%?#>$>$>$=#=#=#<#<#<";";";";";";";";";";!>%?$@"?$?$>$>$>$=#=#=#<#<#<";";";";#;$;#;"?????astropy-regions-f4139aa/docs/_static/regions_logo_dark_path.svg000066400000000000000000000612531521703212700251020ustar00rootroot00000000000000 astropy-regions-f4139aa/docs/_static/regions_logo_light_path.svg000066400000000000000000000613431521703212700252700ustar00rootroot00000000000000 astropy-regions-f4139aa/docs/changelog.rst000066400000000000000000000001321521703212700206760ustar00rootroot00000000000000.. _changelog: ************** Full Changelog ************** .. include:: ../CHANGES.rst astropy-regions-f4139aa/docs/conf.py000066400000000000000000000155321521703212700175260ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Documentation build configuration file. This file is execfile()d with the current directory set to its containing dir. Note that not all possible configuration values are present in this file. All configuration values have a default. Some values are defined in the global Astropy configuration which is loaded here before anything else. See astropy.sphinx.conf for which values are set there. """ import os import sys import tomllib from datetime import UTC, datetime from importlib import metadata from pathlib import Path from sphinx.util import logging logger = logging.getLogger(__name__) try: from sphinx_astropy.conf.v3 import * # noqa: F403 from sphinx_astropy.conf.v3 import extensions, rst_epilog # noqa: E402 except ImportError: msg = ('The documentation requires the sphinx-astropy package to be ' 'installed. Please install the "docs" requirements.') logger.error(msg) sys.exit(1) # Get configuration information from pyproject.toml with (Path(__file__).parents[1] / 'pyproject.toml').open('rb') as fh: project_meta = tomllib.load(fh)['project'] # -- Plot configuration ------------------------------------------------------- plot_rcparams = { 'axes.labelsize': 'large', 'figure.figsize': (6, 6), 'figure.subplot.hspace': 0.5, 'savefig.bbox': 'tight', 'savefig.facecolor': 'none', } plot_apply_rcparams = True plot_html_show_source_link = True plot_formats = ['png', 'hires.png', 'pdf', 'svg'] # Don't use the default - which includes a numpy and matplotlib import plot_pre_code = '' # -- General configuration ---------------------------------------------------- # By default, highlight as Python 3. highlight_language = 'python3' # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '8.2' # keep in sync with pyproject.toml # Extend astropy intersphinx_mapping with packages we use here intersphinx_mapping.update( # noqa: F405 {'photutils': ('https://photutils.readthedocs.io/en/stable/', None), # 'shapely': ('https://shapely.readthedocs.io/en/stable/', None), }) # Exclude astropy intersphinx_mapping for unused packages del intersphinx_mapping['scipy'] # noqa: F405 del intersphinx_mapping['h5py'] # noqa: F405 # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # .inc.rst mean *include* files, don't have sphinx process them # exclude_patterns += ["_templates", "_pkgtemplate.rst", "**/*.inc.rst"] extensions += [ 'sphinx_design', # 'sphinx_reredirects', ] redirects = { } # This is added to the end of RST files - a good place to put # substitutions to be used globally. with open('substitutions.txt') as fh: rst_epilog += fh.read() # -- Project information ------------------------------------------------------ project = project_meta['name'] author = project_meta['authors'][0]['name'] project_copyright = f'2015-{datetime.now(tz=UTC).year}, {author}' github_project = 'astropy/regions' # The version info for the project you're documenting, acts as # replacement for |version| and |release|, also used in various other # places throughout the built documents. # The full version, including alpha/beta/rc tags. release = metadata.version(project) # The short X.Y version. version = '.'.join(release.split('.')[:2]) dev = 'dev' in release # -- Options for HTML output -------------------------------------------------- html_theme_options = { 'navigation_with_keys': False, 'use_edit_page_button': False, 'logo': { 'image_light': 'regions_logo_light_path.svg', 'image_dark': 'regions_logo_dark_path.svg', }, 'icon_links': [ {'name': 'GitHub', 'url': 'https://github.com/astropy/regions', 'icon': 'fa-brands fa-github', 'type': 'fontawesome', }, ], } html_title = f'{project} {release}' html_show_sourcelink = False html_favicon = os.path.join('_static', 'regions_logo.ico') html_static_path = ['_static'] html_css_files = ['custom.css'] # path relative to _static # Output file base name for HTML help builder. htmlhelp_basename = project + 'doc' # Set canonical URL from the Read the Docs Domain html_baseurl = os.environ.get('READTHEDOCS_CANONICAL_URL', '') # A dictionary of values to pass into the template engine's context for # all pages. html_context = { 'default_mode': 'light', 'to_be_indexed': ['stable', 'latest'], 'is_development': dev, 'github_user': 'astropy', 'github_repo': 'regions', 'github_version': 'main', 'doc_path': 'docs', # Tell Jinja2 templates the build is running on Read the Docs 'READTHEDOCS': os.environ.get('READTHEDOCS', '') == 'True', } # Fix size of inheritance diagrams inheritance_graph_attrs = {'size': '""'} # -- Options for LaTeX output ------------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples (source # start file, target name, title, author, documentclass [howto/manual]). latex_documents = [('index', project + '.tex', project + ' Documentation', author, 'manual')] # latex_logo = '_static/regions_banner.pdf' # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples (source start file, name, # description, authors, manual section). man_pages = [('index', project.lower(), project + ' Documentation', [author], 1)] # -- Resolving issue number to links in changelog ----------------------------- github_issues_url = f'https://github.com/{github_project}/issues/' # -- Turn on nitpicky mode for sphinx (to warn about references not found) ---- nitpicky = True # Some warnings are impossible to suppress, and you can list specific # references that should be ignored in a nitpick-exceptions file which # should be inside the docs/ directory. The format of the file should be: # # # # for example: # # py:class astropy.io.votable.tree.Element # py:class astropy.io.votable.tree.SimpleElement # py:class astropy.io.votable.tree.SimpleElementWithContent # # Uncomment the following lines to enable the exceptions: nitpick_ignore = [] nitpick_filename = 'nitpick-exceptions.txt' if os.path.isfile(nitpick_filename): with open(nitpick_filename) as fh: for line in fh: if line.strip() == '' or line.startswith('#'): continue dtype, target = line.split(None, 1) target = target.strip() nitpick_ignore.append((dtype, target)) # -- Options for linkcheck output --------------------------------------------- linkcheck_retry = 5 linkcheck_ignore = [ 'http://data.astropy.org', r'https://github\.com/astropy/regions/(?:issues|pull)/\d+', # Zenodo/doi: 403 Client Error: Forbidden for url r'https://zenodo.org/records/*', r'https://doi.org/*', ] linkcheck_timeout = 180 astropy-regions-f4139aa/docs/development/000077500000000000000000000000001521703212700205435ustar00rootroot00000000000000astropy-regions-f4139aa/docs/development/contributing.rst000066400000000000000000000023401521703212700240030ustar00rootroot00000000000000Reporting Issues and Contributing ================================= Reporting Issues ---------------- If you have found a bug, please report it by creating a new issue on the `Regions GitHub issue tracker `_. That requires creating a `free GitHub account `_ if you do not have one. Please include a minimal example that demonstrates the issue and will allow the developers to reproduce and fix the problem. You may be also asked to provide information about your operating system and a full Python stack trace. The developers will walk you through obtaining a stack trace if it is necessary. Contributing ------------ Like the `Astropy`_ project, this package is made both by and for its users. We accept contributions at all levels, spanning the gamut from fixing a typo in the documentation to developing a major new feature. We welcome contributors who will abide by the `Python Software Foundation Code of Conduct `_. This package follows the same workflow and coding guidelines as `Astropy`_. Please read the `Astropy Contributing documentation `_ to get started. astropy-regions-f4139aa/docs/development/contributors.rst000066400000000000000000000002671521703212700240370ustar00rootroot00000000000000Contributors ============ For the complete list of contributors please see the `Regions contributors page on GitHub `_. astropy-regions-f4139aa/docs/development/index.rst000066400000000000000000000004671521703212700224130ustar00rootroot00000000000000.. _development: *********** Development *********** For Contributors ---------------- .. toctree:: :maxdepth: 1 contributing.rst contributors.rst For Maintainers --------------- .. toctree:: :maxdepth: 1 releasing.rst License ------- .. toctree:: :maxdepth: 1 license.rst astropy-regions-f4139aa/docs/development/license.rst000066400000000000000000000001711521703212700227160ustar00rootroot00000000000000.. _regions_license: License ======= Regions is licensed under a 3-clause BSD license: .. include:: ../../LICENSE.rst astropy-regions-f4139aa/docs/development/releasing.rst000066400000000000000000000160511521703212700232510ustar00rootroot00000000000000.. doctest-skip-all **************************** Package Release Instructions **************************** This document outlines the steps for releasing Regions to `PyPI `_. This process requires admin-level access to the Regions GitHub repository, as it relies on the ability to push directly to the ``main`` branch. These instructions assume the name of the git remote for the main repository is called ``upstream``. #. Check out the branch that you are going to release. This will usually be the ``main`` branch, unless you are making a release from a bugfix branch. To release from a bugfix branch, check out the ``A.B.x`` branch. Use ``git cherry-pick `` (or ``git cherry-pick -m1 `` for merge commits) to backport fixes to the bugfix branch. Also, be sure to push all changes to the repository so that CI can run on the bugfix branch. #. Ensure that `CI tests `_ are passing for the branch you are going to release. Also, ensure that `Read the Docs builds `_ are passing. #. As an extra check, run the tests locally using ``tox`` to thoroughly test the code in isolated environments:: tox -e test-alldeps -- --remote-data tox -e build_docs tox -e linkcheck #. Update the current "What's New" page (``docs/whats_new/.rst``) to make sure that all the changes are listed. Update the ``docs/release_notes/index.rst`` file by changing the section header from "Development Version" to "Current Version". Then commit the changes:: git add docs/whats_new/.rst docs/release_notes/index.rst git commit -m"Finalizing what's new for version " #. Update the ``CHANGES.rst`` file to make sure that all the changes are listed and update the release date from ``unreleased`` to the current date in ``yyyy-mm-dd`` format. Then commit the changes:: git add CHANGES.rst git commit -m'Finalizing changelog for version ' #. Create an annotated git tag (optionally signing with the ``-s`` option) for the version number you are about to release:: git tag -a -m'' git show # show the tag git tag # show all tags .. _resume_release: #. Optionally, :ref:`even more manual tests ` can be run. #. Push this new tag to the upstream repo:: git push upstream The new tag will trigger the automated `Publish workflow `_ to build the source distribution and wheels and upload them to `PyPI `_. #. Create a `GitHub Release `_ by clicking on "Draft a new release", select the tag of the released version, add a release title with the released version, and add the following description:: See the [changelog](https://astropy-regions.readthedocs.io/en/stable/changelog.html) for release notes. Then click "Publish release". This step will trigger an automatic update of the package on Zenodo (see below). #. Check that `Zenodo `_ is updated with the released version. Zenodo is already configured to automatically update with a new published GitHub Release (see above). #. Open a new `GitHub Milestone `_ for the next release. If there are any open issues or pull requests for the new released version, then move them to the next milestone. After there are no remaining open issues or pull requests for the released version then close its GitHub Milestone. #. Go to `Read the Docs `_ and check that the "stable" docs correspond to the new released version. Hide any older released versions (i.e., check "Hidden"). #. Add a new "What's New" page (``docs/whats_new/.rst``) for the next release. Update the ``docs/release_notes/index.rst`` file by changing the section header from "Current Version" to "Development Version" and adding the new "What's New" page for the next release. Then commit the changes:: git add docs/whats_new/.rst docs/release_notes/index.rst git commit -m"Add what's new for version " #. Update ``CHANGES.rst``, adding new sections for the next ``x.y.z`` version, e.g.,:: x.y.z (unreleased) ------------------ General ^^^^^^^ New Features ^^^^^^^^^^^^ Bug Fixes ^^^^^^^^^ API Changes ^^^^^^^^^^^ Then commit the changes and push to the upstream repo:: git add CHANGES.rst git commit -m'Add version to the changelog' git push upstream main #. After the release, the conda-forge bot (``regro-cf-autotick-bot``) will automatically create a pull request to the `Regions feedstock repository `_. The ``meta.yaml`` recipe may need to be edited to update dependencies or versions. Modify (if necessary), review, and merge the PR to create the `conda-forge package `_. .. _manual_tests: Additional Manual Tests ----------------------- These additional manual checks can be run before pushing the release tag to the upstream repository. #. Remove any untracked files (**WARNING: this will permanently remove any files that have not been previously committed**, so make sure that you don't need to keep any of these files):: git clean -dfx #. Check out the release tag:: git checkout #. Ensure the `build `_ and `twine `_ packages are installed and up to date:: pip install build twine --upgrade #. Generate the source distribution tar file:: python -m build --sdist . and perform a preliminary check of the tar file:: python -m twine check --strict dist/* #. Run tests on the generated source distribution by going inside the ``dist`` directory, expanding the tar file, going inside the expanded directory, and running the tests with:: cd dist tar xvfz .tar.gz cd tox -e test-alldeps -- --remote-data tox -e build_docs Optionally, install and test the source distribution in a virtual environment:: pip install -e '.[all,test]' pytest --remote-data or:: pip install '../.tar.gz[all,test]' cd pytest --pyargs regions --remote-data #. Check out the ``main`` branch, go back to the package root directory, and remove the generated files with:: git checkout main cd ../.. git clean -dfx #. Go back to the :ref:`release steps ` where you left off. astropy-regions-f4139aa/docs/getting_started/000077500000000000000000000000001521703212700214105ustar00rootroot00000000000000astropy-regions-f4139aa/docs/getting_started/citation.rst000066400000000000000000000012111521703212700237470ustar00rootroot00000000000000.. _citation: Citing Regions -------------- If you use Regions for a project that leads to a publication, whether directly or as a dependency of another package, please include the following acknowledgment: .. code-block:: text This research made use of Regions, an Astropy package for region handling (Bradley et al. ). where (Bradley et al. ) is a citation to the `Zenodo record `_ of the Regions version that was used. We also encourage citations in the main text wherever appropriate. BibTeX files for all Regions versions can be found at https://doi.org/10.5281/zenodo.5826358. astropy-regions-f4139aa/docs/getting_started/examples.rst000066400000000000000000000061641521703212700237670ustar00rootroot00000000000000.. _examples: Examples ======== Read and plot DS9 region files ------------------------------ The following example shows how to read a DS9 region file and plot the regions on an image using Matplotlib. .. plot:: :include-source: import numpy as np from astropy.io import fits from astropy.utils.data import get_pkg_data_filename from matplotlib import pyplot as plt from regions import Regions image_file = get_pkg_data_filename('tutorials/FITS-images/HorseHead.fits') data = fits.getdata(image_file, ext=0, memmap=False) data = np.rot90(data, k=-1) fig, ax = plt.subplots(figsize=(8, 8)) ax.imshow(data, cmap='gray', origin='lower') region_file = get_pkg_data_filename('data/plot_image.reg', package='regions.io.ds9.tests') regions = Regions.read(region_file, format='ds9') for i, region in enumerate(regions): region.plot(ax=ax) Plotting spherical regions on a full sky image ---------------------------------------------- The next example demonstrates how to plot spherical regions (e.g., a circle and a longitude/latitude "range") on a full sky image (seen in this `Astropy visualization demo `_). .. plot:: :include-source: import astropy.units as u import matplotlib.pyplot as plt from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.utils.data import get_pkg_data_filename from astropy.visualization.wcsaxes.frame import EllipticalFrame from astropy.wcs import WCS from matplotlib import patheffects from regions import CircleSphericalSkyRegion, RangeSphericalSkyRegion image_file = get_pkg_data_filename('allsky/allsky_rosat.fits') image_data = fits.getdata(image_file, ext=0, memmap=False) image_hdr = fits.getheader(image_file) wcs = WCS(image_hdr) fig = plt.figure(figsize=(8, 8)) ax = fig.add_subplot(projection=wcs, frame_class=EllipticalFrame) path_effects = [patheffects.withStroke(linewidth=3, foreground='black')] ax.coords.grid(color='white') ax.coords['glon'].set_ticklabel(color='white', path_effects=path_effects) ax.set_axisbelow(True) ax.set_xlabel(r'Galactic $\ell$', labelpad=10) ax.set_ylabel(r'Galactic $b$') im = ax.imshow(image_data, vmin=0., vmax=300., cmap='gray', origin='lower') # Clip the image to the frame im.set_clip_path(ax.coords.frame.patch) # Image is in galactic coordinates circ = CircleSphericalSkyRegion( SkyCoord(80, 45, unit=u.deg, frame='galactic'), 25 * u.deg) circ.to_pixel( wcs=wcs, boundary_distortions=True, n_vertices=1000, ).plot(ax=ax, edgecolor='red', facecolor='none', lw=2) lon_lat_range = RangeSphericalSkyRegion( frame='galactic', longitude_range=[210, 330] * u.deg, latitude_range=[-55, -15] * u.deg, ) lon_lat_range.to_pixel( wcs=wcs, boundary_distortions=True, n_vertices=250, ).plot(ax=ax, edgecolor='blue', facecolor='none', lw=2) astropy-regions-f4139aa/docs/getting_started/index.rst000066400000000000000000000002311521703212700232450ustar00rootroot00000000000000.. _getting_started: *************** Getting Started *************** .. toctree:: :maxdepth: 1 install overview examples citation astropy-regions-f4139aa/docs/getting_started/install.rst000066400000000000000000000066751521703212700236260ustar00rootroot00000000000000************ Installation ************ Requirements ============ Regions has the following strict requirements: * `Python `_ 3.11 or later * `NumPy `_ 2.0 or later * `Astropy`_ 6.1.7 or later Region also optionally depends on other packages for some features: * `Matplotlib `_ 3.9 or later Installing the latest released version ====================================== Using pip --------- To install Regions with `pip`_, run:: python -m pip install regions If you want to install Regions along with all of its optional dependencies, you can instead do:: python -m pip install "regions[all]" In most cases, this will install a pre-compiled version (called a wheel) of Regions, but if you are using a very recent version of Python or if you are installing Regions on a platform that is not common, Regions will be installed from a source file. In this case you will need a C compiler (e.g., ``gcc`` or ``clang``) to be installed for the installation to succeed (see :ref:`building_source` prerequisites). Using conda ----------- Regions can also be installed using the ``conda`` package manager. There are several methods for installing ``conda`` and many different ways to set up your Python environment, but that is beyond the scope of this documentation. We recommend installing `miniforge `__. Once you have installed ``conda``, you can install Regions using the ``conda-forge`` channel:: conda install -c conda-forge regions .. _building_source: Building from Source ==================== Prerequisites ------------- You will need a compiler suite and the development headers for Python and Numpy in order to build Regions from the source distribution. You do not need to install any other specific build dependencies (such as Cython) since these will be automatically installed into a temporary build environment by `pip`_. On Linux, using the package manager for your distribution will usually be the easiest route. On MacOS X you will need the `XCode`_ command-line tools, which can be installed using:: xcode-select --install Note that you do not need to install the full `XCode`_ distribution. Installing the development version ---------------------------------- Regions is being developed on `GitHub`_. The latest development version of the Regions source code can be retrieved using git:: git clone https://github.com/astropy/regions.git Then to build and install Regions (with all of its optional dependencies), run:: cd regions python -m pip install ".[all]" Alternatively, `pip`_ can be used to retrieve, build, and install the latest development version from `GitHub`_:: python -m pip install "git+https://github.com/astropy/regions.git#egg=regions[all]" Testing an installed Regions ============================ To test your installed version of Regions, you can run the test suite using the `pytest`_ command. Running the test suite requires installing the `pytest-astropy `_ (0.11 or later) package. To run the test suite, use the following command:: pytest --pyargs regions Any test failures can be reported to the `Regions issue tracker `_. .. _pip: https://pip.pypa.io/en/latest/ .. _GitHub: https://github.com/astropy/regions .. _Xcode: https://developer.apple.com/xcode/ .. _pytest: https://docs.pytest.org/en/latest/ astropy-regions-f4139aa/docs/getting_started/overview.rst000066400000000000000000000316371521703212700240220ustar00rootroot00000000000000Overview ======== Introduction ------------ The Regions package provides classes to represent: * Regions defined using pixel coordinates (a "region-on-image"; e.g., `~regions.CirclePixelRegion`) * Regions defined using celestial coordinates, but still in an Euclidean geometry (i.e., a planar projection, as a "region-on-image"; e.g., `~regions.CircleSkyRegion`) * Regions defined using celestial coordinates, and with a spherical geometry (a "region-on-celestial-sphere"; e.g., `~regions.CircleSphericalSkyRegion`) To transform between (planar) sky and pixel regions, a `world coordinate system `_ object (e.g., FITS `~astropy.wcs.WCS`) is needed. To transform between spherical and planar (sky or pixel) regions, in addition to a `WCS `_, it is also necessary to specify whether or not boundary distortions should be included (capturing the WCS projection effects inherent in planar-to-spherical transformations, or the inverse). Regions also provides a unified interface for reading, writing, parsing, and serializing regions data in different formats, including the `DS9 Region Format `_, `CRTF (CASA Region Text Format) `_, and `FITS Region Binary Table `_ format. The code and issue tracker are available at the following links: * Code: https://github.com/astropy/regions * Issue Tracker: https://github.com/astropy/regions/issues Like much astronomy software, Regions is an evolving package. The developers try to maintain backwards compatibility, but at times the API may change if there is a benefit to doing so. If there are specific areas you think API stability is important, please let us know as part of the development process. .. _getting_started-coord: Coordinates ----------- Pixel Coordinates ~~~~~~~~~~~~~~~~~ :class:`~regions.PixCoord` objects are used to represent pixel coordinates. Pixel coordinates are defined with a scalar or an array of ``x`` and ``y`` Cartesian coordinates:: >>> from regions import PixCoord >>> pixcoord = PixCoord(x=42, y=43) >>> pixcoord PixCoord(x=42, y=43) >>> pixcoord.x 42 >>> pixcoord.y 43 >>> pixcoord.xy (42, 43) `~regions.PixCoord` objects can also represent arrays of pixel coordinates. These work in the same way as single-value coordinates, but they store multiple coordinates in a single object. Let's create a 1D array of pixel coordinates:: >>> pixcoord = PixCoord(x=[0, 1], y=[2, 3]) >>> pixcoord PixCoord(x=[0 1], y=[2 3]) >>> pixcoord.x array([0, 1]) >>> pixcoord.y array([2, 3]) >>> pixcoord.xy (array([0, 1]), array([2, 3])) Let's now create a 2D array of pixel coordinates:: >>> pixcoord = PixCoord(x=[[1, 2, 3], [4, 5, 6]], ... y=[[11, 12, 13], [14, 15, 16]]) >>> pixcoord PixCoord(x=[[1 2 3] [4 5 6]], y=[[11 12 13] [14 15 16]]) Sky Coordinates ~~~~~~~~~~~~~~~ :class:`~astropy.coordinates.SkyCoord` objects are used to represent sky coordinates. `~astropy.coordinates.SkyCoord` is a very powerful class that provides a flexible interface for celestial coordinate representation, manipulation, and transformation between systems. See the extensive :ref:`astropy-coordinates` documentation for more details. Let's create a single sky coordinate:: >>> from astropy.coordinates import SkyCoord >>> skycoord = SkyCoord(42, 43, unit='deg', frame='galactic') >>> skycoord Sky coordinates also support array coordinates. These work in the same way as single-value coordinates, but they store multiple coordinates in a single object:: >>> skycoord = SkyCoord(ra=[10, 11, 12], dec=[41, 42, 43], unit='deg') >>> skycoord To represent angles both on the sky and in an image, `~astropy.coordinates.Angle` objects or `~astropy.units.Quantity` objects with angular units can be used. Pixel/Sky Coordinate Transformations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To transform between pixel and sky coordinates, a `world coordinate system `_ object (e.g., `~astropy.wcs.WCS`) is needed. Let's start by creating a WCS object:: >>> from astropy.wcs import WCS >>> wcs = WCS(naxis=2) >>> wcs.wcs.crpix = (180, 90) >>> wcs.wcs.cdelt = (-1, 1) >>> wcs.wcs.crval = (0, 0) >>> wcs.wcs.ctype = ('GLON-AIT', 'GLAT-AIT') Now let's use this WCS to convert between sky and pixel coordinates:: >>> skycoord = SkyCoord(42, 43, unit='deg', frame='galactic') >>> pixcoord = PixCoord.from_sky(skycoord=skycoord, wcs=wcs) >>> pixcoord # doctest: +FLOAT_CMP PixCoord(x=146.2575703393558, y=131.5998051082584) >>> pixcoord.to_sky(wcs=wcs) Pixel Regions ------------- Pixel regions are regions that are defined using pixel coordinates and sizes in pixels. The regions package provides a set of "pixel-based" regions classes, for example, `~regions.CirclePixelRegion`:: >>> from regions import PixCoord, CirclePixelRegion >>> center = PixCoord(x=42, y=43) >>> radius = 4.2 >>> region = CirclePixelRegion(center, radius) You can print the region to get some information about its properties:: >>> print(region) Region: CirclePixelRegion center: PixCoord(x=42, y=43) radius: 4.2 You can access its properties via attributes:: >>> region.center PixCoord(x=42, y=43) >>> region.radius 4.2 See the :ref:`shapes` documentation for the complete list of pixel-based regions and to learn more about :class:`~regions.Region` objects and their capabilities. Sky Regions ----------- Sky regions are regions that are defined using celestial coordinates. Please note they are **not** defined as regions on the celestial sphere, but rather are meant to represent shapes on an image ("region-on-image"). They simply use sky coordinates instead of pixel coordinates to define their position. The remaining shape parameters are converted to pixels using the pixel scale of the image. Let's create a sky region:: >>> from astropy.coordinates import Angle, SkyCoord >>> from regions import CircleSkyRegion >>> center = SkyCoord(42, 43, unit='deg') >>> radius = Angle(3, 'deg') >>> region = CircleSkyRegion(center, radius) Alternatively, one can define the radius using a `~astropy.units.Quantity` object with angular units:: >>> import astropy.units as u >>> from regions import CircleSkyRegion >>> center = SkyCoord(42, 43, unit='deg') >>> radius = 3.0 * u.deg >>> region = CircleSkyRegion(center, radius) You can print the region to get some information about its properties:: >>> print(region) Region: CircleSkyRegion center: radius: 3.0 deg You can access its properties via attributes:: >>> region.center >>> region.radius See the :ref:`shapes` documentation for the complete list of pixel-based regions and to learn more about :class:`~regions.Region` objects and their capabilities. Spherical Sky Regions --------------------- Spherical sky regions are defined using celestial coordinates, and **are** defined as regions on the celestial sphere ("regions-on-celestial-sphere", in contrast to the planar Sky Regions). Spherical sky regions are created using celestial coordinates (as `~astropy.coordinates.SkyCoord`) and angular distances, for instance specified as:: >>> from astropy.coordinates import Angle, SkyCoord >>> from regions import CircleSphericalSkyRegion >>> center = SkyCoord(42, 43, unit='deg') >>> radius = Angle(3, 'deg') >>> region = CircleSphericalSkyRegion(center, radius) Alternatively, one can define the radius using a `~astropy.units.Quantity` object with angular units:: >>> import astropy.units as u >>> from regions import CircleSphericalSkyRegion >>> center = SkyCoord(42, 43, unit='deg') >>> radius = 3.0 * u.deg >>> region = CircleSphericalSkyRegion(center, radius) You can print the region to get some information about its properties:: >>> print(region) Region: CircleSphericalSkyRegion center: radius: 3.0 deg You can access its properties via attributes:: >>> region.center >>> region.radius See the :ref:`shapes` documentation for the complete list of pixel-based regions and to learn more about :class:`~regions.Region` objects and their capabilities. Spherical to planar region transformations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to transform between spherical and planar ("region-on-image") regions, the planar projection (encoded in a `world coordinate system `_ object; e.g., `~astropy.wcs.WCS`) must be specified, along with a specification of whether or not boundary distortions should be included. These distortions (implemented through discrete boundary sampling) capture the impact of the spherical-to-planar (or vice versa) projection described by the WCS. However, it is possible to ignore these distortions (e.g., transforming a spherical circle to a planar circle). The example below demonstrates the difference between a spherical and a planar circle with the same center and radius. Boundary distortions are included when projecting the spherical circle onto the plot. In this full-sky Aitoff projection, the distortions result in points inside the spherical circle falling outside of the planar circle and vice versa. .. plot:: :include-source: false import matplotlib.pyplot as plt import numpy as np from astropy.coordinates import Angle, SkyCoord from astropy.wcs import WCS from regions import CircleSkyRegion, CircleSphericalSkyRegion, PixCoord # Create a full-sky Aitoff WCS wcs = WCS(naxis=2) wcs.wcs.crpix = (180, 90) wcs.wcs.cdelt = (-1, 1) wcs.wcs.crval = (0, 0) wcs.wcs.ctype = ('GLON-AIT', 'GLAT-AIT') shape = (180, 360) # Define skycoords, pixcoords grids lon = np.arange(-180, 181, 10) lat = np.arange(-90, 91, 10) coords = np.array(np.meshgrid(lon, lat)).T.reshape(-1, 2) skycoords = SkyCoord(coords, unit='deg', frame='galactic') pixcoords = PixCoord.from_sky(skycoords, wcs) # Define spherical & planar sky circles sph_circle = CircleSphericalSkyRegion( center=SkyCoord(50, 45, unit='deg', frame='galactic'), radius=Angle('30 deg')) circle = CircleSkyRegion( center=SkyCoord(50, 45, unit='deg', frame='galactic'), radius=Angle('30 deg')) # Note: circle is equivalent to transforming from sph_circle # with sph_circle.to_sky(wcs=wcs, boundary_distortions=False) # Define transformed-to pixel regions pix_circ_distort = sph_circle.to_pixel(wcs=wcs, boundary_distortions=True, n_vertices=1000) pix_circ_nodistort = circle.to_pixel(wcs=wcs) # Get contained points distort_mask = sph_circle.contains(skycoords) nodistort_mask = pix_circ_nodistort.contains(pixcoords) both_skycoords = skycoords[distort_mask & nodistort_mask] distort_only_skycoords = skycoords[distort_mask & ~nodistort_mask] nodistort_only_skycoords = skycoords[~distort_mask & nodistort_mask] # Plot fig = plt.figure() fig.set_size_inches(7, 3.5) ax = fig.add_axes([0.15, 0.1, 0.8, 0.8], projection=wcs, aspect='equal') ax.scatter(skycoords.l.value, skycoords.b.value, label='All', transform=ax.get_transform('galactic'), color='lightgrey') ax.scatter(distort_only_skycoords.l.value, distort_only_skycoords.b.value, color='magenta', label='Only within spherical circle', transform=ax.get_transform('galactic')) ax.scatter(nodistort_only_skycoords.l.value, nodistort_only_skycoords.b.value, color='lime', label='Only within planar circle', transform=ax.get_transform('galactic')) ax.scatter(both_skycoords.l.value, both_skycoords.b.value, color='orange', label='Within both', transform=ax.get_transform('galactic')) pix_circ_distort.plot(ax=ax, edgecolor='red', facecolor='none', alpha=0.8, lw=3) pix_circ_nodistort.plot(ax=ax, edgecolor='green', facecolor='none', alpha=0.8, lw=3) ax.legend(loc='lower right') ax.set_xlim(-0.5, shape[1] - 0.5) ax.set_ylim(-0.5, shape[0] - 0.5) ax.set_title('Spherical vs. Planar circle: ' 'Center=(50°, 45°), radius=30°') astropy-regions-f4139aa/docs/index.rst000066400000000000000000000040761521703212700200710ustar00rootroot00000000000000 :tocdepth: 3 .. |br| raw:: html
.. image:: _static/regions_logo_light_path.svg :class: only-light index-logo :width: 55% :align: center :alt: Astropy Regions logo .. image:: _static/regions_logo_dark_path.svg :class: only-dark index-logo :width: 55% :align: center :alt: Astropy Regions logo *************** Astropy Regions *************** | **Version**: |release| | **Date**: |today| | **Useful links**: :doc:`getting_started/install` | :ref:`Release Notes ` **regions** is an in-development `coordinated package `_ of `Astropy`_ for region handling. This package may eventually be merged into the Astropy core package. .. admonition:: Important If you use Regions for a project that leads to a publication, whether directly or as a dependency of another package, please include an :ref:`acknowledgment and/or citation `. |br| .. toctree:: :maxdepth: 1 :hidden: getting_started/index user_guide/index reference/index development/index Release Notes .. grid:: 3 :gutter: 2 3 4 4 .. grid-item-card:: Getting Started :link: getting_started/index :link-type: doc :text-align: center New to Regions? Check out the getting started guides. They contain an overview of Regions and an introduction to its main concepts. .. grid-item-card:: User Guide :link: user_guide/index :link-type: doc :text-align: center The user guide provides in-depth information on the key concepts of Regions with useful background information and explanation. .. grid-item-card:: API Reference Guide :link: reference/index :link-type: doc :text-align: center The reference guide contains a detailed description of the functions, modules, and objects included in Regions. It assumes that you have an understanding of the key concepts. astropy-regions-f4139aa/docs/make.bat000066400000000000000000000014401521703212700176250ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd astropy-regions-f4139aa/docs/reference/000077500000000000000000000000001521703212700201575ustar00rootroot00000000000000astropy-regions-f4139aa/docs/reference/index.rst000066400000000000000000000005431521703212700220220ustar00rootroot00000000000000.. _api_reference: ******************* API Reference Guide ******************* The API reference guide contains detailed descriptions of the functions, modules, and objects included in Regions. It assumes that you have an understanding of the key concepts presented in the :ref:`user_guide`. .. automodapi:: regions :no-heading: :no-main-docstr: astropy-regions-f4139aa/docs/release_notes/000077500000000000000000000000001521703212700210515ustar00rootroot00000000000000astropy-regions-f4139aa/docs/release_notes/index.rst000066400000000000000000000003351521703212700227130ustar00rootroot00000000000000************* Release Notes ************* The complete changelog contains detailed information about all changes, bug fixes, and improvements in each release. .. toctree:: :maxdepth: 1 Changelog <../changelog> astropy-regions-f4139aa/docs/substitutions.txt000066400000000000000000000017721521703212700217300ustar00rootroot00000000000000.. These are ReST substitutions and links that can be used .. throughout the docs (and docstrings) because they are added to .. ``docs/conf.py::rst_epilog``. .. ------------------------------------------------------------------ .. RST SUBSTITUTIONS .. NumPy .. |ndarray| replace:: :class:`numpy.ndarray` .. Astropy .. |Angle| replace:: `astropy.coordinates.Angle` .. |Latitude| replace:: `astropy.coordinates.Latitude` .. |Longitude| replace:: :class:`astropy.coordinates.Longitude` .. |SkyCoord| replace:: :class:`astropy.coordinates.SkyCoord` .. |Table| replace:: :class:`astropy.table.Table` .. |QTable| replace:: :class:`astropy.table.QTable` .. |Quantity| replace:: :class:`astropy.units.Quantity` .. |Unit| replace:: :class:`astropy.units.UnitBase` .. Regions .. |PixCoord| replace:: `~regions.PixCoord` .. |RegionMeta| replace:: `~regions.RegionMeta` .. |RegionVisual| replace:: `~regions.RegionVisual` .. Matplotlib .. _Matplotlib: https://matplotlib.org/ .. |Patch| replace:: `~matplotlib.patches.Patch` astropy-regions-f4139aa/docs/user_guide/000077500000000000000000000000001521703212700203545ustar00rootroot00000000000000astropy-regions-f4139aa/docs/user_guide/compound.rst000066400000000000000000000202511521703212700227320ustar00rootroot00000000000000Combining Regions ================= There are a few ways to combine any two `~regions.Region` objects into a compound region, i.e., a `~regions.CompoundPixelRegion`, `~regions.CompoundSkyRegion`, or `~regions.CompoundSphericalSkyRegion` object. Let's start by defining two sky regions:: >>> from astropy.coordinates import Angle, SkyCoord >>> from regions import CircleSkyRegion >>> circle1 = CircleSkyRegion( ... center=SkyCoord(1, 2, unit='deg', frame='galactic'), ... radius=Angle('5 deg')) >>> circle2 = CircleSkyRegion( ... center=SkyCoord(-4, 3, unit='deg', frame='galactic'), ... radius=Angle('3 deg')) Intersection (AND) ------------------ To create an intersection compound region, use either the ``&`` operator or the :meth:`~regions.Region.intersection` method:: >>> comp_region = circle1 & circle2 >>> print(comp_region) Region: CompoundSkyRegion region1: Region: CircleSkyRegion center: radius: 5.0 deg region2: Region: CircleSkyRegion center: radius: 3.0 deg operator: >>> comp_region = circle1.intersection(circle2) >>> print(comp_region) Region: CompoundSkyRegion region1: Region: CircleSkyRegion center: radius: 5.0 deg region2: Region: CircleSkyRegion center: radius: 3.0 deg operator: Union (OR) ---------- To create a union compound region, use either the ``|`` operator or the :meth:`~regions.Region.union` method:: >>> comp_region = circle1 | circle2 >>> print(comp_region) Region: CompoundSkyRegion region1: Region: CircleSkyRegion center: radius: 5.0 deg region2: Region: CircleSkyRegion center: radius: 3.0 deg operator: >>> comp_region = circle1.union(circle2) >>> print(comp_region) Region: CompoundSkyRegion region1: Region: CircleSkyRegion center: radius: 5.0 deg region2: Region: CircleSkyRegion center: radius: 3.0 deg operator: Symmetric Difference (XOR) -------------------------- To create a symmetric difference compound region, use either the ``^`` operator or the :meth:`~regions.Region.symmetric_difference` method:: >>> comp_region = circle1 ^ circle2 >>> print(comp_region) Region: CompoundSkyRegion region1: Region: CircleSkyRegion center: radius: 5.0 deg region2: Region: CircleSkyRegion center: radius: 3.0 deg operator: >>> comp_region = circle1.symmetric_difference(circle2) >>> print(comp_region) Region: CompoundSkyRegion region1: Region: CircleSkyRegion center: radius: 5.0 deg region2: Region: CircleSkyRegion center: radius: 3.0 deg operator: Example Illustrating Compound Regions ------------------------------------- The following examples demonstrate how to combine planar sky regions and spherical sky regions, with the same circle centers and radii. .. plot:: :include-source: false # Planar import matplotlib.pyplot as plt import numpy as np from astropy.coordinates import Angle, SkyCoord from astropy.wcs import WCS from regions import CircleSkyRegion # Create a full-sky Aitoff WCS wcs = WCS(naxis=2) wcs.wcs.crpix = (180, 90) wcs.wcs.cdelt = (-1, 1) wcs.wcs.crval = (0, 0) wcs.wcs.ctype = ('GLON-AIT', 'GLAT-AIT') shape = (180, 360) # Define 2 sky circles circle1 = CircleSkyRegion( center=SkyCoord(20, 0, unit='deg', frame='galactic'), radius=Angle('30 deg')) circle2 = CircleSkyRegion( center=SkyCoord(50, 45, unit='deg', frame='galactic'), radius=Angle('30 deg')) # Define skycoords lon = np.arange(-180, 181, 10) lat = np.arange(-90, 91, 10) coords = np.array(np.meshgrid(lon, lat)).T.reshape(-1, 2) skycoords = SkyCoord(coords, unit='deg', frame='galactic') # Get events in AND and XOR compound_and = circle1 & circle2 compound_xor = circle1 ^ circle2 mask_and = compound_and.contains(skycoords, wcs) skycoords_and = skycoords[mask_and] mask_xor = compound_xor.contains(skycoords, wcs) skycoords_xor = skycoords[mask_xor] # Plot fig = plt.figure() fig.set_size_inches(7, 3.5) ax = fig.add_axes([0.15, 0.1, 0.8, 0.8], projection=wcs, aspect='equal') ax.scatter(skycoords.l.value, skycoords.b.value, label='all', transform=ax.get_transform('galactic')) ax.scatter(skycoords_xor.l.value, skycoords_xor.b.value, color='orange', label='xor', transform=ax.get_transform('galactic')) ax.scatter(skycoords_and.l.value, skycoords_and.b.value, color='magenta', label='and', transform=ax.get_transform('galactic')) circle1.to_pixel(wcs=wcs).plot(ax=ax, edgecolor='green', facecolor='none', alpha=0.8, lw=3) circle2.to_pixel(wcs=wcs).plot(ax=ax, edgecolor='red', facecolor='none', alpha=0.8, lw=3) ax.legend(loc='lower right') ax.set_xlim(-0.5, shape[1] - 0.5) ax.set_ylim(-0.5, shape[0] - 0.5) ax.set_title('Planar SkyRegions') .. plot:: :include-source: false # Spherical import matplotlib.pyplot as plt import numpy as np from astropy.coordinates import Angle, SkyCoord from astropy.wcs import WCS from regions import CircleSphericalSkyRegion # Create a full-sky Aitoff WCS wcs = WCS(naxis=2) wcs.wcs.crpix = (180, 90) wcs.wcs.cdelt = (-1, 1) wcs.wcs.crval = (0, 0) wcs.wcs.ctype = ('GLON-AIT', 'GLAT-AIT') shape = (180, 360) # Define 2 spherical sky circles sph_circle1 = CircleSphericalSkyRegion( center=SkyCoord(20, 0, unit='deg', frame='galactic'), radius=Angle('30 deg')) sph_circle2 = CircleSphericalSkyRegion( center=SkyCoord(50, 45, unit='deg', frame='galactic'), radius=Angle('30 deg')) # Define skycoords lon = np.arange(-180, 181, 10) lat = np.arange(-90, 91, 10) coords = np.array(np.meshgrid(lon, lat)).T.reshape(-1, 2) skycoords = SkyCoord(coords, unit='deg', frame='galactic') # Get events in AND and XOR sph_compound_and = sph_circle1 & sph_circle2 sph_compound_xor = sph_circle1 ^ sph_circle2 sph_mask_and = sph_compound_and.contains(skycoords) sph_skycoords_and = skycoords[sph_mask_and] sph_mask_xor = sph_compound_xor.contains(skycoords) sph_skycoords_xor = skycoords[sph_mask_xor] # Plot fig = plt.figure() fig.set_size_inches(7, 3.5) ax = fig.add_axes([0.15, 0.1, 0.8, 0.8], projection=wcs, aspect='equal') ax.scatter(skycoords.l.value, skycoords.b.value, label='all', transform=ax.get_transform('galactic')) ax.scatter(sph_skycoords_xor.l.value, sph_skycoords_xor.b.value, color='orange', label='xor', transform=ax.get_transform('galactic')) ax.scatter(sph_skycoords_and.l.value, sph_skycoords_and.b.value, color='magenta', label='and', transform=ax.get_transform('galactic')) boundary_kwargs = {'boundary_distortions': True, 'n_vertices': 1000} sph_circle1.to_pixel(wcs=wcs, **boundary_kwargs).plot( ax=ax, edgecolor='green', facecolor='none', alpha=0.8, lw=3) sph_circle2.to_pixel(wcs=wcs, **boundary_kwargs).plot( ax=ax, edgecolor='red', facecolor='none', alpha=0.8, lw=3) ax.legend(loc='lower right') ax.set_xlim(-0.5, shape[1] - 0.5) ax.set_ylim(-0.5, shape[0] - 0.5) ax.set_title('Spherical SkyRegions') astropy-regions-f4139aa/docs/user_guide/contains.rst000066400000000000000000000114001521703212700227200ustar00rootroot00000000000000Checking for Points Inside Regions ================================== Points Inside Planar Regions ---------------------------- Let's start by defining both a planar sky and pixel region:: >>> from astropy.coordinates import Angle, SkyCoord >>> from regions import CircleSkyRegion, PixCoord, CirclePixelRegion >>> sky_center = SkyCoord(42, 43, unit='deg') >>> sky_radius = Angle(25, 'deg') >>> sky_region = CircleSkyRegion(sky_center, sky_radius) >>> print(sky_region) Region: CircleSkyRegion center: radius: 25.0 deg >>> pixel_center = PixCoord(x=42, y=43) >>> pixel_radius = 42 >>> pixel_region = CirclePixelRegion(pixel_center, pixel_radius) >>> print(pixel_region) Region: CirclePixelRegion center: PixCoord(x=42, y=43) radius: 42 Let's also define a WCS object:: >>> from astropy.wcs import WCS >>> wcs = WCS(naxis=2) >>> wcs.wcs.crpix = (180, 90) >>> wcs.wcs.cdelt = (-1, 1) >>> wcs.wcs.crval = (0, 0) >>> wcs.wcs.ctype = ('GLON-AIT', 'GLAT-AIT') To test if a given point is inside or outside the region, the Python ``in`` operator can be called:: >>> from regions import PixCoord >>> PixCoord(55, 40) in pixel_region True >>> PixCoord(55, 200) in pixel_region False The ``in`` operator works only for scalar coordinates and pixel regions. If you try to use ``in`` for non-scalar coordinates, you'll get a `ValueError`:: >>> pixcoord = PixCoord([50, 50], [10, 60]) >>> pixcoord in pixel_region Traceback (most recent call last): ... ValueError: coord must be scalar. coord=PixCoord(x=[50 50], y=[10 60]) If you have arrays of coordinates, use the `regions.SkyRegion.contains` or `regions.PixelRegion.contains` methods:: >>> pixcoords = PixCoord.from_sky(sky_center, wcs) >>> pixel_region.contains(pixcoords) np.True_ Note that `regions.SkyRegion.contains` requires a WCS to be passed:: >>> skycoord = SkyCoord([50, 50], [10, 60], unit='deg') >>> sky_region.contains(skycoord, wcs) array([False, True]) Points Inside Spherical Regions ------------------------------- For `~regions.SphericalSkyRegion` objects, checking whether point(s) are contained inside that region requires no other input --- since these regions are defined with a spherical geometry, and not a projected geometry (as captured through the projection encoded in a WCS) as in `~regions.SkyRegion`. Let's define a spherical sky region:: >>> from regions import CircleSphericalSkyRegion >>> sph_sky_center = SkyCoord(42, 43, unit='deg') >>> sph_sky_radius = Angle(25, 'deg') >>> sph_sky_region = CircleSphericalSkyRegion(sph_sky_center, ... sph_sky_radius) >>> print(sph_sky_region) Region: CircleSphericalSkyRegion center: radius: 25.0 deg Use the `regions.SphericalSkyRegion.contains` method to determine which point(s) lie inside or outside the region:: >>> skycoord = SkyCoord([50, 50], [10, 60], unit='deg') >>> sph_sky_region.contains(skycoord) array([False, True]) Boundary Behavior: ``contains`` versus ``covers`` ------------------------------------------------- The ``contains`` method excludes the region boundary: a point that lies exactly on an edge or vertex is considered to be *outside* the region. This is consistent with `Shapely's `_ ``contains`` predicate and the `DE-9IM `_ semantics used by most GIS tools. If you instead want boundary points to be treated as *inside* the region, use the ``covers`` method. It is available on pixel regions (`regions.PixelRegion.covers`), planar sky regions (`regions.SkyRegion.covers`), and spherical sky regions (`regions.SphericalSkyRegion.covers`), and takes the same arguments as the corresponding ``contains`` method. For example, the point ``(84, 43)`` lies exactly on the boundary of our circular pixel region (centered at ``(42, 43)`` with a radius of ``42``), so it is excluded by ``contains`` but included by ``covers``:: >>> boundary_point = PixCoord(84, 43) >>> pixel_region.contains(boundary_point) np.False_ >>> pixel_region.covers(boundary_point) np.True_ For points strictly inside or strictly outside the region, the two methods return identical results:: >>> pixel_region.contains(PixCoord(42, 43)) np.True_ >>> pixel_region.covers(PixCoord(42, 43)) np.True_ .. note:: The ``covers`` method is currently implemented for circle, ellipse, rectangle, polygon, and annulus regions (and their sky and spherical-sky equivalents); calling it on a region type that does not support it raises a `NotImplementedError`. astropy-regions-f4139aa/docs/user_guide/ds9_io.rst000066400000000000000000000045201521703212700222750ustar00rootroot00000000000000DS9 Region File Format Limitations ================================== Region Shapes ------------- The following DS9 regions shapes do not have corresponding `~regions.Region` classes and therefore are not supported: * panda, epanda, and bpanda * vector * ruler * compass * projection Coordinate Frames ----------------- * `~regions.Region` objects represent abstract shapes that are not tied to any particular image or WCS transformation. Therefore, the following DS9 coordinate frames are not supported: * physical * detector * linear * amplifier * tile * wcs and wcs0 * wcs[a-z] If you have a DS9 region file that uses one of these coordinate frames, you can load it into DS9 with its corresponding image/WCS and then save the region with a coordinate frame supported by this package (e.g., image, ICRS, FK5, etc.). * A coordinate frame must be specified in the DS9 region file. This package does not assume a default coordinate frame. * DS9 regions with mixed coordinate systems within a region specifier are not supported. For example, a region file with an ``image`` coordinate frame must not have angular sizes, e.g., ``image; circle(650, 932, 3')`` is not supported. Other Limitations ----------------- * Reading and then writing a DS9 region file will not produce an identical file to the original, but the encoded regions are identical. Therefore, it will produce identical `~regions.Region` objects when read back in again. In other words, read/write/read (or parse/serialize/parse) will exactly roundtrip `~regions.Region` objects. * DS9 composite regions are parsed into separate, independent `~regions.Region` objects. Plotting Differences -------------------- * The point symbol size must be a integer (matplotlib limitation). * Point symbols may not have dashed lines (matplotlib limitation). * Point symbols can be plotted with filled colors, which DS9 does not support. Because of this, DS9 serializations will ignore the ``fill=1`` metadata. * Annulus regions can be plotted with filled colors, which DS9 does not support. Because of this, DS9 serializations will ignore the ``fill=1`` metadata for annulus regions (annulus, ellipse, box). * Text labels are plotted only for `~regions.TextPixelRegion` objects. For other region objects, the text labels are stored in the region metadata. astropy-regions-f4139aa/docs/user_guide/fits_io.rst000066400000000000000000000056401521703212700225470ustar00rootroot00000000000000FITS Region File Format Limitations =================================== Region Shapes ------------- * FITS regions are specified in pixel units, thus a `~regions.SkyRegion` cannot be serialized to the FITS region format. Regions are always parsed and serialized as `~regions.PixelRegion` objects when using the FITS region format. * When reading a ``elliptannulus``, the first ``ROTANG`` will be used for both the inner and outer ellipse. The second ``ROTANG`` will be ignored. In other words, you cannot have an elliptical annulus where the inner and outer ellipse have different rotation angles. * Shapes where the value in the SHAPE column is preceded by an exclamation mark (e.g., ``!circle``) will be read in as a `~regions.PixelRegion` object and have ``include = 0`` in their ``meta`` dictionary. When such objects are serialized, their shape will be prepended by ``!``. * The FITS ``box``, ``rotbox``, ``rectangle``, and ``rotrectangle`` shapes will all be parsed as a `~regions.RectanglePixelRegion`. In turn, `~regions.RectanglePixelRegion` is always serialized as a ``rotbox`` shape. * The following `~regions.PixelRegion` classes do not have corresponding FITS shapes and therefore are not supported (a warning will be raised): * `~regions.RectangleAnnulusPixelRegion` * `~regions.LinePixelRegion` * `~regions.TextPixelRegion` * `~regions.CompoundPixelRegion` * FITS regions are always parsed and serialized into separate regions. Shapes that have the ``COMPONENT`` column will have that value stored in the `~regions.PixelRegion` ``meta`` dictionary with the ``component`` key. Such regions will include the ``COMPONENT`` column when serialized. * FITS parsing and serialization use only the ``include`` and ``component`` metadata and no visual metadata. Coordinate Frames ----------------- * `~regions.Region` objects represent abstract shapes that are not tied to any particular image or WCS transformation. Therefore, any WCS information in the FITS region file header will not be read. Regions are always parsed and serialized as `~regions.PixelRegion` objects when using the FITS region format. However, if desired you can use the WCS information in the FITS region file to convert a `~regions.PixelRegion` object to a `~regions.SkyRegion` object, e.g.,: .. doctest-skip:: >>> from astropy.io import fits >>> from astropy.wcs import WCS >>> header = fits.getheader('my_region.fits', 1) >>> wcs = WCS(header, keysel=('image', 'binary', 'pixel')) >>> sky_region = pix_region.to_sky(wcs) Other Limitations ----------------- * Reading and then writing a FITS region file will not produce an identical file to the original, but the encoded regions are identical. Therefore, it will produce identical `~regions.Region` objects when read back in again. In other words, read/write/read (or parse/serialize/parse) will exactly roundtrip `~regions.Region` objects. astropy-regions-f4139aa/docs/user_guide/index.rst000066400000000000000000000007221521703212700222160ustar00rootroot00000000000000.. _user_guide: ********** User Guide ********** Defining Regions ---------------- .. toctree:: :maxdepth: 1 shapes metadata Using Regions ------------- .. toctree:: :maxdepth: 1 contains compound masks spherical_frame_transform spherical_bounding_regions plotting Region File I/O --------------- .. toctree:: :maxdepth: 1 region_io Shapely Conversions ------------------- .. toctree:: :maxdepth: 1 shapely astropy-regions-f4139aa/docs/user_guide/masks.rst000066400000000000000000000261021521703212700222250ustar00rootroot00000000000000.. _overlap-masks: Computing Overlap Masks ======================= Defining a region mask within its bounding box ---------------------------------------------- For aperture photometry, a common operation is to compute, for a given image and region, a mask or array of pixel indices defining which pixels (in the whole image or a minimal rectangular bounding box) are inside and outside the region. All :class:`~regions.PixelRegion` objects have a :meth:`~regions.PixelRegion.to_mask` method that returns a :class:`~regions.RegionMask` object that contains information about whether pixels are inside the region, and can be used to mask data arrays:: >>> from regions.core import PixCoord >>> from regions.shapes.circle import CirclePixelRegion >>> center = PixCoord(4., 5.) >>> reg = CirclePixelRegion(center, 2.3411) >>> mask = reg.to_mask() >>> mask.data array([[0., 1., 1., 1., 0.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [0., 1., 1., 1., 0.]]) The mask data contains floating point that are between 0 (no overlap) and 1 (full overlap). By default, this is determined by looking only at the central position in each pixel, and:: >>> reg.to_mask() # doctest: +IGNORE_OUTPUT is equivalent to:: >>> reg.to_mask(mode='center') # doctest: +IGNORE_OUTPUT The other mask modes that are available are: * ``mode='exact'``: the overlap is determined using the exact geometrical overlap between pixels and the region. This is slower than using the central position, but allows partial overlap to be treated correctly. The mask data values will be between 0 and 1 for partial-pixel overlap. * ``mode='subpixels'``: the overlap is determined by sub-sampling the pixel using a grid of sub-pixels. The number of sub-pixels to use in this mode should be given using the ``subpixels`` argument. The mask data values will be between 0 and 1 for partial-pixel overlap. Here are what the region masks produced by different modes look like: .. plot:: :include-source: import matplotlib.pyplot as plt from regions.core import PixCoord from regions.shapes.circle import CirclePixelRegion center = PixCoord(26.6, 27.2) reg = CirclePixelRegion(center, 5.2) fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(6, 6)) mask1 = reg.to_mask(mode='center') axes[0, 0].set_title("mode='center'", size=9) axes[0, 0].imshow(mask1.data, origin='lower') mask2 = reg.to_mask(mode='exact') axes[0, 1].set_title("mode='exact'", size=9) axes[0, 1].imshow(mask2.data, origin='lower') mask3 = reg.to_mask(mode='subpixels', subpixels=3) axes[1, 0].set_title("mode='subpixels', subpixels=3", size=9) axes[1, 0].imshow(mask3.data, origin='lower') mask4 = reg.to_mask(mode='subpixels', subpixels=20) axes[1, 1].set_title("mode='subpixels', subpixels=20", size=9) axes[1, 1].imshow(mask4.data, origin='lower') As we've seen above, the :class:`~regions.RegionMask` object has a ``data`` attribute that contains a Numpy array with the mask values. However, if you have, for example, a small circular region with a radius of 3 pixels at a pixel position of (1000, 1000), it would be inefficient to store a large mask array that has a size to cover this position (most of the mask values would be zero). Instead, we store the mask using the minimal array that contains the region mask along with a ``bbox`` attribute that is a :class:`~regions.RegionBoundingBox` object used to indicate where the mask should be applied in an image. Defining a region mask within an image -------------------------------------- :class:`~regions.RegionMask` objects also have a number of methods to make it easy to use the masks with data. The :meth:`~regions.RegionMask.to_image` method can be used to obtain an image of the mask in a 2D array of the given shape. This places the mask in the correct place in the image and deals properly with boundary effects. For this example, let's place the mask in an image with shape (50, 50): .. plot:: :include-source: import matplotlib.pyplot as plt from regions.core import PixCoord from regions.shapes.circle import CirclePixelRegion center = PixCoord(26.6, 27.2) reg = CirclePixelRegion(center, 5.2) mask = reg.to_mask(mode='exact') shape = (50, 50) fig, ax = plt.subplots(figsize=(4, 4)) ax.imshow(mask.to_image(shape), origin='lower') Making image cutouts and multiplying the region mask ---------------------------------------------------- The :meth:`~regions.RegionMask.cutout` method can be used to create a cutout from the input data over the mask bounding box, and the :meth:`~regions.RegionMask.multiply` method can be used to multiply the aperture mask with the input data to create a mask-weighted data cutout. All of these methods properly handle the cases of partial or no overlap of the aperture mask with the data. These masks can be used, for example, as the building blocks for photometry, which we demonstrate with a simple example. We start off by getting an example image: .. doctest-remote-data:: >>> from astropy.io import fits >>> from astropy.utils.data import get_pkg_data_filename >>> filename = get_pkg_data_filename('photometry/M6707HH.fits') # doctest: +IGNORE_OUTPUT >>> hdulist = fits.open(filename) >>> hdu = hdulist[0] We then define a circular aperture region:: >>> from regions.core import PixCoord >>> from regions.shapes.circle import CirclePixelRegion >>> center = PixCoord(158.5, 1053.5) >>> aperture = CirclePixelRegion(center, 4.) We then convert the aperture to a mask and extract a cutout from the data, as well as a cutout with the data multiplied by the mask: .. doctest-remote-data:: >>> mask = aperture.to_mask(mode='exact') >>> data = mask.cutout(hdu.data) >>> weighted_data = mask.multiply(hdu.data) Note that ``weighted_data`` will have zeros where the mask is zero; it therefore should not be used to compute statistics (see :ref:`Masked Statistics ` below). To get the mask-weighted pixel values as a 1D array, excluding the pixels where the mask is zero, use the :meth:`~regions.RegionMask.get_values` method: .. doctest-remote-data:: >>> weighted_data_1d = mask.get_values(hdu.data) >>> hdulist.close() We can take a look at the results to make sure the source overlaps with the aperture: .. doctest-skip:: >>> import matplotlib.pyplot as plt >>> fig, axes = plt.subplots(nrows=1, ncols=3) >>> axes[0].set_title('Mask', size=9) >>> axes[0].imshow(mask.data, origin='lower', ... extent=mask.bbox.extent) >>> axes[1].set_title('Data cutout', size=9) >>> axes[1].imshow(data, origin='lower', ... extent=mask.bbox.extent) >>> axes[2].set_title('Data cutout multiplied by mask', size=9) >>> axes[2].imshow(weighted_data, origin='lower', ... extent=mask.bbox.extent) .. plot:: :include-source: import matplotlib.pyplot as plt from astropy.io import fits from astropy.utils.data import get_pkg_data_filename from regions.core import PixCoord from regions.shapes.circle import CirclePixelRegion filename = get_pkg_data_filename('photometry/M6707HH.fits') hdulist = fits.open(filename) hdu = hdulist[0] center = PixCoord(158.5, 1053.5) aperture = CirclePixelRegion(center, 4.) mask = aperture.to_mask(mode='exact') data = mask.cutout(hdu.data) weighted_data = mask.multiply(hdu.data) fig, axes = plt.subplots(nrows=1, ncols=3) axes[0].set_title('Mask', size=9) axes[0].imshow(mask.data, origin='lower', extent=mask.bbox.extent) axes[1].set_title('Data cutout', size=9) axes[1].imshow(data, origin='lower', extent=mask.bbox.extent) axes[2].set_title('Data cutout multiplied by mask', size=9) axes[2].imshow(weighted_data, origin='lower', extent=mask.bbox.extent) hdulist.close() We can also use the `~regions.RegionMask` ``bbox`` attribute to look at the extent of the mask in the image: .. plot:: :include-source: import matplotlib.pyplot as plt from astropy.io import fits from astropy.utils.data import get_pkg_data_filename from regions.core import PixCoord from regions.shapes.circle import CirclePixelRegion filename = get_pkg_data_filename('photometry/M6707HH.fits') hdulist = fits.open(filename) hdu = hdulist[0] center = PixCoord(158.5, 1053.5) aperture = CirclePixelRegion(center, 4.) mask = aperture.to_mask(mode='exact') fig, ax = plt.subplots() ax.imshow(hdu.data, origin='lower') mask.bbox.plot(ax=ax, color='white') aperture.plot(ax=ax, color='orange') ax.set_xlim(120, 180) ax.set_ylim(1000, 1059) hdulist.close() .. _masked_statistics: Masked Statistics ````````````````` Finally, we can use the mask and data values to compute weighted statistics: .. doctest-remote-data:: >>> import numpy as np >>> np.average(data, weights=mask) # doctest: +FLOAT_CMP np.float64(9364.012674888021) .. _interactive-masks: Interactive Mask Control ------------------------ In the last example we will show how to use a :ref:`Matplotlib selector` widget with a custom ``callback`` function for creating a mask and updating it interactively through the selector. We first create an :class:`~regions.EllipsePixelRegion` and add an ``as_mpl_selector`` property linked to the Matplotlib axes. This can be moved around to position it on different sources, and resized just like its Rectangle counterpart, using the handles of the bounding box. The user-defined callback function here generates a mask from this region and overlays it on the image as an alpha filter (keeping the areas outside shaded). We will use this mask as an aperture as well to calculate integrated and averaged flux, which is updated live in the text field of the plot as well. .. plot:: :include-source: import matplotlib.pyplot as plt import numpy as np from astropy import units as u from astropy.io import fits from astropy.utils.data import get_pkg_data_filename from regions import EllipsePixelRegion, PixCoord filename = get_pkg_data_filename('photometry/M6707HH.fits') hdulist = fits.open(filename) hdu = hdulist[0] fig, ax = plt.subplots() im = ax.imshow(hdu.data, origin='lower') text = ax.text(122, 1002, '', size='small', color='yellow') ax.set_xlim(120, 180) ax.set_ylim(1000, 1059) def update_sel(region): mask = region.to_mask(mode='subpixels', subpixels=10) im.set_alpha((mask.to_image(hdu.data.shape) + 1) / 2) total = mask.multiply(hdu.data).sum() mean = np.average(hdu.data, weights=mask.to_image(hdu.data.shape)) text.set_text(f'Total: {total:g}\nMean: {mean:g}') ellipse = EllipsePixelRegion(center=PixCoord(x=126, y=1031), width=8, height=4, angle=-0 * u.deg, visual={'color': 'yellow'}) selector = ellipse.as_mpl_selector(ax, callback=update_sel) hdulist.close() astropy-regions-f4139aa/docs/user_guide/metadata.rst000066400000000000000000000111141521703212700226640ustar00rootroot00000000000000Region Metadata =============== A :class:`~regions.Region` object has both ``meta`` and ``visual`` attributes that store the metadata of the region. The :class:`~regions.RegionMeta` and :class:`~regions.RegionVisual` classes handle the general metadata and visual attributes, respectively. Both behave like Python dictionaries. General Metadata ---------------- The :class:`~regions.Region` ``meta`` attribute stores additional metadata about the region such as labels, tags, comments, name, etc. that are used for non-display tasks. It can also store the spectral dimensions of the region. The valid keys in the :class:`~regions.RegionMeta` object are: * ``label``: - CRTF, DS9 (text label for a region) - Ex: meta['label'] = 'this is a circle' * ``tag``: - DS9 (All regions may have zero or more tags associated with it, which may be used for grouping and searching.) - Ex: meta['tags'] = ['{Group 1}', '{Group 2}']} * ``include``: - CRTF, DS9 (Region inclusion) - Possible Values: True, False - Ex: meta['include'] = True * ``frame``: - CRTF (Frequency/Velocity Axis) - Possible values: 'REST', 'LSRK', 'LSRD', 'BARY', 'GEO', 'TOPO', 'GALACTO', 'LGROUP', 'CMB' - Default: image value - Ex: meta['frame'] = 'TOPO' * ``range``: - CRTF (Frequency/Velocity Range) - Possible units: GHz, MHz, kHz, km/s, Hz, channel, chan (=channel) - Default: image range - Format: [min, max] - Ex: meta['range'] = [-320 * u.m/u.sec, -330 * u.m/u.s] * ``veltype``: - CRTF (Velocity Calculation) - Possible values: 'RADIO', 'OPTICAL', 'Z', 'BETA', 'GAMMA' - Default: image value - Ex: meta['veltype'] = 'RADIO' * ``restfreq``: - CRTF (Rest Frequency) - Possible values: `~astropy.units.Quantity` object - Default: image value - Ex: meta['restfreq'] = Quantity("1.42GHz") * ``corr``: - CRTF (Correlational Axis) - Possible values: 'I', 'Q', 'U', 'V', 'RR', 'RL', 'LR', 'LL', 'XX', 'XY', 'YX', 'YY', 'RX', 'RY', 'LX', 'LY', 'XR', 'XL', 'YR', 'YL', 'PP', 'PQ', 'QP', 'QQ', 'RCircular', 'LCircular', 'Linear', 'Ptotal', 'Plinear', 'PFtotal', 'PFlinear', 'Pangle' - Default: all planes present in image - Ex: meta['corr'] = ['X', 'Y'] * ``comment``: - DS9, CRTF (Comment on the region) - Ex: meta['comment'] = 'Any comment for the region' * ``line``: - DS9 (The line region may be rendered with arrows, one at each end. To indicate arrows, use the line property. A '1' indicates an arrow, '0' indicates no arrow.) - Ex: meta['line'] = [1, 1] * ``name`` * ``select`` * ``highlite`` * ``fixed`` * ``edit``: - DS9 (The Edit property specifies if the user is allowed to edit the region via the GUI.) - Ex: meta['edit'] = 1 * ``move``: - DS9 (The Move property specifies if the user is allowed to move the region via the GUI. ) - Ex: meta['move'] = 1 * ``rotate``: - DS9 (The Rotate property specifies if the user is allowed to rotate the region via the GUI. ) - Ex: meta['rotate'] = 1 * ``delete``: - DS9 (The Delete property specifies if the user is allowed to delete the region via the GUI. ) - Ex: meta['delete'] = 1 * ``source`` * ``background`` Visual Metadata --------------- The :class:`~regions.Region` ``visual`` attribute stores visual properties for the region such as color, font style, font size, line width, line style, etc. The visual attributes are metadata meant to be used to visualize regions, especially used by plotting libraries such as `Matplotlib`_ . The valid keys in the `~regions.RegionVisual` class are: * ``color``: CRTF, DS9 (Region, symbol and text color) - Possible values: any color recognized by `Matplotlib`_, including hex values - Default: color=green - Ex: visual['color'] = 'blue' * ``dash``: Render region using dashed lines using current dashlist value. * ``font``: Name of the font. * ``dashlist``: Sets dashed line parameters. This does not render the region in dashed lines. * ``symsize``: Size of the symbol. * ``symthick``: Thickness of the symbol. * ``fontsize``: Size of the font. * ``fontstyle``: Style of the font. * ``usetex``: Boolean value whether the label uses TeX. * ``labelpos``: Position of the label. * ``labeloff``: Label offset. * ``linewidth``: Width of the line. * ``linestyle``: Style of the line. * ``fill``: Boolean value whether the region is filled. * ``line``: The line region may be rendered with arrows, one at each end. To indicate arrows, use the line property. A '1' indicates an arrow, '0' indicates no arrow. * ``symbol``/``point``: CRTF, DS9 (Symbol for which a point region is described) - Ex: meta['symbol'] = 'point marker' astropy-regions-f4139aa/docs/user_guide/plotting.rst000066400000000000000000000114541521703212700227530ustar00rootroot00000000000000Plotting Regions with Matplotlib ================================ Plotting Pixel Regions ---------------------- Some `~regions.PixelRegion` objects have an ``as_artist()`` method that returns an equivalent `matplotlib.patches` object. For example :meth:`regions.CirclePixelRegion.as_artist` returns a `matplotlib.patches.Circle` object. To draw a matplotlib patch object, add it to an `matplotlib.axes.Axes` object. .. plot:: :include-source: import matplotlib.pyplot as plt from regions import CirclePixelRegion, PixCoord region = CirclePixelRegion(PixCoord(x=0.3, y=0.42), radius=0.5) artist = region.as_artist() fig, ax = plt.subplots() ax.set_aspect('equal') ax.add_artist(artist) ax.set_xlim([-0.5, 1]) ax.set_ylim([-0.5, 1]) The default keyword arguments for the matplotlib artist depend on the value of the ``default_style`` keyword in the `~regions.RegionVisual` dictionary. This keyword is currently set (to a value of 'ds9') only when reading from DS9 region files. If this keyword is not set or set to 'mpl' or `None`, then the matplotlib defaults will be used, with the exception that fill is turned off for `~matplotlib.patches.Patch` and `~matplotlib.lines.Line2D` artists. The :meth:`regions.PixelRegion.plot` method is a convenience method that combines these two steps (creating a matplotlib patch artist and adding it to an axis). If no axis is passed then it calls ``plt.gca()``. You can shift the origin of the region while plotting by supplying the ``origin`` pixel coordinates to either :meth:`~regions.PixelRegion.plot` or :meth:`~regions.PixelRegion.as_artist`. The :meth:`~regions.PixelRegion.plot` method also takes any keyword argument that the `~matplotlib.patches.Patch` object accepts for those regions represented as patches, or arguments `~matplotlib.lines.Line2D` accepts for point regions. For example: .. plot:: :include-source: import matplotlib.pyplot as plt import numpy as np from regions import CirclePixelRegion, PixCoord fig, ax = plt.subplots() region = CirclePixelRegion(center=PixCoord(x=7, y=5), radius=3) data = np.arange(10 * 15).reshape((10, 15)) ax.imshow(data, cmap='gray', origin='lower') region.plot(ax=ax, color='red', lw=2.0) The documentation for `~regions.RectanglePixelRegion` and `~regions.EllipsePixelRegion` also shows plotting examples. Plotting Sky Regions -------------------- Note that `~regions.SkyRegion` objects do not have an ``as_artist()`` or ``plot()`` method. To plot a `~regions.SkyRegion` object, you will need to convert it to a pixel region (using a WCS object): .. doctest-skip:: >>> from astropy.coordinates import Angle, SkyCoord >>> from regions import CircleSkyRegion >>> sky_center = SkyCoord(42, 43, unit='deg') >>> sky_radius = Angle(25, 'deg') >>> sky_region = CircleSkyRegion(sky_center, sky_radius) >>> pixel_region = sky_region.to_pixel(wcs) >>> pixel_region.plot() Plotting Spherical Sky Regions ------------------------------ Similarly, `~regions.SphericalSkyRegion` objects do not have an ``as_artist()`` or ``plot()`` method. To plot a `~regions.SphericalSkyRegion` object, you will need to convert it to a pixel region (using a WCS object). Boundary distortions can also be included in this conversion (by setting the ``boundary_distortions`` keyword), to capture the effects of the WCS projection from spherical to a planar geometry. See the second example in :ref:`examples`. It is also possible to use the coordinates of a discretized `~regions.SphericalSkyRegion` to show the region's boundary in a figure. .. plot:: :include-source: import matplotlib.pyplot as plt from astropy.coordinates import Angle, SkyCoord from astropy.wcs import WCS from regions import CircleSphericalSkyRegion wcs = WCS(naxis=2) wcs.wcs.crpix = (180, 90) wcs.wcs.cdelt = (-1, 1) wcs.wcs.crval = (0, 0) wcs.wcs.ctype = ('GLON-AIT', 'GLAT-AIT') sph_sky_center = SkyCoord(42, 30, unit='deg', frame='galactic') sph_sky_radius = Angle(12, 'deg') sph_sky_region = CircleSphericalSkyRegion(sph_sky_center, sph_sky_radius) fig = plt.figure() ax = fig.add_subplot(projection=wcs) ax.grid(True) ax.set_xlabel(r'Galactic $\ell$') ax.set_ylabel(r'Galactic $b$') sph_sky_region.to_pixel( wcs=wcs, boundary_distortions=True, n_vertices=1000, ).plot(ax=ax, color='tab:red', lw=3) sph_sky_center2 = SkyCoord(42, 43, unit='deg', frame='galactic') sph_sky_radius2 = Angle(25, 'deg') sph_sky_region2 = CircleSphericalSkyRegion(sph_sky_center2, sph_sky_radius2) poly_sph_sky2 = sph_sky_region2.discretize_boundary(n_vertices=1000) ax.plot( poly_sph_sky2.vertices.l, poly_sph_sky2.vertices.b, lw=2, color='tab:blue', transform=ax.get_transform('galactic'), ) astropy-regions-f4139aa/docs/user_guide/region_io.rst000066400000000000000000000142631521703212700230660ustar00rootroot00000000000000.. _regions_io: Reading/Writing Region Files **************************** The regions package provides a unified interface for reading, writing, parsing, and serializing regions data in different standard formats. Regions I/O =========== The `~regions.Regions` class (which represents a list of `~regions.Region` objects) includes four methods, :meth:`~regions.Regions.read`, :meth:`~regions.Regions.write`, :meth:`~regions.Regions.parse`, and :meth:`~regions.Regions.serialize`, that make it possible to read, write, parse, and serialize region files or region data. The :class:`~regions.Regions` class has built-in support for various input and output formats. A full list of the supported formats and is shown in the table below. The ``Suffix`` column indicates the standard filename suffix for a particular format. ============ ============== ======================== Format Suffix Description ============ ============== ======================== crtf .crtf `CASA Region Text Format `_ ds9 .reg, .ds9 `DS9 Region Format `_ fits .fits `FITS Region Binary Table `_ ============ ============== ======================== Use the :meth:`~regions.Regions.get_formats` method to get the registered I/O formats as a :class:`~astropy.table.Table`:: >>> from regions import Regions >>> print(Regions.get_formats()) Format Parse Serialize Read Write Auto-identify ------ ----- --------- ---- ----- ------------- crtf Yes Yes Yes Yes Yes ds9 Yes Yes Yes Yes Yes fits Yes Yes Yes Yes Yes Read ---- To read in a region file, first import the :class:`~regions.Regions` class, then call the :meth:`~regions.Regions.read` method with the name of the file and the file format, e.g.: .. doctest-skip:: >>> from regions import Regions >>> regions = Regions.read('my_regions.reg', format='ds9') It is also possible to load regions directly from the internet using URLs, e.g.: .. doctest-skip:: >>> regions = Regions.read('https://some.domain.edu/my_regions.reg', format='ds9') If the ``format`` keyword is not set, the :meth:`~regions.Regions.read` method will attempt to automatically determine the file format from the filename extension or the file contents: .. doctest-skip:: >>> regions = Regions.read('my_regions.reg') The underlying file handler will also automatically detect various compressed data formats and transparently uncompress them if supported by the Python installation (see `~astropy.utils.data.get_readable_fileobj`): .. doctest-skip:: >>> regions = Regions.read('my_regions.reg.gz') Write ----- Use the :meth:`~regions.Regions.write` method to write regions to a region file. Like the :meth:`~regions.Regions.read` method, the filename extension will be used to automatically define the format if unspecified. .. doctest-skip:: >>> regions.write('my_regions.crtf', format='crtf') >>> regions.write('my_regions.reg') If the file already exists and you want to overwrite it, then set the ``overwrite`` keyword to `True`: .. doctest-skip:: >>> regions.write('my_regions.reg', overwrite=True) Parse ----- Region data in the form of a string or table may also be parsed into a :class:`~regions.Regions` object by using the :meth:`~regions.Regions.parse` method. The ``format`` keyword must be specified when parsing region data. A region string can be parsed for the ``crtf`` and ``ds9`` formats, while a `~astropy.table.Table` can be parsed for the ``fits`` format:: >>> from regions import Regions >>> regions_str = '# Region file format: DS9\nimage\ncircle(147.10,254.17,3.1) # color=green\n' >>> regions = Regions.parse(regions_str, format='ds9') >>> print(regions) >>> print(regions[0].visual) {'default_style': 'ds9', 'facecolor': 'green', 'edgecolor': 'green'} Serialize --------- Regions can be serialized to a string or table by using the :meth:`~regions.Regions.serialize` method. The ``format`` keyword must be specified when serializing region data. Serializing regions to the ``crtf`` or ``ds9`` format will produce a region string, while the ``fits`` format will produce a `~astropy.table.Table`: .. doctest-skip:: >>> regions_str = regions.serialize(format='ds9') Region Classes I/O ================== Additionally, all of the Region classes (i.e., :class:`~regions.Region` subclasses) support the :meth:`~regions.Regions.write` and :meth:`~regions.Regions.serialize` methods. As an example, let's create a :class:`~regions.CircleSkyRegion` object:: >>> from astropy.coordinates import SkyCoord >>> import astropy.units as u >>> from regions import CircleSkyRegion >>> coord = SkyCoord(202.469575, 47.19525833, unit='deg', frame='fk5') >>> region = CircleSkyRegion(coord, radius=0.01 * u.deg) >>> region , radius=0.01 deg)> To serialize the region:: >>> region.serialize(format='ds9', precision=8) '# Region file format: DS9 astropy/regions\nj2000\ncircle(202.46957500,47.19525833,0.01000000)\n' To write the region to a region file: .. doctest-skip:: >>> region.write('my_region.ds9', format='ds9') Use the :meth:`~regions.Region.get_formats` method to list all available formats and methods for the :class:`~regions.Region` subclasses:: >>> print(region.get_formats()) Format Parse Serialize Read Write Auto-identify ------ ----- --------- ---- ----- ------------- crtf No Yes No Yes Yes ds9 No Yes No Yes Yes fits No Yes No Yes Yes Region File Format Limitations ============================== .. toctree:: :maxdepth: 1 ds9_io fits_io Spherical Regions Limitations ============================= Reading/writing and serializing `~regions.SphericalSkyRegion` objects is not currently implemented. astropy-regions-f4139aa/docs/user_guide/shapely.rst000066400000000000000000000030621521703212700225540ustar00rootroot00000000000000Converting Regions to Shapely Objects ===================================== The `Shapely `__ Python package is a generic package for the manipulation and analysis of geometric objects in the Cartesian plane. For regions in the cartesian plane, it is more feature-complete, powerful, and optimized than this package. The use of Shapely or other Python regions packages that come from the geospatial domain in Astronomy is rare. However, if you have a complex pixel region analysis task, you can consider using Shapely. Either use it directly, by defining Shapely regions via Python code or one of the serialization formats they support, or by writing some Python code to convert `~regions.Region` objects to Shapely objects. Here we give an example of converting a circle to a Shapely object and then polygonize it. One nice feature of Shapely is that it can polygonize all shapes and perform fast polygon-based computations like intersection and union. .. plot:: :include-source: import matplotlib.pyplot as plt from regions import CirclePixelRegion, PixCoord from shapely.geometry import Point # Make an example region region = CirclePixelRegion(center=PixCoord(3, 2), radius=2) # Convert to Shapely point = Point(region.center.x, region.center.y) circle = point.buffer(region.radius) # Actually, this is a polygon approximation of the circle print(circle) # Plot the result x, y = circle.exterior.xy fig, ax = plt.subplots() ax.set_aspect('equal') ax.plot(x, y, 'g-') astropy-regions-f4139aa/docs/user_guide/shapes.rst000066400000000000000000000473721521703212700224060ustar00rootroot00000000000000 .. testsetup:: >>> from astropy.wcs import WCS >>> wcs = WCS(naxis=2) >>> wcs.wcs.crpix = (50, 50) >>> wcs.wcs.crval = (50, 10) >>> wcs.wcs.cdelt = (-0.1 / 3600, 0.1 / 3600) >>> wcs.wcs.ctype = ('RA---TAN', 'DEC--TAN') .. _shapes: Region Shapes ============= Regions provides `~regions.Region` objects, representing projected planar "region-in-image" shapes (defined in pixel or sky coordinates), and also representing spherical "region-on-celestial-sphere" shapes (defined using sky coordinates). The provided `~regions.Region` objects represent shapes such as circles, ellipses, rectangles, polygons, lines, and points. There are also regions defining circular, elliptical, and rectangular annuli, and a longitude/latitude range (in spherical geometry). Defining Shapes --------------- This section shows examples of how to construct a region for each shape that's currently supported. Circle ****** `~regions.CircleSkyRegion`, `~regions.CirclePixelRegion`, and `~regions.CircleSphericalSkyRegion` .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from astropy import units as u >>> from regions import PixCoord >>> from regions import (CircleSkyRegion, CirclePixelRegion, ... CircleSphericalSkyRegion) >>> center_sky = SkyCoord(42, 43, unit='deg', frame='fk5') >>> region_sky = CircleSkyRegion(center=center_sky, radius=3 * u.deg) >>> region_sph_sky = CircleSphericalSkyRegion(center=center_sky, radius=3 * u.deg) >>> region_pix = CirclePixelRegion(center=PixCoord(x=42, y=43), ... radius=4.2) `~regions.CircleAnnulusSkyRegion`, `~regions.CircleAnnulusPixelRegion`, and `~regions.CircleAnnulusSphericalSkyRegion` .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from astropy import units as u >>> from regions import PixCoord >>> from regions import (CircleAnnulusSkyRegion, ... CircleAnnulusPixelRegion, ... CircleAnnulusSphericalSkyRegion) >>> center_sky = SkyCoord(42, 43, unit='deg', frame='fk5') >>> region_sky = CircleAnnulusSkyRegion(center=center_sky, ... inner_radius=3 * u.deg, ... outer_radius=4 * u.deg) >>> region_sph_sky = CircleAnnulusSphericalSkyRegion(center=center_sky, ... inner_radius=3 * u.deg, ... outer_radius=4 * u.deg) >>> region_pix = CircleAnnulusPixelRegion(center=PixCoord(x=42, y=43), ... inner_radius=4.2, ... outer_radius=5.2) Ellipse ******* `~regions.EllipseSkyRegion` and `~regions.EllipsePixelRegion` .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from astropy import units as u >>> from regions import PixCoord >>> from regions import EllipseSkyRegion, EllipsePixelRegion >>> center_sky = SkyCoord(42, 43, unit='deg', frame='fk5') >>> region_sky = EllipseSkyRegion(center=center_sky, ... height=3 * u.deg, width=3 * u.deg, ... angle=5 * u.deg) >>> region_pix = EllipsePixelRegion(center=PixCoord(x=42, y=43), ... height=4.2, width=4.2, ... angle=5 * u.deg) `~regions.EllipseAnnulusSkyRegion` and `~regions.EllipseAnnulusPixelRegion` .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from astropy import units as u >>> from regions import PixCoord >>> from regions import EllipseAnnulusSkyRegion, EllipseAnnulusPixelRegion >>> center_sky = SkyCoord(42, 43, unit='deg', frame='fk5') >>> region_sky = EllipseAnnulusSkyRegion(center=center_sky, ... inner_width=3 * u.deg, ... outer_width=4 * u.deg, ... inner_height=6 * u.deg, ... outer_height=7 * u.deg, ... angle=6 * u.deg) >>> region_pix = EllipseAnnulusPixelRegion(center=PixCoord(x=42, y=43), ... inner_width=4.2, ... outer_width=5.2, ... inner_height=7.2, ... outer_height=8.2, ... angle=6 * u.deg) Rectangle ********* `~regions.RectangleSkyRegion` and `~regions.RectanglePixelRegion` .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from astropy import units as u >>> from regions import PixCoord >>> from regions import RectangleSkyRegion, RectanglePixelRegion >>> center_sky = SkyCoord(42, 43, unit='deg', frame='fk5') >>> region_sky = RectangleSkyRegion(center=center_sky, ... width=3 * u.deg, height=4 * u.deg, ... angle=5 * u.deg) >>> region_pix = RectanglePixelRegion(center=PixCoord(x=42, y=43), ... width=3, height=4, ... angle=5 * u.deg) `~regions.RectangleAnnulusSkyRegion` and `~regions.RectangleAnnulusPixelRegion` .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from astropy import units as u >>> from regions import PixCoord, RectangleAnnulusSkyRegion >>> from regions import RectangleAnnulusPixelRegion >>> center_sky = SkyCoord(42, 43, unit='deg', frame='fk5') >>> region_sky = RectangleAnnulusSkyRegion(center=center_sky, ... inner_width=3 * u.deg, ... outer_width=4 * u.deg, ... inner_height=6 * u.deg, ... outer_height=7 * u.deg, ... angle=15 * u.deg) >>> region_pix = RectangleAnnulusPixelRegion(center=PixCoord(x=42, y=43), ... inner_width=4.2, ... outer_width=5.2, ... inner_height=7.2, ... outer_height=8.2, ... angle=15 * u.deg) Polygon ******* .. Polygons are the most versatile region, since any region can be .. approximated as a polygon. .. .. TODO: explain how polygons are implemented and special polygon .. methods, e.g. how to obtain a polygon approximation for any shape. .. This is not available yet, for now see `spherical_geometry`_ for .. spherical polygons and `Shapely`_ for pixel polygons. .. TODO: after spherical polygons are generalized, remove note .. about spherical polygon only supporting convex polygons. `~regions.PolygonSkyRegion`, `~regions.PolygonPixelRegion`, `~regions.PolygonSphericalSkyRegion`, and `~regions.RegularPolygonPixelRegion`. (Note that currently `~regions.PolygonSphericalSkyRegion` only supports convex polygons.) .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from regions import (PixCoord, ... PolygonSkyRegion, ... PolygonPixelRegion, ... PolygonSphericalSkyRegion) >>> from regions import RegularPolygonPixelRegion >>> vertices = SkyCoord([1, 2, 2], [1, 1, 2], unit='deg', frame='fk5') >>> region_sky = PolygonSkyRegion(vertices=vertices) >>> region_sph_sky = PolygonSphericalSkyRegion(vertices=vertices) >>> vertices = PixCoord(x=[1, 2, 2], y=[1, 1, 2]) >>> region_pix = PolygonPixelRegion(vertices=vertices) >>> center = PixCoord(25, 25) >>> region2_pix = RegularPolygonPixelRegion(center, 6, 15) Range ***** `~regions.RangeSphericalSkyRegion` (Range has no direct analog in planar geometry. Instead, this shape can be transformed by discretizing to a polygon and transforming the polygon.) .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from astropy import units as u >>> from regions import RangeSphericalSkyRegion >>> sph_range = RangeSphericalSkyRegion(frame="galactic", ... longitude_range=[315,45]*u.deg, ... latitude_range=[0,45]*u.deg) Point ***** `~regions.PointSkyRegion` and `~regions.PointPixelRegion` .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from regions import PixCoord, PointSkyRegion, PointPixelRegion >>> center_sky = SkyCoord(42, 43, unit='deg', frame='fk5') >>> region_sky = PointSkyRegion(center=center_sky) >>> region_pix = PointPixelRegion(center=PixCoord(x=42, y=43)) Line **** `~regions.LineSkyRegion` and `~regions.LinePixelRegion` .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from regions import PixCoord, LineSkyRegion, LinePixelRegion >>> start_sky = SkyCoord(42, 43, unit='deg', frame='fk5') >>> end_sky = SkyCoord(52, 53, unit='deg', frame='fk5') >>> region_sky = LineSkyRegion(start=start_sky, end=end_sky) >>> region_pix = LinePixelRegion(start=PixCoord(x=42, y=43), ... end=PixCoord(x=52, y=53)) Text ***** The text regions can be used to annotate a text string on an image. `~regions.TextSkyRegion` and `~regions.TextPixelRegion` .. code-block:: python >>> from astropy.coordinates import SkyCoord >>> from regions import PixCoord, TextSkyRegion, TextPixelRegion >>> center_sky = SkyCoord(42, 43, unit='deg', frame='fk5') >>> region_sky = TextSkyRegion(center=center_sky, text='Demo Text') >>> region_pix = TextPixelRegion(center=PixCoord(x=42, y=43), ... text='Demo Text') Region Transformations ---------------------- For nearly all region shapes there are three classes. First, for planar projections (e.g., "regions-on-image", with Euclidean geometry), there is one class representing a "sky region" and another representing a "pixel region" on a given image. There are also spherical region classes, representing a "celestial sphere region" (with spherical geometry). (The spherical class for some shapes is not currently implemented. Additionally, some spherical classes do not have projected/planar analogs, including `~regions.RangeSphericalSkyRegion` and `~regions.LuneSphericalSkyRegion`.) A key feature of the regions package is that it is possible to convert back and forth between sky and image regions given a WCS object (e.g., `~astropy.wcs.WCS`). For conversions to and from spherical sky regions, it is also necessary to specify how boundary distortions (from projection effects) should be treated. Planar sky and pixel region transformations ******************************************* As an example, let's use the :class:`~regions.CircleSkyRegion`, a planar sky circle region: .. code-block:: python >>> from astropy.coordinates import Angle, SkyCoord >>> from regions import CircleSkyRegion >>> center = SkyCoord(50, 10, unit='deg') >>> radius = Angle(30, 'arcsec') >>> sky_reg = CircleSkyRegion(center, radius) To convert it to a pixel circle region (i.e., :class:`~regions.CirclePixelRegion`), call the :meth:`~regions.SkyRegion.to_pixel` method with a WCS object: .. code-block:: python >>> pix_reg = sky_reg.to_pixel(wcs) >>> print(pix_reg) # doctest: +FLOAT_CMP Region: CirclePixelRegion center: PixCoord(x=49.0, y=49.0) radius: 300.0 Also to convert a :class:`~regions.PixelRegion` to a :class:`~regions.SkyRegion`, call the :meth:`~regions.PixelRegion.to_sky` method with a WCS object: .. code-block:: python >>> sky_reg = pix_reg.to_sky(wcs) >>> print(sky_reg) # doctest: +FLOAT_CMP Region: CircleSkyRegion center: radius: 30.0 arcsec The conversion automatically selects the best method based on the WCS. For WCS with distortions (e.g., SIP) or non-astropy WCS objects (e.g., `GWCS `_), the local Jacobian matrix of the WCS transformation is computed at the region center. This allows the scale factors along the width and height directions to differ, providing accurate conversions even for distorted WCS. For simple WCS without distortions, a faster offset-based method is used, which is exact in that case. Note that all conversion methods are approximations that use the local pixel scale at the region center. Projection effects over the extent of the region are not accounted for. The region shape type is always preserved through the conversion (e.g., a `~regions.CirclePixelRegion` converts to a `~regions.CircleSkyRegion` and vice versa). Spherical to planar region transformations ****************************************** To demonstrate the transformation from spherical to planar regions, let's now use a `~regions.CircleSphericalSkyRegion`, a sky circle defined in spherical geometry: .. code-block:: python >>> from regions import CircleSphericalSkyRegion >>> center = SkyCoord(50, 10, unit='deg') >>> radius = Angle(30, 'arcsec') >>> sph_sky_reg = CircleSphericalSkyRegion(center, radius) To convert it to a planar sky circle region (i.e., :class:`~regions.CircleSkyRegion`), call the :meth:`~regions.SphericalSkyRegion.to_sky` method. If boundary distortions are ignored (i.e., a spherical circle is transformed to a planar circle), then a WCS object is not necessary: .. code-block:: python >>> sky_reg = sph_sky_reg.to_sky(boundary_distortions=False) >>> print(sky_reg) # doctest: +FLOAT_CMP Region: CircleSkyRegion center: radius: 30.0 arcsec To convert to a planar sky region that accounts for boundary distortions arising from projection effects, a WCS object must be passed (defining the projection). In this case, the spherical region boundary is discretized into a polygon before transforming, and a `~regions.PolygonSkyRegion` instance is returned. The number of points per side in the discretization (for a circle, the number of points around the circumference) can be specified through optional keywords that are passed to the `~regions.SphericalSkyRegion.discretize_boundary()` method. .. code-block:: python >>> sky_reg = sph_sky_reg.to_sky(wcs=wcs, ... boundary_distortions=True, ... n_vertices=10) >>> print(sky_reg) # doctest: +FLOAT_CMP Region: PolygonSkyRegion vertices: Similarly, spherical sky regions can be converted to pixel regions by specifying a WCS object and whether to ignore or include boundary distortions (returning either a `~regions.CirclePixelRegion` or `~regions.PolygonPixelRegion`, respectively). .. code-block:: python >>> pix_reg = sph_sky_reg.to_pixel(wcs=wcs, ... boundary_distortions=False) >>> print(pix_reg) # doctest: +FLOAT_CMP Region: CirclePixelRegion center: PixCoord(x=49.0, y=49.0) radius: 300.0 >>> pix_reg2 = sph_sky_reg.to_pixel(wcs=wcs, ... boundary_distortions=True, ... n_vertices=10) >>> print(pix_reg2) # doctest: +FLOAT_CMP Region: PolygonPixelRegion vertices: PixCoord(x=[225.3356 334.317 334.317 225.3356 49.0 ...], y=[291.7051 141.7051 -43.7051 -193.7051 -251.0 ...]) Planar regions can also be transformed to spherical regions, with the `~regions.SkyRegion.to_spherical_sky` and `~regions.PixelRegion.to_spherical_sky` methods. .. code-block:: python >>> center = SkyCoord(50, 10, unit='deg') >>> radius = Angle(30, 'arcsec') >>> sky_reg = CircleSkyRegion(center, radius) >>> sph_sky_reg = sky_reg.to_spherical_sky(boundary_distortions=False) >>> print(sph_sky_reg) # doctest: +FLOAT_CMP Region: CircleSphericalSkyRegion center: radius: 30.0 arcsec .. _regions-as_mpl_selector: Selectors for Regions --------------------- Several geometric regions (at this time, :class:`~regions.RectanglePixelRegion` and :class:`~regions.EllipsePixelRegion`) provide a method :meth:`~regions.RectanglePixelRegion.as_mpl_selector` to create an interactive editable matplotlib widget that will be connected to its parent region. .. plot:: :include-source: import matplotlib.pyplot as plt import numpy as np from regions import PixCoord, RectanglePixelRegion x, y = np.mgrid[-15:16, -10:21] z = np.exp(-(x / 4)**2 - (y / 6)**2) fig, ax = plt.subplots() img = ax.imshow(z, origin='lower') rectangle = RectanglePixelRegion(center=PixCoord(x=12, y=15), width=14, height=10) selector = rectangle.as_mpl_selector(ax) The ``selector`` creates and establishes a link to a matplotlib ``Selector`` widget that allows manually positioning the ``rectangle`` region at the central point, and scaling it by dragging its corner points. Several modifier keys as specified by the ``state_modifier_keys`` parameter to :class:`matplotlib.widgets.RectangleSelector` provide further control of the manipulation of this widget, with the following operations available: - "move": Move the existing shape from anywhere, default: "space". - "clear": Clear the current shape, default: "escape". - "square": Make the shape square, default: "shift". - "center": Change the shape around its center, default: "ctrl". - "rotate": Rotate the shape around its center, default: "r" (toggles, requires Matplotlib 3.6+). Via the optional ``callback`` parameter this method can be passed a custom function that will be called on every update of the region, i.e., after every move or resize of the selector. For an example of its usage see :ref:`Interactive Mask Control`. Multiple Regions ---------------- A `~regions.Region` object can represent only one region, not an array (e.g., vector or list) of regions. This is in contrast to the aperture classes in `Photutils `__ like :class:`photutils.aperture.CircularAperture` that do allow the ``positions`` (but usually not the other parameters) to be arrays: .. doctest-skip:: >>> from photutils import CircularAperture >>> positions = [(1, 2), (3, 4)] >>> apertures = CircularAperture(positions, r=4.2) To represent lists of `~regions.Region` objects, you can store them in Python lists or the `~regions.Regions` class, which effectively acts like a list of regions. To create many similar regions or process many regions you can use for loops or list comprehensions. .. code-block:: python >>> from regions import PixCoord, CirclePixelRegion >>> from regions import Regions >>> regions = [CirclePixelRegion(center=PixCoord(x, y), radius=4.2) ... for x, y in [(1, 2), (3, 4)]] >>> for region in regions: ... print(region.center) PixCoord(x=1, y=2) PixCoord(x=3, y=4) >>> [region.area for region in regions] [55.41769440932395, 55.41769440932395] To create a `~regions.Regions` object, simply pass in a list of regions:: >>> regs = Regions(regions) >>> print(regs[0]) Region: CirclePixelRegion center: PixCoord(x=1, y=2) radius: 4.2 >>> [reg.area for reg in regs] [55.41769440932395, 55.41769440932395] The `~regions.Regions` class also provides a :ref:`unified interface for reading, writing, parsing, and serializing regions data ` in different standard formats. astropy-regions-f4139aa/docs/user_guide/spherical_bounding_regions.rst000066400000000000000000000226311521703212700264770ustar00rootroot00000000000000.. _spherical_bounding_regions: Bounding Regions for On-Sky Spatial Searches ============================================ The `~regions.SphericalSkyRegion` classes also provide `~regions.SphericalSkyRegion.bounding_circle` and `~regions.SphericalSkyRegion.bounding_lonlat` properties, yielding the spherical circle and longitude and latitude ranges enclosing the region, respectively. This bounding information can be helpful when querying databases to select objects with tools such as `Astroquery `_ or `PyVO's TAP service `_. For many regions of interest, the spherical region properties can directly provide the query search boundary (e.g., the center and radius for a circle for a cone search, or the vertices for a polygon region search). However, this depends on what query shapes are supported by the database. When a particular region shape is not supported by a database, these bounding properties can be used to define a "padded" search, and the results can then be downselected to only those within the region using the `~regions.SphericalSkyRegion.contains()`. (In particular, the bounding properties may be helpful when a coordinate frame transformation is necessary to match the database coordinate system; see :ref:`spherical_frame_transform`.) As an example, let's create two spherical regions, a circle and range, and determine their bounding longitude/latitude spans and circle:: >>> from astropy.coordinates import SkyCoord >>> from astropy import units as u >>> from regions import CircleSphericalSkyRegion, RangeSphericalSkyRegion >>> sph_circ = CircleSphericalSkyRegion(SkyCoord(100,-40,unit=u.deg, ... frame="galactic"), ... 30*u.deg) >>> print(sph_circ.bounding_lonlat) # doctest: +FLOAT_CMP (, ) >>> sph_range = RangeSphericalSkyRegion(frame="galactic", ... longitude_range=[315,45]*u.deg, ... latitude_range=[0,45]*u.deg) >>> print(sph_range.bounding_circle) # doctest: +FLOAT_CMP Region: CircleSphericalSkyRegion center: radius: 47.2657903991002 deg (The bounding circle of a circle is always itself, and in the original coordinate frame, the bounding longitude/latitude of a spherical range is equivalent to the region.) Let's now transform these regions into the ICRS frame, and obtain the new bounds, including the new frame longitude/latitude bounds on the transformed Range region:: >>> sph_circ_transf = sph_circ.transform_to("icrs") >>> print(sph_circ_transf.bounding_lonlat) # doctest: +FLOAT_CMP (, ) >>> sph_range_transf = sph_range.transform_to("icrs") >>> print(sph_range_transf.bounding_lonlat) # doctest: +FLOAT_CMP (, ) >>> print(sph_range_transf.bounding_circle) # doctest: +FLOAT_CMP Region: CircleSphericalSkyRegion center: radius: 47.26579039910021 deg These bounding circles and longitude/latitude are visualized below for both the original and transformed coordinate frames. .. plot:: :include-source: false import matplotlib.pyplot as plt import numpy as np from astropy import units as u from astropy.coordinates import SkyCoord from astropy.visualization.wcsaxes.frame import EllipticalFrame from astropy.wcs import WCS from matplotlib.lines import Line2D from regions import (CircleSphericalSkyRegion, RangeSphericalSkyRegion) # Create full-sky Aitoff WCS objects for Galactic and ICRS frames wcs = WCS(naxis=2) wcs.wcs.crpix = (180, 90) wcs.wcs.cdelt = (-1, 1) wcs.wcs.crval = (0, 0) wcs.wcs.ctype = ('GLON-AIT', 'GLAT-AIT') wcs_icrs = WCS(naxis=2) wcs_icrs.wcs.crpix = (180, 90) wcs_icrs.wcs.cdelt = (-1, 1) wcs_icrs.wcs.crval = (0, 0) wcs_icrs.wcs.ctype = ('RA---AIT', 'DEC--AIT') sph_circ = CircleSphericalSkyRegion( SkyCoord(100, -40, unit=u.deg, frame='galactic'), 30 * u.deg) sph_range = RangeSphericalSkyRegion( frame='galactic', longitude_range=[315, 45] * u.deg, latitude_range=[0, 45] * u.deg) sph_circ_transf = sph_circ.transform_to('icrs') sph_range_transf = sph_range.transform_to('icrs') fig = plt.figure() fig.set_size_inches(7, 7) axes = [] axes.append(fig.add_axes([0.15, 0.575, 0.8, 0.425], projection=wcs, frame_class=EllipticalFrame)) axes.append(fig.add_axes([0.15, 0.05, 0.8, 0.425], projection=wcs_icrs, frame_class=EllipticalFrame)) ax = axes[0] ax.coords.grid(color='gray') ax.set_xlabel(r'Galactic $\ell$', labelpad=10) ax.set_ylabel(r'Galactic $b$') ax.set_title('Galactic coordinates', pad=5) patch = sph_circ.to_pixel( wcs=wcs, boundary_distortions=True, n_vertices=1000, ).plot(ax=ax, color='tab:blue', lw=3) sph_range.to_pixel( wcs=wcs, boundary_distortions=True, n_vertices=250, ).plot(ax=ax, color='tab:red', lw=3) bound_color = 'tab:blue' bound_lw = 0.75 bound_zord = 2 bound_lon, bound_lat = sph_circ.bounding_lonlat for i in range(2): # Need to manually "super sample" to get correct distortion. # Unfortunately axv/hline works for just "aitoff" projection, # not doing a WCS it seems. npt = 250 xarr = np.repeat(bound_lon[i].degree, npt) yarr = np.linspace(-90, 90, num=npt, endpoint=True) l1 = Line2D(xarr, yarr, ls='--', color=bound_color, lw=bound_lw, zorder=bound_zord, transform=ax.get_transform('galactic')) xarr = np.linspace(-180, 180, num=npt, endpoint=True) yarr = np.repeat(bound_lat[i].degree, npt) l2 = Line2D(xarr, yarr, ls='--', color=bound_color, lw=bound_lw, zorder=bound_zord, transform=ax.get_transform('galactic')) ax.add_artist(l1) ax.add_artist(l2) bound_color = 'tab:red' sph_range.bounding_circle.to_pixel( wcs=wcs, boundary_distortions=True, n_vertices=1000, ).plot(ax=ax, color='tab:red', lw=bound_lw, ls='--', zorder=bound_zord) patch.set_clip_path(ax.coords.frame.patch) ax.set_xlim(20, 340) ax.set_ylim(10, 170) ax = axes[1] ax.coords.grid(color='gray') ax.set_xlabel('RA', labelpad=10) ax.set_ylabel('Dec') ax.set_title('ICRS coordinates', pad=5) patch = sph_circ_transf.to_pixel( wcs=wcs_icrs, boundary_distortions=True, n_vertices=1000, ).plot(ax=ax, color='tab:blue', lw=3) sph_range_transf.to_pixel( wcs=wcs_icrs, boundary_distortions=True, n_vertices=250, ).plot(ax=ax, color='tab:red', lw=3) bound_color = 'tab:blue' bound_lw = 0.75 bound_zord = 2 bound_lon, bound_lat = sph_circ_transf.bounding_lonlat for i in range(2): # Need to manually "super sample" to get correct distortion. # Unfortunately axv/hline works for just "aitoff" projection, # not doing a WCS it seems. npt = 250 xarr = np.repeat(bound_lon[i].degree, npt) yarr = np.linspace(-90, 90, num=npt, endpoint=True) l1 = Line2D(xarr, yarr, ls='--', color=bound_color, lw=bound_lw, zorder=bound_zord, transform=ax.get_transform('icrs')) xarr = np.linspace(-180, 180, num=npt, endpoint=True) yarr = np.repeat(bound_lat[i].degree, npt) l2 = Line2D(xarr, yarr, ls='--', color=bound_color, lw=bound_lw, zorder=bound_zord, transform=ax.get_transform('icrs')) ax.add_artist(l1) ax.add_artist(l2) bound_color = 'tab:red' bound_lon, bound_lat = sph_range_transf.bounding_lonlat for i in range(2): # Need to manually "super sample" to get correct distortion. # Unfortunately axv/hline works for just "aitoff" projection, # not doing a WCS it seems. npt = 250 xarr = np.repeat(bound_lon[i].degree, npt) yarr = np.linspace(-90, 90, num=npt, endpoint=True) l1 = Line2D(xarr, yarr, ls='--', color=bound_color, lw=bound_lw, zorder=bound_zord, transform=ax.get_transform('icrs')) xarr = np.linspace(-180, 180, num=npt, endpoint=True) yarr = np.repeat(bound_lat[i].degree, npt) l2 = Line2D(xarr, yarr, ls='--', color=bound_color, lw=bound_lw, zorder=bound_zord, transform=ax.get_transform('icrs')) ax.add_artist(l1) ax.add_artist(l2) sph_range_transf.bounding_circle.to_pixel( wcs=wcs_icrs, boundary_distortions=True, n_vertices=1000, ).plot(ax=ax, color='tab:red', lw=bound_lw, ls='--', zorder=bound_zord) patch.set_clip_path(ax.coords.frame.patch) ax.set_xlim(20, 340) ax.set_ylim(10, 170) ax.coords[0].set_format_unit(u.deg) astropy-regions-f4139aa/docs/user_guide/spherical_frame_transform.rst000066400000000000000000000141361521703212700263320ustar00rootroot00000000000000.. _spherical_frame_transform: Coordinate Frame Transformation of Spherical Regions ==================================================== The `~astropy.coordinates.SkyCoord` class provides functionality for representing celestial coordinates and for transforming between different coordinate frames (see the :ref:`astropy-coordinates` documentation for full details). Since the `~regions.SphericalSkyRegion` class represents geometric shapes on the celestial sphere, these idealized shapes can also be represented in, and transformed between, any spherical coordinate reference frame. Transforming spherical regions can be useful in cases where a region of interest is defined in one coordinate frame, but a query searching for targets in this region needs to be specified in second coordinate frame. As an example, let's start by defining two sky regions in the Galactic coordinate frame:: >>> from astropy.coordinates import SkyCoord >>> from astropy import units as u >>> from regions import CircleSphericalSkyRegion, RangeSphericalSkyRegion >>> sph_circ = CircleSphericalSkyRegion(SkyCoord(100,-30,unit=u.deg, ... frame="galactic"), ... 30*u.deg) >>> sph_range = RangeSphericalSkyRegion(frame="galactic", ... longitude_range=[315,45]*u.deg, ... latitude_range=[0,45]*u.deg) >>> print(sph_circ) # doctest: +FLOAT_CMP Region: CircleSphericalSkyRegion center: radius: 30.0 deg >>> print(sph_range) # doctest: +FLOAT_CMP Region: RangeSphericalSkyRegion frame: galactic longitude_range: [315. 45.] deg latitude_range: [ 0. 45.] deg To convert these regions to the ICRS coordinate frame, the `~regions.SphericalSkyRegion.transform_to()` method is used:: >>> sph_circ_transf = sph_circ.transform_to("icrs") >>> sph_range_transf = sph_range.transform_to("icrs") >>> print(sph_circ_transf) # doctest: +FLOAT_CMP Region: CircleSphericalSkyRegion center: radius: 30.0 deg >>> print(sph_range_transf) # doctest: +FLOAT_CMP Region: RangeSphericalSkyRegion frame: icrs longitude_bounds: , center_gc2=)> latitude_bounds: , inner_radius=45.0 deg, outer_radius=90.0 deg)> Note that the Range region boundaries cannot be simply described by longitude/latitude boundaries in a transformed frame, so the underlying circular annulus and lune boundaries (capturing the original frame latitude and longitude bounds, respectively) are used to describe this Range regions after a transformation. These original and transformed regions are shown below on full-sky projections for the Galactic and ICRS coordinate reference frames (respectively). .. plot:: :include-source: false import warnings warnings.filterwarnings('ignore', message='.*unrecognized position.*') import matplotlib.pyplot as plt from astropy import units as u from astropy.coordinates import SkyCoord from astropy.visualization.wcsaxes.frame import EllipticalFrame from astropy.wcs import WCS from regions import (CircleSphericalSkyRegion, RangeSphericalSkyRegion) # Create full-sky Aitoff WCS objects for Galactic and ICRS frames wcs = WCS(naxis=2) wcs.wcs.crpix = (180, 90) wcs.wcs.cdelt = (-1, 1) wcs.wcs.crval = (0, 0) wcs.wcs.ctype = ('GLON-AIT', 'GLAT-AIT') wcs_icrs = WCS(naxis=2) wcs_icrs.wcs.crpix = (180, 90) wcs_icrs.wcs.cdelt = (-1, 1) wcs_icrs.wcs.crval = (0, 0) wcs_icrs.wcs.ctype = ('RA---AIT', 'DEC--AIT') sph_circ = CircleSphericalSkyRegion( SkyCoord(100, -30, unit=u.deg, frame='galactic'), 30 * u.deg) sph_range = RangeSphericalSkyRegion( frame='galactic', longitude_range=[315, 45] * u.deg, latitude_range=[0, 45] * u.deg) sph_circ_transf = sph_circ.transform_to('icrs') sph_range_transf = sph_range.transform_to('icrs') fig = plt.figure() fig.set_size_inches(7, 7) axes = [] axes.append(fig.add_axes([0.15, 0.575, 0.8, 0.425], projection=wcs, frame_class=EllipticalFrame)) axes.append(fig.add_axes([0.15, 0.05, 0.8, 0.425], projection=wcs_icrs, frame_class=EllipticalFrame)) ax = axes[0] ax.coords.grid(color='black') ax.set_xlabel(r'Galactic $\ell$', labelpad=10) ax.set_ylabel(r'Galactic $b$') ax.set_title('Galactic coordinates', pad=5) overlay = ax.get_coords_overlay('icrs') overlay.grid(color='gray', ls='dotted') patch = sph_circ.to_pixel( wcs=wcs, boundary_distortions=True, n_vertices=1000, ).plot(ax=ax, color='tab:blue', lw=3) sph_range.to_pixel( wcs=wcs, boundary_distortions=True, n_vertices=250, ).plot(ax=ax, color='tab:red', lw=3) patch.set_clip_path(ax.coords.frame.patch) ax.set_xlim(20, 340) ax.set_ylim(10, 170) ax = axes[1] ax.coords.grid(color='gray', ls='dotted') ax.set_xlabel('RA', labelpad=10) ax.set_ylabel('Dec') ax.set_title('ICRS coordinates', pad=5) overlay = ax.get_coords_overlay('galactic') overlay.grid(color='black', ls='solid') patch = sph_circ_transf.to_pixel( wcs=wcs_icrs, boundary_distortions=True, n_vertices=1000, ).plot(ax=ax, color='tab:blue', lw=3) sph_range_transf.to_pixel( wcs=wcs_icrs, boundary_distortions=True, n_vertices=250, ).plot(ax=ax, color='tab:red', lw=3) patch.set_clip_path(ax.coords.frame.patch) ax.set_xlim(20, 340) ax.set_ylim(10, 170) ax.coords[0].set_format_unit(u.deg) astropy-regions-f4139aa/pyproject.toml000066400000000000000000000133011521703212700202030ustar00rootroot00000000000000[project] name = 'regions' description = 'An Astropy coordinated package for region handling' readme = 'README.rst' license = 'BSD-3-Clause' license-files = ['LICENSE.rst'] authors = [ {name = 'Regions Developers', email = 'astropy.team@gmail.com'}, ] keywords = [ 'astronomy', 'astrophysics', 'astropy', 'regions', ] classifiers = [ 'Intended Audience :: Science/Research', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Cython', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Topic :: Scientific/Engineering :: Astronomy', ] dynamic = ['version'] requires-python = '>=3.11' dependencies = [ 'astropy >= 6.1.7', 'numpy >= 2.0', ] [project.urls] Homepage = 'https://github.com/astropy/regions' Documentation = 'https://astropy-regions.readthedocs.io/en/stable/' [project.optional-dependencies] all = [ 'matplotlib >= 3.9', 'shapely >= 2.1', ] test = [ 'gwcs >= 0.22', 'pytest-arraydiff >= 0.6.1', 'pytest-astropy >= 0.11', 'pytest-xdist >= 3.7', 'tox >= 4.16', ] docs = [ 'regions[all]', 'sphinx >= 8.2', # keep in sync with docs/conf.py 'sphinx-astropy[confv3] >= 1.11', 'sphinx_design >= 0.6', ] dev = [ 'pre-commit >= 4.0', 'regions[docs,test]', ] [build-system] requires = [ 'cython >= 3.1.2, < 4', 'extension-helpers >= 1.3, < 2', 'numpy >= 2.0', 'setuptools >= 77.0.1', 'setuptools_scm >= 8.2', ] build-backend = 'setuptools.build_meta' [tool.extension-helpers] use_extension_helpers = true [tool.setuptools_scm] write_to = 'regions/version.py' [tool.setuptools] zip-safe = false include-package-data = false [tool.setuptools.packages.find] namespaces = false [tool.setuptools.package-data] '*' = [ 'data/*', ] 'regions.shapes.tests' = [ 'reference/*.txt', 'data/*.fits', ] 'regions.io.crtf.tests' = [ 'data/*.crtf', ] 'regions.io.ds9.tests' = [ 'data/*.reg', ] 'regions.io.fits.tests' = [ 'data/*.fits', ] [tool.pytest.ini_options] minversion = "8.0" testpaths = [ 'regions', 'docs', ] norecursedirs = [ 'docs/_build', 'extern', ] astropy_header = true doctest_plus = 'enabled' text_file_format = 'rst' addopts = [ '-ra', '--color=yes', '--doctest-rst', '--strict-config', '--strict-markers', ] log_level = 'INFO' log_cli_level = 'INFO' xfail_strict = true remote_data_strict = true filterwarnings = [ 'error', # turn warnings into exceptions ] markers = [ 'array_compare' ] [tool.coverage.run] omit = [ 'regions/conftest.py', 'regions/*setup_package*', 'regions/tests/*', 'regions/*/tests/*', 'regions/extern/*', 'regions/version*', '*/regions/conftest.py', '*/regions/*setup_package*', '*/regions/tests/*', '*/regions/*/tests/*', '*/regions/extern/*', '*/regions/version*', ] [tool.coverage.report] exclude_lines = [ 'pragma: no cover', 'except ImportError', 'raise AssertionError', 'raise NotImplementedError', 'def main\\(.*\\):', 'pragma: py{ignore_python_version}', 'def _ipython_key_completions_', ] [tool.isort] skip_glob = [ 'regions/*__init__.py*', ] known_first_party = [ 'regions', 'extension_helpers', ] use_parentheses = true [tool.black] force-exclude = """ ( .* ) """ [tool.bandit] exclude_dirs = ['*/tests/test_casa_mask.py'] [tool.bandit.assert_used] skips = ['*_test.py', '*/test_*.py', '*/tests/helpers.py'] [tool.cibuildwheel] # explicitly skip free-threaded builds skip = ["cp*t-*"] [tool.repo-review] ignore = [ 'MY', # ignore MyPy 'PC110', # ignore using black or ruff-format in pre-commit 'PC111', # ignore using blacken-docs in pre-commit 'PC140', # ignore using mypy in pre-commit 'PC180', # ignore using prettier in pre-commit 'PC901', # ignore using custom pre-commit update message 'PC902', # ignore custom pre-commit CI autofix message 'PP006', # ignore missing dependency-groups in pyproject.toml 'PY005', # ignore having a tests/ folder ] [tool.codespell] ignore-words-list = """ exten, """ [tool.docformatter] wrap-summaries = 72 pre-summary-newline = true make-summary-multi-line = true [tool.numpydoc_validation] checks = [ 'all', # report on all checks, except the below 'ES01', # missing extended summary 'EX01', # missing "Examples" 'GL08', # missing docstring -- FIXME 'RT01', # do not require return type for lazy properties 'RT02', # only type in "Returns" section (no name) 'SA01', # missing "See Also" 'SA04', # missing "See Also" description 'SS06', # single-line summary ] # don't report on objects that match any of these regex; # remember to use single quotes for regex in TOML exclude = [ '\._.*', # private functions/methods '^test_*', # test code '^conftest.*$', # pytest configuration ] [tool.ruff] line-length = 79 [tool.ruff.lint.pylint] max-statements = 130 [tool.ruff.lint] select = [ 'B', 'COM', 'D', 'E', 'F', 'FLY', 'I', 'INT', 'N', 'NPY', 'PERF', 'PL', 'Q', 'RSE', 'SIM', 'UP', 'W' ] ignore = [ 'E501', 'B028', 'B905', # revisit 'D100', 'D101', 'D102', 'D103', 'D105', 'D107', 'D200', 'D205', 'D212', 'D401', 'D404', 'I001', 'PLC0415', # import-outside-top-level 'PLR0912', 'PLR0913', 'PLR0915', 'PLR2004', 'PLW1641', # eq-without-hash 'PLW2901', 'Q000', 'SIM910', 'UP038', ] [tool.ruff.lint.per-file-ignores] '__init__.py' = ['D104', 'I'] 'docs/conf.py' = ['ERA001', 'INP001', 'TRY400'] 'scripts/*.py' = ['INP001'] # not a package [tool.ruff.lint.pydocstyle] convention = 'numpy' [tool.distutils.bdist_wheel] py-limited-api = "cp311" astropy-regions-f4139aa/regions/000077500000000000000000000000001521703212700167375ustar00rootroot00000000000000astropy-regions-f4139aa/regions/__init__.py000066400000000000000000000006431521703212700210530ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Regions is an Astropy coordinated package to provide tools for region handling. """ from ._utils.examples import * # noqa: F401, F403 from .core import * # noqa: F401, F403 from .io import * # noqa: F401, F403 from .shapes import * # noqa: F401, F403 try: from .version import version as __version__ except ImportError: __version__ = '' astropy-regions-f4139aa/regions/_geometry/000077500000000000000000000000001521703212700207315ustar00rootroot00000000000000astropy-regions-f4139aa/regions/_geometry/README.md000066400000000000000000000001351521703212700222070ustar00rootroot00000000000000# This sub-package contains fast geometrical routines that are then used by different shapes astropy-regions-f4139aa/regions/_geometry/__init__.py000066400000000000000000000005141521703212700230420ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Geometry subpackage for low-level geometry functions. """ from .circle_overlap import * # noqa: F401, F403 from .ellipse_overlap import * # noqa: F401, F403 from .polygon_overlap import * # noqa: F401, F403 from .rectangle_overlap import * # noqa: F401, F403 astropy-regions-f4139aa/regions/_geometry/circle_overlap.pyx000066400000000000000000000233131521703212700244660ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True # cython: freethreading_compatible=True """ Tools to calculate the area of overlap between a circle and a pixel grid. The cdef functions are not intended to be called from Python code. They are pure C math functions declared ``noexcept nogil`` so they can be called without the GIL (e.g., from the batch aperture photometry driver), including from multiple threads on free-threaded Python builds. Their signatures are exported via circle_overlap.pxd. """ import numpy as np cimport numpy as np from .core cimport area_arc, area_triangle, floor_sqrt __all__ = ['circle_overlap_grid'] cdef extern from "math.h" nogil: double sqrt(double x) DTYPE = np.float64 ctypedef np.float64_t DTYPE_t def circle_overlap_grid(double xmin, double xmax, double ymin, double ymax, int nx, int ny, double r, int use_exact, int subpixels): """ circle_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, r, use_exact, subpixels) Calculate the fractional overlap between a circle and a pixel grid. The circle is centered on the origin. Parameters ---------- xmin, xmax, ymin, ymax : float The extent of the grid in the x and y direction. The grid is defined by the rectangle with corners (xmin, ymin) and (xmax, ymax). nx, ny : int The grid dimensions in the x and y direction. The grid is defined by the rectangle with corners (xmin, ymin) and (xmax, ymax) and is divided into nx and ny pixels in the x and y direction, respectively. r : float The radius of the circle. use_exact : 0 or 1 Set to ``1`` to use an exact method to calculate the overlap between the circle and each pixel. Set to ``0`` to use a sub-pixel sampling method to calculate the overlap, where each pixel is divided into ``subpixels ** 2`` subpixels and the fraction of subpixels that are within the circle is used to estimate the overlap. subpixels : int The number of subpixels to use in each dimension when using the sub-pixel sampling method. Each pixel is resampled by this factor in each dimension; thus, each pixel is divided into ``subpixels ** 2`` subpixels. A subpixel is included only if its center lies strictly inside the circle; subpixel centers lying exactly on the circle boundary are excluded (weight 0). Returns ------- result : `~numpy.ndarray` (float) A 2D array of shape (ny, nx) giving the fraction of each pixel's area that overlaps with the circle, ranging from 0 to 1. The element at index (j, i) corresponds to the pixel with corners at (xmin + i * dx, ymin + j * dy) and (xmin + (i + 1) * dx, ymin + (j + 1) * dy), where dx and dy are the width of each pixel in the x and y direction, respectively. """ cdef unsigned int i, j cdef double dx, dy, d, pixel_radius cdef double bxmin, bxmax, bymin, bymax cdef double pxmin, pxcen, pxmax, pymin, pycen, pymax # Define output array cdef np.ndarray[DTYPE_t, ndim=2] frac = np.zeros([ny, nx], dtype=DTYPE) cdef double[:, ::1] frac_view = frac # Find the width of each element in x and y dx = (xmax - xmin) / nx dy = (ymax - ymin) / ny # Find the radius of a single pixel pixel_radius = 0.5 * sqrt(dx * dx + dy * dy) # Define bounding box bxmin = -r - 0.5 * dx bxmax = +r + 0.5 * dx bymin = -r - 0.5 * dy bymax = +r + 0.5 * dy with nogil: for i in range(nx): pxmin = xmin + i * dx # lower end of pixel pxcen = pxmin + dx * 0.5 pxmax = pxmin + dx # upper end of pixel if pxmax > bxmin and pxmin < bxmax: for j in range(ny): pymin = ymin + j * dy pycen = pymin + dy * 0.5 pymax = pymin + dy if pymax > bymin and pymin < bymax: # Distance from circle center to pixel center. d = sqrt(pxcen * pxcen + pycen * pycen) # If pixel center is "well within" circle, # count full pixel. if d < r - pixel_radius: frac_view[j, i] = 1.0 # If pixel center is "close" to circle border, # find overlap. elif d < r + pixel_radius: # Either do exact calculation or use # subpixel sampling: if use_exact: frac_view[j, i] = ( circle_overlap_single_exact( pxmin, pymin, pxmax, pymax, r) / (dx * dy)) else: frac_view[j, i] = ( circle_overlap_single_subpixel( pxmin, pymin, pxmax, pymax, r, subpixels)) # Otherwise, it is fully outside circle. # No action needed. return frac cdef double circle_overlap_single_subpixel(double x0, double y0, double x1, double y1, double r, int subpixels) noexcept nogil: """ Return the fraction of overlap between a circle and a single pixel with given extent, using a sub-pixel sampling method. """ cdef unsigned int i, j cdef double x, y, dx, dy, r_squared cdef double frac = 0.0 # accumulator dx = (x1 - x0) / subpixels dy = (y1 - y0) / subpixels r_squared = r ** 2 x = x0 - 0.5 * dx for i in range(subpixels): x += dx y = y0 - 0.5 * dy for j in range(subpixels): y += dy if x * x + y * y < r_squared: frac += 1.0 return frac / (subpixels * subpixels) cdef double circle_overlap_single_exact(double xmin, double ymin, double xmax, double ymax, double r) noexcept nogil: """ Calculate the area of overlap between a circle and a single pixel with given extent, using an exact method. The circle is centered on the origin. """ if 0.0 <= xmin: if 0.0 <= ymin: return circle_overlap_core(xmin, ymin, xmax, ymax, r) elif 0.0 >= ymax: return circle_overlap_core(-ymax, xmin, -ymin, xmax, r) else: return circle_overlap_single_exact(xmin, ymin, xmax, 0.0, r) \ + circle_overlap_single_exact(xmin, 0.0, xmax, ymax, r) elif 0.0 >= xmax: if 0.0 <= ymin: return circle_overlap_core(-xmax, ymin, -xmin, ymax, r) elif 0.0 >= ymax: return circle_overlap_core(-xmax, -ymax, -xmin, -ymin, r) else: return circle_overlap_single_exact(xmin, ymin, xmax, 0.0, r) \ + circle_overlap_single_exact(xmin, 0.0, xmax, ymax, r) else: if 0.0 <= ymin or 0.0 >= ymax: return circle_overlap_single_exact(xmin, ymin, 0.0, ymax, r) \ + circle_overlap_single_exact(0.0, ymin, xmax, ymax, r) else: return circle_overlap_single_exact(xmin, ymin, 0.0, 0.0, r) \ + circle_overlap_single_exact(0.0, ymin, xmax, 0.0, r) \ + circle_overlap_single_exact(xmin, 0.0, 0.0, ymax, r) \ + circle_overlap_single_exact(0.0, 0.0, xmax, ymax, r) cdef double circle_overlap_core(double xmin, double ymin, double xmax, double ymax, double r) noexcept nogil: """ Calculate the area of overlap between a circle and a rectangle, where the rectangle is in the first quadrant and the circle is centered on the origin. Assumes that the center of the circle is <= xmin, ymin (can always modify input to conform to this). """ cdef double area, d1, d2, x1, x2, y1, y2 if xmin * xmin + ymin * ymin > r * r: area = 0.0 elif xmax * xmax + ymax * ymax < r * r: area = (xmax - xmin) * (ymax - ymin) else: area = 0.0 d1 = floor_sqrt(xmax * xmax + ymin * ymin) d2 = floor_sqrt(xmin * xmin + ymax * ymax) if d1 < r and d2 < r: x1, y1 = floor_sqrt(r * r - ymax * ymax), ymax x2, y2 = xmax, floor_sqrt(r * r - xmax * xmax) area = ((xmax - xmin) * (ymax - ymin) - area_triangle(x1, y1, x2, y2, xmax, ymax) + area_arc(x1, y1, x2, y2, r)) elif d1 < r: x1, y1 = xmin, floor_sqrt(r * r - xmin * xmin) x2, y2 = xmax, floor_sqrt(r * r - xmax * xmax) area = (area_arc(x1, y1, x2, y2, r) + area_triangle(x1, y1, x1, ymin, xmax, ymin) + area_triangle(x1, y1, x2, ymin, x2, y2)) elif d2 < r: x1, y1 = floor_sqrt(r * r - ymin * ymin), ymin x2, y2 = floor_sqrt(r * r - ymax * ymax), ymax area = (area_arc(x1, y1, x2, y2, r) + area_triangle(x1, y1, xmin, y1, xmin, ymax) + area_triangle(x1, y1, xmin, y2, x2, y2)) else: x1, y1 = floor_sqrt(r * r - ymin * ymin), ymin x2, y2 = xmin, floor_sqrt(r * r - xmin * xmin) area = (area_arc(x1, y1, x2, y2, r) + area_triangle(x1, y1, x2, y2, xmin, ymin)) return area astropy-regions-f4139aa/regions/_geometry/core.pxd000066400000000000000000000021511521703212700223750ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3 """ Declarations needed to cimport the core geometry functions into other Cython files. All functions are pure C math functions that are safe to call without the GIL. """ cdef double floor_sqrt(double x) noexcept nogil cdef double distance(double x1, double y1, double x2, double y2) noexcept nogil cdef double area_arc(double x1, double y1, double x2, double y2, double r) noexcept nogil cdef double area_triangle(double x1, double y1, double x2, double y2, double x3, double y3) noexcept nogil cdef double area_arc_unit(double x1, double y1, double x2, double y2) noexcept nogil cdef int in_triangle(double x, double y, double x1, double y1, double x2, double y2, double x3, double y3) noexcept nogil cdef double overlap_area_triangle_unit_circle(double x1, double y1, double x2, double y2, double x3, double y3) noexcept nogil astropy-regions-f4139aa/regions/_geometry/core.pyx000066400000000000000000000312021521703212700224210ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True # cython: freethreading_compatible=True """ Core geometry functions for computing the overlap of an aperture and a pixel grid. The cdef functions are not intended to be called from Python code. They are pure C math functions declared ``noexcept nogil`` so they can be called without the GIL, including from multiple threads on free-threaded Python builds. Their signatures are exported via core.pxd. """ cdef extern from "math.h" nogil: double asin(double x) double sin(double x) double sqrt(double x) double fabs(double x) double M_PI double NAN ctypedef struct point: double x double y ctypedef struct intersections: point p1 point p2 cdef double floor_sqrt(double x) noexcept nogil: """ In some of the geometrical functions, we have to take the sqrt of a number and we know that the number should be >= 0. However, in some cases the value is e.g. -1e-10, but we want to treat it as zero, which is what this function does. Note that this does **not** check whether negative values are close or not to zero, so this should be used only in cases where the value is expected to be positive. """ if x > 0: return sqrt(x) else: return 0 cdef double distance(double x1, double y1, double x2, double y2) noexcept nogil: """ Distance between two points in two dimensions. Parameters ---------- x1, y1 : float The coordinates of the first point. x2, y2 : float The coordinates of the second point. Returns ------- d : float The Euclidean distance between the two points. """ return sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) cdef double area_arc(double x1, double y1, double x2, double y2, double r) noexcept nogil: """ Area of a circle arc with radius r between points (x1, y1) and (x2, y2). References ---------- http://mathworld.wolfram.com/CircularSegment.html """ cdef double a, theta a = distance(x1, y1, x2, y2) theta = 2.0 * asin(0.5 * a / r) return 0.5 * r * r * (theta - sin(theta)) cdef double area_triangle(double x1, double y1, double x2, double y2, double x3, double y3) noexcept nogil: """ Area of a triangle defined by three vertices. """ return 0.5 * fabs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) cdef double area_arc_unit(double x1, double y1, double x2, double y2) noexcept nogil: """ Area of a unit circle arc (radius of 1) between points (x1, y1) and (x2, y2). References ---------- http://mathworld.wolfram.com/CircularSegment.html """ cdef double a, theta a = distance(x1, y1, x2, y2) theta = 2.0 * asin(0.5 * a) return 0.5 * (theta - sin(theta)) cdef int in_triangle(double x, double y, double x1, double y1, double x2, double y2, double x3, double y3) noexcept nogil: """ Check if a point (x,y) is inside a triangle. """ cdef int c = 0 c += ((y1 > y) != (y2 > y) and x < (x2 - x1) * (y - y1) / (y2 - y1) + x1) c += ((y2 > y) != (y3 > y) and x < (x3 - x2) * (y - y2) / (y3 - y2) + x2) c += ((y3 > y) != (y1 > y) and x < (x1 - x3) * (y - y3) / (y1 - y3) + x3) return c % 2 == 1 cdef intersections circle_line(double x1, double y1, double x2, double y2) noexcept nogil: """ Intersection of a line defined by two points with a unit circle. Coordinates > 1 in the result indicate that there is no intersection. """ cdef double a, b, delta, dx, dy cdef double tolerance = 1.e-10 cdef intersections inter dx = x2 - x1 dy = y2 - y1 # Initialize to values > 1, indicating no intersection inter.p1.x = 2. inter.p1.y = 2. inter.p2.x = 2. inter.p2.y = 2. if fabs(dx) < tolerance and fabs(dy) < tolerance: return inter if fabs(dx) > fabs(dy): # Find the slope and intercept of the line a = dy / dx b = y1 - a * x1 # Find the determinant of the quadratic equation delta = 1. + a * a - b * b if delta > 0.: # solutions exist delta = sqrt(delta) inter.p1.x = (-a * b - delta) / (1. + a * a) inter.p1.y = a * inter.p1.x + b inter.p2.x = (-a * b + delta) / (1. + a * a) inter.p2.y = a * inter.p2.x + b else: # Find the slope and intercept of the line a = dx / dy b = x1 - a * y1 # Find the determinant of the quadratic equation delta = 1. + a * a - b * b if delta > 0.: # solutions exist delta = sqrt(delta) inter.p1.y = (-a * b - delta) / (1. + a * a) inter.p1.x = a * inter.p1.y + b inter.p2.y = (-a * b + delta) / (1. + a * a) inter.p2.x = a * inter.p2.y + b return inter cdef point circle_segment_single2(double x1, double y1, double x2, double y2) noexcept nogil: """ The intersection of a line with the unit circle. The intersection closest to (x2, y2) is chosen. """ cdef double dx1, dy1, dx2, dy2 cdef intersections inter cdef point pt1, pt2, pt inter = circle_line(x1, y1, x2, y2) pt1 = inter.p1 pt2 = inter.p2 # Can be optimized, but just checking for correctness right now dx1 = fabs(pt1.x - x2) dy1 = fabs(pt1.y - y2) dx2 = fabs(pt2.x - x2) dy2 = fabs(pt2.y - y2) if dx1 > dy1: # compare based on x-axis if dx1 > dx2: pt = pt2 else: pt = pt1 else: if dy1 > dy2: pt = pt2 else: pt = pt1 return pt cdef intersections circle_segment(double x1, double y1, double x2, double y2) noexcept nogil: """ Intersection(s) of a segment with the unit circle. Discard any solution not on the segment. """ cdef intersections inter, inter_new cdef point pt1, pt2 inter = circle_line(x1, y1, x2, y2) pt1 = inter.p1 pt2 = inter.p2 if ((pt1.x > x1 and pt1.x > x2) or (pt1.x < x1 and pt1.x < x2) or (pt1.y > y1 and pt1.y > y2) or (pt1.y < y1 and pt1.y < y2)): pt1.x, pt1.y = 2., 2. if ((pt2.x > x1 and pt2.x > x2) or (pt2.x < x1 and pt2.x < x2) or (pt2.y > y1 and pt2.y > y2) or (pt2.y < y1 and pt2.y < y2)): pt2.x, pt2.y = 2., 2. if pt1.x > 1. and pt2.x < 2.: inter_new.p1 = pt1 inter_new.p2 = pt2 else: inter_new.p1 = pt2 inter_new.p2 = pt1 return inter_new cdef double overlap_area_triangle_unit_circle(double x1, double y1, double x2, double y2, double x3, double y3) noexcept nogil: """ Given a triangle defined by three points (x1, y1), (x2, y2), and (x3, y3), find the area of overlap with the unit circle. """ cdef double d1, d2, d3 cdef bint in1, in2, in3 cdef bint on1, on2, on3 cdef bint intersect13, intersect23 cdef bint cond1, cond2 cdef double area, xp, yp cdef intersections inter cdef point pt1, pt2, pt3, pt4, pt5, pt6 # Find distance of all vertices to circle center d1 = x1 * x1 + y1 * y1 d2 = x2 * x2 + y2 * y2 d3 = x3 * x3 + y3 * y3 # Order vertices by distance from origin if d1 < d2: if d2 < d3: pass elif d1 < d3: x2, y2, d2, x3, y3, d3 = x3, y3, d3, x2, y2, d2 else: (x1, y1, d1, x2, y2, d2, x3, y3, d3) = ( x3, y3, d3, x1, y1, d1, x2, y2, d2) else: if d1 < d3: x1, y1, d1, x2, y2, d2 = x2, y2, d2, x1, y1, d1 elif d2 < d3: (x1, y1, d1, x2, y2, d2, x3, y3, d3) = ( x2, y2, d2, x3, y3, d3, x1, y1, d1) else: (x1, y1, d1, x2, y2, d2, x3, y3, d3) = ( x3, y3, d3, x2, y2, d2, x1, y1, d1) if d1 > d2 or d2 > d3 or d1 > d3: # This should never happen; return NaN to signal an internal # logic error without requiring the GIL to raise an exception. return NAN # Determine number of vertices inside circle in1 = d1 < 1 in2 = d2 < 1 in3 = d3 < 1 # Determine which vertices are on the circle on1 = fabs(d1 - 1) < 1.e-10 on2 = fabs(d2 - 1) < 1.e-10 on3 = fabs(d3 - 1) < 1.e-10 if on3 or in3: # triangle is completely in circle area = area_triangle(x1, y1, x2, y2, x3, y3) elif in2 or on2: # If vertex 1 or 2 are on the edge of the circle, then we # use the dot product to vertex 3 to determine whether an # intersection takes place. intersect13 = not on1 or x1 * (x3 - x1) + y1 * (y3 - y1) < 0. intersect23 = not on2 or x2 * (x3 - x2) + y2 * (y3 - y2) < 0. if intersect13 and intersect23 and not on2: pt1 = circle_segment_single2(x1, y1, x3, y3) pt2 = circle_segment_single2(x2, y2, x3, y3) area = (area_triangle(x1, y1, x2, y2, pt1.x, pt1.y) + area_triangle(x2, y2, pt1.x, pt1.y, pt2.x, pt2.y) + area_arc_unit(pt1.x, pt1.y, pt2.x, pt2.y)) elif intersect13: pt1 = circle_segment_single2(x1, y1, x3, y3) area = (area_triangle(x1, y1, x2, y2, pt1.x, pt1.y) + area_arc_unit(x2, y2, pt1.x, pt1.y)) elif intersect23: pt2 = circle_segment_single2(x2, y2, x3, y3) area = (area_triangle(x1, y1, x2, y2, pt2.x, pt2.y) + area_arc_unit(x1, y1, pt2.x, pt2.y)) else: area = area_arc_unit(x1, y1, x2, y2) elif on1: # The triangle is outside the circle area = 0.0 elif in1: # Check for intersections of far side with circle inter = circle_segment(x2, y2, x3, y3) pt1 = inter.p1 pt2 = inter.p2 pt3 = circle_segment_single2(x1, y1, x2, y2) pt4 = circle_segment_single2(x1, y1, x3, y3) if pt1.x > 1.: # indicates no intersection # Code from `sep.h` cond1 = ((0. - pt3.y) * (pt4.x - pt3.x) > (pt4.y - pt3.y) * (0. - pt3.x)) cond2 = ((y1 - pt3.y) * (pt4.x - pt3.x) > (pt4.y - pt3.y) * (x1 - pt3.x)) if cond1 != cond2: area = (area_triangle(x1, y1, pt3.x, pt3.y, pt4.x, pt4.y) + (M_PI - area_arc_unit(pt3.x, pt3.y, pt4.x, pt4.y))) else: area = (area_triangle(x1, y1, pt3.x, pt3.y, pt4.x, pt4.y) + area_arc_unit(pt3.x, pt3.y, pt4.x, pt4.y)) else: if ((pt2.x - x2)**2 + (pt2.y - y2)**2 < (pt1.x - x2)**2 + (pt1.y - y2)**2): pt1, pt2 = pt2, pt1 area = (area_triangle(x1, y1, pt3.x, pt3.y, pt1.x, pt1.y) + area_triangle(x1, y1, pt1.x, pt1.y, pt2.x, pt2.y) + area_triangle(x1, y1, pt2.x, pt2.y, pt4.x, pt4.y) + area_arc_unit(pt1.x, pt1.y, pt3.x, pt3.y) + area_arc_unit(pt2.x, pt2.y, pt4.x, pt4.y)) else: inter = circle_segment(x1, y1, x2, y2) pt1 = inter.p1 pt2 = inter.p2 inter = circle_segment(x2, y2, x3, y3) pt3 = inter.p1 pt4 = inter.p2 inter = circle_segment(x3, y3, x1, y1) pt5 = inter.p1 pt6 = inter.p2 if pt1.x <= 1.: xp, yp = 0.5 * (pt1.x + pt2.x), 0.5 * (pt1.y + pt2.y) area = (overlap_area_triangle_unit_circle(x1, y1, x3, y3, xp, yp) + overlap_area_triangle_unit_circle(x2, y2, x3, y3, xp, yp)) elif pt3.x <= 1.: xp, yp = 0.5 * (pt3.x + pt4.x), 0.5 * (pt3.y + pt4.y) area = (overlap_area_triangle_unit_circle(x3, y3, x1, y1, xp, yp) + overlap_area_triangle_unit_circle(x2, y2, x1, y1, xp, yp)) elif pt5.x <= 1.: xp, yp = 0.5 * (pt5.x + pt6.x), 0.5 * (pt5.y + pt6.y) area = (overlap_area_triangle_unit_circle(x1, y1, x2, y2, xp, yp) + overlap_area_triangle_unit_circle(x3, y3, x2, y2, xp, yp)) else: # no intersections if in_triangle(0., 0., x1, y1, x2, y2, x3, y3): return M_PI else: return 0. return area astropy-regions-f4139aa/regions/_geometry/ellipse_overlap.pyx000066400000000000000000000205721521703212700246660ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True # cython: freethreading_compatible=True """ Tools to calculate the area of overlap between an ellipse and a pixel grid. The method divides the pixel rectangle into two triangles and reprojects them into a coordinate system where the ellipse becomes the unit circle. It then computes the intersection area between each triangle and the unit circle. The cdef functions are not intended to be called from Python code. They are pure C math functions declared ``noexcept nogil`` so they can be called without the GIL (e.g., from the batch aperture photometry driver), including from multiple threads on free-threaded Python builds. Their signatures are exported via ellipse_overlap.pxd. """ import numpy as np cimport numpy as np from .core cimport overlap_area_triangle_unit_circle __all__ = ['ellipse_overlap_grid'] cdef extern from "math.h" nogil: double sin(double x) double cos(double x) double fmax(double x, double y) DTYPE = np.float64 ctypedef np.float64_t DTYPE_t def ellipse_overlap_grid(double xmin, double xmax, double ymin, double ymax, int nx, int ny, double rx, double ry, double theta, int use_exact, int subpixels): """ ellipse_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, rx, ry, theta, use_exact, subpixels) Calculate the fractional overlap between an ellipse and a pixel grid. The ellipse is centered on the origin. Parameters ---------- xmin, xmax, ymin, ymax : float The extent of the grid in the x and y direction. The grid is defined by the rectangle with corners (xmin, ymin) and (xmax, ymax). nx, ny : int The grid dimensions in the x and y direction. The grid is defined by the rectangle with corners (xmin, ymin) and (xmax, ymax) and is divided into nx and ny pixels in the x and y direction, respectively. rx : float The semimajor axis of the ellipse. ry : float The semiminor axis of the ellipse. theta : float The position angle of the semimajor axis in radians (counterclockwise). use_exact : 0 or 1 Set to ``1`` to use an exact method to calculate the overlap between the ellipse and each pixel. Set to ``0`` to use a sub-pixel sampling method to calculate the overlap, where each pixel is divided into ``subpixels ** 2`` subpixels and the fraction of subpixels that are within the ellipse is used to estimate the overlap. subpixels : int The number of subpixels to use in each dimension when using the sub-pixel sampling method. Each pixel is resampled by this factor in each dimension; thus, each pixel is divided into ``subpixels ** 2`` subpixels. A subpixel is included only if its center lies strictly inside the ellipse; subpixel centers lying exactly on the ellipse boundary are excluded (weight 0). Returns ------- result : `~numpy.ndarray` (float) A 2D array of shape (ny, nx) giving the fraction of each pixel's area that overlaps with the ellipse, ranging from 0 to 1. The element at index (j, i) corresponds to the pixel with corners at (xmin + i * dx, ymin + j * dy) and (xmin + (i + 1) * dx, ymin + (j + 1) * dy), where dx and dy are the width of each pixel in the x and y direction, respectively. """ cdef unsigned int i, j cdef double dx, dy cdef double r cdef double bxmin, bxmax, bymin, bymax cdef double pxmin, pxmax, pymin, pymax cdef double norm # Define output array cdef np.ndarray[DTYPE_t, ndim=2] frac = np.zeros([ny, nx], dtype=DTYPE) cdef double[:, ::1] frac_view = frac # Find the width of each element in x and y dx = (xmax - xmin) / nx dy = (ymax - ymin) / ny norm = 1.0 / (dx * dy) # For now we use a bounding circle and then use that to find a # bounding box but of course this is inefficient and could be done # better. # Find bounding circle radius r = fmax(rx, ry) # Define bounding box bxmin = -r - 0.5 * dx bxmax = +r + 0.5 * dx bymin = -r - 0.5 * dy bymax = +r + 0.5 * dy with nogil: for i in range(nx): pxmin = xmin + i * dx # lower end of pixel pxmax = pxmin + dx # upper end of pixel if pxmax > bxmin and pxmin < bxmax: for j in range(ny): pymin = ymin + j * dy pymax = pymin + dy if pymax > bymin and pymin < bymax: if use_exact: frac_view[j, i] = ( ellipse_overlap_single_exact( pxmin, pymin, pxmax, pymax, rx, ry, theta) * norm) else: frac_view[j, i] = ( ellipse_overlap_single_subpixel( pxmin, pymin, pxmax, pymax, rx, ry, theta, subpixels)) return frac cdef double ellipse_overlap_single_subpixel(double x0, double y0, double x1, double y1, double rx, double ry, double theta, int subpixels) noexcept nogil: """ Return the fraction of overlap between an ellipse and a single pixel with given extent, using a sub-pixel sampling method. """ cdef unsigned int i, j cdef double x, y cdef double frac = 0.0 # Accumulator. cdef double inv_rx_sq, inv_ry_sq cdef double cos_theta = cos(theta) cdef double sin_theta = sin(theta) cdef double dx, dy cdef double x_tr, y_tr dx = (x1 - x0) / subpixels dy = (y1 - y0) / subpixels inv_rx_sq = 1.0 / (rx * rx) inv_ry_sq = 1.0 / (ry * ry) x = x0 - 0.5 * dx for i in range(subpixels): x += dx y = y0 - 0.5 * dy for j in range(subpixels): y += dy # Transform into frame of rotated ellipse x_tr = y * sin_theta + x * cos_theta y_tr = y * cos_theta - x * sin_theta if x_tr * x_tr * inv_rx_sq + y_tr * y_tr * inv_ry_sq < 1.: frac += 1.0 return frac / (subpixels * subpixels) cdef double ellipse_overlap_single_exact(double xmin, double ymin, double xmax, double ymax, double rx, double ry, double theta) noexcept nogil: """ Return the area of overlap between a rectangle and an ellipse. The rectangle is defined by (``xmin``, ``ymin``, ``xmax``, ``ymax``) and the ellipse has major and minor axes ``rx`` and ``ry``, respectively, position angle ``theta``, and is centered at the origin. The method divides the pixel rectangle into two triangles and reprojects them into a coordinate system where the ellipse becomes the unit circle. It then computes the intersection area between each triangle and the unit circle, and sums them to get the total area of overlap. """ cdef double cos_m_theta = cos(-theta) cdef double sin_m_theta = sin(-theta) cdef double scale cdef double x1, y1, x2, y2, x3, y3, x4, y4 # Find scale by which the areas will be shrunk scale = rx * ry # Reproject rectangle to frame of reference in which ellipse is a # unit circle x1, y1 = ((xmin * cos_m_theta - ymin * sin_m_theta) / rx, (xmin * sin_m_theta + ymin * cos_m_theta) / ry) x2, y2 = ((xmax * cos_m_theta - ymin * sin_m_theta) / rx, (xmax * sin_m_theta + ymin * cos_m_theta) / ry) x3, y3 = ((xmax * cos_m_theta - ymax * sin_m_theta) / rx, (xmax * sin_m_theta + ymax * cos_m_theta) / ry) x4, y4 = ((xmin * cos_m_theta - ymax * sin_m_theta) / rx, (xmin * sin_m_theta + ymax * cos_m_theta) / ry) # Divide resulting quadrilateral into two triangles and find # intersection with unit circle return (overlap_area_triangle_unit_circle(x1, y1, x2, y2, x3, y3) + overlap_area_triangle_unit_circle(x1, y1, x4, y4, x3, y3)) * scale astropy-regions-f4139aa/regions/_geometry/polygon_contains.pxd000066400000000000000000000012601521703212700250320ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3 """ Declarations needed to cimport the point-in-polygon functions into other Cython files. All functions are pure C math functions that are safe to call without the GIL. """ cdef int point_in_polygon(double x, double y, const double *vx, const double *vy, int n) noexcept nogil cdef int point_in_polygon_covers(double x, double y, const double *vx, const double *vy, int n) noexcept nogil cdef int point_in_polygon_even_odd(double x, double y, const double *vx, const double *vy, int n) noexcept nogil astropy-regions-f4139aa/regions/_geometry/polygon_contains.pyx000066400000000000000000000211171521703212700250620ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True # cython: freethreading_compatible=True """ Tools for testing whether points are inside polygons. The public API is provided by the Python functions `points_in_polygon` and `points_in_polygon_covers`, which operate on arrays of points and return boolean arrays indicating whether each point is inside the polygon (excluding or including the boundary, respectively). The cdef functions are not intended to be called from Python code. They are pure C math functions declared ``noexcept nogil`` so they can be called without the GIL, including from multiple threads on free-threaded Python builds. Their signatures are exported via polygon_contains.pxd. """ import numpy as np cimport numpy as np __all__ = ['points_in_polygon', 'points_in_polygon_covers'] DTYPE = np.float64 ctypedef np.float64_t DTYPE_t def points_in_polygon(np.ndarray[DTYPE_t, ndim=1] x, np.ndarray[DTYPE_t, ndim=1] y, np.ndarray[DTYPE_t, ndim=1] vx, np.ndarray[DTYPE_t, ndim=1] vy): """ Determine whether points are strictly inside a polygon (excluding boundary). This is consistent with Shapely's ``contains`` function and DE-9IM semantics. Parameters ---------- x, y : ndarray The x and y coordinates of the test points. vx, vy : ndarray The x and y coordinates of the polygon vertices. Returns ------- result : ndarray of bool True if the point is strictly inside the polygon, False otherwise. """ cdef Py_ssize_t i, n cdef int n_poly cdef const double[::1] x_view = np.ascontiguousarray(x) cdef const double[::1] y_view = np.ascontiguousarray(y) cdef const double[::1] vx_view = np.ascontiguousarray(vx) cdef const double[::1] vy_view = np.ascontiguousarray(vy) cdef const double *vx_ptr cdef const double *vy_ptr n = x_view.shape[0] n_poly = vx_view.shape[0] cdef np.ndarray[np.uint8_t, ndim=1] result = np.zeros(n, dtype=np.uint8) cdef np.uint8_t[::1] result_view = result if n_poly > 0: vx_ptr = &vx_view[0] vy_ptr = &vy_view[0] with nogil: for i in range(n): result_view[i] = point_in_polygon(x_view[i], y_view[i], vx_ptr, vy_ptr, n_poly) return result.astype(bool) def points_in_polygon_covers(np.ndarray[DTYPE_t, ndim=1] x, np.ndarray[DTYPE_t, ndim=1] y, np.ndarray[DTYPE_t, ndim=1] vx, np.ndarray[DTYPE_t, ndim=1] vy): """ Determine whether points are inside or on the boundary of a polygon. This is consistent with Shapely's ``covers`` function and DE-9IM semantics. Parameters ---------- x, y : ndarray The x and y coordinates of the test points. vx, vy : ndarray The x and y coordinates of the polygon vertices. Returns ------- result : ndarray of bool True if the point is inside or on the boundary of the polygon, False otherwise. """ cdef Py_ssize_t i, n cdef int n_poly cdef const double[::1] x_view = np.ascontiguousarray(x) cdef const double[::1] y_view = np.ascontiguousarray(y) cdef const double[::1] vx_view = np.ascontiguousarray(vx) cdef const double[::1] vy_view = np.ascontiguousarray(vy) cdef const double *vx_ptr cdef const double *vy_ptr n = x_view.shape[0] n_poly = vx_view.shape[0] cdef np.ndarray[np.uint8_t, ndim=1] result = np.zeros(n, dtype=np.uint8) cdef np.uint8_t[::1] result_view = result if n_poly > 0: vx_ptr = &vx_view[0] vy_ptr = &vy_view[0] with nogil: for i in range(n): result_view[i] = point_in_polygon_covers(x_view[i], y_view[i], vx_ptr, vy_ptr, n_poly) return result.astype(bool) cdef int point_in_polygon(double x, double y, const double *vx, const double *vy, int n) noexcept nogil: """ Determine whether a test point (x, y) is strictly inside a polygon defined by a set of vertices (vx, vy). Points on the boundary (edges and vertices) are NOT considered inside, consistent with Shapely's ``contains`` function and DE-9IM semantics. """ cdef int i, j # The even-odd (ray-casting) rule is reliable for points that are # strictly inside or strictly outside; it is only ambiguous on the # boundary. Points reported as outside are not inside and are not # strictly inside even if they lie on an edge, so no edge scan is # needed for them. Only points reported as inside require an edge # scan to exclude boundary points. if point_in_polygon_even_odd(x, y, vx, vy, n) == 0: return 0 for i in range(n): j = (i + n - 1) % n if point_on_segment(x, y, vx[i], vy[i], vx[j], vy[j]): return 0 return 1 cdef int point_in_polygon_covers(double x, double y, const double *vx, const double *vy, int n) noexcept nogil: """ Determine whether a test point (x, y) is inside or on the boundary of a polygon defined by a set of vertices (vx, vy). Points on the boundary (edges and vertices) ARE considered inside, consistent with Shapely's ``covers`` function and DE-9IM semantics. """ cdef int i, j # The even-odd (ray-casting) rule is reliable for points that are # strictly inside or strictly outside; it is only ambiguous on the # boundary. Points reported as inside are covered regardless of the # boundary, so no edge scan is needed for them. Only points reported # as outside require an edge scan to include boundary points. if point_in_polygon_even_odd(x, y, vx, vy, n) == 1: return 1 for i in range(n): j = (i + n - 1) % n if point_on_segment(x, y, vx[i], vy[i], vx[j], vy[j]): return 1 return 0 cdef int point_in_polygon_even_odd(double x, double y, const double *vx, const double *vy, int n) noexcept nogil: """ Determine whether the point (x, y) is inside a polygon using the even-odd (ray-casting) rule, ignoring the polygon boundary. Boundary behavior is undefined here; callers that need well-defined boundary semantics should use `point_in_polygon` (boundary excluded) or `point_in_polygon_covers` (boundary included). This uses the even-odd rule, as described here: https://en.wikipedia.org/wiki/Even-odd_rule """ cdef int i, j cdef int result = 0 for i in range(n): j = (i + n - 1) % n if (((vy[i] > y) != (vy[j] > y)) and (x < (vx[j] - vx[i]) * (y - vy[i]) / (vy[j] - vy[i]) + vx[i])): result += 1 return result % 2 cdef inline int point_on_segment(double px, double py, double x1, double y1, double x2, double y2) noexcept nogil: """ Check whether the point (px, py) lies on the line segment from (x1, y1) to (x2, y2). The collinearity tolerance is expressed as a perpendicular distance from the point to the segment so that the test behaves consistently regardless of the segment length or the magnitude of the coordinates. The tolerance is an absolute distance in pixel units (1e-10 px), not a relative tolerance, so it is most appropriate for the pixel-scale coordinates used by the region classes. """ cdef double x_min, x_max, y_min, y_max, cross, seg_len_sq cdef double tol = 1e-10 # perpendicular distance tolerance (pixels) # Check bounding box if x1 < x2: x_min = x1 x_max = x2 else: x_min = x2 x_max = x1 if y1 < y2: y_min = y1 y_max = y2 else: y_min = y2 y_max = y1 if px < x_min or px > x_max or py < y_min or py > y_max: return 0 seg_len_sq = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) if seg_len_sq == 0: # Degenerate segment (duplicate vertices); treat it as a point. return 1 if (px == x1 and py == y1) else 0 # The cross product magnitude equals perpendicular_distance times # segment_length, so comparing its square to tol**2 * seg_len_sq # tests the perpendicular distance against tol independently of the # segment length. cross = (px - x1) * (y2 - y1) - (py - y1) * (x2 - x1) if cross * cross > tol * tol * seg_len_sq: return 0 return 1 astropy-regions-f4139aa/regions/_geometry/polygon_overlap.pxd000066400000000000000000000022551521703212700246710ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3 """ Declarations needed to cimport the polygon overlap functions into other Cython files. These single-pixel functions are pure C math functions that are safe to call without the GIL. """ cdef double polygon_pixel_overlap(double pxmin, double pymin, double pxmax, double pymax, double *poly_x, double *poly_y, int n_poly, double *buf_a_x, double *buf_a_y, double *buf_b_x, double *buf_b_y, int buf_size) noexcept nogil cdef double polygon_overlap_single_subpixel(double x0, double y0, double x1, double y1, double *poly_x, double *poly_y, int n_poly, int subpixels, double *xint_buf, double *hxmin_buf, double *hxmax_buf) noexcept nogil astropy-regions-f4139aa/regions/_geometry/polygon_overlap.pyx000066400000000000000000000544621521703212700247250ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True # cython: freethreading_compatible=True """ Tools to calculate the area of overlap between a simple polygon and a pixel grid. The functions here are written so that they can be reused by any aperture whose footprint is a simple polygon (e.g., rotated rectangles, regular polygons, or arbitrary user-supplied polygons). Convexity is not required: the subject is the aperture polygon and the clip polygon is the (always-convex, axis-aligned) pixel rectangle. The cdef functions are not intended to be called from Python code. They are pure C math functions declared ``noexcept nogil`` so they can be called without the GIL (e.g., from the batch aperture photometry driver), including from multiple threads on free-threaded Python builds. Their signatures are exported via polygon_overlap.pxd. """ import numpy as np cimport numpy as np __all__ = ['polygon_overlap_grid'] cdef extern from "math.h" nogil: double fabs(double x) double fmax(double x, double y) double fmin(double x, double y) double floor(double x) double ceil(double x) DTYPE = np.float64 ctypedef np.float64_t DTYPE_t def polygon_overlap_grid(double xmin, double xmax, double ymin, double ymax, int nx, int ny, np.ndarray[DTYPE_t, ndim=1] vertices_x, np.ndarray[DTYPE_t, ndim=1] vertices_y, int use_exact, int subpixels): """ polygon_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, vertices_x, vertices_y, use_exact, subpixels) Calculate the fractional overlap between a simple polygon and a pixel grid. The polygon vertices must define a simple polygon (no self-intersections). The polygon may be convex or non-convex. The vertex order may be either clockwise or counter-clockwise; clockwise input is reversed internally. Parameters ---------- xmin, xmax, ymin, ymax : float The extent of the grid in the x and y direction. The grid is defined by the rectangle with corners (xmin, ymin) and (xmax, ymax). nx, ny : int The grid dimensions in the x and y direction. The grid is defined by the rectangle with corners (xmin, ymin) and (xmax, ymax) and is divided into nx and ny pixels in the x and y direction, respectively. vertices_x, vertices_y : 1D `~numpy.ndarray` The x and y coordinates of the polygon vertices (in the same coordinate frame as the grid extents). The polygon must have at least 3 vertices. use_exact : 0 or 1 Set to ``1`` to use an exact method to calculate the overlap between the polygon and each pixel. Set to ``0`` to use a sub-pixel sampling method to calculate the overlap, where each pixel is divided into ``subpixels ** 2`` subpixels and the fraction of subpixels that are within the polygon is used to estimate the overlap. subpixels : int The number of subpixels to use in each dimension when using the sub-pixel sampling method. Each pixel is resampled by this factor in each dimension; thus, each pixel is divided into ``subpixels ** 2`` subpixels. With ``subpixels == 1`` (the ``center`` method) a pixel is included only if its center falls strictly inside the polygon. Pixel centers lying exactly on a polygon edge are counted as outside, consistent with the circular, elliptical, and rectangular apertures (see ``point_in_polygon``). For ``subpixels > 1`` the same convention applies to each subpixel: a subpixel is included only if its center lies strictly inside the polygon; subpixel centers lying exactly on a polygon edge are excluded (weight 0). Returns ------- result : `~numpy.ndarray` (float) A 2D array of shape (ny, nx) giving the fraction of each pixel's area that overlaps with the polygon, ranging from 0 to 1. The element at index (j, i) corresponds to the pixel with corners at (xmin + i * dx, ymin + j * dy) and (xmin + (i + 1) * dx, ymin + (j + 1) * dy), where dx and dy are the width of each pixel in the x and y direction, respectively. """ cdef int n_poly = vertices_x.shape[0] if vertices_y.shape[0] != n_poly: msg = 'vertices_x and vertices_y must have the same length' raise ValueError(msg) if n_poly < 3: msg = 'polygon must have at least 3 vertices' raise ValueError(msg) if use_exact != 1 and subpixels < 1: msg = 'subpixels must be a strictly positive integer' raise ValueError(msg) # Working buffers, allocated once per call (not per pixel) as a # single block. Each Sutherland-Hodgman clip can at most double the # vertex count of a non-convex polygon, so after the four half-plane # clips the vertex count is bounded by 16 * n_poly. cdef int buf_size = 16 * n_poly cdef double[::1] work = np.empty(2 * n_poly + 4 * buf_size, dtype=DTYPE) cdef double *poly_x = &work[0] cdef double *poly_y = &work[n_poly] cdef double *buf_a_x = &work[2 * n_poly] cdef double *buf_a_y = &work[2 * n_poly + buf_size] cdef double *buf_b_x = &work[2 * n_poly + 2 * buf_size] cdef double *buf_b_y = &work[2 * n_poly + 3 * buf_size] cdef const double[::1] vx_view = np.ascontiguousarray(vertices_x) cdef const double[::1] vy_view = np.ascontiguousarray(vertices_y) _ensure_ccw(vx_view, vy_view, n_poly, poly_x, poly_y) cdef np.ndarray[DTYPE_t, ndim=2] frac = np.zeros([ny, nx], dtype=DTYPE) cdef double[:, ::1] frac_view = frac cdef double dx = (xmax - xmin) / nx cdef double dy = (ymax - ymin) / ny cdef double pxmin, pxmax, pymin, pymax cdef double bbox_xmin, bbox_xmax, bbox_ymin, bbox_ymax cdef double pixel_area = dx * dy cdef int i, j, k cdef int i_min, i_max, j_min, j_max # Polygon bounding box. bbox_xmin = poly_x[0] bbox_xmax = poly_x[0] bbox_ymin = poly_y[0] bbox_ymax = poly_y[0] for k in range(1, n_poly): bbox_xmin = fmin(bbox_xmin, poly_x[k]) bbox_xmax = fmax(bbox_xmax, poly_x[k]) bbox_ymin = fmin(bbox_ymin, poly_y[k]) bbox_ymax = fmax(bbox_ymax, poly_y[k]) # Restrict the pixel loops to the bounding-box index range (pixels # outside the bounding box have zero overlap). The clamping to # [0, nx] and [0, ny] is done in floating point to avoid integer # overflow for polygons far outside the grid. i_min = fmax(0.0, fmin(nx, floor((bbox_xmin - xmin) / dx))) i_max = fmax(0.0, fmin(nx, ceil((bbox_xmax - xmin) / dx))) j_min = fmax(0.0, fmin(ny, floor((bbox_ymin - ymin) / dy))) j_max = fmax(0.0, fmin(ny, ceil((bbox_ymax - ymin) / dy))) if use_exact == 1: with nogil: for i in range(i_min, i_max): pxmin = xmin + i * dx pxmax = pxmin + dx for j in range(j_min, j_max): pymin = ymin + j * dy pymax = pymin + dy frac_view[j, i] = ( polygon_pixel_overlap(pxmin, pymin, pxmax, pymax, poly_x, poly_y, n_poly, buf_a_x, buf_a_y, buf_b_x, buf_b_y, buf_size) / pixel_area) else: with nogil: for i in range(i_min, i_max): pxmin = xmin + i * dx for j in range(j_min, j_max): pymin = ymin + j * dy frac_view[j, i] = polygon_overlap_single_subpixel( pxmin, pymin, pxmin + dx, pymin + dy, poly_x, poly_y, n_poly, subpixels, buf_a_x, buf_a_y, buf_b_x) return frac cdef double polygon_pixel_overlap(double pxmin, double pymin, double pxmax, double pymax, double *poly_x, double *poly_y, int n_poly, double *buf_a_x, double *buf_a_y, double *buf_b_x, double *buf_b_y, int buf_size) noexcept nogil: """ Exact area of overlap between the axis-aligned pixel rectangle [pxmin, pxmax] x [pymin, pymax] and a simple polygon supplied as counter-clockwise vertices. The polygon (subject) is clipped against the four edges of the pixel rectangle (clip) using the Sutherland-Hodgman algorithm, followed by the shoelace area formula. Because the clip polygon is convex, the subject polygon may be either convex or non-convex (it must, however, be simple, i.e., non-self-intersecting). The four ``buf_*`` parameters are caller-allocated working buffers; each must hold at least ``buf_size`` doubles. The caller is responsible for ensuring ``buf_size`` is large enough; each clip can roughly double the vertex count of a non-convex polygon, so 16 times the maximum input polygon size is a safe choice. For convex input polygons, each clip adds at most one vertex, so input size plus four suffices. """ cdef double *cur_x = buf_a_x cdef double *cur_y = buf_a_y cdef double *nxt_x = buf_b_x cdef double *nxt_y = buf_b_y cdef double *tmp cdef int n if n_poly < 3 or n_poly > buf_size: return 0.0 # Clip against x >= pxmin, x <= pxmax, y >= pymin, y <= pymax. # The first clip reads directly from the input polygon, avoiding a # per-pixel copy of the input vertices. n = _clip_against_axis(poly_x, poly_y, n_poly, 0, pxmin, 1, cur_x, cur_y) if n == 0: return 0.0 n = _clip_against_axis(cur_x, cur_y, n, 0, pxmax, 0, nxt_x, nxt_y) if n == 0: return 0.0 tmp = cur_x cur_x = nxt_x nxt_x = tmp tmp = cur_y cur_y = nxt_y nxt_y = tmp n = _clip_against_axis(cur_x, cur_y, n, 1, pymin, 1, nxt_x, nxt_y) if n == 0: return 0.0 tmp = cur_x cur_x = nxt_x nxt_x = tmp tmp = cur_y cur_y = nxt_y nxt_y = tmp n = _clip_against_axis(cur_x, cur_y, n, 1, pymax, 0, nxt_x, nxt_y) if n == 0: return 0.0 return fabs(_polygon_signed_area(nxt_x, nxt_y, n)) cdef int point_in_polygon(double x, double y, double *poly_x, double *poly_y, int n_poly) noexcept nogil: """ Return 1 if ``(x, y)`` lies inside the simple polygon ``(poly_x, poly_y)``, 0 otherwise. Uses the standard ray-casting algorithm so the polygon may be convex or non-convex. Boundary convention ------------------- Points lying exactly on a polygon edge are classified as *outside* (return 0). This matches the ``center`` method of the circular, elliptical, and rectangular apertures, which use a strict inequality (``< r**2``, ``< 1``, and ``< half_width/half_height``, respectively) and therefore also exclude every on-boundary point. In particular, because a rectangle is itself a polygon, a `~photutils.aperture.RectangularAperture` and a `~photutils.aperture.PolygonAperture` with the same four corners produce identical ``center`` masks. The on-edge test is performed first: if ``(x, y)`` lies on any edge (within the closed bounding box of that edge and collinear with it), the point is reported as outside. Otherwise the standard ray-casting parity test is applied for strictly interior/exterior points. This boundary handling matters mainly for the ``center`` method (``subpixels == 1``), where pixel centers commonly fall exactly on an edge (e.g., for integer-coordinate polygons). For subpixel sampling (``subpixels > 1``) on-edge samples are rare. """ cdef int i, j, inside = 0 cdef double xi, yi, xj, yj cdef double cross if n_poly < 3: return 0 j = n_poly - 1 for i in range(n_poly): xi = poly_x[i] yi = poly_y[i] xj = poly_x[j] yj = poly_y[j] # Reject points exactly on this edge (collinear and within the # edge's closed bounding box), so that all boundary points are # classified as outside, consistent with the other apertures. cross = (xj - xi) * (y - yi) - (yj - yi) * (x - xi) if (cross == 0.0 and fmin(xi, xj) <= x <= fmax(xi, xj) and fmin(yi, yj) <= y <= fmax(yi, yj)): return 0 if ((yi > y) != (yj > y)): # x of intersection of polygon edge with horizontal line at y if x < (xj - xi) * (y - yi) / (yj - yi) + xi: inside = 1 - inside j = i return inside cdef double polygon_overlap_single_subpixel(double x0, double y0, double x1, double y1, double *poly_x, double *poly_y, int n_poly, int subpixels, double *xint_buf, double *hxmin_buf, double *hxmax_buf) noexcept nogil: """ Return the fraction of overlap between a simple polygon and a single pixel with the given extent, using a sub-pixel sampling method. The pixel ``[x0, x1] x [y0, y1]`` is divided into ``subpixels ** 2`` subpixels and the fraction of subpixel centers that fall inside the polygon is returned. The polygon may be convex or non-convex. For ``subpixels == 1`` (the ``center`` method) the single pixel-center sample is classified with the full `point_in_polygon` test, so that samples lying exactly on a polygon edge are excluded, consistent with the circular, elliptical, and rectangular apertures. For ``subpixels > 1`` the samples are classified with a scanline parity fill: for each subpixel row, the polygon boundary restricted to the horizontal line at ``y`` is fully characterized once (``O(n_poly)``) as a set of crossing points (from sloped and vertical edges) plus a set of closed x-intervals (from any horizontal edges lying exactly on that line). The crossings are sorted and the row's samples are then classified by a single monotonic sweep instead of re-testing every edge for every sample. This reduces the per-pixel cost from ``O(subpixels ** 2 * n_poly)`` to roughly ``O(subpixels * n_poly + subpixels ** 2)`` while matching the classification of `point_in_polygon`: interior samples are classified by the identical ``x < xint`` parity rule, and every on-boundary sample is excluded (classified as outside). A sample is on the boundary when its x coincides exactly with a crossing (a sloped or vertical edge, including vertices, whose crossing x is computed exactly because the ``y - yi`` term vanishes) or when it lies within the closed x-span of a horizontal edge at that ``y``. This makes the on-boundary exclusion bit-exact for the axis-aligned and integer-coordinate polygons where on-boundary samples actually occur. For samples on the interior of a sloped edge (a rare event for subpixel sampling) the recomputed crossing may differ from the sample x by rounding and the exclusion is not guaranteed. ``xint_buf``, ``hxmin_buf``, and ``hxmax_buf`` are caller-supplied scratch buffers, each of which must hold at least ``n_poly`` doubles. ``xint_buf`` stores the per-row edge crossings, while ``hxmin_buf`` and ``hxmax_buf`` store the endpoints of the per-row horizontal-edge intervals. They are written only within this call, so the function remains thread safe. This is a pure C math function declared ``noexcept nogil`` so it can be called without the GIL (e.g., from the batch aperture photometry driver), including from multiple threads on free-threaded Python builds. """ cdef int i, j, e, prev, m, a, b, ptr, h, hh cdef double x, y, dx, dy, hits cdef double xi, yi, xj, yj, tmp cdef bint inside dx = (x1 - x0) / subpixels dy = (y1 - y0) / subpixels hits = 0.0 if subpixels == 1: # Center method: a single sample at the pixel center. Use # the full point-in-polygon test so that on-edge samples are # classified consistently with the other apertures (excluded). if point_in_polygon(x0 + 0.5 * dx, y0 + 0.5 * dy, poly_x, poly_y, n_poly) == 1: hits = 1.0 return hits # Subpixel sampling (subpixels > 1): scanline parity fill. y = y0 + 0.5 * dy for i in range(subpixels): # Characterize the polygon boundary on the horizontal line # at ``y``. Sloped and vertical edges that straddle the line # each contribute one crossing point (``xint_buf``), using # the same edge-straddle test and intersection arithmetic # as the parity step of ``point_in_polygon``. Horizontal # edges lying exactly on the line cannot straddle it, # so they are recorded separately as closed x-intervals # (``hxmin_buf``/``hxmax_buf``). Together these two sets fully # describe the boundary on the line and let every on-boundary # sample be excluded. m = 0 h = 0 prev = n_poly - 1 for e in range(n_poly): yi = poly_y[e] yj = poly_y[prev] if yi == y and yj == y: xi = poly_x[e] xj = poly_x[prev] hxmin_buf[h] = fmin(xi, xj) hxmax_buf[h] = fmax(xi, xj) h += 1 elif (yi > y) != (yj > y): xi = poly_x[e] xj = poly_x[prev] xint_buf[m] = (xj - xi) * (y - yi) / (yj - yi) + xi m += 1 prev = e # Insertion sort the (typically few) crossings in ascending x. for a in range(1, m): tmp = xint_buf[a] b = a - 1 while b >= 0 and xint_buf[b] > tmp: xint_buf[b + 1] = xint_buf[b] b -= 1 xint_buf[b + 1] = tmp # Sweep the row's samples (monotonically increasing in x). A # sample is inside when an odd number of crossings lie strictly # to its right (the ``x < xint`` parity rule), unless it lies # on the boundary. On-boundary samples are excluded (classified # as outside), consistent with ``point_in_polygon``: a sample # whose x coincides exactly with a crossing lies on a sloped or # vertical edge, and a sample within a horizontal edge's closed # x-span lies on that edge. x = x0 + 0.5 * dx ptr = 0 for j in range(subpixels): while ptr < m and xint_buf[ptr] < x: ptr += 1 inside = (((m - ptr) & 1) == 1 and not (ptr < m and xint_buf[ptr] == x)) if inside: for hh in range(h): if hxmin_buf[hh] <= x <= hxmax_buf[hh]: inside = False break if inside: hits += 1.0 x += dx y += dy return hits / (subpixels * subpixels) cdef inline double _polygon_signed_area(const double *xs, const double *ys, int n) noexcept nogil: """ Signed area of a polygon via the shoelace formula. Positive for counter-clockwise vertices, negative for clockwise vertices. For a Sutherland-Hodgman-clipped polygon the magnitude is the area of the intersection even when the subject was non-convex (any spurious "bridge" edges traverse zero area in pairs). """ cdef double area = 0.0 cdef int i, j if n < 3: return 0.0 for i in range(n): j = i + 1 if j == n: j = 0 area += xs[i] * ys[j] - xs[j] * ys[i] return 0.5 * area cdef inline int _clip_against_axis(double *xs_in, double *ys_in, int n_in, int axis, double bound, int keep_greater, double *xs_out, double *ys_out) noexcept nogil: """ Sutherland-Hodgman clip of a polygon (xs_in, ys_in) against an axis-aligned half-plane. Parameters ---------- axis : int 0 for an x-aligned bound, 1 for a y-aligned bound. bound : double The coordinate value of the clipping line. keep_greater : int 1 to keep points with coordinate >= bound, 0 to keep points with coordinate <= bound. """ cdef int i, n_out = 0 cdef double sx, sy, px, py, sc, pc, t cdef double sign if n_in == 0: return 0 if keep_greater == 1: sign = 1.0 else: sign = -1.0 sx = xs_in[n_in - 1] sy = ys_in[n_in - 1] if axis == 0: sc = sign * (sx - bound) else: sc = sign * (sy - bound) for i in range(n_in): px = xs_in[i] py = ys_in[i] if axis == 0: pc = sign * (px - bound) else: pc = sign * (py - bound) if pc >= 0.0: if sc < 0.0: # entering: emit intersection, then point t = sc / (sc - pc) xs_out[n_out] = sx + t * (px - sx) ys_out[n_out] = sy + t * (py - sy) n_out += 1 xs_out[n_out] = px ys_out[n_out] = py n_out += 1 elif sc >= 0.0: # leaving: emit intersection only t = sc / (sc - pc) xs_out[n_out] = sx + t * (px - sx) ys_out[n_out] = sy + t * (py - sy) n_out += 1 sx = px sy = py sc = pc return n_out cdef void _ensure_ccw(const double[::1] xs, const double[::1] ys, int n, double *out_x, double *out_y) noexcept nogil: """ Copy ``(xs, ys)`` into ``(out_x, out_y)`` ensuring counter-clockwise order via the sign of the signed area. """ cdef double area = _polygon_signed_area(&xs[0], &ys[0], n) cdef int i, k if area >= 0.0: for i in range(n): out_x[i] = xs[i] out_y[i] = ys[i] else: for i in range(n): k = n - 1 - i out_x[i] = xs[k] out_y[i] = ys[k] astropy-regions-f4139aa/regions/_geometry/rectangle_overlap.pyx000066400000000000000000000203671521703212700251770ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True # cython: freethreading_compatible=True """ Tools to calculate the area of overlap between a rectangle and a pixel grid. The cdef function is not intended to be called from Python code. It is a pure C math function declared ``noexcept nogil`` so it can be called without the GIL (e.g., from the batch aperture photometry driver), including from multiple threads on free-threaded Python builds. Its signature is exported via rectangle_overlap.pxd. """ import numpy as np cimport numpy as np from .polygon_overlap cimport polygon_pixel_overlap __all__ = ['rectangle_overlap_grid'] cdef extern from "math.h" nogil: double sin(double x) double cos(double x) double fabs(double x) double fmax(double x, double y) double fmin(double x, double y) double floor(double x) double ceil(double x) DTYPE = np.float64 ctypedef np.float64_t DTYPE_t def rectangle_overlap_grid(double xmin, double xmax, double ymin, double ymax, int nx, int ny, double width, double height, double theta, int use_exact, int subpixels): """ rectangle_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, width, height, theta, use_exact, subpixels) Calculate the fractional overlap between a rectangle and a pixel grid. The rectangle is centered on the origin. Parameters ---------- xmin, xmax, ymin, ymax : float The extent of the grid in the x and y direction. The grid is defined by the rectangle with corners (xmin, ymin) and (xmax, ymax). nx, ny : int The grid dimensions in the x and y direction. The grid is defined by the rectangle with corners (xmin, ymin) and (xmax, ymax) and is divided into nx and ny pixels in the x and y direction, respectively. width : float The width of the rectangle. height : float The height of the rectangle. theta : float The position angle of the rectangle in radians (counterclockwise). use_exact : 0 or 1 Set to ``1`` to use an exact method to calculate the overlap between the rectangle and each pixel. Set to ``0`` to use a sub-pixel sampling method to calculate the overlap, where each pixel is divided into ``subpixels ** 2`` subpixels and the fraction of subpixels that are within the rectangle is used to estimate the overlap. subpixels : int The number of subpixels to use in each dimension when using the sub-pixel sampling method. Each pixel is resampled by this factor in each dimension; thus, each pixel is divided into ``subpixels ** 2`` subpixels. A subpixel is included only if its center lies strictly inside the rectangle; subpixel centers lying exactly on the rectangle boundary are excluded (weight 0). Returns ------- result : `~numpy.ndarray` (float) A 2D array of shape (ny, nx) giving the fraction of each pixel's area that overlaps with the rectangle, ranging from 0 to 1. The element at index (j, i) corresponds to the pixel with corners at (xmin + i * dx, ymin + j * dy) and (xmin + (i + 1) * dx, ymin + (j + 1) * dy), where dx and dy are the width of each pixel in the x and y direction, respectively. """ cdef unsigned int i, j cdef int i_min, i_max, j_min, j_max cdef double pxmin, pxmax, pymin, pymax cdef double dx, dy cdef double half_width = 0.5 * width cdef double half_height = 0.5 * height cdef double cos_theta = cos(theta) cdef double sin_theta = sin(theta) cdef double pixel_area cdef double bbox_dx, bbox_dy cdef double poly_x[4] cdef double poly_y[4] cdef double buf_a_x[32] cdef double buf_a_y[32] cdef double buf_b_x[32] cdef double buf_b_y[32] cdef np.ndarray[DTYPE_t, ndim=2] frac = np.zeros([ny, nx], dtype=DTYPE) cdef double[:, ::1] frac_view = frac dx = (xmax - xmin) / nx dy = (ymax - ymin) / ny if use_exact == 1: # Build the four CCW vertices of the rotated rectangle (centered # on the origin). Local frame vertices in CCW order are # (-w/2, -h/2), ( w/2, -h/2), ( w/2, h/2), (-w/2, h/2). # Rotation by theta: x' = x cos t - y sin t, # y' = x sin t + y cos t. poly_x[0] = -half_width * cos_theta - (-half_height) * sin_theta poly_y[0] = -half_width * sin_theta + (-half_height) * cos_theta poly_x[1] = half_width * cos_theta - (-half_height) * sin_theta poly_y[1] = half_width * sin_theta + (-half_height) * cos_theta poly_x[2] = half_width * cos_theta - half_height * sin_theta poly_y[2] = half_width * sin_theta + half_height * cos_theta poly_x[3] = -half_width * cos_theta - half_height * sin_theta poly_y[3] = -half_width * sin_theta + half_height * cos_theta # Axis-aligned bounding box of the rotated rectangle, used to # restrict the pixel loops to the bounding-box index range # (pixels outside it have zero overlap). The clamping to # [0, nx] and [0, ny] is done in floating point to avoid # integer overflow for rectangles far outside the grid. bbox_dx = (half_width * fabs(cos_theta) + half_height * fabs(sin_theta)) bbox_dy = (half_width * fabs(sin_theta) + half_height * fabs(cos_theta)) pixel_area = dx * dy i_min = fmax(0.0, fmin(nx, floor((-bbox_dx - xmin) / dx))) i_max = fmax(0.0, fmin(nx, ceil((bbox_dx - xmin) / dx))) j_min = fmax(0.0, fmin(ny, floor((-bbox_dy - ymin) / dy))) j_max = fmax(0.0, fmin(ny, ceil((bbox_dy - ymin) / dy))) with nogil: for i in range(i_min, i_max): pxmin = xmin + i * dx pxmax = pxmin + dx for j in range(j_min, j_max): pymin = ymin + j * dy pymax = pymin + dy frac_view[j, i] = ( polygon_pixel_overlap(pxmin, pymin, pxmax, pymax, poly_x, poly_y, 4, buf_a_x, buf_a_y, buf_b_x, buf_b_y, 32) / pixel_area) return frac # Subpixel-sampling fallback with nogil: for i in range(nx): pxmin = xmin + i * dx pxmax = pxmin + dx for j in range(ny): pymin = ymin + j * dy pymax = pymin + dy frac_view[j, i] = rectangle_overlap_single_subpixel( pxmin, pymin, pxmax, pymax, half_width, half_height, cos_theta, sin_theta, subpixels) return frac cdef double rectangle_overlap_single_subpixel(double x0, double y0, double x1, double y1, double half_width, double half_height, double cos_theta, double sin_theta, int subpixels) noexcept nogil: """ Return the fraction of overlap between a rectangle and a single pixel with given extent, using a sub-pixel sampling method. """ cdef unsigned int i, j cdef double x, y, x_tr, y_tr cdef double frac = 0.0 cdef double dx = (x1 - x0) / subpixels cdef double dy = (y1 - y0) / subpixels x = x0 - 0.5 * dx for i in range(subpixels): x += dx y = y0 - 0.5 * dy for j in range(subpixels): y += dy x_tr = y * sin_theta + x * cos_theta y_tr = y * cos_theta - x * sin_theta if fabs(x_tr) < half_width and fabs(y_tr) < half_height: frac += 1.0 return frac / (subpixels * subpixels) astropy-regions-f4139aa/regions/_geometry/tests/000077500000000000000000000000001521703212700220735ustar00rootroot00000000000000astropy-regions-f4139aa/regions/_geometry/tests/__init__.py000066400000000000000000000000001521703212700241720ustar00rootroot00000000000000astropy-regions-f4139aa/regions/_geometry/tests/test_circle_overlap.py000066400000000000000000000025141521703212700264770ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the circle_overlap_grid module. """ import pytest from numpy.testing import assert_allclose from regions._geometry import circle_overlap_grid grid_sizes = [50, 500, 1000] circ_sizes = [0.2, 0.4, 0.8] use_exacts = [0, 1] subsamples = [1, 5, 10] @pytest.mark.parametrize('grid_size', grid_sizes) @pytest.mark.parametrize('circ_size', circ_sizes) @pytest.mark.parametrize('use_exact', use_exacts) @pytest.mark.parametrize('subsample', subsamples) def test_circle_overlap_grid(grid_size, circ_size, use_exact, subsample): """ Test normalization of the overlap grid to make sure that a fully enclosed pixel has a value of 1.0. """ g = circle_overlap_grid(-1.0, 1.0, -1.0, 1.0, grid_size, grid_size, circ_size, use_exact, subsample) assert_allclose(g.max(), 1.0) @pytest.mark.parametrize('use_exact', use_exacts) def test_circle_overlap_grid_no_readonly_inputs(use_exact): """ Test that the scalar-input overlap grid function takes no array inputs (so read-only input arrays cannot occur) and returns a freshly allocated, writeable output array of shape (ny, nx). """ g = circle_overlap_grid(-5.0, 5.0, -5.0, 5.0, 10, 12, 3.0, use_exact, 5) assert g.shape == (12, 10) assert g.flags.writeable astropy-regions-f4139aa/regions/_geometry/tests/test_ellipse_overlap.py000066400000000000000000000031041521703212700266670ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the ellipse_overlap_grid module. """ import pytest from numpy.testing import assert_allclose from regions._geometry import ellipse_overlap_grid grid_sizes = [50, 500, 1000] maj_sizes = [0.2, 0.4, 0.8] min_sizes = [0.2, 0.4, 0.8] angles = [0.0, 0.5, 1.0] use_exacts = [0, 1] subsamples = [1, 5, 10] @pytest.mark.parametrize('grid_size', grid_sizes) @pytest.mark.parametrize('maj_size', maj_sizes) @pytest.mark.parametrize('min_size', min_sizes) @pytest.mark.parametrize('angle', angles) @pytest.mark.parametrize('use_exact', use_exacts) @pytest.mark.parametrize('subsample', subsamples) def test_ellipse_overlap_grid(grid_size, maj_size, min_size, angle, use_exact, subsample): """ Test normalization of the overlap grid to make sure that a fully enclosed pixel has a value of 1.0. """ g = ellipse_overlap_grid(-1.0, 1.0, -1.0, 1.0, grid_size, grid_size, maj_size, min_size, angle, use_exact, subsample) assert_allclose(g.max(), 1.0) @pytest.mark.parametrize('use_exact', use_exacts) def test_ellipse_overlap_grid_no_readonly_inputs(use_exact): """ Test that the scalar-input overlap grid function takes no array inputs (so read-only input arrays cannot occur) and returns a freshly allocated, writeable output array of shape (ny, nx). """ g = ellipse_overlap_grid(-5.0, 5.0, -5.0, 5.0, 10, 12, 4.0, 2.0, 0.5, use_exact, 5) assert g.shape == (12, 10) assert g.flags.writeable astropy-regions-f4139aa/regions/_geometry/tests/test_polygon_overlap.py000066400000000000000000000245151521703212700267320ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the polygon_overlap_grid module. """ import numpy as np import pytest from numpy.testing import assert_allclose from regions._geometry.polygon_overlap import polygon_overlap_grid def test_polygon_overlap_clockwise_input(): """ Clockwise-ordered vertices should give the same result as counter-clockwise ones. """ vx_ccw = np.array([-1.0, 1.0, 1.0, -1.0]) vy_ccw = np.array([-1.0, -1.0, 1.0, 1.0]) vx_cw = vx_ccw[::-1].copy() vy_cw = vy_ccw[::-1].copy() grid_ccw = polygon_overlap_grid(-2.5, 2.5, -2.5, 2.5, 5, 5, vx_ccw, vy_ccw, 1, 1) grid_cw = polygon_overlap_grid(-2.5, 2.5, -2.5, 2.5, 5, 5, vx_cw, vy_cw, 1, 1) assert_allclose(grid_cw, grid_ccw) @pytest.mark.parametrize('use_exact', [1, 0]) def test_polygon_overlap_readonly_vertices(use_exact): """ Regression test that read-only (non-writeable) vertex arrays are accepted and give results identical to writeable arrays. The vertex arrays are buffered into ``const`` typed memoryviews so that read-only arrays do not raise a ``ValueError``. """ vx = np.array([-4.0, 6.0, 3.0, -5.0]) vy = np.array([-3.0, -2.0, 5.0, 4.0]) expected = polygon_overlap_grid(-10.0, 10.0, -10.0, 10.0, 40, 40, vx, vy, use_exact, 5) vx_ro = vx.copy() vy_ro = vy.copy() vx_ro.setflags(write=False) vy_ro.setflags(write=False) result = polygon_overlap_grid(-10.0, 10.0, -10.0, 10.0, 40, 40, vx_ro, vy_ro, use_exact, 5) assert_allclose(result, expected, rtol=1e-12) @pytest.mark.parametrize(('leg', 'dx', 'dy'), [ (3.0, 0.0, 0.0), # base case (1.0, 0.0, 0.0), # small triangle (6.0, 0.0, 0.0), # large triangle (3.0, 0.5, 0.0), # shifted half-pixel in x (3.0, 0.0, 0.5), # shifted half-pixel in y (3.0, 0.25, 0.25), # shifted diagonally (2.0, 1.0, -1.0), # different size with offset ]) def test_polygon_overlap_triangle_area(leg, dx, dy): """ The total overlap of a right triangle with given leg length equals its analytic area, for various sizes and pixel offsets. """ vx = np.array([0.0, leg, 0.0]) + dx vy = np.array([0.0, 0.0, leg]) + dy margin = 0.5 xmin = vx.min() - margin xmax = vx.max() + margin ymin = vy.min() - margin ymax = vy.max() + margin nx = max(4, int(leg * 4)) pixel_area = ((xmax - xmin) / nx) * ((ymax - ymin) / nx) grid = polygon_overlap_grid( xmin, xmax, ymin, ymax, nx, nx, vx, vy, 1, 1, ) assert_allclose(grid.sum() * pixel_area, 0.5 * leg * leg) @pytest.mark.parametrize(('half_side', 'subpix', 'atol'), [ (1.0, 16, 0.05), # base case, 2x2 square (2.0, 16, 0.5), # larger square, 4x4 area (0.5, 4, 0.3), # small square (1.0, 1, 2.0), # subpixel=1 (center-of-pixel, coarse by design) (1.0, 32, 0.02), # high subpixel sampling ]) def test_polygon_overlap_subpixel_mode(half_side, subpix, atol): """ Subpixel mode returns values in [0, 1] and the total weighted area matches the analytic area for various square sizes and sampling sizes. """ s = half_side vx = np.array([-s, s, s, -s]) vy = np.array([-s, -s, s, s]) lim = s + 1.0 nx = 5 pixel_area = (2.0 * lim / nx) ** 2 grid = polygon_overlap_grid(-lim, lim, -lim, lim, nx, nx, vx, vy, 0, subpix) assert grid.min() >= 0.0 assert grid.max() <= 1.0 assert_allclose(grid.sum() * pixel_area, (2.0 * s) ** 2, atol=atol) def test_polygon_overlap_no_intersection(): """ A polygon that does not intersect the grid yields all zeros. """ vx = np.array([10.0, 12.0, 11.0]) vy = np.array([10.0, 10.0, 12.0]) grid = polygon_overlap_grid(-2.0, 2.0, -2.0, 2.0, 4, 4, vx, vy, 1, 1) assert_allclose(grid, 0.0) def test_polygon_overlap_nonconvex_l_shape(): """ The exact-mode kernel handles non-convex (L-shaped) polygons. The L-shape used here has area 12 (4x4 square minus 2x2 corner). """ vx = np.array([0.0, 4.0, 4.0, 2.0, 2.0, 0.0]) vy = np.array([0.0, 0.0, 2.0, 2.0, 4.0, 4.0]) grid = polygon_overlap_grid(-1.0, 5.0, -1.0, 5.0, 60, 60, vx, vy, 1, 1) pixel_area = (6.0 / 60) ** 2 assert_allclose(grid.sum() * pixel_area, 12.0, atol=1e-12) def test_polygon_overlap_nonconvex_star(): """ The exact-mode kernel handles a 5-pointed star (non-convex, non-self-intersecting). The total area should match the shoelace area of the polygon. """ n_points = 5 r_outer, r_inner = 5.0, 2.0 angles = (np.pi / 2 + np.arange(2 * n_points) * np.pi / n_points) radii = np.where(np.arange(2 * n_points) % 2 == 0, r_outer, r_inner) vx = np.ascontiguousarray(radii * np.cos(angles)) vy = np.ascontiguousarray(radii * np.sin(angles)) shoelace = abs(0.5 * np.sum(vx * np.roll(vy, -1) - np.roll(vx, -1) * vy)) grid = polygon_overlap_grid(-6.0, 6.0, -6.0, 6.0, 120, 120, vx, vy, 1, 1) pixel_area = (12.0 / 120) ** 2 assert_allclose(grid.sum() * pixel_area, shoelace, atol=1e-10) @pytest.mark.parametrize(('width', 'height'), [ (2.0, 2.0), # square (4.0, 1.0), # wide rectangle (1.0, 4.0), # tall rectangle (0.5, 0.5), # sub-pixel rectangle (3.0, 2.5), # non-integer dimensions ]) def test_polygon_overlap_rectangle_area(width, height): """ The total overlap of an axis-aligned rectangle equals width * height. """ hw, hh = width / 2.0, height / 2.0 vx = np.array([-hw, hw, hw, -hw]) vy = np.array([-hh, -hh, hh, hh]) margin = 0.5 xmin, xmax = -hw - margin, hw + margin ymin, ymax = -hh - margin, hh + margin nx = max(4, int((width + 2 * margin) * 10)) ny = max(4, int((height + 2 * margin) * 10)) pixel_area = ((xmax - xmin) / nx) * ((ymax - ymin) / ny) grid = polygon_overlap_grid( xmin, xmax, ymin, ymax, nx, ny, vx, vy, 1, 1, ) assert_allclose(grid.sum() * pixel_area, width * height) @pytest.mark.parametrize('radius', [1.0, 2.0, 3.5, 0.8]) def test_polygon_overlap_hexagon_area(radius): """ The total overlap of a regular hexagon equals its analytic area (3*sqrt(3)/2 * radius^2). """ angles = np.pi / 6.0 + np.arange(6) * np.pi / 3.0 vx = np.ascontiguousarray(radius * np.cos(angles)) vy = np.ascontiguousarray(radius * np.sin(angles)) expected_area = 1.5 * np.sqrt(3.0) * radius ** 2 margin = 0.5 n = max(60, int(2.0 * radius * 30)) grid = polygon_overlap_grid( -radius - margin, radius + margin, -radius - margin, radius + margin, n, n, vx, vy, 1, 1, ) pixel_area = ((2.0 * (radius + margin)) / n) ** 2 assert_allclose(grid.sum() * pixel_area, expected_area, atol=1e-10) def test_polygon_overlap_validation(): """ Input validation errors are raised for malformed polygons. """ vx = np.array([0.0, 1.0]) vy = np.array([0.0, 0.0]) match = 'at least 3 vertices' with pytest.raises(ValueError, match=match): polygon_overlap_grid(-1.0, 1.0, -1.0, 1.0, 4, 4, vx, vy, 1, 1) vx = np.array([0.0, 1.0, 0.0]) vy = np.array([0.0, 0.0]) match = 'same length' with pytest.raises(ValueError, match=match): polygon_overlap_grid(-1.0, 1.0, -1.0, 1.0, 4, 4, vx, vy, 1, 1) def test_polygon_overlap_many_vertices(): """ There is no limit on the number of polygon vertices. A 10000-vertex polygon approximating a circle should have the analytic circle area. """ n_verts = 10000 radius = 3.0 angles = np.linspace(0.0, 2 * np.pi, n_verts, endpoint=False) vx = np.ascontiguousarray(radius * np.cos(angles)) vy = np.ascontiguousarray(radius * np.sin(angles)) n = 70 grid = polygon_overlap_grid(-3.5, 3.5, -3.5, 3.5, n, n, vx, vy, 1, 1) pixel_area = (7.0 / n) ** 2 assert_allclose(grid.sum() * pixel_area, np.pi * radius**2, rtol=1e-6) def test_polygon_overlap_subpixel_on_horizontal_boundary(): """ Test that subpixel centers landing exactly on a horizontal polygon edge are excluded (classified as outside). A wide rectangle spanning y in [-1, 1] is sampled by a single pixel with ``subpixels=3``, so the subpixel-row centers fall exactly at y = -1, 0, +1. The top and bottom rows lie exactly on the rectangle's horizontal edges (with all column centers strictly inside in x), so only the middle row (3 of 9 subpixels) is counted, giving 1/3. Before horizontal-edge handling, the bottom edge (lower y, region above) was incorrectly counted, which would give 2/3 instead. """ vx = np.array([-2.0, 2.0, 2.0, -2.0]) vy = np.array([-1.0, -1.0, 1.0, 1.0]) grid = polygon_overlap_grid(-1.5, 1.5, -1.5, 1.5, 1, 1, vx, vy, 0, 3) assert_allclose(grid[0, 0], 1.0 / 3.0) def test_polygon_overlap_subpixel_on_vertical_boundary(): """ Test that subpixel centers landing exactly on a vertical polygon edge are excluded (classified as outside). A tall rectangle spanning x in [-1, 1] is sampled by a single pixel with ``subpixels=3``, so the subpixel-column centers fall exactly at x = -1, 0, +1. The left and right columns lie exactly on the rectangle's vertical edges (with all row centers strictly inside in y), so only the middle column (3 of 9 subpixels) is counted, giving 1/3. """ vx = np.array([-1.0, 1.0, 1.0, -1.0]) vy = np.array([-2.0, -2.0, 2.0, 2.0]) grid = polygon_overlap_grid(-1.5, 1.5, -1.5, 1.5, 1, 1, vx, vy, 0, 3) assert_allclose(grid[0, 0], 1.0 / 3.0) def test_polygon_overlap_subpixel_on_all_boundaries(): """ Test that subpixel centers on horizontal edges, vertical edges, and at corners are all excluded. A unit square spanning [-1, 1] in both axes is sampled by a single pixel with ``subpixels=3``, so the 9 subpixel centers fall at every combination of (-1, 0, +1). Of these, only the central center (0, 0) is strictly interior; the four edge-midpoint centers (two horizontal, two vertical) and the four corner centers all lie on the boundary and are excluded, giving 1/9. """ vx = np.array([-1.0, 1.0, 1.0, -1.0]) vy = np.array([-1.0, -1.0, 1.0, 1.0]) grid = polygon_overlap_grid(-1.5, 1.5, -1.5, 1.5, 1, 1, vx, vy, 0, 3) assert_allclose(grid[0, 0], 1.0 / 9.0) astropy-regions-f4139aa/regions/_geometry/tests/test_rectangle_overlap.py000066400000000000000000000072051521703212700272040ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the rectangle_overlap_grid module. """ import math import numpy as np import pytest from numpy.testing import assert_allclose from regions._geometry import rectangle_overlap_grid grid_sizes = [50, 500, 1000] rect_sizes = [0.2, 0.4, 0.8] angles = [0.0, 0.5, 1.0] subsamples = [1, 5, 10] @pytest.mark.parametrize('grid_size', grid_sizes) @pytest.mark.parametrize('rect_size', rect_sizes) @pytest.mark.parametrize('angle', angles) @pytest.mark.parametrize('subsample', subsamples) def test_rectangle_overlap_grid(grid_size, rect_size, angle, subsample): """ Test normalization of the overlap grid to make sure that a fully enclosed pixel has a value of 1.0. """ g = rectangle_overlap_grid(-1.0, 1.0, -1.0, 1.0, grid_size, grid_size, rect_size, rect_size, angle, 0, subsample) assert_allclose(g.max(), 1.0) def test_axis_aligned_rectangle_exact(): """ Exact mode reproduces the analytic overlap for an axis-aligned rectangle that exactly fills an integer number of pixels. """ grid = rectangle_overlap_grid(-2.5, 2.5, -2.5, 2.5, 5, 5, 2.0, 2.0, 0.0, 1, 1) assert_allclose(grid.sum(), 4.0) expected = np.zeros((5, 5)) # Center pixel fully inside. expected[2, 2] = 1.0 # Edge pixels: half overlap. expected[1, 2] = expected[3, 2] = 0.5 expected[2, 1] = expected[2, 3] = 0.5 # Corner pixels: quarter overlap. expected[1, 1] = expected[1, 3] = 0.25 expected[3, 1] = expected[3, 3] = 0.25 assert_allclose(grid, expected) def test_rectangle_exact_total_area_preserved(): """ For any rotation angle, the sum of the exact overlap fractions times the pixel area equals the analytic rectangle area, provided the rectangle fits inside the grid. """ width, height = 3.7, 1.4 pixel_area = (10.0 / 25) ** 2 for theta in (0.0, 0.1, math.pi / 6, math.pi / 4, math.pi / 3, 1.7): grid = rectangle_overlap_grid(-5.0, 5.0, -5.0, 5.0, 25, 25, width, height, theta, 1, 1) assert_allclose(grid.sum() * pixel_area, width * height, rtol=1e-12, atol=1e-12) def test_rectangle_exact_matches_high_subpixel(): """ The exact result should agree with a high subpixel approximation to within the subpixel-sampling error. """ args = (-3.5, 3.5, -3.5, 3.5, 35, 35, 2.0, 1.0, math.pi / 5) exact = rectangle_overlap_grid(*args, 1, 1) sub = rectangle_overlap_grid(*args, 0, 64) assert np.abs(exact - sub).max() < 0.02 def test_rectangle_exact_full_pixel_is_one(): """ A pixel completely inside the rectangle must have overlap = 1. """ grid = rectangle_overlap_grid(-2.0, 2.0, -2.0, 2.0, 4, 4, 10.0, 10.0, 0.3, 1, 1) assert_allclose(grid, 1.0) def test_rectangle_exact_no_overlap(): """ A rectangle entirely outside the grid yields all zeros. """ grid = rectangle_overlap_grid(10.0, 12.0, 10.0, 12.0, 4, 4, 1.0, 1.0, 0.0, 1, 1) assert_allclose(grid, 0.0) @pytest.mark.parametrize('use_exact', [0, 1]) def test_rectangle_overlap_grid_no_readonly_inputs(use_exact): """ Test that the scalar-input overlap grid function takes no array inputs (so read-only input arrays cannot occur) and returns a freshly allocated, writeable output array of shape (ny, nx). """ g = rectangle_overlap_grid(-5.0, 5.0, -5.0, 5.0, 10, 12, 4.0, 2.0, 0.5, use_exact, 5) assert g.shape == (12, 10) assert g.flags.writeable astropy-regions-f4139aa/regions/_geometry/tests/test_thread_safety.py000066400000000000000000000117011521703212700263260ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Test that the Cython overlap functions return identical results when run concurrently from multiple threads. This catches data races and shared-state issues introduced by ``with nogil:`` sections in the functions. """ from concurrent.futures import ThreadPoolExecutor import numpy as np import pytest from numpy.testing import assert_array_equal from regions._geometry import (circle_overlap_grid, ellipse_overlap_grid, polygon_overlap_grid, rectangle_overlap_grid) from regions._geometry.polygon_contains import (points_in_polygon, points_in_polygon_covers) N_THREADS = 8 N_CALLS_PER_THREAD = 4 def _run_concurrent(fn, *, n_threads=N_THREADS, n_calls=N_CALLS_PER_THREAD): expected = fn() with ThreadPoolExecutor(max_workers=n_threads) as ex: futures = [ex.submit(fn) for _ in range(n_threads * n_calls)] for fut in futures: np.testing.assert_array_equal(fut.result(), expected) @pytest.mark.parametrize('use_exact', [1, 0]) def test_circle_overlap_grid_threadsafe(use_exact): def fn(): return circle_overlap_grid(-15.0, 15.0, -15.0, 15.0, 200, 200, 8.0, use_exact, 5) _run_concurrent(fn) @pytest.mark.parametrize('use_exact', [1, 0]) def test_ellipse_overlap_grid_threadsafe(use_exact): def fn(): return ellipse_overlap_grid(-15.0, 15.0, -15.0, 15.0, 200, 200, 8.0, 5.0, 0.7, use_exact, 5) _run_concurrent(fn) @pytest.mark.parametrize('use_exact', [1, 0]) def test_rectangle_overlap_grid_threadsafe(use_exact): def fn(): return rectangle_overlap_grid(-15.0, 15.0, -15.0, 15.0, 200, 200, 12.0, 7.0, 0.5, use_exact, 5) _run_concurrent(fn) @pytest.mark.parametrize('use_exact', [1, 0]) def test_polygon_overlap_grid_threadsafe(use_exact): n_vertices = 32 angles = np.linspace(0.0, 2.0 * np.pi, n_vertices, endpoint=False) radii = 8.0 + 2.0 * np.sin(5.0 * angles) vx = np.ascontiguousarray(radii * np.cos(angles)) vy = np.ascontiguousarray(radii * np.sin(angles)) def fn(): return polygon_overlap_grid(-15.0, 15.0, -15.0, 15.0, 200, 200, vx, vy, use_exact, 8) _run_concurrent(fn) def test_mixed_functions_concurrent(): """ Run all geometry function types concurrently from many threads. This is a more aggressive smoke test that mixes different functions so that if any of them shares mutable state (e.g., a module-level static buffer), interference between calls would surface. """ n_vertices = 16 angles = np.linspace(0.0, 2.0 * np.pi, n_vertices, endpoint=False) vx = np.ascontiguousarray(7.0 * np.cos(angles)) vy = np.ascontiguousarray(5.0 * np.sin(angles)) expected_circ = circle_overlap_grid(-10.0, 10.0, -10.0, 10.0, 150, 150, 6.0, 1, 5) expected_ell = ellipse_overlap_grid(-10.0, 10.0, -10.0, 10.0, 150, 150, 7.0, 4.0, 0.3, 1, 5) expected_rect = rectangle_overlap_grid(-10.0, 10.0, -10.0, 10.0, 150, 150, 9.0, 5.0, 0.4, 1, 5) expected_poly = polygon_overlap_grid(-10.0, 10.0, -10.0, 10.0, 150, 150, vx, vy, 1, 5) def task(which): if which == 0: return 'c', circle_overlap_grid( -10.0, 10.0, -10.0, 10.0, 150, 150, 6.0, 1, 5) if which == 1: return 'e', ellipse_overlap_grid( -10.0, 10.0, -10.0, 10.0, 150, 150, 7.0, 4.0, 0.3, 1, 5) if which == 2: return 'r', rectangle_overlap_grid( -10.0, 10.0, -10.0, 10.0, 150, 150, 9.0, 5.0, 0.4, 1, 5) return 'p', polygon_overlap_grid( -10.0, 10.0, -10.0, 10.0, 150, 150, vx, vy, 1, 5) expected_map = {'c': expected_circ, 'e': expected_ell, 'r': expected_rect, 'p': expected_poly} with ThreadPoolExecutor(max_workers=N_THREADS) as ex: futures = [ex.submit(task, i % 4) for i in range(N_THREADS * 8)] for future in futures: tag, result = future.result() assert_array_equal(result, expected_map[tag]) @pytest.mark.parametrize('func', [points_in_polygon, points_in_polygon_covers]) def test_points_in_polygon_threadsafe(func): n_vertices = 24 angles = np.linspace(0.0, 2.0 * np.pi, n_vertices, endpoint=False) radii = 8.0 + 2.0 * np.sin(5.0 * angles) vx = np.ascontiguousarray(radii * np.cos(angles)) vy = np.ascontiguousarray(radii * np.sin(angles)) grid = np.linspace(-12.0, 12.0, 80) gx, gy = np.meshgrid(grid, grid) x = np.ascontiguousarray(gx.ravel()) y = np.ascontiguousarray(gy.ravel()) def fn(): return func(x, y, vx, vy) _run_concurrent(fn) astropy-regions-f4139aa/regions/_utils/000077500000000000000000000000001521703212700202365ustar00rootroot00000000000000astropy-regions-f4139aa/regions/_utils/__init__.py000066400000000000000000000002641521703212700223510ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage provides general-purpose utility functions. """ from .wcs_helpers import * # noqa: F401, F403 astropy-regions-f4139aa/regions/_utils/examples.py000066400000000000000000000202771521703212700224360ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst __all__ = ['make_example_dataset'] import numpy as np from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.table import Table from astropy.table import vstack as table_vstack from astropy.utils import lazyproperty from astropy.utils.decorators import deprecated from astropy.wcs import WCS from regions.core.pixcoord import PixCoord @deprecated('0.12', message=('The make_example_dataset function is deprecated ' 'and will be removed in a future version.')) def make_example_dataset(data='simulated', config=None): """ Make an example dataset. This is a factory function for ``ExampleDataset`` objects. The following config options are available (default values shown): * ``crval = 0, 0`` * ``crpix = 180, 90`` * ``cdelt = -1, 1`` * ``shape = 180, 360`` * ``ctype = 'GLON-AIT', 'GLAT-AIT'`` Parameters ---------- data : {'simulated', 'fermi'} The dataset to use. config : dict or None Configuration options. Returns ------- dataset : ``ExampleDataset`` An example dataset object. """ return _make_example_dataset(data=data, config=config) def _make_example_dataset(data='simulated', config=None): """ Make an example dataset. This is a factory function for ``ExampleDataset`` objects. The following config options are available (default values shown): * ``crval = 0, 0`` * ``crpix = 180, 90`` * ``cdelt = -1, 1`` * ``shape = 180, 360`` * ``ctype = 'GLON-AIT', 'GLAT-AIT'`` Parameters ---------- data : {'simulated', 'fermi'} The dataset to use. config : dict or None Configuration options. Returns ------- dataset : ``ExampleDataset`` An example dataset object. Examples -------- Make an example dataset: >>> from regions._utils.examples import _make_example_dataset >>> config = dict(crpix=(18, 9), cdelt=(-10, 10), shape=(18, 36)) >>> dataset = _make_example_dataset(data='simulated', config=config) Access properties of the ``dataset`` object: >>> dataset.source_table # doctest: +IGNORE_OUTPUT >>> dataset.event_table # doctest: +IGNORE_OUTPUT >>> dataset.wcs # doctest: +IGNORE_OUTPUT >>> dataset.image # doctest: +IGNORE_OUTPUT >>> dataset.hdu_list # doctest: +IGNORE_OUTPUT """ if data == 'simulated': return ExampleDatasetSimulated(config=config) elif data == 'fermi': return ExampleDatasetFermi(config=config) else: raise ValueError(f'Invalid selection data: {data}') class ExampleDataset: """ Base class for an example dataset. Parameters ---------- config : dict or None Configuration options. """ def __init__(self, config=None): config_default = dict() # These parameters are FITS WCS order: (x, y) config_default['crval'] = 0, 0 config_default['crpix'] = 180, 90 config_default['cdelt'] = -1, 1 # This parameter is numpy order: (y, x) config_default['shape'] = 180, 360 config_default['ctype'] = 'GLON-AIT', 'GLAT-AIT' # Callers can update the default config parameters they want. if config: config_default.update(config) self.config = config_default @lazyproperty def wcs(self): """ World coordinate system (`~astropy.wcs.WCS`). """ wcs = WCS(naxis=2) wcs.wcs.crval = self.config['crval'] wcs.wcs.crpix = self.config['crpix'] wcs.wcs.cdelt = self.config['cdelt'] wcs.wcs.ctype = self.config['ctype'] return wcs @lazyproperty def image(self): """ Return a "counts" image (`~astropy.io.fits.ImageHDU`). """ events = self.event_table skycoord = SkyCoord(events['GLON'], events['GLAT'], unit='deg', frame='galactic') pixcoord = PixCoord.from_sky(skycoord=skycoord, wcs=self.wcs) shape = self.config['shape'] bins = [np.arange(shape[0] + 1), np.arange(shape[1] + 1)] sample = np.vstack((pixcoord.y, pixcoord.x)).T data, _ = np.histogramdd(sample=sample, bins=bins) data = data.astype('float32') header = self.wcs.to_header() return fits.ImageHDU(data=data, header=header, name='image') @lazyproperty def hdu_list(self): """ HDU list (`~astropy.io.fits.HDUList`). Different pieces collected together in a HDU list. This method makes it easy to write the example dataset to a FITS file with multiple HDUs. """ hdu_list = fits.HDUList() hdu = _table_to_bintable(self.source_table) hdu.name = 'sources' hdu_list.append(hdu) hdu = _table_to_bintable(self.event_table) hdu.name = 'events' hdu_list.append(hdu) hdu = self.image hdu.name = 'image' hdu_list.append(hdu) return hdu_list class ExampleDatasetSimulated(ExampleDataset): """ Example simulated dataset. Similar to `ExampleDatasetFermi`, but simulated, not requiring any data files. """ @lazyproperty def source_table(self): """ Source table (`~astropy.table.Table`). Columns: GLON, GLAT, COUNTS """ table = Table() table['GLON'] = np.array([0, 45, 45], dtype='float32') table['GLAT'] = np.array([0, 0, 45], dtype='float32') table['COUNTS'] = np.array([100, 100, 100], dtype='int32') return table @lazyproperty def event_table(self): """ Event table (`~astropy.table.Table`). Columns: GLON, GLAT, SOURCE_IDX """ # Create event list table for each source tables = [] for source in self.source_table: lon = source['GLON'] * np.ones(source['COUNTS']) lat = source['GLAT'] * np.ones(source['COUNTS']) # TODO: scatter positions assuming Gaussian PSF on the sky # using SkyOffsetFrame. # coord = SkyCoord(lon, lat, unit='deg', frame='galactic') table = Table() table['GLON'] = lon table['GLAT'] = lat table['SOURCE_IDX'] = source.index tables.append(table) # Stack all tables together table = table_vstack(tables) return table class ExampleDatasetFermi(ExampleDataset): """ Example real dataset using Fermi-LAT 2FHL source catalog and event list. When saving the HDU list to a FITS file, the file size is 748K, with the 65k EVENTS taking up most of the space. """ @lazyproperty def source_table(self): """ Source table (`~astropy.table.Table`). Columns: GLON, GLAT, COUNTS """ url = ('https://github.com/gammapy/gammapy-extra/raw/master/datasets/' 'fermi_2fhl/gll_psch_v08.fit.gz') table = Table.read(url, hdu='2FHL Source Catalog') table.rename_column('Npred', 'COUNTS') table.keep_columns(['GLON', 'GLAT', 'COUNTS']) table.meta.clear() return table @lazyproperty def event_table(self): """ Event table (`~astropy.table.Table`). Columns: GLON, GLAT """ url = ('https://github.com/gammapy/gammapy-extra/raw/master/datasets/' 'fermi_2fhl/2fhl_events.fits.gz') table = Table.read(url, hdu='EVENTS') table.rename_column('L', 'GLON') table.rename_column('B', 'GLAT') table.keep_columns(['GLON', 'GLAT']) table.meta.clear() return table def _table_to_bintable(table): """ Convert a `~astropy.table.Table` to a `astropy.io.fits.BinTable`. """ data = table.as_array() header = fits.Header() header.update(table.meta) name = table.meta.pop('name', None) return fits.BinTableHDU(data, header, name=name) if __name__ == '__main__': dataset = make_example_dataset(data='simulated') dataset.hdu_list.writeto('example-dataset-simulated.fits', overwrite=True) dataset = make_example_dataset(data='fermi') dataset.hdu_list.writeto('example-dataset-fermi.fits', overwrite=True) astropy-regions-f4139aa/regions/_utils/optional_deps.py000066400000000000000000000013221521703212700234460ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Checks for optional dependencies using lazy import from `PEP 562 `_. """ import importlib # This list is a duplicate of the dependencies in pyproject.toml "all" # and "test". gwcs is used only in tests. optional_deps = ['gwcs', 'matplotlib', 'shapely'] deps = {key.upper(): key for key in optional_deps} __all__ = [f'HAS_{pkg}' for pkg in deps] def __getattr__(name): if name in __all__: try: importlib.import_module(deps[name[4:]]) except ImportError: return False return True raise AttributeError(f'Module {__name__!r} has no attribute {name!r}.') astropy-regions-f4139aa/regions/_utils/spherical_helpers.py000066400000000000000000000500721521703212700243100ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides spherical sky region calculation helper tools. """ import astropy.units as u import numpy as np from astropy.coordinates import (BaseCoordinateFrame, Latitude, Longitude, SkyCoord, SphericalRepresentation, UnitSphericalRepresentation, cartesian_to_spherical, frame_transform_graph) __all__ = [] def get_astropy_frame_class(frame): """ Get a frame class from the input `frame`, which could be a frame name string, or frame class. Direct port of `_get_frame_class()` from https://github.com/astropy/astropy/blob/v7.2.0/astropy/coordinates/sky_coordinate_parsers.py to avoid importing private methods. Parameters ---------- frame : str or `~astropy.coordinates.BaseCoordinateFrame` instance The frame as a string or astropy frame class. Returns ------- frame_cls : A (sub)class of `~astropy.coordinates.BaseCoordinateFrame` The frame as an astropy frame class. """ if isinstance(frame, str): frame_names = frame_transform_graph.get_names() if frame not in frame_names: raise ValueError( f'Coordinate frame name "{frame}" is not a known ' f"coordinate frame ({sorted(frame_names)})", ) frame_cls = frame_transform_graph.lookup_name(frame) elif isinstance(frame, type) and issubclass(frame, BaseCoordinateFrame): frame_cls = frame else: raise ValueError( 'Coordinate frame must be a frame name or frame class, not a' f" '{frame.__class__.__name__}'", ) return frame_cls def cross_product_skycoord2skycoord(c1, c2): """ Compute cross product of two sky coordinates (from a spherical representation), returning a third sky coordinate (on a spherical representation). Parameters ---------- c1 : `~astropy.coordinates.SkyCoord` The first SkyCoord. c2 : `~astropy.coordinates.SkyCoord` The second SkyCoord. Returns ------- `~astropy.coordinates.SkyCoord` The cross product as a SkyCoord, with a spherical representation. """ cross = c1.frame.represent_as('cartesian').cross( c2.frame.represent_as('cartesian')) c_cart = cross / cross.norm() _, lat, lon = cartesian_to_spherical(c_cart.x, c_cart.y, c_cart.z) return SkyCoord(lon, lat, frame=c1.frame) def cross_product_sum_skycoord2skycoord(coos): """ Cross product sum of vertices, assuming vertices are in CW order. Use to determine minimum distance between all vertices, as a centroid definition. Parameters ---------- coos : `~astropy.coordinates.SkyCoord` The vertices as a SkyCoord. Returns ------- `~astropy.coordinates.SkyCoord` The minimum distance centroid cross product as a SkyCoord, with a spherical representation. """ # verts are in CW order: verts_cart = coos.frame.represent_as('cartesian') crosssum = None for i in range(len(verts_cart)): if crosssum is None: crosssum = verts_cart[i - 1].cross(verts_cart[i]) else: crosssum += verts_cart[i - 1].cross(verts_cart[i]) # Normalize sum of cross products to get cartesian representation of # centroid === minimum distance to other points location c_cart = crosssum / crosssum.norm() _, lat, lon = cartesian_to_spherical(c_cart.x, c_cart.y, c_cart.z) # Ensure internal data representation format is consistent # with input coordinate convention: unit = coos[0].represent_as('spherical').lon.unit return SkyCoord(lon.to(unit), lat.to(unit), frame=coos.frame) def bounding_lonlat_poles_processing( region, lons_arr, lats_arr, inner_region=None): """ Check if region covers either pole & modify latitude bounds accordingly. Parameters ---------- region : `~regions.SphericalSkyRegion` The SphericalSkyRegion region. lons_arr : list-like [`~astropy.coordinates.Longitude`] Initial estimate of longitude bounds. lats_arr : list-like [`~astropy.coordinates.Latitude`] Initial estimate of latitude bounds. inner_region : `~regions.SphericalSkyRegion`, optional The inner region, for an annulus (which modifies the pole logic). Default is None (for a simply-connected region). Returns ------- lons_arr : list-like [`~astropy.coordinates.Longitude`] or None Corrected longitude bounds. Is None if the region goes over the pole (as the region contains all longitudes). lats_arr : list-like [`~astropy.coordinates.Latitude`] Corrected latitude bounds. """ # Check if shape covers either pole & modify lats arr accordingly: poles = SkyCoord([0, 0], [-90, 90], unit=u.deg, frame=region.frame) # ------------------------------------ # Simply connected region if inner_region is None: pole_contains = region.contains(poles) if np.any(pole_contains): lons_arr = None # S pole: if pole_contains[0]: lats_arr[0] = -90 * u.deg # N pole if pole_contains[1]: lats_arr[1] = 90 * u.deg if lons_arr is not None: return Longitude(lons_arr), Latitude(lats_arr) return None, Latitude(lats_arr) # ------------------------------------ # Inner region set: annulus logic: pole_contains = inner_region.contains(poles) if np.any(pole_contains): lats_raw_inner = get_circle_latitude_tangent_limits( inner_region.center, inner_region.radius) # S pole: if pole_contains[0]: # Change lower lats bound to be the minimum of the # inner boundary itself lats_arr[0] = lats_raw_inner[0] # N pole if pole_contains[1]: # Change upper lats bound to be the maximum of the # inner boundary itself lats_arr[1] = lats_raw_inner[1] if lons_arr is not None: return Longitude(lons_arr), Latitude(lats_arr) return None, Latitude(lats_arr) def _get_circle_latitude_tangent_points(center, radius): # Get the points on an arbitrary circle that # intersect the tangent latitude circle limits. # This includes lon values, with +180deg folding # for over the pole cases crepr = center.represent_as('spherical') lons = [crepr.lon, crepr.lon] lats = [crepr.lat - radius, crepr.lat + radius] # Fold if over poles: if lats[0] < -90 * u.deg: lats[0] = -180 * u.deg - lats[0] lons[0] = lons[0] + 180 * u.deg if lats[1] > 90 * u.deg: lats[1] = 180 * u.deg - lats[1] lons[1] = lons[1] + 180 * u.deg return SkyCoord(lons, lats, frame=center.frame) def _get_circle_longitude_tangent_points(center, radius): # Get the points on an arbitrary circle that # intersect the tangent longitude circle limits. # This includes lat values = 0deg # (all longitude circles have centers on equator) crepr = center.represent_as('spherical') lats = [0 * u.deg, 0 * u.deg] lats_ref = np.array( [(crepr.lat - radius).to_value(u.deg), (crepr.lat + radius).to_value(u.deg)], ) lons = [] if np.any(np.abs(lats_ref) > 90): return None if np.any(np.abs(lats_ref) == 90): return SkyCoord( [crepr.lon - 90 * u.deg, crepr.lon + 90 * u.deg], lats, frame=center.frame, ) for sgn in [-1, 1]: # Do 1 then -1, because of lon increasing to east lon_gc = crepr.lon - sgn * ( np.arccos( np.sin(radius.to_value(u.radian)) / np.cos(crepr.lat.to_value(u.radian)), ) * u.radian ).to(u.deg) lons.append(lon_gc + sgn * 90 * u.deg) return SkyCoord(lons, lats, frame=center.frame) def get_circle_latitude_tangent_limits(center, radius): """ For an arbitrary spherical circle with center (lon0, lat0) and radius R0, get the tangent latitude circle limits (encoded at latitude values, not radii of the latitude circles). Use these to determine the latitude bounding limits from those tangents. (the points where the latitude circles intersect this circle) (Tangent circles have Rlat = 90 - (lat0 +- R0), equivalent to latitude limits lat0 +- R0) Ignores any issues with "over the pole" bounds -- this only computes the values of the tangent circles. Other processing will handle over the pole bounds logic. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The circle center as a SkyCoord. radius : `~astropy.coordinates.Angle` or `~astropy.Quantity` The circle radius (as an |Angle| or |Quantity| with angular units). Returns ------- latitude_limits : `~astropy.coordinates.Latitude` Length two |Latitude| with the latitudes of the tangent circles. """ tan_lat_pts = _get_circle_latitude_tangent_points(center, radius) tan_lat_pts = tan_lat_pts.represent_as('spherical') return Latitude(tan_lat_pts.lat).to(u.deg) def get_circle_longitude_tangent_limits(center, radius): """ For an arbitrary spherical circle with center (lon0, lat0) and radius R0, get the longitudes of the centers of the tangent "longitude great circles" (as all longitude circles have latitude=0deg). Use these to determine the longitude bounds for this circle (the points where the longitude GCs intersect the circle). If | lat0 +- R0 | > 90deg: lon_bounds = None (Crosses pole, so no tangent longitude great circle exists -- all longitude lines [great circles] cross this circle.) If | lat0 +- R0 | = 90deg: lon_bounds = [lon0-90,lon0+90] (Touches either pole, so spans full 180 of longitudes centered on lon0) If | lat0 +- R0 | < 90deg: lon0 +- arccos[ sin(R0) / cos(lat0) ] Parameters ---------- center : `~astropy.coordinates.SkyCoord` The circle center as a SkyCoord. radius : `~astropy.coordinates.Angle` or `~astropy.Quantity` The circle radius (as an |Angle| or |Quantity| with angular units). Returns ------- longitude_limits : `~astropy.coordinates.Longitude` Length two |Longitude| with the longitudes of the centers of the tangent longitude great circles. """ tan_lon_pts = _get_circle_longitude_tangent_points(center, radius) if tan_lon_pts is None: return None tan_lon_pts = tan_lon_pts.represent_as('spherical') return Longitude(tan_lon_pts.lon).to(u.deg) def _add_tan_pts_if_in_pa_range( coord_list, tan_pts, gc, wrap_ang, pas_verts_wrap, coord=None, ): pas_tan_pts = gc.center.position_angle(tan_pts).to(u.deg) # CHECK RANGES: # To handle possible cases of lon values "wrapping around" across # the standard 360->0 wrap, wrap lon values + coord values # around the first entry angle pas_tan_pts_wrap = pas_tan_pts.wrap_at(wrap_ang) in_range = (pas_tan_pts_wrap >= pas_verts_wrap[0]) & ( pas_tan_pts_wrap <= pas_verts_wrap[1] ) if np.any(in_range): tptrepr = tan_pts.represent_as('spherical') coord_list = np.append(coord_list, getattr(tptrepr, coord)[in_range]) return coord_list def _check_edge_lt_pi(pas_verts, wrap_ang): pas_verts_wrap = pas_verts.wrap_at(wrap_ang) is_valid_arc_length = ( (pas_verts_wrap[1] - pas_verts_wrap[0]).to(u.deg) <= 180 * u.deg) return pas_verts_wrap, is_valid_arc_length def _validate_vertices_ordering(verts, gc, gc_center=None): if gc is not None: gc_center = gc.center pas_verts = gc_center.position_angle(verts).to(u.deg) wrap_ang = pas_verts[0] # Check ordering of vertices pas: # Principle: ALL EDGES must be <= 180 deg of length # So difference of pa[1] - pa[0] <= 180 deg # If not, swap the order. pas_verts_wrap, is_valid_arc_length = _check_edge_lt_pi( pas_verts, wrap_ang) if not is_valid_arc_length: wrap_ang_opp = pas_verts[-1] pas_verts_wrap_opp, is_valid_arc_length_opp = _check_edge_lt_pi( pas_verts[::-1], wrap_ang_opp, ) if is_valid_arc_length_opp: return pas_verts_wrap_opp, wrap_ang_opp raise ValueError('Invalid arc') # should never occur return pas_verts_wrap, wrap_ang def _validate_lon_bounds_ordering(lons_arr, centroid): # Invert longitude order if centroid is outside of range: wrap_ang = lons_arr[0] centroid_lon_wrap = centroid.represent_as( 'spherical').lon.wrap_at(wrap_ang) in_range_lon = (centroid_lon_wrap >= lons_arr[0].wrap_at(wrap_ang)) & ( centroid_lon_wrap <= lons_arr[1].wrap_at(wrap_ang) ) if not in_range_lon: lons_arr = lons_arr[::-1] return lons_arr def get_edge_raw_lonlat_bounds_circ_edges(vertices, centroid, gcs): """ Get the raw longitude / latitude bounds from the circle edges of spherical sky region. Parameters ---------- vertices : `~astropy.coordinates.SkyCoord` The vertices as a SkyCoord. centroid : `~astropy.coordinates.SkyCoord` The polygon centroid as a SkyCoord. gcs : `~regions.CircleSphericalSkyRegion` The circle boundaries as CircleSphericalSkyRegion instances. Returns ------- longitude_limits, latitude_limits: `~astropy.coordinates.Longitude` Length two |Longitude| and |Latitude| with the computed longitude/latitude bounds from the polygon edges. """ # Consider lon/lat of vertices: may produce min/max bounds: vrepr = vertices.represent_as('spherical') # Special handling: # Exclude vertices from longitude bounds if any is on a pole lons_list = [] lats_list = [] for v in vrepr: if np.abs(v.lat.to(u.deg).deg) < 90: lons_list.append(v.lon) lats_list.append(v.lat) lons_list = Longitude(lons_list, unit=u.radian) lats_list = Latitude(lats_list, unit=u.radian) # Need to also check for "out bulging" from edges, # as far as latitude/lon bounds: # eg, 2 vertices at ~60deg: the gc arc goes ~closer to the pole; # a circle centered close to the pole but not extending over it: # WIDE lon bounds # Requires checking if the "bounding" points are *ON* the polygons. for i, gc in enumerate(gcs): # PAs from gc center to vertices: verts = SkyCoord(np.concatenate([[vertices[i - 1]], [vertices[i]]])) pas_verts_wrap, wrap_ang = _validate_vertices_ordering(verts, gc) # -------------------------------------------------------- # Latitude tangent points from bound circle as len 2 SkyCoord: # Only add to the list if the tangent point is located along this edge tan_lat_pts = _get_circle_latitude_tangent_points(gc.center, gc.radius) lats_list = _add_tan_pts_if_in_pa_range( lats_list, tan_lat_pts, gc, wrap_ang, pas_verts_wrap, coord='lat', ) # -------------------------------------------------------- # Longitude tangent points from bound circle as len 2 SkyCoord: # Only add to the list if the tangent point is located along this edge tan_lon_pts = _get_circle_longitude_tangent_points( gc.center, gc.radius) if tan_lon_pts is not None: lons_list = _add_tan_pts_if_in_pa_range( lons_list, tan_lon_pts, gc, wrap_ang, pas_verts_wrap, coord='lon', ) lons_arr = [lons_list.min(), lons_list.max()] lats_arr = [lats_list.min(), lats_list.max()] # -------------------------------------------------------- # Invert longitude order if centroid is outside of range: lons_arr = _validate_lon_bounds_ordering(lons_arr, centroid) return Longitude(lons_arr).to(u.deg), Latitude(lats_arr).to(u.deg) def _discretize_edge_boundary(vertices, circ, n_vertices, circ_center=None, circ_radius=None): if circ is not None: circ_center = circ.center circ_radius = circ.radius # Discretize an edge boundary defined by a circle, geodetic or not: # either great circle arc, or a span of a non-great circle # (e.g., constant lat edges of RangeSphericalSkyRegion) # For every edge boundary: determine range of PAs spanned by lines # connecting circle center to the two vertices bounding that edge: pas_verts = circ_center.position_angle(vertices).to(u.deg) pas_verts_wrap, wrap_ang = _validate_vertices_ordering( vertices, circ, gc_center=circ_center, ) # Need to wrap angles to calculate span: wrap at lower value: pas_verts_wrap = pas_verts.wrap_at(wrap_ang) pa_span = pas_verts_wrap[1] - pas_verts_wrap[0] # Sample angle range over pa_span with Npoints, and offset # to start at pas_verts[0] theta = np.linspace(0, 1, num=n_vertices, endpoint=False) * pa_span + pas_verts[0] # Calculate directional offsets to get boundary discretization, # with vertices as SkyCoords bound_verts = circ_center.directional_offset_by(theta, circ_radius) return bound_verts def discretize_line_boundary(coords, n_vertices): """ Discretize all edge boundaries for spherical sky regions. Parameters ---------- coords : `~astropy.coordinates.SkyCoord` The start, end of the line as a SkyCoord. n_vertices : int The number of points for discretization along each edge. Returns ------- edge_bound_verts : `~astropy.coordinates.SkyCoord` The discretized boundary edge vertices. """ # Iterate over full set of vertices & boundary circles for # a region (polygon or range) gc_center = cross_product_skycoord2skycoord(coords[0], coords[1]) gc_radius = 90 * u.deg bound_verts = _discretize_edge_boundary( coords, None, n_vertices, circ_center=gc_center, circ_radius=gc_radius) return SkyCoord( SkyCoord( bound_verts, representation_type=UnitSphericalRepresentation, ), representation_type=SphericalRepresentation, ) def discretize_all_edge_boundaries(vertices, circs, n_vertices): """ Discretize all edge boundaries for spherical sky regions. Parameters ---------- vertices : `~astropy.coordinates.SkyCoord` The vertices of the spherical sky region. circs : `~regions.CircleSphericalSkyRegion` The circle boundaries as CircleSphericalSkyRegion instances. n_vertices : int The number of points for discretization along the boundary. Returns ------- all_edge_bound_verts : `~astropy.coordinates.SkyCoord` The discretized boundary edge vertices. """ # Iterate over full set of vertices & boundary circles for # a region (polygon or range) # Verify n_vertices >= the number of vertices: if n_vertices < len(vertices): raise ValueError( f"n_vertices must be greater than {len(vertices)} " 'to discretize/polygonize this region!', ) # Return if n_vertices = len(vertices) if n_vertices == len(vertices): return vertices # Determine the number of vertices per edge: npt_edge, remainder = np.divmod(n_vertices, len(vertices)) all_edge_bound_verts = None for i, circ in enumerate(circs): npt = npt_edge + 1 if i < remainder else npt_edge # PAs from gc center to vertices: verts = SkyCoord(np.concatenate([[vertices[i - 1]], [vertices[i]]])) bound_verts = _discretize_edge_boundary(verts, circ, npt) if all_edge_bound_verts is None: all_edge_bound_verts = bound_verts else: # Specify representation type, as in some cases # concatenate introduces distances/non unit spherical # representation all_edge_bound_verts = SkyCoord( np.concatenate( [all_edge_bound_verts.copy(), bound_verts], ), representation_type=UnitSphericalRepresentation, ) return all_edge_bound_verts astropy-regions-f4139aa/regions/_utils/tests/000077500000000000000000000000001521703212700214005ustar00rootroot00000000000000astropy-regions-f4139aa/regions/_utils/tests/__init__.py000066400000000000000000000000001521703212700234770ustar00rootroot00000000000000astropy-regions-f4139aa/regions/_utils/tests/conftest.py000066400000000000000000000113251521703212700236010ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Pytest configuration and WCS test fixtures for regions._utils tests. """ import astropy.units as u import numpy as np import pytest from astropy.coordinates import SkyCoord from astropy.wcs import WCS # WCS test constants WCS_CENTER = SkyCoord(100 * u.deg, 30 * u.deg) WCS_CDELT_ARCSEC = 0.1 def _make_simple_wcs(skycoord, resolution, size, *, rotation_deg=0.0): """ Create a simple TAN WCS with optional rotation. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The center sky coordinate (CRVAL). resolution : `~astropy.units.Quantity` The pixel scale (CDELT) as an angular quantity. size : int Number of pixels along each axis. rotation_deg : float, optional Rotation angle in degrees (default: 0). Returns ------- wcs : `~astropy.wcs.WCS` The WCS object. """ cdelt_deg = resolution.to_value(u.deg) wcs = WCS(naxis=2) wcs.wcs.crpix = [size / 2 + 0.5, size / 2 + 0.5] wcs.wcs.crval = [skycoord.ra.deg, skycoord.dec.deg] wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] if rotation_deg == 0.0: wcs.wcs.cdelt = [-cdelt_deg, cdelt_deg] else: angle_rad = np.radians(rotation_deg) cos_a = np.cos(angle_rad) sin_a = np.sin(angle_rad) wcs.wcs.cd = [[-cdelt_deg * cos_a, cdelt_deg * sin_a], [cdelt_deg * sin_a, cdelt_deg * cos_a]] return wcs def _make_sip_wcs(): """ Create a TAN WCS with small SIP distortion terms. Returns ------- wcs : `~astropy.wcs.WCS` The WCS object with SIP distortion. """ wcs = WCS(naxis=2) wcs.wcs.crpix = [10.5, 10.5] wcs.wcs.crval = [WCS_CENTER.ra.deg, WCS_CENTER.dec.deg] wcs.wcs.cdelt = [-WCS_CDELT_ARCSEC / 3600, WCS_CDELT_ARCSEC / 3600] wcs.wcs.ctype = ['RA---TAN-SIP', 'DEC--TAN-SIP'] # Small SIP distortion coefficients m = 2 # A/B order wcs.sip = None # will be populated from the header sip_header = wcs.to_header() sip_header['CTYPE1'] = 'RA---TAN-SIP' sip_header['CTYPE2'] = 'DEC--TAN-SIP' sip_header['A_ORDER'] = m sip_header['B_ORDER'] = m sip_header['A_2_0'] = 1e-7 sip_header['A_0_2'] = 1e-7 sip_header['B_2_0'] = 1e-7 sip_header['B_0_2'] = 1e-7 return WCS(sip_header) class _MockGWCS: """ Minimal WCS-like wrapper without a ``has_distortion`` attribute, used to simulate a generalized WCS (e.g., gwcs) so the dispatch helpers fall back to the Jacobian path. """ def __init__(self, real_wcs): self._wcs = real_wcs def world_to_pixel(self, *args, **kwargs): return self._wcs.world_to_pixel(*args, **kwargs) def pixel_to_world(self, *args, **kwargs): return self._wcs.pixel_to_world(*args, **kwargs) @pytest.fixture def simple_wcs(): """ Non-distorted TAN WCS aligned with the celestial axes. """ return _make_simple_wcs(WCS_CENTER, WCS_CDELT_ARCSEC * u.arcsec, 20) @pytest.fixture def rotated_wcs(): """ Non-distorted TAN WCS with a 25-degree rotation (CD matrix). """ return _make_simple_wcs(WCS_CENTER, WCS_CDELT_ARCSEC * u.arcsec, 20, rotation_deg=25.0) @pytest.fixture def sip_wcs(): """ TAN WCS with small SIP distortion terms. """ return _make_sip_wcs() @pytest.fixture def nonsquare_wcs(): """ Non-distorted TAN WCS with non-square pixels (0.03 x 0.05 deg). """ wcs = WCS(naxis=2) wcs.wcs.crpix = [10.5, 10.5] wcs.wcs.crval = [WCS_CENTER.ra.deg, WCS_CENTER.dec.deg] wcs.wcs.cdelt = [-0.03, 0.05] wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] return wcs @pytest.fixture def flipped_wcs(): """ Non-distorted TAN WCS with a flipped parity (North down, East left). Both CDELT values are negative, so the determinant of the pixel scale matrix is positive (parity = +1). This is the opposite parity from the standard astronomical convention (North up, East left) and reproduces the mirrored-aperture bug reported for such WCS. """ wcs = WCS(naxis=2) wcs.wcs.crpix = [10.5, 10.5] wcs.wcs.crval = [WCS_CENTER.ra.deg, WCS_CENTER.dec.deg] wcs.wcs.cdelt = [-WCS_CDELT_ARCSEC / 3600, -WCS_CDELT_ARCSEC / 3600] wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] return wcs @pytest.fixture def center_xy_coord(simple_wcs): """ Return the center (x, y) tuple at CRPIX of the simple WCS. """ x, y = simple_wcs.world_to_pixel(WCS_CENTER) return (x, y) @pytest.fixture def mock_gwcs(simple_wcs): """ A WCS-like object without ``has_distortion`` (simulates gwcs), forcing the dispatch helpers to use the Jacobian path. """ return _MockGWCS(simple_wcs) astropy-regions-f4139aa/regions/_utils/tests/test_examples.py000066400000000000000000000024351521703212700246330ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import numpy as np from numpy.testing import assert_allclose from regions._utils.examples import _make_example_dataset class TestExampleSimulatedDataset: def setup_method(self): self.dataset = _make_example_dataset(data='simulated') def test_source_table(self): source_table = self.dataset.source_table assert len(source_table) == 3 def test_event_table(self): # source_table = self.dataset.source_table event_table = self.dataset.event_table assert len(event_table) == 300 def test_wcs(self): wcs = self.dataset.wcs assert_allclose(wcs.wcs.crval, (0, 0)) assert_allclose(wcs.wcs.crpix, (180, 90)) assert_allclose(wcs.wcs.cdelt, (-1, 1)) assert wcs.wcs.ctype[0] == 'GLON-AIT' assert wcs.wcs.ctype[1] == 'GLAT-AIT' def test_image(self): image = self.dataset.image assert image.data.shape == (180, 360) assert np.nansum(image.data) == 300 def test_hdu_list(self): hdu_list = self.dataset.hdu_list # Check that all data is present assert hdu_list[1] == hdu_list['sources'] assert hdu_list[2] == hdu_list['events'] assert hdu_list[3] == hdu_list['image'] astropy-regions-f4139aa/regions/_utils/tests/test_wcs_helpers.py000066400000000000000000001070171521703212700253350ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the _wcs_helpers module. """ import astropy.units as u import numpy as np import pytest from astropy.coordinates import Angle, SkyCoord from astropy.io.fits import Header from astropy.wcs import WCS as APWCS from numpy.testing import assert_allclose from regions._utils.wcs_helpers import (compute_local_wcs_jacobian, jacobian_pixel_to_sky_mean_scale, jacobian_sky_to_pixel_mean_scale, pixel_shape_to_sky_svd, pixel_to_sky_mean_scale, pixel_to_sky_svd_scales, sky_shape_to_pixel_svd, sky_to_pixel_mean_scale, sky_to_pixel_svd_scales, wcs_pixel_scale_angle) from regions.tests.helpers import WCS_CDELT_ARCSEC, WCS_CENTER # WCS centers that historically broke the flat-sky finite-difference # Jacobian and SVD shape conversions: # # * near the celestial poles, where the small ``cos(dec)`` factor makes # the ``xi = cos(dec) * dRA`` small-angle approximation fail and the # (xi, eta) sampling becomes wildly nonlinear; and # # * across the RA = 0 / 360 wraparound, where naive longitude # subtraction crosses the 0 / 360 cut. TROUBLESOME_CENTERS = [ pytest.param(0.0, 80.0, id='pole_dec=80'), pytest.param(0.0, 89.0, id='pole_dec=89'), pytest.param(0.0, 89.99, id='pole_dec=89.99'), pytest.param(0.0, -80.0, id='pole_dec=-80'), pytest.param(0.0, -89.99, id='pole_dec=-89.99'), pytest.param(0.0, 30.0, id='ra_wrap=0'), pytest.param(0.001, 30.0, id='ra_wrap=0.001'), pytest.param(359.999, 30.0, id='ra_wrap=359.999'), ] def _make_sip_wcs(ra_deg, dec_deg): """ Build a small TAN-SIP WCS centered at (ra_deg, dec_deg). The SIP terms are tiny but nonzero, ensuring that ``compute_local_wcs_jacobian`` is exercised (the jacobian is only used for distorted WCS). """ header = Header() header['NAXIS'] = 2 header['NAXIS1'] = 20 header['NAXIS2'] = 20 header['CRPIX1'] = 10.5 header['CRPIX2'] = 10.5 header['CRVAL1'] = ra_deg header['CRVAL2'] = dec_deg header['CTYPE1'] = 'RA---TAN-SIP' header['CTYPE2'] = 'DEC--TAN-SIP' cdelt = WCS_CDELT_ARCSEC / 3600.0 header['CD1_1'] = -cdelt header['CD1_2'] = 0.0 header['CD2_1'] = 0.0 header['CD2_2'] = cdelt header['A_ORDER'] = 2 header['A_2_0'] = 1e-6 header['B_ORDER'] = 2 header['B_0_2'] = 1e-6 return APWCS(header) class TestComputeLocalWCSJacobian: """ Tests for `compute_local_wcs_jacobian`. """ def test_shape(self, simple_wcs): """ The Jacobian must be a 2x2 array. """ jac = compute_local_wcs_jacobian(WCS_CENTER, simple_wcs) assert jac.shape == (2, 2) def test_simple_wcs_diagonal(self, simple_wcs): """ For an axis-aligned TAN WCS the Jacobian should be nearly diagonal with magnitudes ~ 1/WCS_CDELT_ARCSEC. """ jac = compute_local_wcs_jacobian(WCS_CENTER, simple_wcs) # Off-diagonal elements should be near zero assert_allclose(jac[0, 1], 0.0, atol=1e-4) assert_allclose(jac[1, 0], 0.0, atol=1e-4) # Diagonal: RA axis (pix/arcsec) is negative (RA increases left) expected_scale = 1.0 / WCS_CDELT_ARCSEC assert_allclose(np.abs(jac[0, 0]), expected_scale) assert_allclose(np.abs(jac[1, 1]), expected_scale) def test_rotated_wcs(self, rotated_wcs): """ For a rotated WCS the off-diagonal elements should be nonzero, but the singular values should still match 1/WCS_CDELT_ARCSEC. """ jac = compute_local_wcs_jacobian(WCS_CENTER, rotated_wcs) sv = np.linalg.svd(jac, compute_uv=False) expected_scale = 1.0 / WCS_CDELT_ARCSEC assert_allclose(sv, expected_scale, rtol=1e-6) def test_sip_wcs(self, sip_wcs): """ For a SIP WCS the Jacobian should still be close to the undistorted value near the reference pixel. """ jac = compute_local_wcs_jacobian(WCS_CENTER, sip_wcs) sv = np.linalg.svd(jac, compute_uv=False) expected_scale = 1.0 / WCS_CDELT_ARCSEC assert_allclose(sv, expected_scale, rtol=1e-5) def test_inverse_of_forward(self, simple_wcs): """ The Jacobian should be the inverse of the forward d(sky)/d(pixel) matrix derived from 1-pixel offsets. """ jac = compute_local_wcs_jacobian(WCS_CENTER, simple_wcs) # A 1-pixel step should map to ~CDELT arcsec in sky forward = np.linalg.inv(jac) # Diagonal magnitudes should be ~WCS_CDELT_ARCSEC assert_allclose(np.abs(forward[0, 0]), WCS_CDELT_ARCSEC) assert_allclose(np.abs(forward[1, 1]), WCS_CDELT_ARCSEC) def test_determinant_sign(self, simple_wcs): """ Standard WCS (RA increasing to the left) should have negative determinant. """ jac = compute_local_wcs_jacobian(WCS_CENTER, simple_wcs) assert np.linalg.det(jac) < 0 @pytest.mark.parametrize(('center_ra', 'center_dec'), TROUBLESOME_CENTERS) def test_well_conditioned_at_pole_and_wrap(self, center_ra, center_dec): """ Regression test: the near-singular values of the Jacobian must match the expected ``1 / WCS_CDELT_ARCSEC`` value even near the celestial poles and across the RA = 0 / 360 wraparound. """ wcs = _make_sip_wcs(center_ra, center_dec) skycoord = SkyCoord(center_ra * u.deg, center_dec * u.deg) jac = compute_local_wcs_jacobian(skycoord, wcs) sv = np.linalg.svd(jac, compute_uv=False) assert_allclose(sv, 1.0 / WCS_CDELT_ARCSEC, rtol=1e-3) @pytest.mark.parametrize(('center_ra', 'center_dec'), TROUBLESOME_CENTERS) def test_parity_at_pole_and_wrap(self, center_ra, center_dec): """ Regression test: the determinant (parity) of the Jacobian must stay negative for a standard RA-increases-to-the-left WCS at all declinations and RA values, including near the poles and across the wraparound. """ wcs = _make_sip_wcs(center_ra, center_dec) skycoord = SkyCoord(center_ra * u.deg, center_dec * u.deg) jac = compute_local_wcs_jacobian(skycoord, wcs) assert np.linalg.det(jac) < 0 class TestWcsPixelScaleAngle: """ Tests for `wcs_pixel_scale_angle`. """ def test_return_types(self, simple_wcs): """ Should return (tuple, float, Angle). """ xy_coord, scale, angle = wcs_pixel_scale_angle( WCS_CENTER, simple_wcs) assert isinstance(xy_coord, tuple) assert isinstance(scale, float) assert isinstance(angle, Angle) def test_simple_wcs_scale(self, simple_wcs): """ For a simple TAN WCS, scale should equal CDELT in arcsec/pixel. """ _, scale, _ = wcs_pixel_scale_angle(WCS_CENTER, simple_wcs) assert_allclose(scale, WCS_CDELT_ARCSEC) def test_simple_wcs_angle(self, simple_wcs): """ For an axis-aligned TAN WCS with CDELT=[-c, c], North is along +y, so the angle should be ~90 degrees. """ _, _, angle = wcs_pixel_scale_angle(WCS_CENTER, simple_wcs) assert_allclose(angle.deg, 90.0) def test_angle_wrapped(self, simple_wcs): """ The angle should be in [0, 360) degrees. """ _, _, angle = wcs_pixel_scale_angle(WCS_CENTER, simple_wcs) assert 0.0 <= angle.deg < 360.0 def test_rotated_wcs_angle(self, rotated_wcs): """ For a 25-degree rotated WCS, the North angle should shift by ~25 degrees from the axis-aligned value (~90 deg). """ _, _, angle = wcs_pixel_scale_angle(WCS_CENTER, rotated_wcs) # The rotation should be about 90 - 25 = 65 degrees assert_allclose(angle.deg, 90.0 - 25.0) def test_rotated_wcs_scale(self, rotated_wcs): """ Rotation should not change the pixel scale. """ _, scale, _ = wcs_pixel_scale_angle(WCS_CENTER, rotated_wcs) assert_allclose(scale, WCS_CDELT_ARCSEC) def test_nonsquare_wcs_scale(self, nonsquare_wcs): """ For non-square pixels the scale should be the geometric mean. """ _, scale, _ = wcs_pixel_scale_angle(WCS_CENTER, nonsquare_wcs) expected = np.sqrt(0.03 * 0.05) * 3600 assert_allclose(scale, expected, rtol=1e-5) def test_pixel_coordinate(self, simple_wcs): """ The returned xy_coord should match world_to_pixel. """ xy_coord, _, _ = wcs_pixel_scale_angle(WCS_CENTER, simple_wcs) x_exp, y_exp = simple_wcs.world_to_pixel(WCS_CENTER) assert_allclose(xy_coord[0], x_exp) assert_allclose(xy_coord[1], y_exp) def test_off_center_position(self, simple_wcs): """ Test a position away from the WCS reference pixel. """ skycoord = SkyCoord(100.5 * u.deg, 30.5 * u.deg) _, scale, angle = wcs_pixel_scale_angle(skycoord, simple_wcs) assert scale > 0 assert 0.0 <= angle.deg < 360.0 class TestJacobianMeanScale: """ Tests for `jacobian_sky_to_pixel_mean_scale` and `jacobian_pixel_to_sky_mean_scale`. """ def test_sky_to_pixel_return_types(self, simple_wcs): """ Should return (tuple, float). """ pix_position, scale = jacobian_sky_to_pixel_mean_scale( WCS_CENTER, simple_wcs) assert isinstance(pix_position, tuple) assert isinstance(scale, (float, np.floating)) def test_pixel_to_sky_return_types(self, simple_wcs, center_xy_coord): """ Should return (SkyCoord, float). """ sky_position, scale = jacobian_pixel_to_sky_mean_scale( center_xy_coord, simple_wcs) assert isinstance(sky_position, SkyCoord) assert isinstance(scale, (float, np.floating)) def test_sky_to_pixel_simple_scale(self, simple_wcs): """ For an isotropic WCS, the mean scale should equal 1/WCS_CDELT_ARCSEC. """ _, scale = jacobian_sky_to_pixel_mean_scale( WCS_CENTER, simple_wcs) assert_allclose(scale, 1.0 / WCS_CDELT_ARCSEC) def test_pixel_to_sky_simple_scale(self, simple_wcs, center_xy_coord): """ For an isotropic WCS, the mean scale should equal WCS_CDELT_ARCSEC. """ _, scale = jacobian_pixel_to_sky_mean_scale( center_xy_coord, simple_wcs) assert_allclose(scale, WCS_CDELT_ARCSEC) def test_roundtrip_scale(self, simple_wcs): """ Sky -> pixel mean_scale * pixel -> sky mean_scale should ~ 1. """ center_pix, s2p = jacobian_sky_to_pixel_mean_scale( WCS_CENTER, simple_wcs) _, p2s = jacobian_pixel_to_sky_mean_scale(center_pix, simple_wcs) assert_allclose(s2p * p2s, 1.0) def test_sip_wcs_positive(self, sip_wcs): """ Mean scale should be positive for distorted WCS. """ _, scale = jacobian_sky_to_pixel_mean_scale( WCS_CENTER, sip_wcs) assert scale > 0 def test_center_coordinates(self, simple_wcs): """ The returned pix_position should match world_to_pixel. """ pix_position, _ = jacobian_sky_to_pixel_mean_scale( WCS_CENTER, simple_wcs) x_exp, y_exp = simple_wcs.world_to_pixel(WCS_CENTER) assert_allclose(pix_position[0], x_exp) assert_allclose(pix_position[1], y_exp) def test_nonsquare_mean_scale(self, nonsquare_wcs): """ For non-square pixels the mean scale should be the arithmetic mean of the two singular values (1/cdelt_x and 1/cdelt_y in pix/arcsec). """ _, scale = jacobian_sky_to_pixel_mean_scale( WCS_CENTER, nonsquare_wcs) cdelt_x = 0.03 * 3600 cdelt_y = 0.05 * 3600 expected = 0.5 * (1.0 / cdelt_x + 1.0 / cdelt_y) assert_allclose(scale, expected, rtol=1e-6) class TestDispatchMeanScale: """ Tests for `sky_to_pixel_mean_scale` and `pixel_to_sky_mean_scale` dispatch helpers. """ def test_no_distortion_returns(self, simple_wcs): """ Should return (tuple, float) for non-distorted WCS. """ pix_position, scale = sky_to_pixel_mean_scale(WCS_CENTER, simple_wcs) assert isinstance(pix_position, tuple) assert isinstance(scale, float) def test_distortion_returns(self, sip_wcs): """ Should return (tuple, float/np.floating) for distorted WCS. """ pix_position, scale = sky_to_pixel_mean_scale(WCS_CENTER, sip_wcs) assert isinstance(pix_position, tuple) assert isinstance(scale, (float, np.floating)) def test_no_distortion_scale(self, simple_wcs): """ For a simple WCS, mean scale should be 1/WCS_CDELT_ARCSEC. """ _, scale = sky_to_pixel_mean_scale(WCS_CENTER, simple_wcs) assert_allclose(scale, 1.0 / WCS_CDELT_ARCSEC) def test_distortion_scale(self, sip_wcs): """ For a SIP WCS near the reference pixel, the mean scale should be close to the undistorted value. """ _, scale = sky_to_pixel_mean_scale(WCS_CENTER, sip_wcs) assert_allclose(scale, 1.0 / WCS_CDELT_ARCSEC, rtol=1e-6) def test_pixel_to_sky_no_distortion(self, simple_wcs, center_xy_coord): """ For a simple WCS, pixel_to_sky mean scale should be WCS_CDELT_ARCSEC. """ sky_position, scale = pixel_to_sky_mean_scale( center_xy_coord, simple_wcs) assert isinstance(sky_position, SkyCoord) assert_allclose(scale, WCS_CDELT_ARCSEC) def test_pixel_to_sky_distortion(self, sip_wcs): """ For a SIP WCS, pixel_to_sky mean scale should be close to WCS_CDELT_ARCSEC near the reference pixel. """ xy_coord = (9.5, 9.5) sky_position, scale = pixel_to_sky_mean_scale(xy_coord, sip_wcs) assert isinstance(sky_position, SkyCoord) assert_allclose(scale, WCS_CDELT_ARCSEC, rtol=1e-6) def test_roundtrip(self, simple_wcs): """ Sky -> pixel mean_scale * pixel -> sky mean_scale should ~ 1. """ center_pix, s2p = sky_to_pixel_mean_scale( WCS_CENTER, simple_wcs) _, p2s = pixel_to_sky_mean_scale(center_pix, simple_wcs) assert_allclose(s2p * p2s, 1.0) def test_roundtrip_sip(self, sip_wcs): """ Roundtrip with SIP WCS should give product ~ 1. """ center_pix, s2p = sky_to_pixel_mean_scale( WCS_CENTER, sip_wcs) _, p2s = pixel_to_sky_mean_scale(center_pix, sip_wcs) assert_allclose(s2p * p2s, 1.0) def test_consistency_offset_jacobian(self, simple_wcs): """ For a simple WCS, both mean-scale paths should agree. """ # Offset path (via dispatch) c1, s1 = sky_to_pixel_mean_scale(WCS_CENTER, simple_wcs) # Jacobian path (direct call) c2, s2 = jacobian_sky_to_pixel_mean_scale( WCS_CENTER, simple_wcs) assert_allclose(c1[0], c2[0]) assert_allclose(c1[1], c2[1]) assert_allclose(s1, s2) class TestGWCSDispatch: """ Test that dispatch helpers correctly handle WCS objects without the ``has_distortion`` attribute (e.g., GWCS), defaulting to the Jacobian path. """ def test_no_has_distortion_attr(self, mock_gwcs): """ The mock should not have has_distortion. """ assert not hasattr(mock_gwcs, 'has_distortion') def test_sky_to_pixel_mean_scale_uses_jacobian(self, mock_gwcs): """ Without has_distortion, should use the Jacobian path. """ pix_position, scale = sky_to_pixel_mean_scale(WCS_CENTER, mock_gwcs) assert isinstance(pix_position, tuple) assert scale > 0 def test_pixel_to_sky_mean_scale_uses_jacobian(self, mock_gwcs): """ Without has_distortion, should use the Jacobian path. """ xy_coord = (9.5, 9.5) sky_position, scale = pixel_to_sky_mean_scale(xy_coord, mock_gwcs) assert isinstance(sky_position, SkyCoord) assert scale > 0 def test_gwcs_scale_matches_simple(self, mock_gwcs, simple_wcs): """ The mock GWCS (Jacobian path) should give scales close to the simple WCS (offset path). """ _, scale_offset = sky_to_pixel_mean_scale( WCS_CENTER, simple_wcs) _, scale_jac = sky_to_pixel_mean_scale( WCS_CENTER, mock_gwcs) assert_allclose(scale_offset, scale_jac) class TestSVDShapeConversions: """ Tests for `pixel_shape_to_sky_svd` and `sky_shape_to_pixel_svd`. """ def test_pixel_to_sky_return_types(self, simple_wcs, center_xy_coord): """ Should return (SkyCoord, float, float, Angle). """ center, w, h, angle = pixel_shape_to_sky_svd( center_xy_coord, simple_wcs, 10.0, 5.0, 0.5) assert isinstance(center, SkyCoord) assert isinstance(w, (float, np.floating)) assert isinstance(h, (float, np.floating)) assert isinstance(angle, Angle) def test_sky_to_pixel_return_types(self, simple_wcs): """ Should return (tuple, float, float, Angle). """ center, w, h, angle = sky_shape_to_pixel_svd( WCS_CENTER, simple_wcs, 36.0, 18.0, 0.5) assert isinstance(center, tuple) assert isinstance(w, (float, np.floating)) assert isinstance(h, (float, np.floating)) assert isinstance(angle, Angle) def test_roundtrip_sky_pixel_sky(self, simple_wcs): """ Sky -> pixel -> sky should recover the original ellipse. """ sky_w, sky_h, sky_a = 36.0, 18.0, 0.5 center_pix, pw, ph, pa = sky_shape_to_pixel_svd( WCS_CENTER, simple_wcs, sky_w, sky_h, sky_a) _, rw, rh, ra = pixel_shape_to_sky_svd( center_pix, simple_wcs, pw, ph, pa.rad) assert_allclose(rw, sky_w, rtol=1e-6) assert_allclose(rh, sky_h, rtol=1e-6) assert_allclose(ra.rad, sky_a, rtol=1e-4) def test_roundtrip_pixel_sky_pixel(self, simple_wcs, center_xy_coord): """ Pixel -> sky -> pixel should recover the original ellipse. """ pix_w, pix_h, pix_a = 10.0, 5.0, 0.3 _, sw, sh, sa = pixel_shape_to_sky_svd( center_xy_coord, simple_wcs, pix_w, pix_h, pix_a) _, rw, rh, ra = sky_shape_to_pixel_svd( WCS_CENTER, simple_wcs, sw, sh, sa.rad) assert_allclose(rw, pix_w, rtol=1e-6) assert_allclose(rh, pix_h, rtol=1e-6) assert_allclose(ra.rad, pix_a, rtol=1e-4) def test_simple_wcs_width_height_scale(self, simple_wcs, center_xy_coord): """ For a simple WCS, pixel dimensions should scale by WCS_CDELT_ARCSEC. """ pix_w, pix_h = 10.0, 5.0 _, sw, sh, _ = pixel_shape_to_sky_svd( center_xy_coord, simple_wcs, pix_w, pix_h, 0.0) assert_allclose(sw, pix_w * WCS_CDELT_ARCSEC, rtol=1e-5) assert_allclose(sh, pix_h * WCS_CDELT_ARCSEC, rtol=1e-5) def test_height_larger_than_width(self, simple_wcs, center_xy_coord): """ When height > width, the SVD should still correctly assign widths and heights. """ pix_w, pix_h = 5.0, 10.0 _, sw, sh, _ = pixel_shape_to_sky_svd( center_xy_coord, simple_wcs, pix_w, pix_h, 0.0) # Width should be smaller than height in sky coords too assert sw < sh def test_sip_wcs_positive_sizes(self, sip_wcs): """ Sizes should be positive for distorted WCS. """ xy_coord = (9.5, 9.5) _, sw, sh, _ = pixel_shape_to_sky_svd( xy_coord, sip_wcs, 8.0, 4.0, 0.0) assert sw > 0 assert sh > 0 def test_sip_wcs_roundtrip(self, sip_wcs): """ Roundtrip with SIP WCS should recover the original ellipse. """ sky_w, sky_h, sky_a = 0.36, 0.18, 0.7 center_pix, pw, ph, pa = sky_shape_to_pixel_svd( WCS_CENTER, sip_wcs, sky_w, sky_h, sky_a) _, rw, rh, ra = pixel_shape_to_sky_svd( center_pix, sip_wcs, pw, ph, pa.rad) assert_allclose(rw, sky_w, rtol=1e-5) assert_allclose(rh, sky_h, rtol=1e-5) assert_allclose(ra.rad, sky_a, rtol=1e-4) def test_angle_wrapped(self, simple_wcs, center_xy_coord): """ The output angle should be in [0, 360) degrees. """ _, _, _, angle = pixel_shape_to_sky_svd( center_xy_coord, simple_wcs, 10.0, 5.0, 0.5) assert 0.0 <= angle.deg < 360.0 @pytest.mark.parametrize('angle_deg', [0.0, 40.0, 130.0]) def test_circular_input_preserves_angle(self, rotated_wcs, angle_deg): """ For a circular input shape (width == height), the SVD principal axis is arbitrary, so the helper falls back to the mapped width semi-axis to preserve the input rotation angle. The angle must round-trip through pixel -> sky -> pixel. """ pixcoord = (9.5, 9.5) angle_rad = np.deg2rad(angle_deg) sky_center, sw, sh, sky_angle = pixel_shape_to_sky_svd( pixcoord, rotated_wcs, 6.0, 6.0, angle_rad) assert_allclose(sw, sh, rtol=1e-8) _, pw, ph, pix_angle = sky_shape_to_pixel_svd( sky_center, rotated_wcs, sw, sh, sky_angle.rad) assert_allclose(pw, ph, rtol=1e-8) diff = (pix_angle.deg - angle_deg + 180) % 360 - 180 assert_allclose(diff, 0.0, atol=1e-6) @pytest.mark.parametrize(('center_ra', 'center_dec'), TROUBLESOME_CENTERS) def test_roundtrip_at_pole_and_wrap(self, center_ra, center_dec): """ Regression test: a directed sky shape converted to pixels and back must round-trip to itself near the celestial poles and across the RA = 0 / 360 wraparound. """ wcs = _make_sip_wcs(center_ra, center_dec) skycoord = SkyCoord(center_ra * u.deg, center_dec * u.deg) center, pw, ph, pa = sky_shape_to_pixel_svd( skycoord, wcs, 2.0, 1.0, np.deg2rad(30)) assert pw > 1.0 assert ph > 1.0 _, sw, sh, _ = pixel_shape_to_sky_svd( center, wcs, pw, ph, pa.to_value(u.radian)) assert_allclose(sw, 2.0, rtol=1e-3) assert_allclose(sh, 1.0, rtol=1e-3) def _project_sky_ellipse_boundary(skycoord, wcs, a_arcsec, b_arcsec, pa_rad, *, npts=720): """ Project the boundary of a sky ellipse to pixel coordinates. The boundary points are computed in the tangent plane (``xi`` = East, ``eta`` = North) and offset from ``skycoord`` using great-circle geometry, then converted to pixel coordinates with ``wcs.world_to_pixel``. This is an independent ground truth for the pixel image of a sky ellipse. """ theta = np.linspace(0, 2 * np.pi, npts, endpoint=False) xi = (a_arcsec * np.cos(theta) * np.sin(pa_rad) + b_arcsec * np.sin(theta) * np.cos(pa_rad)) eta = (a_arcsec * np.cos(theta) * np.cos(pa_rad) - b_arcsec * np.sin(theta) * np.sin(pa_rad)) sep = np.hypot(xi, eta) * u.arcsec posang = np.arctan2(xi, eta) * u.rad pts = skycoord.directional_offset_by(posang, sep) x, y = wcs.world_to_pixel(pts) return np.asarray(x, dtype=float), np.asarray(y, dtype=float) def _ellipse_implicit_residual(x, y, center, width, height, angle_rad): """ Evaluate the implicit ellipse equation for points ``(x, y)``. Returns ``(u / a)**2 + (v / b)**2`` where ``(u, v)`` are the point offsets from ``center`` rotated into the ellipse frame and ``a``, ``b`` are the semi-axes. Points exactly on the ellipse boundary yield 1. """ cx, cy = center dx = x - cx dy = y - cy cos_a = np.cos(angle_rad) sin_a = np.sin(angle_rad) u_axis = dx * cos_a + dy * sin_a v_axis = -dx * sin_a + dy * cos_a return (u_axis / (width / 2)) ** 2 + (v_axis / (height / 2)) ** 2 class TestFlippedParityWCS: """ Regression tests for sky <-> pixel shape conversions with a flipped-parity WCS (North down, East left; positive determinant). Such a WCS previously produced apertures that were mirrored about the x-axis relative to the correct orientation. """ PA_DEGS = (0.0, 30.0, 75.0, 130.0, 228.0, 310.0) def test_flipped_wcs_has_positive_parity(self, flipped_wcs): """ The flipped WCS fixture must have a positive-determinant pixel scale matrix (parity = +1), opposite the standard convention. """ assert np.linalg.det(flipped_wcs.pixel_scale_matrix) > 0 @pytest.mark.parametrize('pa_deg', PA_DEGS) def test_sky_to_pixel_matches_projection(self, flipped_wcs, pa_deg): """ The pixel ellipse from ``sky_shape_to_pixel_svd`` must match the true projection of the sky ellipse boundary (not its mirror image) for a flipped-parity WCS. """ a_arcsec, b_arcsec = 3.0, 1.5 pa_rad = np.deg2rad(pa_deg) center, pw, ph, pangle = sky_shape_to_pixel_svd( WCS_CENTER, flipped_wcs, 2 * a_arcsec, 2 * b_arcsec, pa_rad) x, y = _project_sky_ellipse_boundary( WCS_CENTER, flipped_wcs, a_arcsec, b_arcsec, pa_rad) resid = _ellipse_implicit_residual( x, y, center, pw, ph, pangle.rad) # All projected boundary points must lie on the pixel ellipse. assert_allclose(resid, 1.0, atol=1e-3) @pytest.mark.parametrize('pa_deg', PA_DEGS) def test_normal_wcs_matches_projection(self, simple_wcs, pa_deg): """ The same check for a standard-parity WCS (a control case that already worked before the fix). """ a_arcsec, b_arcsec = 3.0, 1.5 pa_rad = np.deg2rad(pa_deg) center, pw, ph, pangle = sky_shape_to_pixel_svd( WCS_CENTER, simple_wcs, 2 * a_arcsec, 2 * b_arcsec, pa_rad) x, y = _project_sky_ellipse_boundary( WCS_CENTER, simple_wcs, a_arcsec, b_arcsec, pa_rad) resid = _ellipse_implicit_residual( x, y, center, pw, ph, pangle.rad) assert_allclose(resid, 1.0, atol=1e-3) def test_roundtrip_sky_pixel_sky(self, flipped_wcs): """ Sky -> pixel -> sky should recover the original ellipse for a flipped-parity WCS. """ sky_w, sky_h, sky_a = 6.0, 3.0, np.deg2rad(228.0) center_pix, pw, ph, pa = sky_shape_to_pixel_svd( WCS_CENTER, flipped_wcs, sky_w, sky_h, sky_a) _, rw, rh, ra = pixel_shape_to_sky_svd( center_pix, flipped_wcs, pw, ph, pa.rad) assert_allclose(rw, sky_w, rtol=1e-6) assert_allclose(rh, sky_h, rtol=1e-6) assert_allclose(ra.rad, sky_a, rtol=1e-4) def _make_sheared_wcs(): """ Build an anisotropic, sheared (non-orthogonal CD matrix) TAN WCS. The unequal, non-orthogonal CD terms make the local Jacobian both anisotropic and rotated, so the SVD principal axes have a nontrivial orientation. This exercises the angle convention of the scale helpers. """ wcs = APWCS(naxis=2) wcs.wcs.crpix = [50, 50] wcs.wcs.crval = [WCS_CENTER.ra.deg, WCS_CENTER.dec.deg] wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] wcs.wcs.cd = np.array([[-0.0008, 0.0003], [0.0002, 0.0005]]) return wcs def _project_pixel_circle_to_sky_tangent(pixcoord, wcs, radius_pix, *, npts=720): """ Project a pixel circle boundary to tangent-plane sky offsets. The boundary points of a pixel circle are converted to sky coordinates and expressed as tangent-plane offsets (``xi`` = East, ``eta`` = North) in arcsec relative to the circle center, using great-circle separation and position angle. This is an independent ground truth for the sky image of a pixel circle. """ theta = np.linspace(0, 2 * np.pi, npts, endpoint=False) cx, cy = pixcoord x = cx + radius_pix * np.cos(theta) y = cy + radius_pix * np.sin(theta) center = wcs.pixel_to_world(cx, cy) pts = wcs.pixel_to_world(x, y) arcsec_per_rad = 3600.0 * np.degrees(1) sep = center.separation(pts).rad posang = center.position_angle(pts).rad xi = sep * np.sin(posang) * arcsec_per_rad eta = sep * np.cos(posang) * arcsec_per_rad return np.asarray(xi, dtype=float), np.asarray(eta, dtype=float) def _sky_ellipse_implicit_residual(xi, eta, width, height, pa_rad): """ Evaluate the implicit sky-ellipse equation in the tangent plane. The major axis (of full length ``width``) is at position angle ``pa_rad`` measured from North (``eta``) toward East (``xi``). Points exactly on the ellipse boundary yield 1. """ cos_pa = np.cos(pa_rad) sin_pa = np.sin(pa_rad) p_major = xi * sin_pa + eta * cos_pa p_minor = xi * cos_pa - eta * sin_pa return (p_major / (width / 2)) ** 2 + (p_minor / (height / 2)) ** 2 class TestSVDScales: """ Tests for `sky_to_pixel_svd_scales` and `pixel_to_sky_svd_scales`. """ def test_sky_to_pixel_return_types(self, simple_wcs): """ Should return (tuple, float, float, Angle). """ center, smaj, smin, angle = sky_to_pixel_svd_scales( WCS_CENTER, simple_wcs) assert isinstance(center, tuple) assert isinstance(smaj, (float, np.floating)) assert isinstance(smin, (float, np.floating)) assert isinstance(angle, Angle) def test_pixel_to_sky_return_types(self, simple_wcs, center_xy_coord): """ Should return (SkyCoord, float, float, Angle). """ center, smaj, smin, angle = pixel_to_sky_svd_scales( center_xy_coord, simple_wcs) assert isinstance(center, SkyCoord) assert isinstance(smaj, (float, np.floating)) assert isinstance(smin, (float, np.floating)) assert isinstance(angle, Angle) def test_sky_to_pixel_simple_isotropic(self, simple_wcs): """ For a non-distorted, square-pixel WCS the two scale factors are equal and match 1 / WCS_CDELT_ARCSEC (pixels per arcsec). """ _, smaj, smin, angle = sky_to_pixel_svd_scales( WCS_CENTER, simple_wcs) expected = 1.0 / WCS_CDELT_ARCSEC assert_allclose(smaj, expected, rtol=1e-6) assert_allclose(smin, expected, rtol=1e-6) assert 0.0 <= angle.deg < 360.0 def test_pixel_to_sky_simple_isotropic(self, simple_wcs, center_xy_coord): """ For a non-distorted, square-pixel WCS the two scale factors are equal and match WCS_CDELT_ARCSEC (arcsec per pixel). """ _, smaj, smin, angle = pixel_to_sky_svd_scales( center_xy_coord, simple_wcs) assert_allclose(smaj, WCS_CDELT_ARCSEC, rtol=1e-6) assert_allclose(smin, WCS_CDELT_ARCSEC, rtol=1e-6) assert 0.0 <= angle.deg < 360.0 def test_sky_to_pixel_nonsquare_anisotropic(self, nonsquare_wcs): """ For non-square pixels the major scale exceeds the minor scale, and both match the inverse of the corresponding pixel scales. """ _, smaj, smin, _ = sky_to_pixel_svd_scales(WCS_CENTER, nonsquare_wcs) assert smaj > smin # cdelt = [-0.03, 0.05] deg; pixels/arcsec = 1 / (cdelt_arcsec) assert_allclose(smaj, 1.0 / (0.03 * 3600), rtol=1e-5) assert_allclose(smin, 1.0 / (0.05 * 3600), rtol=1e-5) def test_pixel_to_sky_nonsquare_anisotropic(self, nonsquare_wcs): """ For non-square pixels the major scale exceeds the minor scale, and both match the corresponding pixel scales (arcsec/pixel). """ center, smaj, smin, _ = pixel_to_sky_svd_scales((9.5, 9.5), nonsquare_wcs) assert isinstance(center, SkyCoord) assert smaj > smin assert_allclose(smaj, 0.05 * 3600, rtol=1e-5) assert_allclose(smin, 0.03 * 3600, rtol=1e-5) def test_scales_descending(self, sip_wcs): """ The returned scales are singular values in descending order, so the major scale is always >= the minor scale. """ _, smaj_s, smin_s, _ = sky_to_pixel_svd_scales(WCS_CENTER, sip_wcs) _, smaj_p, smin_p, _ = pixel_to_sky_svd_scales((9.5, 9.5), sip_wcs) assert smaj_s >= smin_s assert smaj_p >= smin_p @pytest.mark.parametrize('wcs_name', ['rotated_wcs', 'flipped_wcs']) def test_sky_to_pixel_matches_projection(self, wcs_name, request): """ A circular sky region converted with the SVD scales must match the true projection of the sky circle boundary, including the correct major-axis orientation and parity. """ wcs = request.getfixturevalue(wcs_name) radius = 5.0 # arcsec center, smaj, smin, pixel_angle = sky_to_pixel_svd_scales( WCS_CENTER, wcs) x, y = _project_sky_ellipse_boundary( WCS_CENTER, wcs, radius, radius, 0.0) resid = _ellipse_implicit_residual( x, y, center, 2 * radius * smaj, 2 * radius * smin, pixel_angle.rad) assert_allclose(resid, 1.0, atol=1e-3) def test_sky_to_pixel_matches_projection_sheared(self): """ For an anisotropic, sheared WCS the projected sky circle is an ellipse whose principal axes and orientation must match the SVD scales and pixel angle. """ wcs = _make_sheared_wcs() radius = 20.0 # arcsec center, smaj, smin, pixel_angle = sky_to_pixel_svd_scales( WCS_CENTER, wcs) assert smaj > smin x, y = _project_sky_ellipse_boundary( WCS_CENTER, wcs, radius, radius, 0.0) resid = _ellipse_implicit_residual( x, y, center, 2 * radius * smaj, 2 * radius * smin, pixel_angle.rad) assert_allclose(resid, 1.0, atol=2e-3) def test_pixel_to_sky_matches_projection_sheared(self): """ For an anisotropic, sheared WCS the sky image of a pixel circle is an ellipse whose principal axes and position angle must match the SVD scales and sky angle. """ wcs = _make_sheared_wcs() pixcoord = (50.0, 50.0) radius_pix = 10.0 _, smaj, smin, sky_angle = pixel_to_sky_svd_scales(pixcoord, wcs) assert smaj > smin xi, eta = _project_pixel_circle_to_sky_tangent( pixcoord, wcs, radius_pix) resid = _sky_ellipse_implicit_residual( xi, eta, 2 * radius_pix * smaj, 2 * radius_pix * smin, sky_angle.rad) assert_allclose(resid, 1.0, atol=2e-3) def test_inverse_scale_consistency(self): """ The singular values of the Jacobian and its inverse are reciprocals, so the pixel-to-sky major (minor) scale equals the reciprocal of the sky-to-pixel minor (major) scale. """ wcs = _make_sheared_wcs() center_pix, smaj_p, smin_p, _ = sky_to_pixel_svd_scales( WCS_CENTER, wcs) _, smaj_s, smin_s, _ = pixel_to_sky_svd_scales(center_pix, wcs) assert_allclose(smaj_s, 1.0 / smin_p, rtol=1e-6) assert_allclose(smin_s, 1.0 / smaj_p, rtol=1e-6) def test_angles_wrapped(self): """ Both helpers return angles wrapped to [0, 360) degrees. """ wcs = _make_sheared_wcs() _, _, _, pixel_angle = sky_to_pixel_svd_scales(WCS_CENTER, wcs) _, _, _, sky_angle = pixel_to_sky_svd_scales((50.0, 50.0), wcs) assert 0.0 <= pixel_angle.deg < 360.0 assert 0.0 <= sky_angle.deg < 360.0 astropy-regions-f4139aa/regions/_utils/wcs_helpers.py000066400000000000000000000704571521703212700231430ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tools for WCS helpers. """ import astropy.units as u import numpy as np from astropy.coordinates import Angle def _has_distortion(wcs): """ Return True if the WCS has distortions or is non-FITS. """ return getattr(wcs, 'has_distortion', True) def _sky_to_pixel_jacobian(skycoord, wcs): """ Set up common values for sky-to-pixel Jacobian-based conversions. Returns the pixel center, the local Jacobian matrix, and the WCS parity. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The sky coordinate of the region center. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- center : tuple of float The ``(x, y)`` pixel center position. jacobian : 2x2 `~numpy.ndarray` The Jacobian matrix ``d(pixel)/d(sky_arcsec)``. parity : float The sign of ``det(jacobian)`` (+1 or -1). """ x0, y0 = wcs.world_to_pixel(skycoord) center = (float(x0), float(y0)) jacobian = compute_local_wcs_jacobian(skycoord, wcs) parity = np.sign(np.linalg.det(jacobian)) return center, jacobian, parity def _pixel_to_sky_jacobian(pixcoord, wcs): """ Set up common values for pixel-to-sky Jacobian-based conversions. Returns the sky center, the local Jacobian matrix, its inverse, and the WCS parity. Parameters ---------- pixcoord : tuple of float The ``(x, y)`` pixel coordinate of the region center. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- center : `~astropy.coordinates.SkyCoord` The sky center position. jacobian : 2x2 `~numpy.ndarray` The Jacobian matrix ``d(pixel)/d(sky_arcsec)``. jacobian_inv : 2x2 `~numpy.ndarray` The inverse Jacobian ``d(sky_arcsec)/d(pixel)``. parity : float The sign of ``det(jacobian)`` (+1 or -1). """ center = wcs.pixel_to_world(pixcoord[0], pixcoord[1]) jacobian = compute_local_wcs_jacobian(center, wcs) jacobian_inv = np.linalg.inv(jacobian) parity = np.sign(np.linalg.det(jacobian)) return center, jacobian, jacobian_inv, parity def _svd_ellipse_from_composite(m_comp, *, width_col_idx=0, sky_angle=False, input_circular=False): """ Extract ellipse width, height, and angle from a composite matrix using SVD. Given a 2x2 matrix ``m_comp`` whose columns represent the mapped semi-axis vectors of an ellipse, perform SVD and return the full widths, heights, and rotation angle, preserving the width/height assignment of the input ellipse. Parameters ---------- m_comp : 2x2 `~numpy.ndarray` The composite matrix whose SVD gives the output ellipse axes. width_col_idx : int, optional The column index (0 or 1) of ``m_comp`` that corresponds to the width semi-axis. Default is 0. sky_angle : bool, optional If True, the composite matrix columns are tangent-plane (``xi`` = East, ``eta`` = North) vectors, so the returned angle is a sky position angle (PA) measured from North toward East. If False (the default), the columns are pixel (``x``, ``y``) vectors and the returned angle is measured counterclockwise from the positive x-axis. input_circular : bool, optional If True, the input ellipse is known to be circular (width == height), so the SVD's principal axis is meaningless and the rotation angle is taken directly from the mapped width semi- axis. Default is False. Returns ------- out_width : float The full width of the output ellipse. out_height : float The full height of the output ellipse. angle : `~astropy.coordinates.Angle` The rotation angle of the width axis, wrapped to [0, 360) degrees. """ u_mat, s_vals, _vt = np.linalg.svd(m_comp) # SVD returns singular values in descending order, so s_vals[0] # corresponds to the major axis. Determine whether the major axis # corresponds to the width or height by checking alignment with the # mapped width semi-axis. width_col = m_comp[:, width_col_idx] if (np.abs(np.dot(u_mat[:, 0], width_col)) >= np.abs(np.dot(u_mat[:, 1], width_col))): # Major axis aligns with width out_width = 2 * s_vals[0] out_height = 2 * s_vals[1] angle_col = u_mat[:, 0] else: # Major axis aligns with height; swap out_width = 2 * s_vals[1] out_height = 2 * s_vals[0] angle_col = u_mat[:, 1] # Fix SVD sign ambiguity: ensure the angle direction aligns with the # mapped width semi-axis if np.dot(angle_col, width_col) < 0: angle_col = -angle_col # When the input ellipse is circular (width == height), the SVD's # principal direction has no physical meaning: any rotation of a # circle yields an identical shape, so the SVD picks an arbitrary # principal axis derived from the Jacobian. To preserve the input # rotation angle, fall back to the mapped width semi-axis direction # (which carries the input theta through the Jacobian). if input_circular: width_norm = np.linalg.norm(width_col) if width_norm > 0: angle_col = width_col / width_norm # Compute the rotation angle if sky_angle: # Sky position angle (PA) measured from North (eta/Dec) toward # East. The composite-matrix columns are tangent-plane vectors # with components (xi=East, eta=North), so the PA is simply # arctan2(xi, eta). The local Jacobian (or its inverse) used to # build the composite matrix already encodes the WCS parity, so # no additional parity correction is needed here. angle = Angle( np.rad2deg(np.arctan2(angle_col[0], angle_col[1])) * u.deg, ).wrap_at(360 * u.deg) else: # Pixel angle: measured from +x toward +y angle = Angle( np.rad2deg(np.arctan2(angle_col[1], angle_col[0])) * u.deg, ).wrap_at(360 * u.deg) return out_width, out_height, angle def jacobian_sky_to_pixel_mean_scale(skycoord, wcs): """ Compute the pixel center and isotropic (mean) scale factor for a sky-to-pixel conversion using SVD of the local WCS Jacobian. This function is used for circular regions (circles and circle annuli) where a single isotropic scale factor is needed to preserve the circular shape. The scale factor is the mean of the two singular values of the Jacobian matrix. Singular Value Decomposition (SVD) of the 2x2 Jacobian ``J`` yields ``J = U @ diag(s1, s2) @ V^T``, where ``s1`` and ``s2`` are the singular values representing the maximum and minimum stretch factors of the linear transformation. Using their mean as the scale factor is the best isotropic approximation to the (potentially anisotropic) Jacobian, in the sense that it minimizes the sum of squared residuals between the true (elliptical) mapping and the isotropic (circular) approximation. For a WCS without distortion and with equal pixel scales in x and y, ``s1 == s2`` and the mean is exact. For distorted WCS or non-square pixels, the two singular values may differ, and the mean provides a balanced compromise. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The sky coordinate of the region center. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- center : tuple of float The ``(x, y)`` pixel center position. mean_scale : float The mean scale factor (pixels per arcsec), computed as the mean of the two singular values of the Jacobian. """ center, jacobian, _ = _sky_to_pixel_jacobian(skycoord, wcs) scales = np.linalg.svd(jacobian, compute_uv=False) # Mean of singular values gives the best isotropic approximation return center, np.mean(scales) def jacobian_pixel_to_sky_mean_scale(pixcoord, wcs): """ Compute the sky center and isotropic (mean) scale factor for a pixel-to-sky conversion using SVD of the inverse Jacobian. This is the inverse of `jacobian_sky_to_pixel_mean_scale`. It is used for circular pixel regions (circles and circle annuli) where a single isotropic scale factor is needed to preserve the circular shape. The inverse Jacobian ``J^{-1} = d(sky)/d(pixel)`` maps pixel offsets to tangent-plane offsets. Its singular values represent the maximum and minimum angular extents per pixel. The mean of these singular values provides the best isotropic approximation for converting pixel radii to sky angular radii. Parameters ---------- pixcoord : tuple of float The ``(x, y)`` pixel coordinate of the region center. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- center : `~astropy.coordinates.SkyCoord` The sky center position. mean_scale : float The mean scale factor (arcsec per pixel), computed as the mean of the two singular values of the inverse Jacobian. """ center, _, jacobian_inv, _ = _pixel_to_sky_jacobian(pixcoord, wcs) scales = np.linalg.svd(jacobian_inv, compute_uv=False) # Mean of singular values gives the best isotropic approximation return center, np.mean(scales) def compute_local_wcs_jacobian(skycoord, wcs): """ Compute the local 2x2 Jacobian matrix d(pixel)/d(tangent-plane) at the given sky coordinate using 1-pixel finite differences. The Jacobian matrix ``J`` linearizes the WCS transformation in the neighborhood of ``skycoord``. It maps infinitesimal offsets in the tangent-plane coordinate system (in arcsec) to pixel coordinate offsets (in pixels):: [dx, dy]^T ~ J @ [d_xi, d_eta]^T The tangent-plane coordinate system has two orthogonal axes: * ``xi`` (RA direction): offset along Right Ascension, increasing to the East. * ``eta`` (Dec direction): offset along Declination, increasing to the North. The Jacobian is computed by making 1-pixel offsets in x and y, converting the resulting pixel positions to sky coordinates, and measuring the tangent-plane displacements in arcsec. The (xi, eta) offsets are computed from the great-circle separation and position angle between the center and each offset point, so the formula is well-defined at the celestial poles and across the longitude wraparound (RA = 0 / 360). This gives the forward Jacobian ``F = d(sky_arcsec)/d(pixel)``, which is then inverted to obtain ``J = F^{-1} = d(pixel)/d(sky_arcsec)``. Using 1-pixel steps ensures numerical stability across all pixel scales. This function works with any WCS that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`), because it relies only on the ``world_to_pixel`` and ``pixel_to_world`` methods. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The sky coordinate at which to evaluate the Jacobian. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- jacobian : 2x2 `~numpy.ndarray` The Jacobian matrix ``J`` such that ``[dx, dy]^T ≈ J @ [d_xi, d_eta]^T``, with units of pixels/arcsec. """ # Reference pixel position x0, y0 = wcs.world_to_pixel(skycoord) # Sky positions at 1-pixel offsets in x and y sky0 = wcs.pixel_to_world(x0, y0) sky_x = wcs.pixel_to_world(x0 + 1, y0) sky_y = wcs.pixel_to_world(x0, y0 + 1) # Compute tangent-plane offsets (xi, eta) in arcsec from the # great-circle separation and position angle to the offset point. # Position angle is measured from North (eta) toward East (xi), # counterclockwise as seen on the sky from outside. # This formulation is intrinsically wrap-safe (no longitude # subtractions) and pole-safe (no division by cos(dec)). arcsec_per_rad = 3600.0 * np.degrees(1) sep_x = sky0.separation(sky_x).rad pa_x = sky0.position_angle(sky_x).rad sep_y = sky0.separation(sky_y).rad pa_y = sky0.position_angle(sky_y).rad dxi_x = sep_x * np.sin(pa_x) * arcsec_per_rad deta_x = sep_x * np.cos(pa_x) * arcsec_per_rad dxi_y = sep_y * np.sin(pa_y) * arcsec_per_rad deta_y = sep_y * np.cos(pa_y) * arcsec_per_rad # Forward Jacobian F = d(sky_arcsec)/d(pixel), shape (2, 2). # Rows are (xi, eta), columns are (px_x, px_y). forward = np.array([[dxi_x, dxi_y], [deta_x, deta_y]]) # Invert to get J = d(pixel)/d(sky_arcsec) return np.linalg.inv(forward) def sky_to_pixel_mean_scale(skycoord, wcs): """ Convert a sky region center to pixel coordinates with an isotropic scale factor. For a WCS without distortion, this uses the `wcs_pixel_scale_angle` offset method. For a WCS with distortion (or a non-astropy WCS like GWCS), this uses the SVD of the local Jacobian matrix via `jacobian_sky_to_pixel_mean_scale`. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The sky coordinate of the region center. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- center : tuple of float The ``(x, y)`` pixel center position. mean_scale : float The mean scale factor (pixels per arcsec). """ # Non-FITS WCS (e.g., GWCS) and astropy.wcs.WCS with distortions # should use the Jacobian method to compute the pixel scales and # angle. if not _has_distortion(wcs): center, pixscale, _ = wcs_pixel_scale_angle(skycoord, wcs) return center, 1.0 / pixscale return jacobian_sky_to_pixel_mean_scale(skycoord, wcs) def pixel_to_sky_mean_scale(pixcoord, wcs): """ Convert a pixel region center to sky coordinates with an isotropic scale factor. For a WCS without distortion, this uses the `wcs_pixel_scale_angle` offset method. For a WCS with distortion (or a non-astropy WCS like GWCS), this uses the SVD of the inverse Jacobian matrix via `jacobian_pixel_to_sky_mean_scale`. Parameters ---------- pixcoord : tuple of float The ``(x, y)`` pixel coordinate of the region center. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- center : `~astropy.coordinates.SkyCoord` The sky center position. mean_scale : float The mean scale factor (arcsec per pixel). """ # Non-FITS WCS (e.g., GWCS) and astropy.wcs.WCS with distortions # should use the Jacobian method to compute the pixel scales and # angle. if not _has_distortion(wcs): center = wcs.pixel_to_world(pixcoord[0], pixcoord[1]) _, pixscale, _ = wcs_pixel_scale_angle(center, wcs) return center, pixscale return jacobian_pixel_to_sky_mean_scale(pixcoord, wcs) def pixel_shape_to_sky_svd(pixcoord, wcs, width, height, pixel_angle_rad): """ Convert a pixel ellipse to a sky ellipse using SVD. This builds the composite matrix ``M_sky = J^{-1} @ M_pix`` where ``M_pix`` encodes the pixel ellipse semi-axes and rotation, and ``J^{-1}`` is the local inverse Jacobian. The SVD of ``M_sky`` gives the exact sky ellipse semi-axes and orientation. This handles WCS shear correctly: the sky image of a pixel ellipse is always an ellipse, and SVD extracts its true principal axes, regardless of whether the Jacobian's mapped width and height directions are orthogonal. Parameters ---------- pixcoord : tuple of float The ``(x, y)`` pixel coordinate of the ellipse center. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). width : float The full width of the pixel ellipse (before rotation) in pixels. height : float The full height of the pixel ellipse (before rotation) in pixels. pixel_angle_rad : float The pixel rotation angle in radians. This is the angle of the ellipse's width axis measured counterclockwise from the positive x-axis. Returns ------- center : `~astropy.coordinates.SkyCoord` The sky center position. sky_width : float The full width of the sky ellipse in arcsec. sky_height : float The full height of the sky ellipse in arcsec. sky_angle : `~astropy.coordinates.Angle` The sky position angle (PA) of the width axis, measured counterclockwise from North (the latitude/Dec axis), wrapped to [0, 360) degrees. """ center, _, jacobian_inv, _ = _pixel_to_sky_jacobian(pixcoord, wcs) # Build M_pix: columns are pixel semi-axis vectors cos_a = np.cos(pixel_angle_rad) sin_a = np.sin(pixel_angle_rad) half_w = 0.5 * width half_h = 0.5 * height m_pix = np.array([[half_w * cos_a, -half_h * sin_a], [half_w * sin_a, half_h * cos_a]]) # M_sky = J^{-1} @ M_pix: columns are sky semi-axis vectors m_sky = jacobian_inv @ m_pix sky_width, sky_height, sky_angle = _svd_ellipse_from_composite( m_sky, sky_angle=True, input_circular=np.isclose(width, height)) return center, sky_width, sky_height, sky_angle def sky_shape_to_pixel_svd(skycoord, wcs, width_arcsec, height_arcsec, sky_angle_rad): """ Convert a sky ellipse to a pixel ellipse using SVD. This builds the composite matrix ``M_pix = J @ M_sky`` where ``M_sky`` encodes the sky ellipse semi-axes and rotation, and ``J`` is the local Jacobian. The SVD of ``M_pix`` gives the exact pixel ellipse semi-axes and orientation. This handles WCS shear correctly: the pixel image of a sky ellipse is always an ellipse, and SVD extracts its true principal axes, regardless of whether the Jacobian's mapped width and height directions are orthogonal. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The sky coordinate of the ellipse center. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). width_arcsec : float The full width of the sky ellipse in arcsec. height_arcsec : float The full height of the sky ellipse in arcsec. sky_angle_rad : float The sky rotation angle in radians as a position angle (PA). This is the angle of the ellipse's width axis measured counterclockwise from North (the latitude/Dec axis). Returns ------- center : tuple of float The ``(x, y)`` pixel center position. pixel_width : float The full width of the pixel ellipse in pixels. pixel_height : float The full height of the pixel ellipse in pixels. pixel_angle : `~astropy.coordinates.Angle` The pixel rotation angle of the width axis, measured counterclockwise from the positive x-axis, wrapped to [0, 360) degrees. """ center, jacobian, _ = _sky_to_pixel_jacobian(skycoord, wcs) # Build M_sky: columns are sky semi-axis vectors in tangent-plane # coordinates (xi=East, eta=North). The width axis is at the given # PA from North (toward East), so its tangent-plane components are # (sin(PA), cos(PA)); the height axis is perpendicular, at PA+90. # The local Jacobian already encodes the WCS parity, so no manual # parity factor is applied here. cos_pa = np.cos(sky_angle_rad) sin_pa = np.sin(sky_angle_rad) half_w = 0.5 * width_arcsec half_h = 0.5 * height_arcsec m_sky = np.array([[half_w * sin_pa, half_h * cos_pa], [half_w * cos_pa, -half_h * sin_pa]]) # M_pix = J @ M_sky: columns are pixel semi-axis vectors m_pix = jacobian @ m_sky pixel_width, pixel_height, pixel_angle = _svd_ellipse_from_composite( m_pix, input_circular=np.isclose(width_arcsec, height_arcsec)) return center, pixel_width, pixel_height, pixel_angle def sky_to_pixel_svd_scales(skycoord, wcs): """ Compute the pixel center, principal-axis scale factors, and pixel angle for a sky-to-pixel conversion using SVD of the local Jacobian. Uses the singular value decomposition (SVD) of the local Jacobian ``J = d(pixel)/d(sky_arcsec)`` to find the natural principal axes of the WCS transformation at the given sky position. The singular values give the scale factors along the major and minor axes of the ellipse that a unit circle in sky space maps to in pixel space. The left singular vectors give the directions of those axes in pixel coordinates. This is the appropriate method for converting a circular sky region to a pixel ellipse, as the resulting ellipse accurately represents the true shape of the WCS mapping (i.e., the tightest-fitting pixel ellipse that contains the sky circle). Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The sky coordinate of the region center. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- center : tuple of float The ``(x, y)`` pixel center position. scale_major : float The scale factor along the major (maximum-stretch) axis (pixels per arcsec). scale_minor : float The scale factor along the minor (minimum-stretch) axis (pixels per arcsec). pixel_angle : `~astropy.coordinates.Angle` The pixel rotation angle of the major axis, measured counterclockwise from the positive x-axis, wrapped to [0, 360) degrees. """ center, jacobian, _ = _sky_to_pixel_jacobian(skycoord, wcs) u_mat, s_vals, _vt = np.linalg.svd(jacobian) # Pixel angle of the major axis: direction of u_mat[:, 0] in pixel # space. No parity correction is needed because pixel space has no # axis reflection. pixel_angle = Angle( np.rad2deg(np.arctan2(u_mat[1, 0], u_mat[0, 0])) * u.deg, ).wrap_at(360 * u.deg) return center, s_vals[0], s_vals[1], pixel_angle def pixel_to_sky_svd_scales(pixcoord, wcs): """ Compute the sky center, principal-axis scale factors, and sky angle for a pixel-to-sky conversion using SVD of the inverse Jacobian. Uses the singular value decomposition (SVD) of the local inverse Jacobian ``J^{-1} = d(sky)/d(pixel)`` to find the natural principal axes of the WCS transformation at the given pixel position. The singular values give the scale factors along the major and minor axes of the ellipse that a unit circle in pixel space maps to in sky space. The left singular vectors give the directions of those axes in tangent-plane coordinates. This is the appropriate method for converting a circular pixel region to a sky ellipse, as the resulting ellipse accurately represents the true shape of the WCS mapping (i.e., the tightest-fitting sky ellipse that contains the pixel circle). Parameters ---------- pixcoord : tuple of float The ``(x, y)`` pixel coordinate of the region center. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- center : `~astropy.coordinates.SkyCoord` The sky center position. scale_major : float The scale factor along the major (maximum-stretch) axis (arcsec per pixel). scale_minor : float The scale factor along the minor (minimum-stretch) axis (arcsec per pixel). sky_angle : `~astropy.coordinates.Angle` The sky position angle (PA) of the major axis, measured counterclockwise from North (the latitude/Dec axis), wrapped to [0, 360) degrees. """ center, _, jacobian_inv, _ = _pixel_to_sky_jacobian(pixcoord, wcs) u_mat, s_vals, _vt = np.linalg.svd(jacobian_inv) # Sky position angle (PA) of the major axis, measured from North # (eta/Dec) toward East (xi/RA). The columns of u_mat are # tangent-plane vectors with components (xi=East, eta=North), so the # PA is arctan2(xi, eta). The inverse Jacobian already encodes the # WCS parity, so no manual parity correction is needed here. sky_angle = Angle( np.rad2deg(np.arctan2(u_mat[0, 0], u_mat[1, 0])) * u.deg, ).wrap_at(360 * u.deg) return center, s_vals[0], s_vals[1], sky_angle def wcs_pixel_scale_angle(skycoord, wcs): """ Calculate the pixel coordinate, scale, and WCS rotation angle at the position of a sky coordinate. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The SkyCoord coordinate. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- pixcoord : tuple of float The ``(x, y)`` pixel coordinate. scale : float The pixel scale in arcsec/pixel. angle : `~astropy.coordinates.Angle` The angle measured counterclockwise from the positive x axis to the "North" axis of the celestial coordinate system, wrapped to [0, 360) degrees. Notes ----- If distortions are present in the WCS, the x and y pixel scales likely differ. This function computes independent x and y scales and takes their geometric mean. """ # Convert to pixel coordinates x, y = wcs.world_to_pixel(skycoord) pixcoord = (float(x), float(y)) # Position-dependent scale using 1-pixel offsets in x and y. # The pixel scale is the geometric mean of the two directional # scales. sky0 = wcs.pixel_to_world(x, y) sky_x = wcs.pixel_to_world(x + 1, y) sky_y = wcs.pixel_to_world(x, y + 1) cdelt_x = sky0.separation(sky_x).arcsec cdelt_y = sky0.separation(sky_y).arcsec scale = np.sqrt(cdelt_x * cdelt_y) # Compute the angle by offsetting in latitude by exactly the local # cdelt (geometric-mean pixel scale in degrees). This ensures # the finite-difference derivative probes the same scale of the # distortion field. cdelt_deg = scale / 3600 # arcsec -> deg skycoord_offset = skycoord.directional_offset_by( 0.0, cdelt_deg * u.deg) x_offset, y_offset = wcs.world_to_pixel(skycoord_offset) dx = x_offset - x dy = y_offset - y angle_rad = np.arctan2(dy, dx) angle = Angle(np.rad2deg(angle_rad) * u.deg).wrap_at(360 * u.deg) return pixcoord, scale, angle astropy-regions-f4139aa/regions/conftest.py000066400000000000000000000015461521703212700211440ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Configuration file for the pytest test suite. """ try: from pytest_astropy_header.display import (PYTEST_HEADER_MODULES, TESTED_VERSIONS) ASTROPY_HEADER = True except ImportError: ASTROPY_HEADER = False def pytest_configure(config): if ASTROPY_HEADER: config.option.astropy_header = True # Customize the following lines to add/remove entries from the # list of packages for which version numbers are displayed when # running the tests. PYTEST_HEADER_MODULES.clear() deps = ['NumPy', 'Matplotlib', 'Astropy', 'Shapely'] for dep in deps: PYTEST_HEADER_MODULES[dep] = dep.lower() from regions import __version__ TESTED_VERSIONS['regions'] = __version__ astropy-regions-f4139aa/regions/core/000077500000000000000000000000001521703212700176675ustar00rootroot00000000000000astropy-regions-f4139aa/regions/core/__init__.py000066400000000000000000000007431521703212700220040ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage provides the core functionality for regions. """ from .attributes import * # noqa: F401, F403 from .bounding_box import * # noqa: F401, F403 from .compound import * # noqa: F401, F403 from .core import * # noqa: F401, F403 from .mask import * # noqa: F401, F403 from .metadata import * # noqa: F401, F403 from .pixcoord import * # noqa: F401, F403 from .regions import * # noqa: F401, F403 astropy-regions-f4139aa/regions/core/attributes.py000066400000000000000000000172701521703212700224360ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ The module provides several custom descriptor classes for attribute validation of region classes. """ import abc import numpy as np from astropy.coordinates import SkyCoord from astropy.units import Quantity from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord __all__ = [] class RegionAttribute(abc.ABC): """ Base descriptor class for region attribute validation. Parameters ---------- doc : str, optional The documentation string for the attribute. """ def __init__(self, doc=''): self.__doc__ = doc def __set_name__(self, owner, name): self.name = name def __get__(self, instance, owner): if instance is None: return self # pragma: no cover return instance.__dict__[self.name] def __set__(self, instance, value): self._validate(value) instance.__dict__[self.name] = value def __delete__(self, instance): raise AttributeError(f'cannot delete {self.name!r}') @abc.abstractmethod def _validate(self, value): """ Validate the attribute value. An exception is raised if the value is invalid. """ raise NotImplementedError # pragma: no cover class ScalarPixCoord(RegionAttribute): """ Descriptor class to check that value is a scalar `~regions.PixCoord`. """ def _validate(self, value): if not (isinstance(value, PixCoord) and value.isscalar): raise ValueError(f'{self.name!r} must be a scalar PixCoord') class OneDPixCoord(RegionAttribute): """ Descriptor class to check that value is a 1D `~regions.PixCoord`. """ def _validate(self, value): if not (isinstance(value, PixCoord) and not value.isscalar and value.x.ndim == 1): raise ValueError(f'{self.name!r} must be a 1D PixCoord') class PositiveScalar(RegionAttribute): """ Descriptor class to check that value is a strictly positive (> 0) scalar float/int (not `~astropy.units.Quantity`). """ def _validate(self, value): if isinstance(value, Quantity): raise ValueError(f'{self.name!r} must be a scalar integer or ' 'float') if not np.isscalar(value) or value <= 0: raise ValueError(f'{self.name!r} must be a strictly positive ' 'scalar') class ScalarSkyCoord(RegionAttribute): """ Descriptor class to check that value is a scalar `~astropy.coordinates.SkyCoord`. """ def _validate(self, value): if not (isinstance(value, SkyCoord) and value.isscalar): raise ValueError(f'{self.name!r} must be a scalar SkyCoord') class OneDSkyCoord(RegionAttribute): """ Descriptor class to check that value is a 1D `~astropy.coordinates.SkyCoord`. """ def _validate(self, value): if not (isinstance(value, SkyCoord) and value.ndim == 1): raise ValueError(f'{self.name!r} must be a 1D SkyCoord') class ScalarAngle(RegionAttribute): """ Descriptor class to check that value is a scalar angle, either an `~astropy.coordinates.Angle` or `~astropy.units.Quantity` with angular units. """ def _validate(self, value): if isinstance(value, Quantity): if not value.isscalar: raise ValueError(f'{self.name!r} must be a scalar') if not value.unit.physical_type == 'angle': raise ValueError(f'{self.name!r} must have angular units') else: raise ValueError(f'{self.name!r} must be a scalar angle') class PositiveScalarAngle(RegionAttribute): """ Descriptor class to check that value is a strictly positive scalar angle, either an `~astropy.coordinates.Angle` or `~astropy.units.Quantity` with angular units. """ def _validate(self, value): if isinstance(value, Quantity): if not value.isscalar: raise ValueError(f'{self.name!r} must be a scalar') if not value.unit.physical_type == 'angle': raise ValueError(f'{self.name!r} must have angular units') if not value > 0: raise ValueError(f'{self.name!r} must be strictly positive') else: raise ValueError(f'{self.name!r} must be a strictly positive ' 'scalar angle') class TwoValAngleorNone(RegionAttribute): """ Descriptor class to check that value is a list-like array of length 2 or None, with angles having either `~astropy.coordinates.Angle` or `~astropy.units.Quantity` with angular units. """ def _validate(self, value): if value is None: pass elif isinstance(value, Quantity): if len(value) != 2: raise ValueError(f'{self.name!r} must be length 2') if not value.unit.physical_type == 'angle': raise ValueError(f'{self.name!r} must have angular units') else: try: if len(value) == 2: # Check if list-like with individual entries having angular # units: for val in value: if isinstance(val, Quantity): if not val.unit.physical_type == 'angle': raise ValueError( f'{self.name!r} must have angular units') else: raise ValueError( f'{self.name!r} must be an angle of length 2') else: raise ValueError( f'{self.name!r} must be an angle of length 2') except TypeError: raise ValueError( f'{self.name!r} must be an angle of length 2') from None class RegionType(RegionAttribute): """ Descriptor class to check the region type of value. Parameters ---------- name : str The name of the attribute. regionclass : `~regions.Region` The region class to check. """ def __init__(self, name, regionclass): super().__init__(name) self.regionclass = regionclass def _validate(self, value): if not isinstance(value, self.regionclass): raise ValueError(f'{self.name!r} must be a ' f'{self.regionclass.__name__} object') class RegionMetaDescr(RegionAttribute): """ Descriptor class for the region meta dictionary. If input as a pure `dict`, it will be converted to a `RegionMeta` object. """ def __set__(self, instance, value): # RegionMeta subclasses dict if isinstance(value, dict) and not isinstance(value, RegionMeta): value = RegionMeta(value) super().__set__(instance, value) def _validate(self, value): if not isinstance(value, RegionMeta): raise ValueError(f'{self.name!r} must be a dict or RegionMeta ' 'object') class RegionVisualDescr(RegionAttribute): """ Descriptor class for the region visual dictionary. If input as a pure `dict`, it will be converted to a `RegionVisual` object. """ def __set__(self, instance, value): # RegionVisual subclasses dict if isinstance(value, dict) and not isinstance(value, RegionVisual): value = RegionVisual(value) super().__set__(instance, value) def _validate(self, value): if not isinstance(value, RegionVisual): raise ValueError(f'{self.name!r} must be a dict or RegionVisual ' 'object') astropy-regions-f4139aa/regions/core/bounding_box.py000066400000000000000000000301111521703212700227120ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines a class for a rectangular bounding box. """ import numpy as np from astropy.io.fits.util import _is_int __all__ = ['RegionBoundingBox'] class RegionBoundingBox: """ A rectangular bounding box in integer (not float) pixel indices. Parameters ---------- ixmin, ixmax, iymin, iymax : int The bounding box pixel indices. Note that the upper values (``iymax`` and ``ixmax``) are exclusive as for normal slices in Python. The lower values (``ixmin`` and ``iymin``) must not be greater than the respective upper values (``ixmax`` and ``iymax``). Examples -------- >>> from regions import RegionBoundingBox >>> # constructing a RegionBoundingBox like this is cryptic: >>> bbox = RegionBoundingBox(1, 10, 2, 20) >>> # it's better to use keyword arguments for readability: >>> bbox = RegionBoundingBox(ixmin=1, ixmax=10, iymin=2, iymax=20) >>> bbox # nice repr, useful for interactive work RegionBoundingBox(ixmin=1, ixmax=10, iymin=2, iymax=20) >>> # sometimes it's useful to check if two bounding boxes are the same >>> bbox == RegionBoundingBox(ixmin=1, ixmax=10, iymin=2, iymax=20) True >>> bbox == RegionBoundingBox(ixmin=7, ixmax=10, iymin=2, iymax=20) False >>> # "center" and "shape" can be useful when working with numpy arrays >>> bbox.center # numpy order: (y, x) (10.5, 5.0) >>> bbox.shape # numpy order: (y, x) (18, 9) >>> # "extent" is useful when plotting the RegionBoundingBox with >>> # matplotlib >>> bbox.extent # matplotlib order: (x, y) (0.5, 9.5, 1.5, 19.5) """ def __init__(self, ixmin, ixmax, iymin, iymax): if not _is_int(ixmin): raise TypeError('ixmin must be an integer') if not _is_int(ixmax): raise TypeError('ixmax must be an integer') if not _is_int(iymin): raise TypeError('iymin must be an integer') if not _is_int(iymax): raise TypeError('iymax must be an integer') if ixmin > ixmax: raise ValueError('ixmin must be <= ixmax') if iymin > iymax: raise ValueError('iymin must be <= iymax') self.ixmin = ixmin self.ixmax = ixmax self.iymin = iymin self.iymax = iymax @classmethod def from_float(cls, xmin, xmax, ymin, ymax): """ Return the smallest bounding box that fully contains a given rectangle defined by float coordinate values. Following the pixel index convention, an integer index corresponds to the center of a pixel and the pixel edges span from (index - 0.5) to (index + 0.5). For example, the pixel edge spans of the following pixels are: - pixel 0: from -0.5 to 0.5 - pixel 1: from 0.5 to 1.5 - pixel 2: from 1.5 to 2.5 In addition, because `RegionBoundingBox` upper limits are exclusive (by definition), 1 is added to the upper pixel edges. See examples below. Parameters ---------- xmin, xmax, ymin, ymax : float Float coordinates defining a rectangle. The lower values (``xmin`` and ``ymin``) must not be greater than the respective upper values (``xmax`` and ``ymax``). Returns ------- bbox : `RegionBoundingBox` object The minimal ``RegionBoundingBox`` object fully containing the input rectangle coordinates. Examples -------- >>> from regions import RegionBoundingBox >>> RegionBoundingBox.from_float(xmin=1.0, xmax=10.0, ... ymin=2.0, ymax=20.0) RegionBoundingBox(ixmin=1, ixmax=11, iymin=2, iymax=21) >>> RegionBoundingBox.from_float(xmin=1.4, xmax=10.4, ... ymin=1.6, ymax=10.6) RegionBoundingBox(ixmin=1, ixmax=11, iymin=2, iymax=12) """ ixmin = int(np.floor(xmin + 0.5)) ixmax = int(np.ceil(xmax + 0.5)) iymin = int(np.floor(ymin + 0.5)) iymax = int(np.ceil(ymax + 0.5)) return cls(ixmin, ixmax, iymin, iymax) def __eq__(self, other): if not isinstance(other, RegionBoundingBox): raise TypeError('Can compare RegionBoundingBox only to another ' 'RegionBoundingBox.') return ((self.ixmin == other.ixmin) and (self.ixmax == other.ixmax) and (self.iymin == other.iymin) and (self.iymax == other.iymax)) def __or__(self, other): return self.union(other) def __and__(self, other): return self.intersection(other) def __repr__(self): return (f'{self.__class__.__name__}(ixmin={self.ixmin}, ' f'ixmax={self.ixmax}, iymin={self.iymin}, ' f'iymax={self.iymax})') @property def center(self): """ The ``(y, x)`` center of the bounding box. """ return (0.5 * (self.iymax - 1 + self.iymin), 0.5 * (self.ixmax - 1 + self.ixmin)) @property def shape(self): """ The ``(ny, nx)`` shape of the bounding box. """ return self.iymax - self.iymin, self.ixmax - self.ixmin def get_overlap_slices(self, shape): """ Get slices for the overlapping part of the bounding box and an 2D array. Parameters ---------- shape : 2-tuple of int The shape of the 2D array. Returns ------- slices_large : tuple of slices or `None` A tuple of slice objects for each axis of the large array, such that ``large_array[slices_large]`` extracts the region of the large array that overlaps with the small array. `None` is returned if there is no overlap of the bounding box with the given image shape. slices_small : tuple of slices or `None` A tuple of slice objects for each axis of an array enclosed by the bounding box such that ``small_array[slices_small]`` extracts the region that is inside the large array. `None` is returned if there is no overlap of the bounding box with the given image shape. """ if len(shape) != 2: raise ValueError('input shape must have 2 elements.') xmin = self.ixmin xmax = self.ixmax ymin = self.iymin ymax = self.iymax if xmin >= shape[1] or ymin >= shape[0] or xmax <= 0 or ymax <= 0: # no overlap of the bounding box with the input shape return None, None slices_large = (slice(max(ymin, 0), min(ymax, shape[0])), slice(max(xmin, 0), min(xmax, shape[1]))) slices_small = (slice(max(-ymin, 0), min(ymax - ymin, shape[0] - ymin)), slice(max(-xmin, 0), min(xmax - xmin, shape[1] - xmin))) return slices_large, slices_small @property def extent(self): """ The extent of the mask, defined as the ``(xmin, xmax, ymin, ymax)`` bounding box from the bottom-left corner of the lower- left pixel to the upper-right corner of the upper-right pixel. The upper edges here are the actual pixel positions of the edges, i.e., they are not "exclusive" indices used for python indexing. This is useful for plotting the bounding box using Matplotlib. """ return (self.ixmin - 0.5, self.ixmax - 0.5, self.iymin - 0.5, self.iymax - 0.5) def as_artist(self, **kwargs): """ Return a `matplotlib.patches.Rectangle` that represents the bounding box. Parameters ---------- **kwargs : dict Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- result : `matplotlib.patches.Rectangle` A matplotlib rectangular patch. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt import numpy as np from regions import RegionBoundingBox bbox = RegionBoundingBox(2, 7, 3, 8) fig, ax = plt.subplots() rng = np.random.default_rng(0) ax.imshow(rng.random((10, 10)), origin='lower') ax.add_patch(bbox.as_artist(facecolor='none', edgecolor='white', lw=2.)) """ from matplotlib.patches import Rectangle return Rectangle(xy=(self.extent[0], self.extent[2]), width=self.shape[1], height=self.shape[0], **kwargs) def to_region(self): """ Return a `~regions.RectanglePixelRegion` that represents the bounding box. """ from regions.core.pixcoord import PixCoord from regions.shapes import RectanglePixelRegion xypos = PixCoord(*self.center[::-1]) height, width = self.shape return RectanglePixelRegion(center=xypos, width=width, height=height) def plot(self, origin=(0, 0), ax=None, **kwargs): """ Plot the `RegionBoundingBox` on a matplotlib `~matplotlib.axes.Axes` instance. Parameters ---------- origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. ax : `matplotlib.axes.Axes`, optional If `None`, then the current `~matplotlib.axes.Axes` instance is used. **kwargs : dict Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : `matplotlib.patches.Patch` The matplotlib patch object for the plotted bounding box. The patch can be used, for example, when adding a plot legend. """ reg = self.to_region() return reg.plot(origin=origin, ax=ax, **kwargs) def union(self, other): """ Return a `RegionBoundingBox` representing the union of this `RegionBoundingBox` with another `RegionBoundingBox`. Parameters ---------- other : `~regions.RegionBoundingBox` The `RegionBoundingBox` to join with this one. Returns ------- result : `~regions.RegionBoundingBox` A `RegionBoundingBox` representing the union of the input `RegionBoundingBox` with this one. """ if not isinstance(other, RegionBoundingBox): raise TypeError('RegionBoundingBox can be joined only with ' 'another RegionBoundingBox.') ixmin = min((self.ixmin, other.ixmin)) ixmax = max((self.ixmax, other.ixmax)) iymin = min((self.iymin, other.iymin)) iymax = max((self.iymax, other.iymax)) return RegionBoundingBox(ixmin=ixmin, ixmax=ixmax, iymin=iymin, iymax=iymax) def intersection(self, other): """ Return a `RegionBoundingBox` representing the intersection of this `RegionBoundingBox` with another `RegionBoundingBox`. Parameters ---------- other : `~regions.RegionBoundingBox` The `RegionBoundingBox` to intersect with this one. Returns ------- result : `~regions.RegionBoundingBox` A `RegionBoundingBox` representing the intersection of the input `RegionBoundingBox` with this one. """ if not isinstance(other, RegionBoundingBox): raise TypeError('RegionBoundingBox can be intersected only with ' 'another RegionBoundingBox.') ixmin = max(self.ixmin, other.ixmin) ixmax = min(self.ixmax, other.ixmax) iymin = max(self.iymin, other.iymin) iymax = min(self.iymax, other.iymax) if ixmax < ixmin or iymax < iymin: return None return RegionBoundingBox(ixmin=ixmin, ixmax=ixmax, iymin=iymin, iymax=iymax) astropy-regions-f4139aa/regions/core/compound.py000066400000000000000000000430631521703212700220730ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import operator as op import astropy.units as u import numpy as np from astropy.coordinates import Latitude, Longitude from regions._utils.spherical_helpers import bounding_lonlat_poles_processing from regions.core.attributes import RegionType from regions.core.core import PixelRegion, SkyRegion, SphericalSkyRegion from regions.core.mask import RegionMask __all__ = ['CompoundPixelRegion', 'CompoundSkyRegion', 'CompoundSphericalSkyRegion'] class CompoundPixelRegion(PixelRegion): """ A class that represents the logical combination of two regions in pixel coordinates. Parameters ---------- region1 : `~regions.PixelRegion` The inner Pixel region. region2 : `~regions.PixelRegion` The outer Pixel region. operator : callable A callable binary operator. meta : `~regions.RegionMeta`, optional A dictionary that stores the meta attributes of this region. visual : `~regions.RegionVisual`, optional A dictionary that stores the visual meta attributes of this region. """ _params = ('region1', 'region2', 'operator') _mpl_artist = 'Patch' region1 = RegionType('region1', PixelRegion) region2 = RegionType('region2', PixelRegion) def __init__(self, region1, region2, operator, meta=None, visual=None): if not callable(operator): raise TypeError('operator must be callable') self.region1 = region1 self.region2 = region2 if meta is None: self.meta = region1.meta else: self.meta = meta if visual is None: self.visual = region1.visual else: self.visual = visual self._operator = operator @property def operator(self): return self._operator def contains(self, pixcoord): return self.operator(self.region1.contains(pixcoord), self.region2.contains(pixcoord)) def covers(self, pixcoord): return self.operator(self.region1.covers(pixcoord), self.region2.covers(pixcoord)) def to_mask(self, mode='center', subpixels=1): if mode != 'center': raise NotImplementedError mask1 = self.region1.to_mask(mode=mode, subpixels=subpixels) mask2 = self.region2.to_mask(mode=mode, subpixels=subpixels) # Common bounding box bbox = self.bounding_box # Pad mask1.data and mask2.data to get the same shape padded_data = list() for mask in (mask1, mask2): pleft = abs(mask.bbox.ixmin - bbox.ixmin) pright = abs(bbox.ixmax - mask.bbox.ixmax) ptop = abs(bbox.iymax - mask.bbox.iymax) pbottom = abs(mask.bbox.iymin - bbox.iymin) padded_data.append(np.pad(mask.data, ((pbottom, ptop), (pleft, pright)), 'constant')) data = self.operator(*np.array(padded_data, dtype=int)) return RegionMask(data=data, bbox=bbox) def to_sky(self, wcs): skyreg1 = self.region1.to_sky(wcs=wcs) skyreg2 = self.region2.to_sky(wcs=wcs) return CompoundSkyRegion(region1=skyreg1, operator=self.operator, region2=skyreg2, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): sphreg1 = self.region1.to_spherical_sky( wcs=wcs, boundary_distortions=boundary_distortions, n_vertices=n_vertices, ) sphreg2 = self.region2.to_spherical_sky( wcs=wcs, boundary_distortions=boundary_distortions, n_vertices=n_vertices, ) return CompoundSphericalSkyRegion( region1=sphreg1, region2=sphreg2, operator=self.operator, meta=self.meta.copy(), visual=self.visual.copy(), ) @staticmethod def _make_annulus_path(patch_inner, patch_outer): """ Define a matplotlib annulus path from two patches. This preserves the cubic Bezier curves (CURVE4) of the aperture paths. Taken from ``photutils.aperture.core``. """ import matplotlib.path as mpath path_inner = patch_inner.get_path() transform_inner = patch_inner.get_transform() path_inner = transform_inner.transform_path(path_inner) path_outer = patch_outer.get_path() transform_outer = patch_outer.get_transform() path_outer = transform_outer.transform_path(path_outer) verts_inner = path_inner.vertices[:-1][::-1] verts_inner = np.concatenate((verts_inner, [verts_inner[-1]])) verts = np.vstack((path_outer.vertices, verts_inner)) codes = np.hstack((path_outer.codes, path_inner.codes)) return mpath.Path(verts, codes) def as_artist(self, origin=(0, 0), **kwargs): """ Return a matplotlib patch object for this region (`matplotlib.patches.PathPatch`). Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `~matplotlib.patches.PathPatch`. Returns ------- patch : `~matplotlib.patches.PathPatch` A matplotlib patch object. """ if (self.region1.center == self.region2.center and self.operator is op.xor): import matplotlib.patches as mpatches # set mpl_kwargs before as_artist is called on region1 and # region2 mpl_kwargs = self.visual.define_mpl_kwargs(self._mpl_artist) mpl_kwargs.update(kwargs) patch_inner = self.region1.as_artist(origin=origin) patch_outer = self.region2.as_artist(origin=origin) path = self._make_annulus_path(patch_inner, patch_outer) patch = mpatches.PathPatch(path, **mpl_kwargs) return patch else: raise ValueError('unable to convert region to matplotlib ' f'artist: {self}') @property def bounding_box(self): return self.region1.bounding_box | self.region2.bounding_box @property def area(self): raise NotImplementedError def rotate(self, center, angle): """ Rotate the region. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `~regions.PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- region : `CompoundPixelRegion` The rotated region (which is an independent copy). """ region1 = self.region1.rotate(center, angle) region2 = self.region2.rotate(center, angle) return self.copy(region1=region1, region2=region2) class CompoundSkyRegion(SkyRegion): """ A class that represents the logical combination of two regions in sky coordinates. Parameters ---------- region1 : `~regions.SkyRegion` The inner sky region. region2 : `~regions.SkyRegion` The outer sky region. operator : callable A callable binary operator. meta : `~regions.RegionMeta`, optional A dictionary that stores the meta attributes of this region. visual : `~regions.RegionVisual`, optional A dictionary that stores the visual meta attributes of this region. """ _params = ('region1', 'region2', 'operator') region1 = RegionType('region1', SkyRegion) region2 = RegionType('region2', SkyRegion) def __init__(self, region1, region2, operator, meta=None, visual=None): if not callable(operator): raise TypeError('operator must be callable') self.region1 = region1 self.region2 = region2 if meta is None: self.meta = region1.meta else: self.meta = meta if visual is None: self.visual = region1.visual else: self.visual = visual self._operator = operator @property def operator(self): return self._operator def contains(self, skycoord, wcs): return self.operator(self.region1.contains(skycoord, wcs), self.region2.contains(skycoord, wcs)) def covers(self, skycoord, wcs): return self.operator(self.region1.covers(skycoord, wcs), self.region2.covers(skycoord, wcs)) def to_pixel(self, wcs): pixreg1 = self.region1.to_pixel(wcs=wcs) pixreg2 = self.region2.to_pixel(wcs=wcs) return CompoundPixelRegion(region1=pixreg1, operator=self.operator, region2=pixreg2, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): sphreg1 = self.region1.to_spherical_sky( wcs=wcs, boundary_distortions=boundary_distortions, n_vertices=n_vertices, ) sphreg2 = self.region2.to_spherical_sky( wcs=wcs, boundary_distortions=boundary_distortions, n_vertices=n_vertices, ) return CompoundSphericalSkyRegion( region1=sphreg1, region2=sphreg2, operator=self.operator, meta=self.meta.copy(), visual=self.visual.copy(), ) def as_artist(self, ax, **kwargs): raise NotImplementedError class CompoundSphericalSkyRegion(SphericalSkyRegion): """ A class that represents the logical combination of two regions in spherical sky coordinates. Parameters ---------- region1 : `~regions.SphericalSkyRegion` First spherical sky region. region2 : `~regions.SphericalSkyRegion` Second spherical sky region. operator : callable A callable binary operator. meta : `~regions.RegionMeta`, optional A dictionary that stores the meta attributes of this region. visual : `~regions.RegionVisual`, optional A dictionary that stores the visual meta attributes of this region. """ _params = ('region1', 'region2', 'operator') region1 = RegionType('region1', SphericalSkyRegion) region2 = RegionType('region2', SphericalSkyRegion) def __init__(self, region1, region2, operator, meta=None, visual=None): if not callable(operator): raise TypeError('operator must be callable') self._validate_regions(region1, region2) self.region1 = region1 self.region2 = region2 if meta is None: self.meta = region1.meta else: self.meta = meta if visual is None: self.visual = region1.visual else: self.visual = visual self._operator = operator @staticmethod def _validate_regions(region1, region2): # Ensure both regions are in the same frame: from regions.shapes.whole_sky import WholeSphericalSkyRegion # If one region is WholeSphericalSkyRegion, skip check: if (not (isinstance(region1, WholeSphericalSkyRegion) | isinstance(region2, WholeSphericalSkyRegion)) and not region1.frame.is_equivalent_frame(region2.frame)): raise ValueError( 'To create a compound region, "region1" & "region2"' ' must be in the same coordinate frame!', ) @property def operator(self): return self._operator @property def frame(self): return self.region1.frame @property def bounding_circle(self): from regions.shapes.circle import CircleSphericalSkyRegion if self.operator in [op.or_]: # Union: # Compute connection between constituent two bounding circles, # then total "span" on either side, then half and get center. circ1 = self.region1.bounding_circle circ2 = self.region2.bounding_circle pa = circ1.center.position_angle(circ2.center) e1 = circ1.center.directional_offset_by( 180 * u.deg + pa, circ1.radius) e2 = circ2.center.directional_offset_by(pa, circ2.radius) sep = e1.separation(e2) cnew = e1.directional_offset_by(pa, sep / 2) return CircleSphericalSkyRegion(cnew, sep / 2) # Disjoint set / XOR: # Not clean # Intersection: # NOT anywhere near as clean. Much better to define for each subclass # that has a compound region as their internal region logic. # TODO: # General solution requires finding intersections of boundaries, # and then making a bounding circle. Doable with composed # circle boundaries & polygon-specific logic from spherical-geometry # Maybe better to not make bounding_circle and bounding_lonlat # abstract classes of the base SphericalSkyRegion, and only # add as properties to individual explicitly defined classes? raise NotImplementedError def _get_edge_raw_lonlat_bounds(self): if self.operator in [op.or_]: lons1, lats1 = self.region1.bounding_lonlat lons2, lats2 = self.region2.bounding_lonlat if (lons1 is None) | (lons2 is None): return (None, Latitude([np.minimum(lats1[0], lats2[0]), np.maximum(lats1[1], lats2[1])]).to(u.deg)) # Both lon ranges defined: return (Longitude([np.minimum(lons1[0], lons2[0]), np.maximum(lons1[1], lons2[1])]).to(u.deg), Latitude([np.minimum(lats1[0], lats2[0]), np.maximum(lats1[1], lats2[1])]).to(u.deg)) # XOR, Intersection: not implemented raise NotImplementedError @property def bounding_lonlat(self): if self.operator in [op.or_]: # Union: # Find min max from constituent bounds lons_arr, lats_arr = self._get_edge_raw_lonlat_bounds() # Check if shape covers either pole & modify lats arr accordingly: lons_arr, lats_arr = bounding_lonlat_poles_processing( self, lons_arr, lats_arr, ) return lons_arr, lats_arr # Disjoint set / XOR: # Not clean # Intersection: # NOT anywhere near as clean. Much better to define for each subclass # that has a compound region as their internal region logic. # TODO: # General solution requires finding intersections of boundaries, # and then making a bounding lonlat. Doable with composed # circle boundaries & polygon-specific logic from spherical-geometry # Maybe better to not make bounding_circle and bounding_lonlat # abstract classes of the base SphericalSkyRegion, and only # add as properties to individual explicitly defined classes? raise NotImplementedError def contains(self, coord): return self.operator( self.region1.contains(coord), self.region2.contains(coord), ) def covers(self, coord): return self.operator( self.region1.covers(coord), self.region2.covers(coord), ) def transform_to(self, frame, merge_attributes=True): # Transform the consitituent regions then recompose: return self.__class__( self.region1.transform_to( frame, merge_attributes=merge_attributes), self.region2.transform_to( frame, merge_attributes=merge_attributes), self.operator, meta=self.meta, visual=self.visual, ) def discretize_boundary(self, *, n_vertices=100): return CompoundSphericalSkyRegion( self.region1.discretize_boundary(n_vertices=n_vertices), self.region2.discretize_boundary(n_vertices=n_vertices), operator=self.operator, meta=self.meta.copy(), visual=self.visual.copy(), ) def to_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): planarreg1 = self.region1.to_sky( wcs=wcs, boundary_distortions=boundary_distortions, n_vertices=n_vertices, ) planarreg2 = self.region2.to_sky( wcs=wcs, boundary_distortions=boundary_distortions, n_vertices=n_vertices, ) return CompoundSkyRegion( region1=planarreg1, region2=planarreg2, operator=self.operator, meta=self.meta.copy(), visual=self.visual.copy(), ) def to_pixel(self, wcs, *, boundary_distortions=False, n_vertices=None): pixreg1 = self.region1.to_pixel( wcs, boundary_distortions=boundary_distortions, n_vertices=n_vertices, ) pixreg2 = self.region2.to_pixel( wcs, boundary_distortions=boundary_distortions, n_vertices=n_vertices, ) return CompoundPixelRegion( region1=pixreg1, region2=pixreg2, operator=self.operator, meta=self.meta.copy(), visual=self.visual.copy(), ) astropy-regions-f4139aa/regions/core/core.py000066400000000000000000001315141521703212700211760ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import abc import copy import operator import astropy.units as u import numpy as np from astropy.coordinates import BaseCoordinateFrame, SkyCoord from regions._utils.spherical_helpers import (bounding_lonlat_poles_processing, get_astropy_frame_class) from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord from regions.core.registry import RegionsRegistry __all__ = ['Region', 'PixelRegion', 'SkyRegion', 'SphericalSkyRegion', 'ComplexSphericalSkyRegion'] __doctest_skip__ = ['Region.serialize', 'Region.write'] def _format_float(val, max_decimals=4, sci_threshold=1e5): """ Format a float for repr/str output. Trailing zeros are stripped (a single trailing zero after a decimal point is always kept). Scientific notation is used for values with absolute value >= ``sci_threshold`` or <= 1e-5. Parameters ---------- val : float The float value to format. max_decimals : int, optional The maximum number of decimal places. sci_threshold : float, optional The threshold above which scientific notation is used. """ val = float(val) if (np.isfinite(val) and (abs(val) >= sci_threshold or (0 < abs(val) <= 1e-5))): s = f'{val:.{max_decimals}e}' mantissa, exponent = s.split('e') mantissa = mantissa.rstrip('0') if mantissa.endswith('.'): mantissa += '0' result = f'{mantissa}e{exponent}' else: result = f'{val:.{max_decimals}f}' result = result.rstrip('0') if result.endswith('.'): result += '0' return result class Region(abc.ABC): """ Base class for all regions. """ _params = () def copy(self, **changes): """ Make an independent (deep) copy. Parameters ---------- **changes : dict Changes to make to the region parameters. """ fields = list(self._params) + ['meta', 'visual'] for field in fields: if field not in changes: changes[field] = copy.deepcopy(getattr(self, field)) return self.__class__(**changes) @staticmethod def _format_param_value(val, *, max_decimals=4, max_elements=5): """ Format a region parameter value for use in repr/str output. Floats (including pixel coordinates) are formatted using `_format_float` with at most ``max_decimals`` decimal places and trailing zeros stripped. `~regions.PixCoord` arrays and `~astropy.coordinates.SkyCoord` arrays with more than ``max_elements`` elements are truncated to the first ``max_elements``. Parameters ---------- val : object The parameter value to format. max_decimals : int, optional The maximum number of decimal places for floating-point values. max_elements : int, optional The maximum number of array elements to display before truncating with ``...``. """ if isinstance(val, SkyCoord): if not val.isscalar and len(val) > max_elements: result = str(val[:max_elements]).rstrip('>') + ', ...]>' else: result = str(val) elif isinstance(val, PixCoord): if val.isscalar: if isinstance(val.x, float): x_fmt = _format_float(val.x, max_decimals) y_fmt = _format_float(val.y, max_decimals) result = f'PixCoord(x={x_fmt}, y={y_fmt})' else: result = f'PixCoord(x={val.x}, y={val.y})' else: n = len(val.x) xs = val.x[:max_elements] if n > max_elements else val.x ys = val.y[:max_elements] if n > max_elements else val.y if np.issubdtype(val.x.dtype, np.floating): x_str = ' '.join( _format_float(x, max_decimals) for x in xs) y_str = ' '.join( _format_float(y, max_decimals) for y in ys) else: x_str = ' '.join(str(x) for x in xs) y_str = ' '.join(str(y) for y in ys) if n > max_elements: x_str += ' ...' y_str += ' ...' result = f'PixCoord(x=[{x_str}], y=[{y_str}])' elif isinstance(val, u.Quantity): result = f'{_format_float(val.value, max_decimals)} {val.unit}' elif isinstance(val, float) and not isinstance(val, bool): result = _format_float(val, max_decimals) else: result = str(val) return result def _cls_info(self): """ Return a list of ``(param_name, formatted_value)`` tuples for all region parameters, used by `__repr__` and `__str__`. """ info = [] if self._params is not None: for param in self._params: val = getattr(self, param) if param == 'text': # Place quotes around text value formatted = repr(val) else: formatted = self._format_param_value(val) info.append((param, formatted)) return info def __repr__(self): prefix = self.__class__.__name__ cls_info = ', '.join(f'{k}={v}' for k, v in self._cls_info()) return f'<{prefix}({cls_info})>' def __str__(self): cls_info = [('Region', self.__class__.__name__)] + self._cls_info() return '\n'.join(f'{key}: {val}' for key, val in cls_info) @staticmethod def _is_close(val1, val2): """ Helper to compare PixCoord, Quantity, SkyCoord, and other parameters. For SkyCoord objects, both coordinates are converted to ICRS and their RA/Dec values are compared using `numpy.allclose` with default tolerances, enabling tolerance-based equivalency similar to PixCoord and Quantity parameters. SkyCoord objects must have equivalent frames to be considered close. Parameters ---------- val1, val2 : object Values to compare. Returns ------- bool True if values are considered close/equal, False otherwise. """ # PixCoord has its own __eq__ with built-in tolerances if isinstance(val1, PixCoord): return val1 == val2 # SkyCoord comparison using RA/DEC allclose if isinstance(val1, SkyCoord) and isinstance(val2, SkyCoord): try: # Frames must be equivalent, convert to ICRS for comparison if val1.frame.is_equivalent_frame(val2.frame): ra_close = np.allclose(val1.icrs.ra.deg, val2.icrs.ra.deg) dec_close = np.allclose(val1.icrs.dec.deg, val2.icrs.dec.deg) return ra_close and dec_close except (AttributeError, ValueError): pass return False # Try np.allclose for Quantities and arrays try: return np.allclose(val1, val2) except (TypeError, ValueError): try: # Fallback for dicts/objects (e.g., empty dicts) where # np.allclose fails. return np.all(val1 == val2) except (TypeError, ValueError): return False def __eq__(self, other): """ Equality operator for Region. Regions are considered equal if all their properties are equal or close. - PixCoord centers use built-in tolerance (np.allclose with defaults) - Quantity parameters allow different units if directly convertible - SkyCoord centers are compared using allclose on RA/DEC values - Other parameters use strict equality """ if not isinstance(other, self.__class__): return False # Define the parameters to check self_params = list(self._params) + ['meta', 'visual'] other_params = list(other._params) + ['meta', 'visual'] # Check that both have identical parameter sets if self_params != other_params: return False # Check that all parameter values are close return all(self._is_close(getattr(self, p), getattr(other, p)) for p in self_params) def __ne__(self, other): """ Inequality operator for Region. Parameters ---------- other : `Region` The other region that will be compared. """ return not (self == other) @abc.abstractmethod def intersection(self, other): """ Return a region representing the intersection of this region with ``other``. Parameters ---------- other : `Region` The other region to use for the intersection. """ raise NotImplementedError @abc.abstractmethod def symmetric_difference(self, other): """ Return the union of the two regions minus any areas contained in the intersection of the two regions. Parameters ---------- other : `Region` The other region to use for the symmetric difference. """ raise NotImplementedError @abc.abstractmethod def union(self, other): """ Return a region representing the union of this region with ``other``. Parameters ---------- other : `Region` The other region to use for the union. """ raise NotImplementedError def __and__(self, other): return self.intersection(other) def __or__(self, other): return self.union(other) def __xor__(self, other): return self.symmetric_difference(other) @classmethod def get_formats(cls): """ Get the registered I/O formats as a Table. """ return RegionsRegistry.get_formats(Region) def write(self, filename, format=None, overwrite=False, **kwargs): """ Write the region to a region file in the specified format. This method allows writing a file in many supported data formats, e.g.,:: >>> reg.write('new_regions.reg', format='ds9') >>> reg.write('new_regions.crtf', format='crtf') >>> reg.write('new_regions.fits', format='fits') A list of the available formats for `~regions.Region` is available using:: >>> from regions import Region >>> Region.get_formats() Parameters ---------- filename : str The filename or URL of the file to write. format : str, optional The file format specifier. overwrite : bool, optional If True, overwrite the output file if it exists. Raises an `OSError` if False and the output file exists. Default is False. **kwargs : dict, optional Keyword arguments passed to the data writer. """ return RegionsRegistry.write([self], filename, Region, format=format, overwrite=overwrite, **kwargs) def serialize(self, format=None, **kwargs): """ Serialize the region to a region string or table. This method allows serializing regions in many supported data formats, e.g.,:: >>> reg1_str = reg.serialize(format='ds9') >>> reg2_str = reg.serialize(format='crtf') >>> reg3_tbl = reg.serialize(format='fits') A list of the available formats for `~regions.Region` is available using:: >>> from regions import Region >>> Region.get_formats() Parameters ---------- format : str, optional The file format specifier. **kwargs : dict, optional Keyword arguments passed to the data serializer. """ return RegionsRegistry.serialize([self], Region, format=format, **kwargs) @staticmethod def _validate_planar_spherical_transform( wcs, boundary_distortions): # Validate whether inputs are valid for planar <-> spherical # transformations if boundary_distortions and (wcs is None): raise ValueError( "'wcs' must be set if `boundary_distortions=True`", ) class PixelRegion(Region): """ Base class for all regions defined in pixel coordinates. """ meta = RegionMeta() visual = RegionVisual() def intersection(self, other): """ Return a region representing the intersection of this region with ``other``. Parameters ---------- other : `Region` The other region to use for the intersection. """ from regions.core.compound import CompoundPixelRegion return CompoundPixelRegion(region1=self, region2=other, operator=operator.and_) def symmetric_difference(self, other): """ Return the union of the two regions minus any areas contained in the intersection of the two regions. Parameters ---------- other : `Region` The other region to use for the symmetric difference. """ from regions.core.compound import CompoundPixelRegion return CompoundPixelRegion(region1=self, region2=other, operator=operator.xor) def union(self, other): """ Return a region representing the union of this region with ``other``. Parameters ---------- other : `Region` The other region to use for the union. """ from regions.core.compound import CompoundPixelRegion return CompoundPixelRegion(region1=self, region2=other, operator=operator.or_) @abc.abstractmethod def contains(self, pixcoord): """ Check whether a position or positions fall inside the region, excluding the boundary. This method considers points on the boundary as outside the region, consistent with Shapely's ``contains`` function and DE-9IM semantics. Parameters ---------- pixcoord : `~regions.PixCoord` The position or positions to check. Returns ------- result : bool or `~numpy.ndarray` A boolean or boolean array indicating whether the position(s) are inside the region. See Also -------- covers : Check if points are inside or on the boundary. """ msg = ("The 'contains' method is not supported for " f'{self.__class__.__name__}.') raise NotImplementedError(msg) def __contains__(self, coord): if not coord.isscalar: raise ValueError(f'coord must be scalar. coord={coord}') return self.contains(coord) def covers(self, pixcoord): """ Check whether a position or positions fall inside the region, including the boundary. This method considers points on the boundary as inside the region, consistent with Shapely's ``covers`` function and DE-9IM semantics. Parameters ---------- pixcoord : `~regions.PixCoord` The position or positions to check. Returns ------- result : bool or `~numpy.ndarray` A boolean or boolean array indicating whether the position(s) are inside or on the boundary of the region. See Also -------- contains : Check if points are strictly inside (excludes boundary). """ msg = ("The 'covers' method is not supported for " f'{self.__class__.__name__}.') raise NotImplementedError(msg) @abc.abstractmethod def to_sky(self, wcs): """ Return a region defined in sky coordinates. Parameters ---------- wcs : `~astropy.wcs.WCS` The world coordinate system transformation to use to convert from pixels to sky coordinates. Returns ------- sky_region : `~regions.SkyRegion` The sky region. Notes ----- The conversion between pixel and sky coordinates is an approximation. The pixel region shape is mapped to a sky region shape using the local pixel scale and angle at the region center. Projection effects over the extent of the region are not accounted for. The region shape type is always preserved (e.g., a `CirclePixelRegion` converts to a `CircleSkyRegion`). For WCS with distortions (e.g., SIP), the local Jacobian matrix of the WCS transformation is used to compute directional scale factors and angle. For WCS without distortions, a local pixel scale and angle are computed using offset-based methods. """ raise NotImplementedError @abc.abstractmethod def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): """ Convert to an equivalent spherical `~regions.SphericalSkyRegion` instance. Parameters ---------- wcs : `~astropy.wcs.WCS` instance The world coordinate system transformation to use to convert between pixel and sky coordinates. boundary_distortions : bool, optional If `True`, the projection-induced distortions of the region's boundary are preserved by discretizing the boundary into a polygon and transforming that polygon. If `False` (default), the region is converted to an equivalent idealized shape that ignores these boundary distortions. n_vertices : int, optional The number of polygon vertices for boundary discretization. This keyword will have no effect unless ``boundary_distortions=True``. Default is 100. Returns ------- spherical_sky_region : `~regions.SphericalSkyRegion` A spherical sky region, with an equivalent shape (if ``boundary_distortions`` is False), or a discretized polygon of the boundary (if ``boundary_distortions`` is True). """ raise NotImplementedError @property @abc.abstractmethod def area(self): """ The exact analytical area of the region shape. """ raise NotImplementedError @property @abc.abstractmethod def bounding_box(self): """ The minimal bounding box (in integer pixel coordinates) that contains the region. """ raise NotImplementedError @abc.abstractmethod def to_mask(self, mode='center', subpixels=5): """ Return a mask for the region. Parameters ---------- mode : {'center', 'exact', 'subpixels'}, optional The method used to determine the overlap of the region on the pixel grid. Not all options are available for all region types. Note that the more precise methods are generally slower. The following methods are available: * ``'center'``: A pixel is considered to be entirely in or out of the region depending on whether its center is in or out of the region. The returned mask will contain values only of 0 (out) and 1 (in). * ``'exact'`` (default): The exact fractional overlap of the region and each pixel is calculated. The returned mask will contain values between 0 and 1. * ``'subpixel'``: A pixel is divided into subpixels (see the ``subpixels`` keyword), each of which are considered to be entirely in or out of the region depending on whether its center is in or out of the region. If ``subpixels=1``, this method is equivalent to ``'center'``. The returned mask will contain values between 0 and 1. subpixels : int, optional For the ``'subpixel'`` mode, resample pixels by this factor in each dimension. That is, each pixel is divided into ``subpixels ** 2`` subpixels. Returns ------- mask : `~regions.RegionMask` A mask for the region. """ raise NotImplementedError @staticmethod def _validate_mode(mode, subpixels): valid_modes = ('center', 'exact', 'subpixels') if mode not in valid_modes: raise ValueError(f'Invalid mask mode: {mode} (should be one ' f'of {valid_modes}') if (mode == 'subpixels' and (not isinstance(subpixels, int) or subpixels <= 0)): raise ValueError(f'Invalid subpixels value: {subpixels} ' '(should be a strictly positive integer)') @abc.abstractmethod def as_artist(self, origin=(0, 0), **kwargs): """ Convert to matplotlib patch object for this region. Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. **kwargs : dict Any keyword arguments accepted by `~matplotlib.patches.Patch`. Returns ------- patch : `~matplotlib.patches.Patch` A matplotlib patch. """ raise NotImplementedError def plot(self, origin=(0, 0), ax=None, **kwargs): """ Plot the region on a matplotlib `~matplotlib.axes.Axes` instance. Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. ax : `~matplotlib.axes.Axes` or `None`, optional The matplotlib axes on which to plot. If `None`, then the current `~matplotlib.axes.Axes` instance is used. **kwargs : dict Any keyword arguments accepted by `~matplotlib.patches.Patch`. Returns ------- artist : `matplotlib.artist.Artist` The matplotlib artist (typically a `~matplotlib.patches.Patch` object) for the plotted region. The artist can be used, for example, when adding a plot legend. """ import matplotlib.pyplot as plt if ax is None: ax = plt.gca() artist = self.as_artist(origin=origin, **kwargs) ax.add_artist(artist) return artist class SkyRegion(Region): """ Base class for all regions defined in celestial coordinates. """ def intersection(self, other): """ Return a region representing the intersection of this region with ``other``. Parameters ---------- other : `Region` The other region to use for the intersection. """ from regions.core.compound import CompoundSkyRegion return CompoundSkyRegion(region1=self, region2=other, operator=operator.and_) def symmetric_difference(self, other): """ Return the union of the two regions minus any areas contained in the intersection of the two regions. Parameters ---------- other : `Region` The other region to use for the symmetric difference. """ from regions.core.compound import CompoundSkyRegion return CompoundSkyRegion(region1=self, region2=other, operator=operator.xor) def union(self, other): """ Return a region representing the union of this region with ``other``. Parameters ---------- other : `Region` The other region to use for the union. """ from regions.core.compound import CompoundSkyRegion return CompoundSkyRegion(region1=self, region2=other, operator=operator.or_) def contains(self, skycoord, wcs): """ Check whether a sky coordinate falls inside the region. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The position or positions to check. wcs : `~astropy.wcs.WCS` The world coordinate system transformation to use to convert between sky and pixel coordinates. """ pixel_region = self.to_pixel(wcs) pixcoord = PixCoord.from_sky(skycoord, wcs) return pixel_region.contains(pixcoord) def covers(self, skycoord, wcs): """ Check whether a sky coordinate falls inside or on the boundary of the region. This method considers points on the boundary as inside the region, consistent with Shapely's ``covers`` function and DE-9IM semantics. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The position or positions to check. wcs : `~astropy.wcs.WCS` The world coordinate system transformation to use to convert between sky and pixel coordinates. See Also -------- contains : Check if points are strictly inside (excludes boundary). Notes ----- This method requires the pixel region returned by ``to_pixel`` to implement a ``covers`` method. It is currently available for circle, ellipse, rectangle, and polygon regions; for other regions a `NotImplementedError` is raised. """ pixel_region = self.to_pixel(wcs) pixcoord = PixCoord.from_sky(skycoord, wcs) return pixel_region.covers(pixcoord) @abc.abstractmethod def to_pixel(self, wcs): """ Return the equivalent region defined in pixel coordinates. Parameters ---------- wcs : `~astropy.wcs.WCS` The world coordinate system transformation to use to convert between sky and pixel coordinates. Returns ------- pixel_region : `~regions.PixelRegion` A pixel region. Notes ----- The conversion between sky and pixel coordinates is an approximation. The sky region shape is mapped to a pixel region shape using the local pixel scale and angle at the region center. Projection effects over the extent of the region are not accounted for. The region shape type is always preserved (e.g., a `CircleSkyRegion` converts to a `CirclePixelRegion`). For WCS with distortions (e.g., SIP), the local Jacobian matrix of the WCS transformation is used to compute directional scale factors and angle. For WCS without distortions, a local pixel scale and angle are computed using offset-based methods. """ raise NotImplementedError @abc.abstractmethod def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): """ Convert to an equivalent spherical `~regions.SphericalSkyRegion` instance. Parameters ---------- wcs : `~astropy.wcs.WCS` instance, optional The world coordinate system transformation to use to convert between sky and pixel coordinates. Required if transforming with boundary distortions (if ``boundary_distortions`` is True). Ignored if boundary distortions not included. boundary_distortions : bool, optional If `True`, the projection-induced distortions of the region's boundary are preserved by discretizing the boundary into a polygon and transforming that polygon. If `False` (default), the region is converted to an equivalent idealized shape that ignores these boundary distortions. n_vertices : int, optional The number of polygon vertices for boundary discretization. This keyword will have no effect unless ``boundary_distortions=True``. Default is 100. Returns ------- spherical_sky_region : `~regions.SphericalSkyRegion` A spherical sky region, with an equivalent shape (if ``boundary_distortions`` is False), or a discretized polygon of the boundary (if ``boundary_distortions`` is True). """ raise NotImplementedError class SphericalSkyRegion(Region): """ Base class for all spherical sky regions (compared to the implicitly planar SkyRegions). """ def intersection(self, other): """ Return a region representing the intersection of this region with ``other``. Parameters ---------- other : `~regions.SphericalSkyRegion` The other region to use for the intersection. """ from .compound import CompoundSphericalSkyRegion return CompoundSphericalSkyRegion( region1=self, region2=other, operator=operator.and_, ) def symmetric_difference(self, other): """ Return the union of the two regions minus any areas contained in the intersection of the two regions. Parameters ---------- other : `~regions.SphericalSkyRegion` The other region to use for the symmetric difference. """ from .compound import CompoundSphericalSkyRegion return CompoundSphericalSkyRegion( region1=self, region2=other, operator=operator.xor, ) def union(self, other): """ Return a region representing the union of this region with ``other``. Parameters ---------- other : `~regions.SphericalSkyRegion` The other region to use for the union. """ from .compound import CompoundSphericalSkyRegion return CompoundSphericalSkyRegion( region1=self, region2=other, operator=operator.or_, ) @staticmethod def _standardize_frame(frame): # Standardize frame format: get as an astropy coordinate frame class frame = get_astropy_frame_class(frame) return frame def _validate_frame_transformation(self, frame): # Validate the transformation from a spherical region's # current frame to a target frame # Standardize target frame input: frame = self._standardize_frame(frame) # Check if current frame is transformable to new frame: if (isinstance(self.frame, type) and issubclass(self.frame, BaseCoordinateFrame)): # Need to make an instance to check if transformable: self_frame = self.frame() else: self_frame = self.frame if not self_frame.is_transformable_to(frame): raise ValueError(f"Cannot transform from {self.frame} to {frame}!") # TODO: check for offset origin transformations return frame @property def frame(self): """ Coordinate frame of the region. """ if 'center' in self._params: return self.center.frame elif 'vertices' in self._params: return self.vertices[0].frame else: raise AttributeError( "Either 'center' or 'vertices' must be an attribute/property " 'of the SphericalSkyRegion.', ) @property @abc.abstractmethod def bounding_circle(self): """ Bounding circle for the spherical sky region. Returns ------- circle_sph_sky_region: `~regions.CircleSphericalSkyRegion` A circle spherical sky region object. """ raise NotImplementedError def _validate_lonlat_bounds(self, lons_arr, lats_arr, inner_region=None): # Check if shape covers either pole & modify lats arr accordingly: lons_arr, lats_arr = bounding_lonlat_poles_processing( self, lons_arr, lats_arr, inner_region=inner_region, ) return lons_arr, lats_arr @property @abc.abstractmethod def bounding_lonlat(self): """ Bounding longitude and latitude of the spherical sky region, in the region's frame. Returns ------- lons_arr : list of `~astropy.coordinates.Longitude` List of lower, upper boundary longitude values. lats_arr : list of `~astropy.coordinates.Latitude` List of lower, upper boundary latitude values. """ raise NotImplementedError @abc.abstractmethod def contains(self, coord): """ Check whether a sky coordinate falls inside the spherical sky region, excluding the boundary. This method considers points on the boundary as outside the region, consistent with Shapely's ``contains`` function and DE-9IM semantics. Parameters ---------- coord : `~astropy.coordinates.SkyCoord` The position or positions to check. See Also -------- covers : Check if points are inside or on the boundary. """ raise NotImplementedError def covers(self, coord): """ Check whether a sky coordinate falls inside the spherical sky region, including the boundary. This method considers points on the boundary as inside the region, consistent with Shapely's ``covers`` function and DE-9IM semantics. Parameters ---------- coord : `~astropy.coordinates.SkyCoord` The position or positions to check. See Also -------- contains : Check if points are strictly inside (excludes boundary). """ msg = ("The 'covers' method is not supported for " f'{self.__class__.__name__}.') raise NotImplementedError(msg) @abc.abstractmethod def transform_to(self, frame, merge_attributes=True): """ Transform the `~regions.SphericalSkyRegion` instance into another instance with a different coordinate reference frame. The precise frame transformed to depends on ``merge_attributes``. If `False`, the destination frame is used exactly as passed in. But this is often not quite what one wants. E.g., suppose one wants to transform an ICRS coordinate that has an obstime attribute to FK4; in this case, one likely would want to use this information. Thus, the default for ``merge_attributes`` is `True`, in which the precedence is as follows: (1) explicitly set (i.e., non-default) values in the destination frame; (2) explicitly set values in the source; (3) default value in the destination frame. Note that in either case, any explicitly set attributes on the source |SkyCoord| that are not part of the destination frame's definition are kept (stored on the resulting |SkyCoord|), and thus one can round-trip (e.g., from FK4 to ICRS to FK4 without losing obstime). Parameters ---------- frame : str, or `~astropy.coordinates.BaseCoordinateFrame` class \ or instance The frame to transform this coordinate into. merge_attributes : bool, optional Whether the default attributes in the destination frame are allowed to be overridden by explicitly set attributes in the source (see note above; default: `True`). Returns ------- sph_sky_region : `~regions.SphericalSkyRegion` A new spherical sky region represented in the ``frame`` frame. """ raise NotImplementedError @abc.abstractmethod def discretize_boundary(self, *, n_vertices=100): """ Discretize the boundary into a `~regions.PolygonSphericalSkyRegion`, as an approximation where all sides follow great circles. Parameters ---------- n_vertices : int, optional Number of points along the region's boundary. Default is 100. Returns ------- poly_sky_region: `~regions.PolygonSphericalSkyRegion` Spherical sky polygon object. """ raise NotImplementedError @abc.abstractmethod def to_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): """ Convert to a planar `~regions.SkyRegion`. Parameters ---------- wcs : `~astropy.wcs.WCS`, optional The world coordinate system transformation to use to convert between sky and pixel coordinates. Required if transforming with boundary distortions (if ``boundary_distortions`` is True). Ignored if boundary distortions not included. boundary_distortions : bool, optional If `True`, the projection-induced distortions of the region's boundary are preserved by discretizing the boundary into a polygon and transforming that polygon. If `False` (default), the region is converted to an equivalent idealized shape that ignores these boundary distortions. n_vertices : int, optional The number of polygon vertices for boundary discretization. This keyword will have no effect unless ``boundary_distortions=True``. Default is 100. Returns ------- sky_region : `~regions.SkyRegion` A planar sky region, with an equivalent shape (if ``boundary_distortions`` is False), or a discretized polygon of the boundary (if ``boundary_distortions`` is True). """ raise NotImplementedError @abc.abstractmethod def to_pixel(self, wcs, *, boundary_distortions=False, n_vertices=None): """ Convert to a planar `~regions.PixelRegion`. Parameters ---------- wcs : `~astropy.wcs.WCS` The world coordinate system transformation to use to convert between sky and pixel coordinates. boundary_distortions : bool, optional If `True`, the projection-induced distortions of the region's boundary are preserved by discretizing the boundary into a polygon and transforming that polygon. If `False` (default), the region is converted to an equivalent idealized shape that ignores these boundary distortions. n_vertices : int, optional The number of polygon vertices for boundary discretization. This keyword will have no effect unless ``boundary_distortions=True``. Default is 100. Returns ------- pixel_region : `~regions.PixelRegion` A pixel region, with an equivalent shape (if ``boundary_distortions`` is False), or a discretized polygon of the boundary (if ``boundary_distortions`` is True). """ raise NotImplementedError def write(self, filename, format=None, overwrite=False, **kwargs): # Note explicitly that this is not yet implemented. raise NotImplementedError def serialize(self, format=None, **kwargs): # Note explicitly that this is not yet implemented. raise NotImplementedError class ComplexSphericalSkyRegion(SphericalSkyRegion): """ Base class for complex cases, where the definitional parameters do not / cannot transform between coordinate frames (including `~regions.RangeSphericalSkyRegion`). """ # Because the parameters don't transform, # these object cannot be defined simply as "compound regions" # So some repr and str methods are changed. # (For now, this is just RangeSphericalSkyRegion, # but breaking out these methods/properties to streamline). # Instead, a variable set _boundaries is used, containing the # subclass boundary properties. These boundary properties are, # in order of preference, # (a) accessed from `_[boundname]`, for transformed # instances of subclass shapes that # don't simply map in parameterization from frame to frame (eg, Range) # (b) derived on-the-fly _boundaries = () def __repr__(self): prefix = f'{self.__class__.__name__}' cls_info = [] _do_params_info = False if (self._params is not None) and ( (len(self._params) > 1) | (self._params[0] != 'frame')): # Only do param-based repr if params other than "frame" are set # If only param is "frame", make repr with boundary info _do_params_info = True if _do_params_info: for param in self._params: if param == 'text': # place quotes around text value keyval = f'{param}={getattr(self, param)!r}' elif param == 'frame': attr = getattr(self, param) keyval = f'{param}={getattr(attr, "name", repr(attr))}' else: keyval = f'{param}={getattr(self, param)}' cls_info.append(keyval) cls_info = ',\n'.join(cls_info) return f'<{prefix}(\n{cls_info}\n)>' else: # First check if "frame" in self._params: if (self._params is not None) and self._params[0] == 'frame': param = 'frame' attr = getattr(self, param) keyval = f'{param}={getattr(attr, "name", repr(attr))}' cls_info.append(keyval) # If "params" is None, eg for a "transformed" # complex shape which is no longer described by the "high-level" # region parameters, instead give information on the regions # contained within _boundaries info: for bound in self._boundaries: keyval = f'{bound}={repr(getattr(self, bound))}' cls_info.append(keyval) cls_info = ',\n'.join(cls_info) return f'<{prefix}(\n{cls_info}\n)>' def __str__(self): cls_info = [('Region', self.__class__.__name__)] _do_params_info = False if (self._params is not None) and ( (len(self._params) > 1) | (self._params[0] != 'frame')): # Only do param-based str if params other than "frame" are set. # If only param is "frame", make str with boundary info _do_params_info = True if _do_params_info: for param in self._params: if param in ['text']: keyval = (param, repr(getattr(self, param))) elif param == 'frame': attr = getattr(self, param) keyval = (param, getattr(attr, 'name', repr(attr))) else: keyval = (param, getattr(self, param)) cls_info.append(keyval) return '\n'.join([f'{key}: {val}' for key, val in cls_info]) else: # First check if "frame" in self._params: if (self._params is not None) and (self._params[0] == 'frame'): param = 'frame' attr = getattr(self, param) keyval = (param, getattr(attr, 'name', repr(attr))) cls_info.append(keyval) # If "params" is None, eg for a transformed complex shape, # instead give information on the regions contained # within _boundaries info: for bound in self._boundaries: keyval = (bound, repr(getattr(self, bound))) cls_info.append(keyval) return '\n'.join([f'{key}: {val}' for key, val in cls_info]) @property @abc.abstractmethod def _compound_region(self): """ Compound region containing composite boundaries for this complex region shape. """ def copy(self, **changes): # Boundaries: only copy stashed internal attributes "_{bound name}", # as otherwise these are derived on-the-fly. boundaries_interalattr = [f"_{bn}" for bn in list(self._boundaries)] fields = boundaries_interalattr + [ '_frame', '_vertices', '_is_original_frame', '_params', ] + ['meta', 'visual'] if self._params is not None: fields += list(self._params) for field in fields: if (field not in changes) & hasattr(self, field): changes[field] = copy.deepcopy(getattr(self, field)) return self.__class__(**changes) @property def frame(self): frame = getattr(self, '_frame', None) if frame is None: return self._compound_region.frame return frame astropy-regions-f4139aa/regions/core/mask.py000066400000000000000000000262511521703212700212020ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines a class for region masks. """ import warnings import astropy.units as u import numpy as np from astropy.utils import minversion __all__ = ['RegionMask'] COPY_IF_NEEDED = False if not minversion(np, '2.0.0.dev') else None class RegionMask: """ Class for a region mask. Parameters ---------- data : array_like A 2D array representing the fractional overlap of a region on the pixel grid. This should be the full-sized (i.e., not truncated) array that is the direct output of one of the low-level "geometry" functions. bbox : `regions.RegionBoundingBox` The bounding box object defining the region minimal bounding box. Examples -------- Usage examples are provided in the :ref:`overlap-masks` section of the docs. """ def __init__(self, data, bbox): self.data = np.asanyarray(data) if self.data.shape != bbox.shape: raise ValueError('mask data and bounding box must have the same ' 'shape') self.bbox = bbox self._mask = (self.data == 0) def __array__(self, dtype=None, copy=COPY_IF_NEEDED): """ Array representation of the mask data array (e.g., for matplotlib). """ return np.array(self.data, dtype=dtype, copy=copy) @property def shape(self): """ The shape of the mask data array. """ return self.data.shape def get_overlap_slices(self, shape): """ Get slices for the overlapping part of the region mask and a 2D array. Parameters ---------- shape : 2-tuple of int The shape of the 2D array. Returns ------- slices_large : tuple of slices or `None` A tuple of slice objects for each axis of the large array, such that ``large_array[slices_large]`` extracts the region of the large array that overlaps with the small array. `None` is returned if there is no overlap of the bounding box with the given image shape. slices_small : tuple of slices or `None` A tuple of slice objects for each axis of the region mask array such that ``small_array[slices_small]`` extracts the region that is inside the large array. `None` is returned if there is no overlap of the bounding box with the given image shape. """ return self.bbox.get_overlap_slices(shape) def to_image(self, shape, dtype=float): """ Return an image of the mask in a 2D array of the given shape, taking any edge effects into account. Parameters ---------- shape : tuple of int The ``(ny, nx)`` shape of the output array. dtype : data-type, optional The desired data type for the array. This should be a floating data type if the `RegionMask` was created with the "exact" or "subpixel" mode, otherwise the fractional mask weights will be altered. A integer data type may be used if the `RegionMask` was created with the "center" mode. Returns ------- result : `~numpy.ndarray` A 2D array of the mask. """ if len(shape) != 2: raise ValueError('input shape must have 2 elements.') # find the overlap of the mask on the output image shape slices_large, slices_small = self.get_overlap_slices(shape) if slices_small is None: return None # no overlap # insert the mask into the output image image = np.zeros(shape, dtype=dtype) image[slices_large] = self.data[slices_small] return image def cutout(self, data, fill_value=0.0, copy=False): """ Create a cutout from the input data over the mask bounding box, taking any edge effects into account. Parameters ---------- data : array_like A 2D array on which to apply the region mask. fill_value : float, optional The value used to fill pixels where the region mask does not overlap with the input ``data``. The default is 0. copy : bool, optional If `True` then the returned cutout array will always be hold a copy of the input ``data``. If `False` and the mask is fully within the input ``data``, then the returned cutout array will be a view into the input ``data``. In cases where the mask partially overlaps or has no overlap with the input ``data``, the returned cutout array will always hold a copy of the input ``data`` (i.e., this keyword has no effect). Returns ------- result : `~numpy.ndarray` or `None` A 2D array cut out from the input ``data`` representing the same cutout region as the region mask. If there is a partial overlap of the region mask with the input data, pixels outside of the data will be assigned to ``fill_value``. `None` is returned if there is no overlap of the region with the input ``data``. """ data = np.asanyarray(data) if data.ndim != 2: raise ValueError('data must be a 2D array.') # find the overlap of the mask on the output image shape slices_large, slices_small = self.get_overlap_slices(data.shape) if slices_small is None: return None # no overlap cutout_shape = (slices_small[0].stop - slices_small[0].start, slices_small[1].stop - slices_small[1].start) if cutout_shape == self.shape: cutout = data[slices_large] if copy: cutout = np.copy(cutout) return cutout # cutout is always a copy for partial overlap dtype = float if ~np.isfinite(fill_value) else data.dtype cutout = np.zeros(self.shape, dtype=dtype) cutout[:] = fill_value cutout[slices_small] = data[slices_large] if isinstance(data, u.Quantity): cutout <<= data.unit return cutout def multiply(self, data, fill_value=0.0): """ Multiply the region mask with the input data, taking any edge effects into account. The result is a mask-weighted cutout from the data. Parameters ---------- data : array_like or `~astropy.units.Quantity` The 2D array to multiply with the region mask. fill_value : float, optional The value is used to fill pixels where the region mask does not overlap with the input ``data``. The default is 0. Returns ------- result : `~numpy.ndarray` or `None` A 2D mask-weighted cutout from the input ``data``. If there is a partial overlap of the region mask with the input data, pixels outside of the data will be assigned to ``fill_value`` before being multiplied with the mask. `None` is returned if there is no overlap of the region with the input ``data``. """ cutout = self.cutout(data, fill_value=fill_value) if cutout is None: return None else: # ignore multiplication with non-finite data values with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) weighted_cutout = cutout * self.data # fill values outside of the mask but within the bounding box weighted_cutout[self._mask] = fill_value return weighted_cutout def _get_overlap_cutouts(self, shape, mask=None): """ Get the aperture mask weights, pixel mask, and slice for the overlap with the input shape. If input, the ``mask`` is included in the output pixel mask cutout. Parameters ---------- shape : tuple of int The shape of data. mask : array_like (bool), optional A boolean mask with the same shape as ``shape`` where a `True` value indicates a masked pixel. Returns ------- slices_large : tuple of slices or `None` A tuple of slice objects for each axis of the large array of given ``shape``, such that ``large_array[slices_large]`` extracts the region of the large array that overlaps with the small array. `None` is returned if there is no overlap of the bounding box with the given image shape. aper_weights: 2D float `~numpy.ndarray` The cutout aperture mask weights for the overlap. pixel_mask: 2D bool `~numpy.ndarray` The cutout pixel mask for the overlap. Notes ----- This method is separate from ``get_values`` to facilitate applying the same slices, aper_weights, and pixel_mask to multiple associated arrays (e.g., data and error arrays). It is used in this way by the `PixelAperture.do_photometry` method. """ if mask is not None and mask.shape != shape: raise ValueError('mask and data must have the same shape') slc_large, slc_small = self.get_overlap_slices(shape) if slc_large is None: # no overlap return None, None, None aper_weights = self.data[slc_small] pixel_mask = (aper_weights > 0) # good pixels if mask is not None: pixel_mask &= ~mask[slc_large] return slc_large, aper_weights, pixel_mask def get_values(self, data, mask=None): """ Get the mask-weighted pixel values from the data as a 1D array. If the `~regions.RegionMask` was created with ``mode='center'``, (where the mask weights are only 1 or 0), then the returned values will simply be pixel values extracted from the data. Parameters ---------- data : array_like or `~astropy.units.Quantity` The 2D array from which to get mask-weighted values. mask : array_like (bool), optional A boolean mask with the same shape as ``data`` where a `True` value indicates the corresponding element of ``data`` is not returned in the result. Returns ------- result : `~numpy.ndarray` A 1D array of mask-weighted pixel values from the input ``data``. If there is no overlap of the region with the input ``data``, the result will be an empty array with shape (0,). """ slc_large, aper_weights, pixel_mask = self._get_overlap_cutouts( data.shape, mask=mask) if slc_large is None: return np.array([]) # ignore multiplication with non-finite data values with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) # pixel_mask is used so that pixels value where data = 0 and # aper_weights != 0 are still returned return (data[slc_large] * aper_weights)[pixel_mask] astropy-regions-f4139aa/regions/core/metadata.py000066400000000000000000000200451521703212700220220ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module proves classes to handle region metadata. """ from copy import deepcopy __all__ = ['Meta', 'RegionMeta', 'RegionVisual'] class Meta(dict): """ A base class for region metadata. Parameters ---------- seq : dict-like, optional A dictionary or other mapping object to initialize the metadata. **kwargs Additional keyword arguments to initialize the metadata. """ valid_keys = [] key_mapping = {} def __init__(self, seq=None, **kwargs): super().__init__() if seq: if isinstance(seq, dict): for key, val in seq.items(): self.__setitem__(key, val) else: for key, val in seq: self.__setitem__(key, val) if len(kwargs) > 0: for key, val in kwargs.items(): self.__setitem__(key, val) def __setitem__(self, key, value): key = self.key_mapping.get(key, key) if key in self.valid_keys: super().__setitem__(key, value) else: raise KeyError(f'{key} is not a valid key for this class.') def __getitem__(self, item): item = self.key_mapping.get(item, item) return super().__getitem__(item) def update(self, *args, **kwargs): """ Update the metadata with the input dictionary. Parameters ---------- *args : dict-like A dictionary or other mapping object to update the metadata. **kwargs : dict-like Additional keyword arguments to update the metadata. """ if args: if len(args) > 1: raise ValueError('Only one argument can be input') other = dict(args[0]) for key, value in other.items(): self[key] = value for key, value in kwargs.items(): self[key] = value def setdefault(self, key, value=None): if key not in self: self[key] = value return self[key] def copy(self): """ Make a deep copy of this object. """ return deepcopy(self) class RegionMeta(Meta): """ A dictionary subclass that holds the meta attributes of the region. """ valid_keys = ['background', 'comment', 'component', 'composite', 'corr', 'delete', 'edit', 'fixed', 'frame', 'highlite', 'include', 'label', 'line', 'move', 'name', 'range', 'restfreq', 'rotate', 'select', 'source', 'tag', 'text', 'textrotate', 'type', 'veltype'] key_mapping = {} class RegionVisual(Meta): """ A dictionary subclass which holds the visual attributes of the region. """ valid_keys = ['color', 'dash', 'dashlist', 'fill', 'font', 'fontname', 'fontsize', 'fontstyle', 'fontweight', 'labeloff', 'labelpos', 'labelcolor', 'line', 'linestyle', 'linewidth', 'marker', 'markersize', 'symbol', 'symsize', 'symthick', 'textangle', 'textrotate', 'usetex', 'default_style', 'dashes', 'markeredgewidth', 'rotation', 'facecolor', 'edgecolor'] key_mapping = {'point': 'symbol', 'width': 'linewidth'} def _define_default_mpl_kwargs(self, artist): """ Define the default matplotlib kwargs for the specified artist. The kwargs depend on the value of self.visual['default_style'], which can be set when reading region files. If this keywords is not set or set to 'mpl' or `None`, then the matplotlib defaults will be used, with the exception fill is turned off for Patch and Line2D objects. Parameters ---------- artist : {'Text', 'Line2D', 'Patch'} The matplotlib artist type. Returns ------- result : dict A dictionary of matplotlib keyword arguments. """ kwargs = {} default_style = self.get('default_style', None) if default_style is None or default_style == 'mpl': # do not fill by default, which is the only change from # matplotlib defaults if artist == 'Patch': kwargs['fill'] = False elif artist == 'point': # Line2D with marker only kwargs['fillstyle'] = 'none' kwargs['marker'] = 'o' return kwargs # 'ds9' style is set when reading from ds9 region files elif default_style == 'ds9': kwargs['color'] = '#00ff00' # green if artist == 'Text': kwargs['ha'] = 'center' # text horizontal alignment kwargs['va'] = 'center' # text vertical alignment elif artist == 'point': # Line2D with marker only from regions.io.ds9.core import ds9_valid_symbols kwargs['marker'] = ds9_valid_symbols['boxcircle'] kwargs['markersize'] = 11 kwargs['markeredgecolor'] = kwargs.pop('color') kwargs['fillstyle'] = 'none' elif artist == 'Patch': kwargs['edgecolor'] = kwargs.pop('color') kwargs['fill'] = False return kwargs def _to_mpl_kwargs(self, artist): """ Convert the visual metadata to a dictionary of matplotlib keyword arguments for the given artist. Parameters ---------- artist : {'Text', 'Line2D', 'Patch', 'point'} The matplotlib artist type. 'point' is a special case for Line2D with a marker only. Returns ------- result : dict A dictionary of matplotlib keyword arguments. """ if artist == 'Text': keymap = {'font': 'family', 'fontstyle': 'style', 'fontweight': 'weight', 'fontsize': 'size', 'textangle': 'rotation'} elif artist == 'Line2D': keymap = {} elif artist == 'Patch': keymap = {'color': 'edgecolor', 'fill': 'fill'} elif artist == 'point': keymap = {'symsize': 'markersize', 'color': 'markeredgecolor', 'linewidth': 'markeredgewidth', 'fill': 'fillstyle'} else: raise ValueError('invalid artist type') kwargs = {} for name, val in self.items(): if name in keymap: # NOTE: this will override existing mpl kwargs kwargs[keymap[name]] = val else: kwargs[name] = val default_style = kwargs.pop('default_style', None) if default_style == 'ds9': for key, val in kwargs.items(): if val == 'green': # X11/mpl green is #008000, ds9 uses #00ff00 kwargs[key] = '#00ff00' return kwargs def define_mpl_kwargs(self, artist): """ Define a dictionary of matplotlib keywords for the input ``artist`` from the region's ``visual`` properties. Parameters ---------- artist : {'Text', 'Line2D', 'Patch', 'point'} The matplotlib artist type. Returns ------- result : dict A dictionary of matplotlib keyword arguments. """ if artist not in ('Patch', 'Line2D', 'Text', 'point'): raise ValueError(f'artist "{artist}" is not supported') kwargs = self._define_default_mpl_kwargs(artist) kwargs.update(self._to_mpl_kwargs(artist)) remove_keys = [] if artist != 'Text': remove_keys.extend(['fontname', 'fontsize', 'fontweight', 'fontstyle']) else: remove_keys.extend(['linewidth']) for key in remove_keys: kwargs.pop(key, None) if 'fontstyle' in kwargs: kwargs['fontstyle'].replace('roman', 'normal') return kwargs astropy-regions-f4139aa/regions/core/pixcoord.py000066400000000000000000000166011521703212700220740ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import copy import numpy as np from astropy.coordinates import SkyCoord __all__ = ['PixCoord'] # Define global variables for the default 'origin' and 'mode' used for # WCS transformations. _DEFAULT_WCS_ORIGIN = 0 _DEFAULT_WCS_MODE = 'all' class PixCoord: """ A class for pixel coordinates. This class can represent a scalar or an array of pixel coordinates. `~regions.PixCoord` objects can be added or subtracted to each other. They can also be compared for equality. The data members are either numbers or `~numpy.ndarray` (not `~astropy.units.Quantity` objects with unit "pixel"). Given a `astropy.wcs.WCS` object, it can be transformed to and from a `~astropy.coordinates.SkyCoord` object. Parameters ---------- x : float or array-like Pixel coordinate x value. y : float or array-like Pixel coordinate y value. Examples -------- Usage examples are provided in the :ref:`getting_started-coord` section of the documentation. """ def __init__(self, x, y): x, y = np.broadcast_arrays(x, y) if x.shape == (): self.x, self.y = x.item(), y.item() else: self.x, self.y = x, y def copy(self): return self.__class__(copy.deepcopy(self.x), copy.deepcopy(self.y)) @staticmethod def _validate(obj, name, expected='any'): """ Validate that a given object is a valid `PixCoord`. Parameters ---------- obj : `PixCoord` The object to check. name : str The parameter name used for error messages. expected : {'any', 'scalar', 'array'} What kind of PixCoord to check for. Returns ------- obj : `PixCoord` The input object, if valid. """ if not isinstance(obj, PixCoord): raise TypeError(f'{name!r} must be a PixCoord') if expected == 'any': pass elif expected == 'scalar': if not obj.isscalar: raise ValueError(f'{name!r} must be a scalar PixCoord') elif expected == 'array': if obj.isscalar: raise ValueError(f'{name!r} must be a PixCoord array') else: raise ValueError(f'Invalid value for "expected": {expected!r}') return obj @property def isscalar(self): """ Whether the instance is scalar (e.g., a single (x, y) coordinate). """ return np.isscalar(self.x) def __repr__(self): return f'{self.__class__.__name__}(x={self.x}, y={self.y})' def __len__(self): if self.isscalar: raise TypeError(f'Scalar {self.__class__.__name__!r} object has ' 'no len()') return len(self.x) def __iter__(self): for (x, y) in zip(self.x, self.y, strict=True): yield PixCoord(x=x, y=y) def __getitem__(self, key): if self.isscalar: raise IndexError(f'Scalar {self.__class__.__name__!r} cannot be ' 'indexed or sliced.') # Let Numpy do the slicing x = self.x[key] y = self.y[key] return PixCoord(x=x, y=y) def __add__(self, other): if not isinstance(other, self.__class__): raise TypeError('Can add only to another PixCoord') return self.__class__(self.x + other.x, self.y + other.y) def __sub__(self, other): if not isinstance(other, self.__class__): raise TypeError('Can subtract only from another PixCoord') return self.__class__(self.x - other.x, self.y - other.y) def __eq__(self, other): """ Check whether ``other`` is `PixCoord` object and whether their abscissa and ordinate values are equal using `np.testing.assert_allclose` with its default tolerance values. """ if isinstance(other, self.__class__): return np.allclose([self.x, self.y], [other.x, other.y]) return False def to_sky(self, wcs, origin=_DEFAULT_WCS_ORIGIN, mode=_DEFAULT_WCS_MODE): """ Convert to a `~astropy.coordinates.SkyCoord`. Parameters ---------- wcs : `~astropy.wcs.WCS` The WCS to use to convert pixels to world coordinates. origin : int, optional Whether to return 0 or 1-based pixel coordinates. mode : {'all', 'wcs'}, optional Whether to do the transformation including distortions (``'all'``) or only including only the core WCS transformation (``'wcs'``). Returns ------- coord : `~astropy.coordinates.SkyCoord` A new object with sky coordinates corresponding to the pixel coordinates. """ return SkyCoord.from_pixel(xp=self.x, yp=self.y, wcs=wcs, origin=origin, mode=mode) @classmethod def from_sky(cls, skycoord, wcs, origin=_DEFAULT_WCS_ORIGIN, mode=_DEFAULT_WCS_MODE): """ Create `PixCoord` from a `~astropy.coordinates.SkyCoord`. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The sky coordinate. wcs : `~astropy.wcs.WCS` The WCS to use to convert pixels to world coordinates. origin : int, optional Whether to return 0 or 1-based pixel coordinates. mode : {'all', 'wcs'}, optional Whether to do the transformation including distortions (``'all'``) or only including only the core WCS transformation (``'wcs'``). Returns ------- coord : `PixCoord` A new `PixCoord` object at the position of the input sky coordinates. """ x, y = skycoord.to_pixel(wcs=wcs, origin=origin, mode=mode) return cls(x=x, y=y) def separation(self, other): r""" Calculate the separation to another pixel coordinate. This is the two-dimensional Cartesian separation :math:`d` where .. math:: d = \sqrt{(x_1 - x_2) ^ 2 + (y_1 - y_2) ^ 2} Parameters ---------- other : `PixCoord` The other pixel coordinate. Returns ------- separation : `numpy.array` The separation in pixels. """ dx = other.x - self.x dy = other.y - self.y return np.hypot(dx, dy) @property def xy(self): """ A 2-tuple ``(x, y)`` for this coordinate. """ return self.x, self.y def rotate(self, center, angle): """ Rotate the pixel coordinate. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- coord : `PixCoord` The rotated coordinates (which is an independent copy). """ dx = self.x - center.x dy = self.y - center.y vec = np.array([dx, dy]) cosa, sina = np.cos(angle), np.sin(angle) rotation_matrix = np.array([[cosa, -sina], [sina, cosa]]) vec = np.matmul(rotation_matrix, vec) return self.__class__(center.x + vec[0], center.y + vec[1]) astropy-regions-f4139aa/regions/core/regions.py000066400000000000000000000210571521703212700217140ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides a Regions class. """ from regions.core.core import Region from regions.core.registry import RegionsRegistry __all__ = ['Regions'] __doctest_skip__ = ['Regions.read', 'Regions.write', 'Regions.parse', 'Regions.serialize'] class Regions: """ Class to hold a list of `~regions.Region` objects. This class provides a unified I/O interface that supports reading, writing, parsing, and serializing many region data formats. Parameters ---------- regions : list of `~regions.Region` The list of region objects. """ def __init__(self, regions=(), /): if regions == (): regions = [] for item in regions: if not isinstance(item, Region): raise TypeError('Input regions must be a list of Region ' 'objects') self.regions = regions def __getitem__(self, index): newregions = self.regions[index] if isinstance(newregions, Region): # one item return newregions else: newcls = object.__new__(self.__class__) newcls.regions = newregions return newcls def __repr__(self): cls_name = self.__class__.__name__ if not self.regions: return f'<{cls_name}([])>' regions_str = ',\n '.join(repr(r) for r in self.regions) return f'<{cls_name}([\n {regions_str}\n])>' def __str__(self): return '\n'.join(repr(r) for r in self.regions) def __len__(self): return len(self.regions) def append(self, region): """ Append the region to the end of the list of regions. Parameters ---------- region : `~regions.Region` The region to append. """ if not isinstance(region, Region): raise TypeError('Input region must be a Region object') self.regions.append(region) def extend(self, regions): """ Extend the list of regions by appending elements from the input regions. Parameters ---------- regions : `~regions.Regions` or list of `~regions.Region` A `~regions.Regions` object or a list of regions to include. """ if isinstance(regions, Regions): self.regions.extend(regions.regions) else: for item in regions: if not isinstance(item, Region): raise TypeError('Input regions must be a list of Region ' 'objects') self.regions.extend(regions) def insert(self, index, region): """ Insert the region before index. Parameters ---------- index : int The list index. region : `~regions.Region` The region to insert. """ self.regions.insert(index, region) def reverse(self): """ Reverse the list of regions in place. """ self.regions.reverse() def pop(self, index=-1): """ Remove and return the region at index. Parameters ---------- index : int, optional The index of the region to remove. Returns ------- result : `~regions.Region` The removed region. """ return self.regions.pop(index) def copy(self): """ Return a shallow copy of this object. """ newcls = object.__new__(self.__class__) newcls.regions = self.regions.copy() return newcls @classmethod def get_formats(cls): """ Get the registered I/O formats as a Table. """ return RegionsRegistry.get_formats(cls) @classmethod def read(cls, filename, format=None, cache=False, **kwargs): """ Read and parse a region file and return as a Regions object. This method allows reading a file in many supported data formats, e.g.,:: >>> from regions import Regions >>> reg1 = Regions.read('regions.reg', format='ds9') >>> reg2 = Regions.read('regions.crtf', format='crtf') >>> reg3 = Regions.read('regions.fits', format='fits') A list of the available formats for `~regions.Regions` is available using:: >>> Regions.get_formats() Parameters ---------- filename : str The filename or URL of the file to read. format : str, optional The file format specifier. cache : bool or 'update', optional Whether to cache the contents of remote URLs. If 'update', check the remote URL for a new version but store the result in the cache. **kwargs : dict, optional Keyword arguments passed to the data reader. Returns ------- result : `~regions.Regions` A `~regions.Regions` object containing the file contents. """ return RegionsRegistry.read(filename, cls, format=format, cache=cache, **kwargs) @classmethod def parse(cls, data, format=None, **kwargs): """ Parse a region string or table and return as a Regions object. This method allows parsing region data in many supported data formats, e.g.,:: >>> from regions import Regions >>> reg1 = Regions.parse(regions_str, format='ds9') >>> reg2 = Regions.parse(regions_str, format='crtf') >>> reg3 = Regions.parse(regions_tbl, format='fits') A list of the available formats for `~regions.Regions` is available using:: >>> Regions.get_formats() Parameters ---------- data : str or `~astropy.table.Table` The region data to parse. format : str, optional The file format specifier. **kwargs : dict, optional Keyword arguments passed to the data parser. Returns ------- result : `~regions.Regions` A `~regions.Regions` object containing the data contents. """ return RegionsRegistry.parse(data, cls, format=format, **kwargs) def write(self, filename, format=None, overwrite=False, **kwargs): """ Write the regions to a region file in the specified format. This method allows writing a file in many supported data formats, e.g.,:: >>> from regions import Regions >>> reg = Regions.read('regions.reg', format='ds9') >>> reg.write('new_regions.reg', format='ds9') >>> reg.write('new_regions.crtf', format='crtf') >>> reg.write('new_regions.fits', format='fits') A list of the available formats for `~regions.Regions` is available using:: >>> Regions.get_formats() Parameters ---------- filename : str The filename or URL of the file to write. format : str, optional The file format specifier. overwrite : bool, optional If True, overwrite the output file if it exists. Raises an `OSError` if False and the output file exists. Default is False. **kwargs : dict, optional Keyword arguments passed to the data writer. """ return RegionsRegistry.write(self.regions, filename, self.__class__, format=format, overwrite=overwrite, **kwargs) def serialize(self, format=None, **kwargs): """ Serialize the regions to a region string or table. This method allows serializing regions in many supported data formats, e.g.,:: >>> from regions import Regions >>> reg = Regions.read('regions.reg', format='ds9') >>> reg1_str = reg.serialize(format='ds9') >>> reg2_str = reg.serialize(format='crtf') >>> reg3_tbl = reg.serialize(format='fits') A list of the available formats for `~regions.Regions` is available using:: >>> Regions.get_formats() Parameters ---------- format : str, optional The file format specifier. **kwargs : dict, optional Keyword arguments passed to the data serializer. """ return RegionsRegistry.serialize(self.regions, self.__class__, format=format, **kwargs) astropy-regions-f4139aa/regions/core/registry.py000066400000000000000000000206641521703212700221210ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides a RegionsRegistry class. """ from astropy.table import Table __all__ = [] class IORegistryError(Exception): """ Exception class for various registry errors. """ class RegionsRegistry: """ Class to hold a registry to read, write, parse, and serialize regions in various formats. """ registry = {} @classmethod def register(cls, classobj, methodname, filetype): def inner_wrapper(wrapped_func): key = (classobj, methodname, filetype) if key in cls.registry: raise ValueError(f'{methodname} for {filetype} is already ' f'registered for {classobj.__name__}') cls.registry[key] = wrapped_func return wrapped_func return inner_wrapper @classmethod def get_identifiers(cls, classobj): return [key for key in cls.registry if key[0] == classobj and key[1] == 'identify'] @classmethod def _no_format_error(cls, classobj): msg = ('Format could not be identified based on the file name or ' 'contents, please provide a "format" argument.' f'\n{cls._get_format_table_str(classobj)}') raise IORegistryError(msg) @classmethod def identify_format(cls, filename, classobj, methodname): format = None identifiers = cls.get_identifiers(classobj) if identifiers: for identifier in identifiers: if cls.registry[identifier](methodname, filename): format = identifier[2] break # finds the first valid filetype if format is None: cls._no_format_error(classobj) return format @classmethod def read(cls, filename, classobj, format=None, **kwargs): """ Read in a regions file. Parameters ---------- filename : str The file to read from. classobj : class The class to read the regions into. format : str, optional The format of the file. If not provided, the format will be identified based on the file name or contents. **kwargs : dict, optional Additional keyword arguments to pass to the reader. Returns ------- regions : classobj The regions object read from the file. """ if format is None: format = cls.identify_format(filename, classobj, 'read') key = (classobj, 'read', format) try: reader = cls.registry[key] except KeyError: msg = (f'No reader defined for format "{format}" and class ' f'"{classobj.__name__}".\n' f'{cls._get_format_table_str(classobj)}') raise IORegistryError(msg) from None return reader(filename, **kwargs) @classmethod def parse(cls, data, classobj, format=None, **kwargs): """ Parse a regions string or table. Parameters ---------- data : str, Table The data to parse. classobj : class The class to parse the regions into. format : str, optional The format of the data. If not provided, an error will be raised. **kwargs : dict, optional Additional keyword arguments to pass to the parser. Returns ------- regions : classobj The regions object parsed from the data. """ if format is None: cls._no_format_error(classobj) key = (classobj, 'parse', format) try: parser = cls.registry[key] except KeyError: msg = (f'No parser defined for format "{format}" and class ' f'"{classobj.__name__}".\n' f'{cls._get_format_table_str(classobj)}') raise IORegistryError(msg) from None return parser(data, **kwargs) @classmethod def write(cls, regions, filename, classobj, format=None, **kwargs): """ Write to a regions file. Parameters ---------- regions : classobj The regions object to write. filename : str The file to write to. classobj : class The class of the regions object. format : str, optional The format of the file. If not provided, an error will be raised. **kwargs : dict, optional Additional keyword arguments to pass to the writer. """ if format is None: format = cls.identify_format(filename, classobj, 'write') key = (classobj, 'write', format) try: writer = cls.registry[key] except KeyError: msg = (f'No writer defined for format "{format}" and class ' f'"{classobj.__name__}".\n' f'{cls._get_format_table_str(classobj)}') raise IORegistryError(msg) from None return writer(regions, filename, **kwargs) @classmethod def serialize(cls, regions, classobj, format=None, **kwargs): """ Serialize to a regions string or table. Parameters ---------- regions : classobj The regions object to serialize. classobj : class The class of the regions object. format : str, optional The format of the data. If not provided, an error will be raised. **kwargs : dict, optional Additional keyword arguments to pass to the serializer. Returns ------- data : str, Table The data serialized from the regions object. """ if format is None: cls._no_format_error(classobj) key = (classobj, 'serialize', format) try: serializer = cls.registry[key] except KeyError: msg = (f'No serializer defined for format "{format}" and class ' f'"{classobj.__name__}".\n' f'{cls._get_format_table_str(classobj)}') raise IORegistryError(msg) from None return serializer(regions, **kwargs) @classmethod def get_formats(cls, classobj): """ Get the registered I/O formats as a Table. Parameters ---------- classobj : class The class to get the formats for. Returns ------- tbl : Table The table of formats. """ filetypes = list({key[2] for key in cls.registry if key[0] == classobj}) rows = [['Format', 'Parse', 'Serialize', 'Read', 'Write', 'Auto-identify']] for filetype in sorted(filetypes): keys = {key[1] for key in cls.registry if key[0] == classobj and key[2] == filetype} row = [filetype] for methodname in rows[0][1:]: name = ('identify' if 'identify' in methodname else methodname.lower()) row.append('Yes' if name in keys else 'No') rows.append(row) if len(rows) == 1: return Table() cols = list(zip(*rows, strict=True)) tbl = Table() for col in cols: tbl[col[0]] = col[1:] return tbl @classmethod def _get_format_table_str(cls, classobj): lines = ['', f'The available formats for the {classobj.__name__} ' 'class are:', ''] tbl = cls.get_formats(classobj) lines.extend(tbl.pformat(max_lines=-1, max_width=80)) return '\n'.join(lines) def _update_docstring(classobj, methodname): """ Update the docstring to include a table of all available registered formats and methods. """ import re if methodname == 'identify': return lines = getattr(classobj, methodname).__doc__.splitlines() matches = [re.search(r'(\S)', line) for line in lines[1:]] left_indent = ' ' * min(match.start() for match in matches if match) new_lines = RegionsRegistry._get_format_table_str(classobj).splitlines() lines.extend([left_indent + line for line in new_lines]) try: # classmethod getattr(classobj, methodname).__func__.__doc__ = '\n'.join(lines) except AttributeError: # instancemethod getattr(classobj, methodname).__doc__ = '\n'.join(lines) return astropy-regions-f4139aa/regions/core/tests/000077500000000000000000000000001521703212700210315ustar00rootroot00000000000000astropy-regions-f4139aa/regions/core/tests/__init__.py000066400000000000000000000000001521703212700231300ustar00rootroot00000000000000astropy-regions-f4139aa/regions/core/tests/test_bounding_box.py000066400000000000000000000122401521703212700251160ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the bounding_box module. """ import pytest from numpy.testing import assert_allclose from regions._utils.optional_deps import HAS_MATPLOTLIB from regions.core.bounding_box import RegionBoundingBox from regions.core.pixcoord import PixCoord from regions.shapes import RectanglePixelRegion def test_bounding_box_init(): bbox = RegionBoundingBox(1, 10, 2, 20) assert bbox.ixmin == 1 assert bbox.ixmax == 10 assert bbox.iymin == 2 assert bbox.iymax == 20 def test_bounding_box_init_minmax(): with pytest.raises(ValueError): RegionBoundingBox(100, 1, 1, 100) with pytest.raises(ValueError): RegionBoundingBox(1, 100, 100, 1) def test_bounding_box_inputs(): with pytest.raises(TypeError): RegionBoundingBox([1], [10], [2], [9]) with pytest.raises(TypeError): RegionBoundingBox([1, 2], 10, 2, 9) with pytest.raises(TypeError): RegionBoundingBox(1.0, 10.0, 2.0, 9.0) with pytest.raises(TypeError): RegionBoundingBox(1.3, 10, 2, 9) with pytest.raises(TypeError): RegionBoundingBox(1, 10.3, 2, 9) with pytest.raises(TypeError): RegionBoundingBox(1, 10, 2.3, 9) with pytest.raises(TypeError): RegionBoundingBox(1, 10, 2, 9.3) def test_bounding_box_from_float(): # This is the example from the method docstring bbox = RegionBoundingBox.from_float(xmin=1.0, xmax=10.0, ymin=2.0, ymax=20.0) assert bbox == RegionBoundingBox(ixmin=1, ixmax=11, iymin=2, iymax=21) bbox = RegionBoundingBox.from_float(xmin=1.4, xmax=10.4, ymin=1.6, ymax=10.6) assert bbox == RegionBoundingBox(ixmin=1, ixmax=11, iymin=2, iymax=12) def test_bounding_box_eq(): bbox = RegionBoundingBox(1, 10, 2, 20) assert bbox == RegionBoundingBox(1, 10, 2, 20) assert bbox != RegionBoundingBox(9, 10, 2, 20) assert bbox != RegionBoundingBox(1, 99, 2, 20) assert bbox != RegionBoundingBox(1, 10, 9, 20) assert bbox != RegionBoundingBox(1, 10, 2, 99) with pytest.raises(TypeError): assert bbox == (1, 10, 2, 20) def test_bounding_box_repr(): bbox = RegionBoundingBox(1, 10, 2, 20) assert repr(bbox) == ('RegionBoundingBox(ixmin=1, ixmax=10, iymin=2, ' 'iymax=20)') def test_bounding_box_shape(): bbox = RegionBoundingBox(1, 10, 2, 20) assert bbox.shape == (18, 9) def test_bounding_box_center(): bbox = RegionBoundingBox(1, 10, 2, 20) assert bbox.center == (10.5, 5) def test_bounding_box_get_overlap_slices(): bbox = RegionBoundingBox(1, 10, 2, 20) slc = ((slice(2, 20, None), slice(1, 10, None)), (slice(0, 18, None), slice(0, 9, None))) assert bbox.get_overlap_slices((50, 50)) == slc bbox = RegionBoundingBox(-10, -1, 2, 20) assert bbox.get_overlap_slices((50, 50)) == (None, None) bbox = RegionBoundingBox(-10, 10, -10, 20) slc = ((slice(0, 20, None), slice(0, 10, None)), (slice(10, 30, None), slice(10, 20, None))) assert bbox.get_overlap_slices((50, 50)) == slc def test_bounding_box_extent(): bbox = RegionBoundingBox(1, 10, 2, 20) assert_allclose(bbox.extent, (0.5, 9.5, 1.5, 19.5)) @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_bounding_box_as_artist(): bbox = RegionBoundingBox(1, 10, 2, 20) patch = bbox.as_artist() assert_allclose(patch.get_xy(), (0.5, 1.5)) assert_allclose(patch.get_width(), 9) assert_allclose(patch.get_height(), 18) @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_bounding_box_plot(): from matplotlib.patches import Patch bbox = RegionBoundingBox(1, 10, 2, 20) patch = bbox.plot() assert isinstance(patch, Patch) @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_bounding_box_to_region(): bbox = RegionBoundingBox(1, 10, 2, 20) region = RectanglePixelRegion(PixCoord(5.0, 10.5), width=9., height=18.) bbox_region = bbox.to_region() assert_allclose(bbox_region.center.xy, region.center.xy) assert bbox_region.width == region.width assert bbox_region.height == region.height assert bbox_region.angle == region.angle def test_bounding_box_union(): bbox1 = RegionBoundingBox(1, 10, 2, 20) bbox2 = RegionBoundingBox(5, 21, 7, 32) bbox_union_expected = RegionBoundingBox(1, 21, 2, 32) bbox_union1 = bbox1 | bbox2 bbox_union2 = bbox1.union(bbox2) assert bbox_union1 == bbox_union_expected assert bbox_union1 == bbox_union2 with pytest.raises(TypeError): bbox1.union((5, 21, 7, 32)) def test_bounding_box_intersect(): bbox1 = RegionBoundingBox(1, 10, 2, 20) bbox2 = RegionBoundingBox(5, 21, 7, 32) bbox_intersect_expected = RegionBoundingBox(5, 10, 7, 20) bbox_intersect1 = bbox1 & bbox2 bbox_intersect2 = bbox1.intersection(bbox2) assert bbox_intersect1 == bbox_intersect_expected assert bbox_intersect1 == bbox_intersect2 with pytest.raises(TypeError): bbox1.intersection((5, 21, 7, 32)) assert bbox1.intersection(RegionBoundingBox(30, 40, 50, 60)) is None astropy-regions-f4139aa/regions/core/tests/test_compound.py000066400000000000000000000231711521703212700242720ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the compound module. """ import operator import astropy.units as u import numpy as np import pytest from astropy.coordinates import Latitude, Longitude, SkyCoord from numpy.testing import assert_allclose from regions._utils.examples import _make_example_dataset from regions.core import (CompoundPixelRegion, CompoundSkyRegion, CompoundSphericalSkyRegion, PixCoord, RegionBoundingBox) from regions.shapes import (CirclePixelRegion, CircleSkyRegion, CircleSphericalSkyRegion) from regions.tests.helpers import make_simple_wcs @pytest.fixture(scope='session', name='wcs') def fixture_wcs(): config = dict(crpix=(18, 9), cdelt=(-10, 10), shape=(18, 36)) dataset = _make_example_dataset(config=config) return dataset.wcs class TestCompoundPixel: """ Test compound pixel regions. """ # Two circles that overlap in one column c1 = CirclePixelRegion(PixCoord(5, 5), 4) c2 = CirclePixelRegion(PixCoord(11, 5), 4) def test_copy(self): reg = (self.c1 | self.c2).copy() assert isinstance(reg, CompoundPixelRegion) assert reg.operator is operator.or_ assert isinstance(reg.region1, CirclePixelRegion) assert reg.region1.center.xy == (5, 5) assert reg.region1.radius == 4 assert reg.region1.meta == {} assert reg.region1.visual == {} assert isinstance(reg.region2, CirclePixelRegion) assert reg.region2.center.xy == (11, 5) assert reg.region2.radius == 4 assert reg.region2.meta == {} assert reg.region2.visual == {} def test_rotate(self): reg = (self.c1 | self.c2).rotate(PixCoord(0, 0), 360 * u.deg) assert isinstance(reg, CompoundPixelRegion) assert reg.operator is operator.or_ assert isinstance(reg.region1, CirclePixelRegion) assert_allclose(reg.region1.center.xy, (5, 5)) assert reg.region1.radius == 4 assert reg.region1.meta == {} assert reg.region1.visual == {} assert isinstance(reg.region2, CirclePixelRegion) assert_allclose(reg.region2.center.xy, (11, 5)) assert reg.region2.radius == 4 assert reg.region2.meta == {} assert reg.region2.visual == {} def test_union(self, wcs): union = self.c1 | self.c2 assert isinstance(union, CompoundPixelRegion) mask = union.to_mask() assert_allclose(mask.data[:, 7], [0, 0, 1, 1, 1, 1, 1, 0, 0]) assert_allclose(mask.data[:, 6], [0, 1, 1, 1, 1, 1, 1, 1, 0]) union_to_sphsky = union.to_spherical_sky(wcs) assert isinstance(union_to_sphsky, CompoundSphericalSkyRegion) union_to_sky = union.to_sky(wcs) assert isinstance(union_to_sky, CompoundSkyRegion) def test_intersection(self): intersection = self.c1 & self.c2 mask = intersection.to_mask() assert_allclose(mask.data[:, 7], [0, 0, 1, 1, 1, 1, 1, 0, 0]) assert_allclose(mask.data[:, 6], [0, 0, 0, 0, 0, 0, 0, 0, 0]) def test_symdiff(self): symdiff = self.c1 ^ self.c2 mask = symdiff.to_mask() assert_allclose(mask.data[:, 7], [0, 0, 0, 0, 0, 0, 0, 0, 0]) assert_allclose(mask.data[:, 6], [0, 1, 1, 1, 1, 1, 1, 1, 0]) def test_to_mask(self): # fixes #168 pixcoord3 = PixCoord(1, 1) c3 = CirclePixelRegion(pixcoord3, 4) union2 = self.c1 | c3 mask1 = union2.to_mask() pixcoord4 = PixCoord(9, 9) c4 = CirclePixelRegion(pixcoord4, 4) union3 = self.c1 | c4 mask2 = union3.to_mask() # mask1 and mask2 should be equal assert_allclose(mask1.data, mask2.data) ref_data = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) assert_allclose(mask1.data, ref_data) def test_bounding_box(self): bbox = (self.c1 | self.c2).bounding_box assert bbox == RegionBoundingBox(1, 16, 1, 10) def test_invalid(self): with pytest.raises(ValueError): self.c1 | 1.0 with pytest.raises(ValueError): self.c1 | PixCoord(1.0, 2.0) def test_compound_sky(): skycoord1 = SkyCoord(0 * u.deg, 0 * u.deg, frame='galactic') c1 = CircleSkyRegion(skycoord1, 1 * u.deg) skycoord2 = SkyCoord(1 * u.deg, 1 * u.deg, frame='galactic') c2 = CircleSkyRegion(skycoord2, 0.5 * u.deg) test_coord1 = SkyCoord(1.2 * u.deg, 1.2 * u.deg, frame='galactic') test_coord2 = SkyCoord(0.5 * u.deg, 0.5 * u.deg, frame='galactic') test_coord3 = SkyCoord(0.7 * u.deg, 0.7 * u.deg, frame='galactic') test_coord4 = SkyCoord(2 * u.deg, 5 * u.deg, frame='galactic') wcs = make_simple_wcs(skycoord1, 0.1 * u.deg, 20) assert c2.contains(test_coord1, wcs) and not c1.contains(test_coord1, wcs) assert not c2.contains(test_coord2, wcs) and c1.contains(test_coord2, wcs) assert c1.contains(test_coord3, wcs) and c2.contains(test_coord3, wcs) assert (not c2.contains(test_coord4, wcs) and not c1.contains(test_coord4, wcs)) coords = SkyCoord([test_coord1, test_coord2, test_coord3, test_coord4], frame='galactic') union = c1 | c2 assert (union.contains(coords, wcs) == [True, True, True, False]).all() intersection = c1 & c2 assert ((intersection.contains(coords, wcs) == [False, False, True, False]).all()) diff = c1 ^ c2 assert (diff.contains(coords, wcs) == [True, True, False, False]).all() c3 = CircleSkyRegion(test_coord4, 0.1 * u.deg) union = c1 | c2 | c3 assert (union.contains(coords, wcs) == [True, True, True, True]).all() intersection = c1 & c2 & c3 assert ((intersection.contains(coords, wcs) == [False, False, False, False]).all()) diff = c1 ^ c2 ^ c3 assert (diff.contains(coords, wcs) == [True, True, False, True]).all() assert 'Compound' in str(union) # Test keeping the input meta meta = {'test_meta': True} compound = CompoundSkyRegion(c1, c2, operator.and_, meta=meta) assert compound.meta['test_meta'] union_to_sphsky = union.to_spherical_sky(wcs=wcs) assert isinstance(union_to_sphsky, CompoundSphericalSkyRegion) union_to_pixel = union.to_pixel(wcs) assert isinstance(union_to_pixel, CompoundPixelRegion) def test_compound_spherical_sky(wcs): skycoord1 = SkyCoord(0 * u.deg, 0 * u.deg, frame='galactic') c1 = CircleSphericalSkyRegion(skycoord1, 1 * u.deg) skycoord2 = SkyCoord(1 * u.deg, 1 * u.deg, frame='galactic') c2 = CircleSphericalSkyRegion(skycoord2, 0.5 * u.deg) test_coord1 = SkyCoord(1.2 * u.deg, 1.2 * u.deg, frame='galactic') test_coord2 = SkyCoord(0.5 * u.deg, 0.5 * u.deg, frame='galactic') test_coord3 = SkyCoord(0.7 * u.deg, 0.7 * u.deg, frame='galactic') test_coord4 = SkyCoord(2 * u.deg, 5 * u.deg, frame='galactic') assert c2.contains(test_coord1) and not c1.contains(test_coord1) assert not c2.contains(test_coord2) and c1.contains(test_coord2) assert c1.contains(test_coord3) and c2.contains(test_coord3) assert (not c2.contains(test_coord4) and not c1.contains(test_coord4)) coords = SkyCoord([test_coord1, test_coord2, test_coord3, test_coord4], frame='galactic') union = c1 | c2 assert (union.contains(coords) == [True, True, True, False]).all() intersection = c1 & c2 assert ((intersection.contains(coords) == [False, False, True, False]).all()) diff = c1 ^ c2 assert (diff.contains(coords) == [True, True, False, False]).all() c3 = CircleSphericalSkyRegion(test_coord4, 0.1 * u.deg) union = c1 | c2 | c3 assert (union.contains(coords) == [True, True, True, True]).all() intersection = c1 & c2 & c3 assert ((intersection.contains(coords) == [False, False, False, False]).all()) diff = c1 ^ c2 ^ c3 assert (diff.contains(coords) == [True, True, False, True]).all() assert 'Compound' in str(union) union_to_sky = union.to_sky(wcs=wcs) assert isinstance(union_to_sky, CompoundSkyRegion) union_to_pixel = union.to_pixel(wcs) assert isinstance(union_to_pixel, CompoundPixelRegion) assert isinstance(union.bounding_circle, CircleSphericalSkyRegion) bound_lonlat = union.bounding_lonlat assert isinstance(bound_lonlat[0], Longitude) assert isinstance(bound_lonlat[1], Latitude) assert isinstance(union.transform_to('icrs'), CompoundSphericalSkyRegion) assert isinstance(union.discretize_boundary(n_vertices=2), CompoundSphericalSkyRegion) skycoord4 = SkyCoord(1 * u.deg, 89 * u.deg, frame='galactic') c4 = CircleSphericalSkyRegion(skycoord4, 2.5 * u.deg) union = c1 | c4 bound_lonlat = union.bounding_lonlat assert bound_lonlat[0] is None assert isinstance(bound_lonlat[1], Latitude) astropy-regions-f4139aa/regions/core/tests/test_mask.py000066400000000000000000000161721521703212700234040ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the mask module. """ import astropy.units as u import numpy as np import pytest from astropy.utils import minversion from numpy.testing import assert_allclose, assert_almost_equal from regions.core.bounding_box import RegionBoundingBox from regions.core.mask import RegionMask from regions.core.pixcoord import PixCoord from regions.shapes import CircleAnnulusPixelRegion, CirclePixelRegion NUMPY_LT_2_0 = not minversion(np, '2.0.dev') COPY_IF_NEEDED = False if NUMPY_LT_2_0 else None POSITIONS = [(-20, -20), (-20, 20), (20, -20), (60, 60)] def test_mask_input_shapes(): with pytest.raises(ValueError): mask_data = np.ones((10, 10)) bbox = RegionBoundingBox(5, 10, 5, 10) RegionMask(mask_data, bbox) def test_mask_array(): mask_data = np.ones((10, 10)) bbox = RegionBoundingBox(5, 15, 5, 15) mask = RegionMask(mask_data, bbox) data = np.array(mask) assert_allclose(data, mask.data) def test_mask_copy(): bbox = RegionBoundingBox(5, 15, 5, 15) mask = RegionMask(np.ones((10, 10)), bbox) mask_copy = np.array(mask, copy=True) mask_copy[0, 0] = 100.0 assert mask.data[0, 0] == 1.0 mask = RegionMask(np.ones((10, 10)), bbox) mask_copy = np.array(mask, copy=False) mask_copy[0, 0] = 100.0 assert mask.data[0, 0] == 100.0 # no copy; copy=None returns a copy only if __array__ returns a copy # copy=None was introduced in NumPy 2.0 mask = RegionMask(np.ones((10, 10)), bbox) mask_copy = np.array(mask, copy=COPY_IF_NEEDED) mask_copy[0, 0] = 100.0 assert mask.data[0, 0] == 100.0 # no copy mask = RegionMask(np.ones((10, 10)), bbox) mask_copy = np.asarray(mask) mask_copy[0, 0] = 100.0 assert mask.data[0, 0] == 100.0 # needs to copy because of the dtype change mask = RegionMask(np.ones((10, 10)), bbox) mask_copy = np.asarray(mask, dtype=int) mask_copy[0, 0] = 100.0 assert mask.data[0, 0] == 1.0 def test_mask_get_overlap_slices(): aper = CirclePixelRegion(PixCoord(5, 5), radius=10.) mask = aper.to_mask() slc = ((slice(0, 16, None), slice(0, 16, None)), (slice(5, 21, None), slice(5, 21, None))) assert mask.get_overlap_slices((25, 25)) == slc def test_mask_cutout_shape(): mask_data = np.ones((10, 10)) bbox = RegionBoundingBox(5, 15, 5, 15) mask = RegionMask(mask_data, bbox) with pytest.raises(ValueError): mask.cutout(np.arange(10)) with pytest.raises(ValueError): mask.to_image((10,)) def test_mask_cutout_copy(): data = np.ones((50, 50)) aper = CirclePixelRegion(PixCoord(25, 25), radius=10.) mask = aper.to_mask() cutout = mask.cutout(data, copy=True) data[25, 25] = 100. assert cutout[10, 10] == 1. # test quantity data data2 = np.ones((50, 50)) * u.adu cutout2 = mask.cutout(data2, copy=True) assert cutout2.unit == data2.unit data2[25, 25] = 100. * u.adu assert cutout2[10, 10].value == 1. @pytest.mark.parametrize('position', POSITIONS) def test_mask_cutout_no_overlap(position): data = np.ones((50, 50)) aper = CirclePixelRegion(PixCoord(position[0], position[1]), radius=10.) mask = aper.to_mask() cutout = mask.cutout(data) assert cutout is None weighted_data = mask.multiply(data) assert weighted_data is None image = mask.to_image(data.shape) assert image is None @pytest.mark.parametrize('position', POSITIONS) def test_mask_cutout_partial_overlap(position): data = np.ones((50, 50)) aper = CirclePixelRegion(PixCoord(position[0], position[1]), radius=30.) mask = aper.to_mask() cutout = mask.cutout(data) assert cutout.shape == mask.shape weighted_data = mask.multiply(data) assert weighted_data.shape == mask.shape image = mask.to_image(data.shape) assert image.shape == data.shape def test_mask_multiply(): radius = 10. data = np.ones((50, 50)) region = CirclePixelRegion(PixCoord(25, 25), radius=radius) mask = region.to_mask(mode='exact') data_weighted = mask.multiply(data) assert_almost_equal(np.sum(data_weighted), np.pi * radius**2) # test that multiply() returns a copy data[25, 25] = 100. assert data_weighted[10, 10] == 1. def test_mask_multiply_quantity(): radius = 10. data = np.ones((50, 50)) * u.adu region = CirclePixelRegion(PixCoord(25, 25), radius=radius) mask = region.to_mask(mode='exact') data_weighted = mask.multiply(data) assert data_weighted.unit == u.adu assert_almost_equal(np.sum(data_weighted.value), np.pi * radius**2) # test that multiply() returns a copy data[25, 25] = 100. * u.adu assert data_weighted[10, 10].value == 1. @pytest.mark.parametrize('value', (np.nan, np.inf)) def test_mask_nonfinite_fill_value(value): region = CircleAnnulusPixelRegion(PixCoord(0, 0), 10, 20) data = np.ones((101, 101)).astype(int) cutout = region.to_mask().cutout(data, fill_value=value) assert ~np.isfinite(cutout[0, 0]) def test_mask_multiply_fill_value(): region = CircleAnnulusPixelRegion(PixCoord(0, 0), 10, 20) data = np.ones((101, 101)).astype(int) cutout = region.to_mask().multiply(data, fill_value=np.nan) xypos = ((20, 20), (5, 5), (5, 35), (35, 5), (35, 35)) for x, y in xypos: assert np.isnan(cutout[y, x]) def test_mask_nonfinite_in_bbox(): """ Regression test that non-finite data values outside of the mask but within the bounding box are set to zero. """ data = np.ones((101, 101)) data[33, 33] = np.nan data[67, 67] = np.inf data[33, 67] = -np.inf data[22, 22] = np.nan data[22, 23] = np.inf radius = 20. reg1 = CirclePixelRegion(PixCoord(50, 50), radius) reg2 = CirclePixelRegion(PixCoord(5, 5), radius) wdata1 = reg1.to_mask(mode='exact').multiply(data) assert_allclose(np.sum(wdata1), np.pi * radius**2) wdata2 = reg2.to_mask(mode='exact').multiply(data) assert_allclose(np.sum(wdata2), 561.6040111923013) @pytest.mark.parametrize('x, y, exp_shape', [(0, 0, 245), (50, 50, 940), (100, 100, 245)]) def test_mask_get_values(x, y, exp_shape): aper = CircleAnnulusPixelRegion(PixCoord(x, y), inner_radius=10, outer_radius=20) data = np.ones((101, 101)) values = aper.to_mask(mode='center').get_values(data) assert values.shape == (exp_shape,) assert_allclose(np.sum(values), exp_shape) def test_mask_get_values_no_overlap(): aper = CirclePixelRegion(PixCoord(-100, -100), radius=3) data = np.ones((51, 51)) values = aper.to_mask().get_values(data) assert values.shape == (0,) def test_mask_get_values_mask(): aper = CirclePixelRegion(PixCoord(24.5, 24.5), radius=10.) data = np.ones((51, 51)) mask = aper.to_mask(mode='exact') with pytest.raises(ValueError): mask.get_values(data, mask=np.ones(3)) arr = mask.get_values(data, mask=None) assert_allclose(np.sum(arr), 100. * np.pi) data_mask = np.zeros(data.shape, dtype=bool) data_mask[25:] = True arr2 = mask.get_values(data, mask=data_mask) assert_allclose(np.sum(arr2), 100. * np.pi / 2.) astropy-regions-f4139aa/regions/core/tests/test_metadata.py000066400000000000000000000037171521703212700242320ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the metadata module. """ import pytest from regions.core.metadata import RegionMeta, RegionVisual def test_region_meta(): meta_dict = {'text': 'hello world', 'tag': ['Tag1', 'Tag2']} meta = RegionMeta(meta_dict) for key, val in meta_dict.items(): assert val == meta[key] meta2 = meta.copy() assert isinstance(meta2, RegionMeta) text = 'new' meta['text'] = text assert meta2['text'] != text with pytest.raises(KeyError): RegionMeta({'invalid': 1}) with pytest.raises(KeyError): meta.update({'invalid': 1}) with pytest.raises(KeyError): meta.update(invalid=1) with pytest.raises(KeyError): meta.setdefault('invalid', 1) def test_region_visual(): meta_dict = {'color': 'blue', 'fontsize': 12} meta = RegionVisual(meta_dict) for key, val in meta_dict.items(): assert val == meta[key] meta2 = meta.copy() assert isinstance(meta2, RegionVisual) color = 'green' meta['color'] = color assert meta2['color'] != color with pytest.raises(KeyError): RegionVisual({'invalid': 1}) with pytest.raises(KeyError): meta.update({'invalid': 1}) with pytest.raises(KeyError): meta.update(invalid=1) with pytest.raises(KeyError): meta.setdefault('invalid', 1) def test_region_visual_mpl_kwargs(): meta_dict = {'color': 'blue'} meta = RegionVisual(meta_dict) kwargs = meta.define_mpl_kwargs('Patch') expected = {'edgecolor': 'blue', 'fill': False} assert kwargs == expected kwargs = meta.define_mpl_kwargs('point') expected = {'markeredgecolor': 'blue', 'fillstyle': 'none', 'marker': 'o'} assert kwargs == expected kwargs = meta.define_mpl_kwargs('Text') expected = {'color': 'blue'} assert kwargs == expected with pytest.raises(ValueError): meta.define_mpl_kwargs('invalid') astropy-regions-f4139aa/regions/core/tests/test_pixcoord.py000066400000000000000000000163471521703212700243040ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the pixcoord module. """ import astropy.units as u import numpy as np import pytest from numpy.testing import assert_allclose, assert_equal from regions._utils.examples import _make_example_dataset from regions.core.pixcoord import PixCoord @pytest.fixture(scope='session', name='wcs') def fixture_wcs(): config = dict(crpix=(18, 9), cdelt=(-10, 10), shape=(18, 36)) dataset = _make_example_dataset(config=config) return dataset.wcs def test_pixcoord_copy_scalar(): pc1 = PixCoord(x=1, y=2) pc2 = pc1.copy() pc1.x = 99 pc1.y = 99 assert pc2.xy == (1, 2) def test_pixcoord_copy_array(): pc1 = PixCoord(x=[1, 2], y=[3, 4]) pc2 = pc1.copy() pc1.x[0] = 99 pc1.y[0] = 99 assert_equal(pc2.x, [1, 2]) assert_equal(pc2.y, [3, 4]) def test_pixcoord_basic_dimension(): with pytest.raises(ValueError): PixCoord(np.array([1, 2]), [3, 4, 5, 6]) def test_pixcoord_basics_scalar(): pc0 = PixCoord(x=1, y=2) pc1 = PixCoord(x=np.array(1), y=2) pc2 = PixCoord(x=np.array(1), y=np.array(2)) assert pc0 == pc1 assert pc1 == pc2 assert pc0.x == 1 assert pc0.y == 2 assert pc0.isscalar assert pc1.isscalar assert pc2.isscalar assert str(pc0) == 'PixCoord(x=1, y=2)' assert repr(pc0) == 'PixCoord(x=1, y=2)' with pytest.raises(TypeError): _ = len(pc0) with pytest.raises(IndexError): _ = pc0[0] def test_pixcoord_basics_array_1d(): pc0 = PixCoord(x=[1, 2, 3], y=[11, 22, 33]) pc1 = PixCoord(x=1, y=[1, 2, 3]) assert_equal(pc0.x, [1, 2, 3]) assert_equal(pc0.y, [11, 22, 33]) assert_equal(pc1.x, [1, 1, 1]) assert_equal(pc1.y, [1, 2, 3]) assert not pc0.isscalar assert not pc1.isscalar assert str(pc0) == 'PixCoord(x=[1 2 3], y=[11 22 33])' assert str(pc1) == 'PixCoord(x=[1 1 1], y=[1 2 3])' assert repr(pc0) == 'PixCoord(x=[1 2 3], y=[11 22 33])' assert repr(pc1) == 'PixCoord(x=[1 1 1], y=[1 2 3])' assert len(pc0) == 3 assert len(pc1) == 3 pc2 = list(iter(pc0))[-1] assert pc2.x == 3 assert pc2.y == 33 pc3 = pc0[-1] assert pc3.isscalar assert pc3.x == 3 assert pc3.y == 33 pc4 = pc0[1:] assert len(pc4) == 2 assert_equal(pc4.x, [2, 3]) assert_equal(pc4.y, [22, 33]) def test_pixcoord_basics_array_2d(): pc0 = PixCoord([[1, 2, 3], [4, 5, 6]], [[11, 12, 13], [14, 15, 16]]) assert_equal(pc0.x, [[1, 2, 3], [4, 5, 6]]) assert_equal(pc0.y, [[11, 12, 13], [14, 15, 16]]) assert pc0.isscalar is False expected = ('PixCoord(x=[[1 2 3]\n [4 5 6]], ' 'y=[[11 12 13]\n [14 15 16]])') assert str(pc0) == expected assert repr(pc0) == expected assert len(pc0) == 2 assert_equal(pc0[0].x, pc0.x[0]) assert_equal(pc0[0].y, pc0.y[0]) pc1 = list(iter(pc0)) assert_equal(pc1[0].x, pc0.x[0]) assert_equal(pc1[0].y, pc0.y[0]) def test_pixcoord_to_sky_scalar(wcs): pc1 = PixCoord(x=18, y=9) sc = pc1.to_sky(wcs=wcs) assert sc.name == 'galactic' assert_allclose(sc.data.lon.deg, 349.88093995435634) assert_allclose(sc.data.lat.deg, 10.003028030623508) pc2 = PixCoord.from_sky(skycoord=sc, wcs=wcs) assert isinstance(pc2.x, float) assert pc2.isscalar assert pc1 == pc2 def test_pixcoord_to_sky_array_1d(wcs): pc1 = PixCoord(x=[17, 18], y=[8, 9]) sc = pc1.to_sky(wcs=wcs) assert sc.name == 'galactic' assert_allclose(sc.data.lon.deg, [0, 349.88094]) assert_allclose(sc.data.lat.deg, [0, 10.003028]) pc2 = PixCoord.from_sky(skycoord=sc, wcs=wcs) assert isinstance(pc2.x, np.ndarray) assert pc2.x.shape == (2,) assert not pc2.isscalar assert pc1 == pc2 def test_pixcoord_to_sky_array_2d(wcs): pc0 = PixCoord(x=[[17, 18]], y=[[8, 9]]) pc1 = PixCoord(x=[[17, 17, 17], [18, 18, 18]], y=[[8, 8, 8], [9, 9, 9]]) sc0 = pc0.to_sky(wcs=wcs) assert sc0.name == 'galactic' sc1 = pc1.to_sky(wcs=wcs) assert sc1.name == 'galactic' assert_allclose(sc1.data.lon.deg, [[0, 0, 0], [349.88094, 349.88094, 349.88094]]) assert_allclose(sc1.data.lat.deg, [[0, 0, 0], [10.003028, 10.003028, 10.003028]]) assert_allclose(sc0.data.lon.deg, [[0, 349.88094]]) assert_allclose(sc0.data.lat.deg, [[0, 10.003028]]) pc2 = PixCoord.from_sky(skycoord=sc0, wcs=wcs) assert isinstance(pc2.x, np.ndarray) assert pc2.x.shape == (1, 2) assert not pc2.isscalar assert pc0 == pc2 def test_pixcoord_separation_scalar(): pc1 = PixCoord(x=1, y=2) pc2 = PixCoord(x=4, y=6) sep = pc1.separation(pc2) assert_allclose(sep, 5) def test_pixcoord_separation_array_1d(): pc1 = PixCoord(x=[1, 1], y=[2, 2]) pc2 = PixCoord(x=[4, 4], y=[6, 6]) sep = pc1.separation(pc2) assert_allclose(sep, [5, 5]) def test_pixcoord_separation_array_2d(): pc1 = PixCoord(x=[[1, 1]], y=[[2, 2]]) pc2 = PixCoord(x=[[4, 4]], y=[[6, 6]]) sep = pc1.separation(pc2) assert isinstance(sep, np.ndarray) assert sep.shape == (1, 2) assert_allclose(sep, [[5, 5]]) def test_equality(): arr = np.array([1, 2]) pc1 = PixCoord(arr[0], arr[1]) pc2 = PixCoord(arr[0] + 0.0000001, arr[1]) assert pc1 != arr assert pc1 == PixCoord(arr[0], arr[1]) assert pc1 == pc2 pc3 = PixCoord([[1, 2, 3], [4, 5, 6]], [[11, 12, 13], [14, 15, 16]]) pc4 = PixCoord([[1, 2, 3], [4, 5, 6]], [[11.0000002, 12, 13], [14, 15, 16]]) assert pc3 == pc4 assert pc3 == PixCoord([[1, 2, 3], [4, 5, 6]], [[11, 12, 13], [14, 15, 16]]) def test_pixcoord_xy(): arr = np.array([1, 2]) pc = PixCoord(arr[0], arr[1]) assert pc.xy[0] == pc.x assert pc.xy[1] == pc.y def test_pixcoord_rotate_scalar(): point = PixCoord(3, 4) center = PixCoord(2, 3) point = point.rotate(center, 90 * u.deg) assert_allclose(point.xy, (1, 4)) def test_pixcoord_rotate_array(): point = PixCoord([3, 3], [4, 4]) center = PixCoord(2, 3) point = point.rotate(center, 90 * u.deg) assert_allclose(point.xy, ([1, 1], [4, 4])) def test_pixcoord_addition(): point1 = PixCoord([3, 3], [4, 4]) point2 = PixCoord([5, 7], [6, 1]) center = PixCoord(2, 3) p1 = point1 + point2 p2 = point2 + point1 assert p1 == p2 assert_equal(p1.x, (8, 10)) assert_equal(p1.y, (10, 5)) p3 = point1 + center p4 = center + point1 assert p3 == p4 assert_equal(p3.x, (5, 5)) assert_equal(p3.y, (7, 7)) p5 = point1 + point1 + point1 assert_equal(p5.x, point1.x * 3) assert_equal(p5.y, point1.y * 3) with pytest.raises(TypeError): point1 + 10 with pytest.raises(ValueError): point1 + PixCoord([1, 1, 1], [2, 2, 2]) def test_pixcoord_subtraction(): point1 = PixCoord([3, 3], [4, 4]) point2 = PixCoord([5, 7], [6, 1]) center = PixCoord(2, 3) p1 = point2 - point1 assert_equal(p1.x, (2, 4)) assert_equal(p1.y, (2, -3)) p2 = point1 - center assert_equal(p2.x, (1, 1)) assert_equal(p2.y, (1, 1)) with pytest.raises(TypeError): point1 - 10 with pytest.raises(ValueError): point1 - PixCoord([1, 1, 1], [2, 2, 2]) astropy-regions-f4139aa/regions/core/tests/test_regions.py000066400000000000000000000162741521703212700241220ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the regions module. """ import pytest from astropy import units as u from astropy.coordinates import SkyCoord from astropy.table import Table from regions import CirclePixelRegion, CircleSkyRegion, RectanglePixelRegion from regions.core import PixCoord, Region, Regions def test_regions_inputs(): regs = [] for radius in range(1, 5): center = PixCoord(14, 21) regs.append(CirclePixelRegion(center, radius=radius)) regions = Regions(regs) assert len(regions) == 4 def test_regions_no_input(): regions = Regions() assert len(regions) == 0 regions = Regions([]) assert len(regions) == 0 def test_regions_invalid_input(): match = "'int' object is not iterable" with pytest.raises(TypeError, match=match): Regions(1) match = 'Input regions must be a list of Region objects' with pytest.raises(TypeError, match=match): Regions([1]) with pytest.raises(TypeError, match=match): Regions([1, 2, 3]) def test_regions_append(): regions = Regions() for radius in range(1, 5): center = PixCoord(14, 21) regions.append(CirclePixelRegion(center, radius=radius)) assert len(regions) == 4 match = 'Input region must be a Region object' with pytest.raises(TypeError, match=match): regions.append(1) def test_regions_extend(): regions = Regions() regs = [] for radius in range(1, 5): center = PixCoord(14, 21) regs.append(CirclePixelRegion(center, radius=radius)) regions.extend(regs) assert len(regions) == 4 regions.extend(regions) assert len(regions) == 8 match = 'Input regions must be a list of Region objects' with pytest.raises(TypeError, match=match): regions.extend([1]) def test_regions_methods(): regions = Regions() for radius in range(1, 5): center = PixCoord(14, 21) regions.append(CirclePixelRegion(center, radius=radius)) reg_slc = regions[0:2] assert len(reg_slc) == 2 reg = CirclePixelRegion(PixCoord(0, 0), radius=1) regions.insert(0, reg) assert len(regions) == 5 assert regions[0] == reg regions2 = regions.copy() regions.reverse() assert regions.regions == regions2.regions[::-1] outreg = regions.pop(-1) assert outreg == reg def test_regions_get_formats(): reg = CirclePixelRegion(PixCoord(0, 0), radius=1) regions = Regions([reg]) tbl = regions.get_formats() assert isinstance(tbl, Table) assert len(tbl) == 3 def test_regions_repr(): reg = CirclePixelRegion(PixCoord(0, 0), radius=1) regions = Regions([reg]) reg_repr = '' regions_str = reg_repr regions_repr = f'' assert str(regions) == regions_str assert repr(regions) == regions_repr def test_region_equivalency(): """ Test the __eq__ method of Region and its subclasses to ensure that it correctly identifies when two regions are equal, and that it uses np.allclose for comparing parameters that are numerical in nature. """ reg1 = CirclePixelRegion(PixCoord(0, 0), radius=1) reg2 = CirclePixelRegion(PixCoord(0, 0), radius=1) assert reg1 == reg2 # __eq__ uses np.allclose for comparing regions, so this should be True reg3 = CirclePixelRegion(PixCoord(0, 0), radius=1 + 1e-10) assert reg1 == reg3 # Different radius should not be equal reg_diff = CirclePixelRegion(PixCoord(0, 0), radius=2) assert reg1 != reg_diff # Test SkyRegion equality reg4 = CircleSkyRegion(SkyCoord(ra=0 * u.deg, dec=0 * u.deg), radius=1 * u.arcsec) reg5 = CircleSkyRegion(SkyCoord(ra=0 * u.deg, dec=0 * u.deg), radius=1 * u.arcsec) assert reg4 == reg5 reg6 = CircleSkyRegion(SkyCoord(ra=0 * u.deg, dec=0 * u.deg), radius=1 * u.arcsec + 1e-10 * u.arcsec) assert reg4 == reg6 # Different classes should not be equal assert reg1 != reg4 # Test SkyCoord center allclose comparison # Small coordinate differences should be considered equal reg7 = CircleSkyRegion(SkyCoord(ra=0 * u.deg + 1e-10 * u.deg, dec=0 * u.deg), radius=1 * u.arcsec) assert reg4 == reg7 # Declination offset reg8 = CircleSkyRegion(SkyCoord(ra=0 * u.deg, dec=0 * u.deg + 1e-10 * u.deg), radius=1 * u.arcsec) assert reg4 == reg8 # Both RA and Dec with small offsets reg9 = CircleSkyRegion(SkyCoord(ra=0 * u.deg + 1e-10 * u.deg, dec=0 * u.deg + 1e-10 * u.deg), radius=1 * u.arcsec) assert reg4 == reg9 # Significantly different SkyCoord should not be equal reg10 = CircleSkyRegion(SkyCoord(ra=0.01 * u.deg, dec=0 * u.deg), radius=1 * u.arcsec) assert reg4 != reg10 # SkyCoord with different frames should not be equal reg_gal = CircleSkyRegion(SkyCoord(l=0 * u.deg, b=0 * u.deg, frame='galactic'), radius=1 * u.arcsec) assert reg4 != reg_gal def test_is_close_skycoord_frame_error(monkeypatch): """ Test that if the is_equivalent_frame method of the SkyCoord frame raises a ValueError, then _is_close should catch that and return False rather than propagating the exception. This is to ensure that if the frames of the SkyCoords cannot be compared for equivalency, we should not consider the coordinates to be close, but we also should not raise an exception. """ def raises_value_error(self, other): raise ValueError c1 = SkyCoord(ra=0 * u.deg, dec=0 * u.deg) c2 = SkyCoord(ra=0 * u.deg, dec=0 * u.deg) monkeypatch.setattr(type(c1.frame), 'is_equivalent_frame', raises_value_error) assert not Region._is_close(c1, c2) def test_is_close_non_comparable(): """ Test that if the __eq__ method of the objects being compared raises a TypeError, then _is_close should return False rather than propagating the exception. """ class BadCompare: def __eq__(self, other): raise TypeError bad = BadCompare() assert not Region._is_close(bad, bad) def test_region_eq_param_mismatch(): """ Test that if two regions are of the same class but have mismatched _params, then they should not be considered equal. This is a sanity check to ensure that the __eq__ method is correctly comparing the parameters of the regions. """ reg1 = CirclePixelRegion(PixCoord(0, 0), radius=1) reg2 = CirclePixelRegion(PixCoord(0, 0), radius=1) reg2._params = () # simulate mismatched parameter set assert reg1 != reg2 def test_compound_covers(): """ Test covers method on compound regions. """ rect = RectanglePixelRegion(center=PixCoord(1, 1), width=2, height=2) comp_union = rect.union(rect) assert comp_union.covers(PixCoord(1, 1)) comp_inter = rect.intersection(rect) assert comp_inter.covers(PixCoord(2, 2)) # on the boundary astropy-regions-f4139aa/regions/io/000077500000000000000000000000001521703212700173465ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/__init__.py000066400000000000000000000012621521703212700214600ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage provides tools for reading and writing region files. """ from .crtf.connect import * # noqa: F401, F403 from .crtf.core import * # noqa: F401, F403 from .crtf.read import * # noqa: F401, F403 from .crtf.write import * # noqa: F401, F403 from .ds9.connect import * # noqa: F401, F403 from .ds9.core import * # noqa: F401, F403 from .ds9.read import * # noqa: F401, F403 from .ds9.write import * # noqa: F401, F403 from .fits.connect import * # noqa: F401, F403 from .fits.core import * # noqa: F401, F403 from .fits.read import * # noqa: F401, F403 from .fits.write import * # noqa: F401, F403 astropy-regions-f4139aa/regions/io/crtf/000077500000000000000000000000001521703212700203045ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/crtf/__init__.py000066400000000000000000000002541521703212700224160ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage provides tools for reading and writing CRTF (CASA Region Text Format) region files. """ astropy-regions-f4139aa/regions/io/crtf/connect.py000066400000000000000000000025711521703212700223140ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst from astropy.utils.data import get_readable_fileobj from regions.core import Region, Regions from regions.core.registry import RegionsRegistry __all__ = [] @RegionsRegistry.register(Region, 'identify', 'crtf') @RegionsRegistry.register(Regions, 'identify', 'crtf') def is_crtf(methodname, filepath): """ Identify a CRTF region file. Parameters ---------- methodname : {'read', 'write'} The method name called that needs auto-identification. filepath : str The path to the file. Returns ------- result : bool Returns `True` if the given file is a CRTF region file. """ all_exten = ('.crtf', '.crtf.gz') exten = {'read': all_exten, 'write': all_exten[0]} if methodname == 'write': return filepath.lower().endswith(exten[methodname]) elif methodname == 'read': if (isinstance(filepath, str) and filepath.lower().endswith(exten[methodname])): return True else: with get_readable_fileobj(filepath, encoding='binary') as fileobj: signature = '#CRTF' pos = fileobj.tell() sig = fileobj.read(len(signature)) fileobj.seek(pos) return sig == signature or sig == signature.encode() else: return False astropy-regions-f4139aa/regions/io/crtf/core.py000066400000000000000000000022351521703212700216100ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst from astropy.utils.exceptions import AstropyUserWarning __all__ = ['CRTFRegionParserWarning', 'CRTFRegionParserError'] class CRTFRegionParserWarning(AstropyUserWarning): """ A generic warning class for CRTF region parsing. """ class CRTFRegionParserError(ValueError): """ A generic error class for CRTF region parsing. """ # Valid symbols in CRTF valid_symbols = {'.': 'point', ',': 'pixel', 'o': 'circle', 'v': 'triangle_down', '^': 'triangle_up', '<': 'triangle_left', '>': 'triangle_right', '1': 'tri_down', '2': 'tri_up', '3': 'tri_left', '4': 'tri_right', 's': 'square', 'p': 'pentagon', '*': 'star', 'h': 'hexagon1', 'H': 'hexagon2', '+': 'plus', 'x': 'x', 'D': 'diamond', 'd': 'thin_diamond', '|': 'vline', '_': 'hline'} astropy-regions-f4139aa/regions/io/crtf/io_core.py000066400000000000000000000550731521703212700223070ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import numbers from warnings import warn import astropy.units as u from astropy.coordinates import (Angle, SkyCoord, UnitSphericalRepresentation, frame_transform_graph) from astropy.utils.exceptions import AstropyUserWarning from regions.core.core import PixCoord, SkyRegion from regions.core.metadata import RegionMeta, RegionVisual from regions.io.crtf.core import CRTFRegionParserWarning from regions.shapes import (CircleAnnulusPixelRegion, CircleAnnulusSkyRegion, CirclePixelRegion, CircleSkyRegion, EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion, EllipsePixelRegion, EllipseSkyRegion, LinePixelRegion, LineSkyRegion, PointPixelRegion, PointSkyRegion, PolygonPixelRegion, PolygonSkyRegion, RectangleAnnulusPixelRegion, RectangleAnnulusSkyRegion, RectanglePixelRegion, RectangleSkyRegion, RegularPolygonPixelRegion, TextPixelRegion, TextSkyRegion) __all__ = [] regions_attributes = dict(circle=['center', 'radius'], ellipse=['center', 'width', 'height', 'angle'], rectangle=['center', 'width', 'height', 'angle'], polygon=['vertices'], circleannulus=['center', 'inner_radius', 'outer_radius'], ellipseannulus=['center', 'inner_width', 'inner_height', 'outer_width', 'outer_height', 'angle'], line=['start', 'end'], point=['center'], text=['center']) regions_attributes['rectangleannulus'] = regions_attributes['ellipseannulus'] # Map the region names in the respective format to the ones available in # this package reg_mapping = {'CRTF': {x: x for x in regions_attributes}} reg_mapping['CRTF']['rotbox'] = 'rectangle' reg_mapping['CRTF']['box'] = 'rectangle' reg_mapping['CRTF']['centerbox'] = 'rectangle' reg_mapping['CRTF']['poly'] = 'polygon' reg_mapping['CRTF']['symbol'] = 'point' reg_mapping['CRTF']['text'] = 'text' reg_mapping['CRTF']['annulus'] = 'circleannulus' # valid astropy coordinate frames in their respective formats valid_coordsys = {'CRTF': ['image', 'fk5', 'fk4', 'galactic', 'geocentrictrueecliptic', 'supergalactic', 'icrs']} # Map astropy's coordinate frame names with their respective name in # the file format. coordsys_mapping = {'CRTF': {x: x.upper() for x in valid_coordsys['CRTF']}} # Coordinate frame names must be uppercase following the CASA CRTF syntax coordsys_mapping['CRTF']['geocentrictrueecliptic'] = 'ECLIPTIC' coordsys_mapping['CRTF']['fk5'] = 'J2000' coordsys_mapping['CRTF']['fk4'] = 'B1950' coordsys_mapping['CRTF']['supergalactic'] = 'SUPERGAL' class RegionConversionError(ValueError): """ A generic error class for Shape to Region conversions. """ class _ShapeList(list): """ A class to hold a list of `~regions.Shape` objects. """ def to_regions(self): """ Convert to a list of `~regions.Region` objects. """ regions = [] for shape in self: # Skip elliptical multi-annulus for now if shape.region_type == 'ellipse' and len(shape.coord) > 5: msg = f'Skipping elliptical annulus {shape}' warn(msg, AstropyUserWarning) continue if shape.region_type in ['box'] and shape.format_type == 'CRTF': msg = f'Skipping {shape.region_type} {shape}' warn(msg, CRTFRegionParserWarning) continue region = shape.to_region() regions.append(region) return regions def to_crtf(self, coordsys='fk5', fmt='.6f', radunit='deg'): """ Convert to CRTF (CASA Region Text Format) region strings. Parameters ---------- coordsys : str An Astropy coordinate system that overrides the coordinate system frame for all regions. fmt : str A python string format defining the output precision. Default is '.6f', which is accurate to 0.0036 arcseconds. radunit : str The unit of the radius. Returns ------- region_string : str A CRFT region string. """ crtf_strings = { 'circle': '{0}circle[[{1:FMT}deg, {2:FMT}deg], {3:FMT}RAD]', 'circleannulus': ('{0}annulus[[{1:FMT}deg, {2:FMT}deg], ' '[{3:FMT}RAD, {4:FMT}RAD]]'), # Make sure that width goes to minor axis and height to # major axis 'ellipse': ('{0}ellipse[[{1:FMT}deg, {2:FMT}deg], [{4:FMT}RAD, ' '{3:FMT}RAD], {5:FMT}deg]'), 'rectangle': ('{0}rotbox[[{1:FMT}deg, {2:FMT}deg], [{3:FMT}RAD, ' '{4:FMT}RAD], {5:FMT}deg]'), 'polygon': '{0}poly[{1}]', 'point': '{0}point[[{1:FMT}deg, {2:FMT}deg]]', 'symbol': '{0}symbol[[{1:FMT}deg, {2:FMT}deg], {symbol}]', 'text': '{0}text[[{1:FMT}deg, {2:FMT}deg], \'{text}\']', 'line': ('{0}line[[{1:FMT}deg, {2:FMT}deg], [{3:FMT}deg, ' '{4:FMT}deg]]')} output = '#CRTFv0\n' if radunit == 'arcsec': # arcseconds are allowed for all but image coordinates if coordsys.lower() not in ('image',): radunitstr = '"' else: raise ValueError('Radius unit arcsec not valid for ' f'coordsys {coordsys}') else: radunitstr = radunit for key, val in crtf_strings.items(): crtf_strings[key] = val.replace('FMT', fmt).replace('RAD', radunitstr) # CASA does not support global coordinate specification, even # though the documentation for the specification explicitly # states that it does. global_coord = coordsys_mapping['CRTF'][coordsys.lower()] output += f'global coord={global_coord}\n' for shape in self: shape.check_crtf() shape.meta = _to_crtf_meta(shape.meta) # if unspecified, include is True. Despite the # specification, CASA does *not* support a preceding "+". If # you want a region included, leave the opening character # blank. include = '' if shape.include in (False, '-'): include = '-' if shape.meta.get('type', 'reg') == 'ann': include += 'ann ' if shape.meta.get('label', '') != '': shape.meta['label'] = f"'{shape.meta['label']}'" keylist = ('include', 'comment', 'symbol', 'coord', 'text', 'range', 'corr', 'type') meta_pairs = [] for key, val in shape.meta.items(): if key not in keylist: meta_pairs.append(f'{key}={val}') meta_str = ', '.join(meta_pairs) # The first item should be the coordinates, since CASA # cannot recognize a region without an inline coordinate # specification. It can be, but does not need to be, # comma-separated at the start. shape_coordsys = shape.coordsys if shape_coordsys.lower() != coordsys.lower(): coord = coordsys_mapping['CRTF'][coordsys.lower()] if meta_str.strip(): meta_str = f'coord={coord}, ' + meta_str else: # if there is no metadata at all (above), the # trailing comma is incorrect meta_str = f'coord={coord}' if 'comment' in shape.meta: meta_str += ', ' + shape.meta['comment'] if 'range' in shape.meta: shape.meta['range'] = [str(str(x).replace(' ', '')) for x in shape.meta['range']] meta_str += f", range={shape.meta['range']}".replace("'", '') if 'corr' in shape.meta: meta_str += f", corr={shape.meta['corr']}".replace("'", '') coord = [] if coordsys not in ['image', 'physical']: for val in shape.coord: if (isinstance(val, Angle) or (radunit == '' or radunit is None)): coord.append(float(val.value)) else: coord.append(float(val.to_value(radunit))) else: for val in shape.coord: if isinstance(val, u.Quantity): coord.append(float(val.value)) else: coord.append(float(val)) if (shape.region_type in ['ellipse', 'rectangle'] and len(shape.coord) % 2 == 1): coord[-1] = float(shape.coord[-1].to_value(u.deg)) if shape.region_type == 'polygon': vals = [f'[{x:{fmt}}deg, {y:{fmt}}deg]' for x, y in zip(coord[::2], coord[1::2], strict=True)] coord = ', '.join(vals) line = crtf_strings['polygon'].format(include, coord) elif shape.region_type == 'point': if 'symbol' in shape.meta: line = crtf_strings['symbol'].format( include, *coord, symbol=shape.meta['symbol']) else: line = crtf_strings['point'].format(include, *coord) elif shape.region_type == 'ellipse': coord[2:] = [x / 2 for x in coord[2:]] if len(coord) % 2 == 1: coord[-1] *= 2 line = crtf_strings['ellipse'].format(include, *coord) elif shape.region_type == 'text': line = crtf_strings['text'].format( include, *coord, text=shape.meta['text']) else: line = crtf_strings[shape.region_type].format(include, *coord) if meta_str.strip(): output += f'{line}, {meta_str}\n' else: output += f'{line}\n' return output class _Shape: """ Helper class to represent a CRTF Region. This serves as intermediate step in the parsing process. Parameters ---------- coordsys : str An Astropy Coordinate system frame used in the region. region_type : str The type of the region (as defined in this package). coord : list of `~astropy.coordinates.Angle` or `~astropy.units.Quantity` The region coordinates. meta : dict The meta attributes. composite : bool Flag indicting whether the region is a Composite region. include : bool Flag indicating where to include or exclude the region. """ shape_to_sky_region = dict(circle=CircleSkyRegion, ellipse=EllipseSkyRegion, rectangle=RectangleSkyRegion, polygon=PolygonSkyRegion, circleannulus=CircleAnnulusSkyRegion, ellipseannulus=EllipseAnnulusSkyRegion, rectangleannulus=RectangleAnnulusSkyRegion, line=LineSkyRegion, point=PointSkyRegion, text=TextSkyRegion) shape_to_pixel_region = dict(circle=CirclePixelRegion, ellipse=EllipsePixelRegion, rectangle=RectanglePixelRegion, polygon=PolygonPixelRegion, circleannulus=CircleAnnulusPixelRegion, ellipseannulus=EllipseAnnulusPixelRegion, rectangleannulus=RectangleAnnulusPixelRegion, line=LinePixelRegion, point=PointPixelRegion, text=TextPixelRegion) error = RegionConversionError def __init__(self, coordsys, region_type, coord, meta, composite, include): self._coordsys = coordsys self._region_type = region_type self.coord = coord self.meta = meta self.composite = composite self.include = include @property def coordsys(self): return self._coordsys @coordsys.setter def coordsys(self, value): self._coordsys = value.lower() self._validate() @property def region_type(self): return self._region_type @region_type.setter def region_type(self, value): self._region_type = value.lower() self._validate() def __str__(self): ss = self.__class__.__name__ ss += f'\nType: {self.meta.get("type", "reg")}' ss += f'\nCoord sys: {self.coordsys}' ss += f'\nRegion type: {self.region_type}' if self.region_type == 'symbol': ss += f"\nSymbol: {self.meta['symbol']}" if self.region_type == 'text': ss += f"\nText: {self.meta['text']}" ss += f'\nMeta: {self.meta}' ss += f'\nComposite: {self.composite}' ss += f'\nInclude: {self.include}' ss += '\n' return ss def convert_coords(self): """ Process a list of coordinates. This mainly searches for a tuple of coordinates in the coordinate list and creates a SkyCoord or PixCoord object from them if appropriate for a given region type. This involves again some coordinate transformation, so this step could be moved to the parsing process. """ if self.coordsys in ['image', 'physical']: coords = self._convert_pix_coords() else: coords = self._convert_sky_coords() if self.region_type == 'line': coords = [coords[0][0], coords[0][1]] if self.region_type == 'text': coords.append(self.meta['text']) return coords def _convert_sky_coords(self): """ Convert to sky coordinates. """ parsed_angles = [] for x, y in zip(self.coord[:-1:2], self.coord[1::2], strict=True): if isinstance(x, Angle) and isinstance(y, Angle): parsed_angles.append((x, y)) frame = frame_transform_graph.lookup_name(self.coordsys) if len(parsed_angles) == 0: raise ValueError('error parsing region') lon, lat = zip(*parsed_angles, strict=True) if (hasattr(lon, '__len__') and hasattr(lat, '__len__') and len(lon) == 1 and len(lat) == 1): # force entries to be scalar if they are length-1 lon, lat = u.Quantity(lon[0]), u.Quantity(lat[0]) else: # otherwise, they are vector quantities lon, lat = u.Quantity(lon), u.Quantity(lat) sphcoords = UnitSphericalRepresentation(lon, lat) coords = [SkyCoord(frame(sphcoords))] if self.region_type != 'polygon': coords += self.coord[len(coords * 2):] return coords def _convert_pix_coords(self): """ Convert to pixel coordinates. """ if self.region_type in ['polygon', 'line']: # have to special-case polygon in the phys coord case # b/c can't typecheck when iterating as in sky coord case coords = [PixCoord(self.coord[0::2], self.coord[1::2])] else: temp = [_.value for _ in self.coord] coord = PixCoord(temp[0], temp[1]) coords = [coord] + temp[2:] # The angle remains as a quantity object. Modulus check makes # sure that it works for ellipse/rectangle annulus. if (self.region_type in ['ellipse', 'rectangle'] and len(coords) % 2 == 0): coords[-1] = self.coord[-1] return coords def to_region(self): """ Convert to a `~regions.Region` object. """ coords = self.convert_coords() viz_keywords = ['color', 'dash', 'dashlist', 'width', 'font', 'symsize', 'symbol', 'symsize', 'fontsize', 'fontstyle', 'usetex', 'labelpos', 'labeloff', 'labelcolor', 'linewidth', 'linestyle', 'point', 'textangle', 'fontweight', 'symthick', 'default_style', 'fill', 'textrotate', 'fontstyle'] if isinstance(coords[0], SkyCoord): reg = self.shape_to_sky_region[self.region_type](*coords) elif isinstance(coords[0], PixCoord): reg = self.shape_to_pixel_region[self.region_type](*coords) else: self._raise_error('No central coordinate') reg.visual = RegionVisual() reg.meta = RegionMeta() # both 'text' and 'label' should be set to the same value, where # we default to the 'text' value since that is the one used by # ds9 regions label = self.meta.get('text', self.meta.get('label', '')) if label != '': reg.meta['label'] = label for key in self.meta: if key in viz_keywords: reg.visual[key] = self.meta[key] else: reg.meta[key] = self.meta[key] reg.meta['include'] = self.include return reg def _raise_error(self, msg): raise self.error(msg) def check_crtf(self): """ Check for CRTF compatibility. """ if self.region_type not in regions_attributes: raise ValueError(f'"{self.region_type}" is not a valid region ' 'type') if self.coordsys not in valid_coordsys['CRTF']: raise ValueError(f'"{self.coordsys}" is not a valid coordinate ' 'reference frame') def _validate(self): """ Check whether all the attributes of this object is valid. """ if self.region_type not in regions_attributes: raise ValueError(f'"{self.region_type}" is not a valid region ' 'type') if self.coordsys not in valid_coordsys['CRTF']: raise ValueError(f'"{self.coordsys}" is not a valid coordinate ' 'reference frame') def _to_shape_list(region_list, coordinate_system='fk5'): """ Convert a list of regions into a `~regions.ShapeList` object. Parameters ---------- region_list : list A list of `regions.Region` objects. coordinate_system : str, optional The Astropy coordinate system frame in which all the coordinates present in the ``region_list`` will be converted. Default is 'fk5'. Returns ------- shape_list: `regions.ShapeList` A `~regions.ShapeList` object. """ shape_list = _ShapeList() for region in region_list: if isinstance(region, SkyRegion): reg_type = region.__class__.__name__[:-9].lower() elif isinstance(region, RegularPolygonPixelRegion): reg_type = 'polygon' else: reg_type = region.__class__.__name__[:-11].lower() if reg_type == 'polygon': coord = region.vertices else: coord = [] for val in regions_attributes[reg_type]: coord.append(getattr(region, val)) if coordinate_system: coordsys = coordinate_system else: coordsys = (coord[0].name if isinstance(region, SkyRegion) else 'image') new_coord = [] for val in coord: if isinstance(val, Angle): # convert Angle to Quantity; Angle values get units # stripped in serialization, but Quantity gets converted new_coord.append(u.Quantity(val)) elif isinstance(val, (u.Quantity, numbers.Number)): new_coord.append(val) elif isinstance(val, PixCoord): new_coord.append(u.Quantity(val.x, u.dimensionless_unscaled)) new_coord.append(u.Quantity(val.y, u.dimensionless_unscaled)) else: frame = frame_transform_graph.lookup_name(coordsys) new_coord.append(Angle(val.transform_to(frame).spherical.lon)) new_coord.append(Angle(val.transform_to(frame).spherical.lat)) meta = dict(region.meta) meta.update(region.visual) if reg_type == 'text': meta['text'] = meta.get('text', meta.pop('label', '')) include = region.meta.pop('include', True) shape_list.append(_Shape(coordsys, reg_type, new_coord, meta, False, include)) return shape_list def _to_crtf_meta(shape_meta): """ Make the metadata CRTF compatible by filtering and mapping the valid keys. Parameters ---------- shape_meta : dict A meta attribute of a `regions.Region` object. Returns ------- meta : dict A CRTF compatible meta dictionary. """ # please refer : https://casaguides.nrao.edu/index.php/CASA_Region_Format # meta keys allowed in CRTF valid_keys = ['label', 'include', 'frame', 'range', 'veltype', 'restfreq', 'coord', 'type', 'text', 'corr'] # visual keys allowed in CRTF valid_keys += ['color', 'width', 'font', 'symthick', 'symsize', 'fontsize', 'fontstyle', 'usetex', 'labelpos', 'labeloff', 'linewidth', 'linestyle', 'symbol'] key_mappings = {} return _to_io_meta(shape_meta, valid_keys, key_mappings) def _to_io_meta(shape_meta, valid_keys, key_mappings): """ Make metadata compatible with a specific IO by filtering and mapping to its valid keys. Parameters ---------- shape_meta : dict A meta attribute of a `regions.Region` object. valid_keys : list The valid keys of a particular file format. key_mappings : dict A dictionary mapping of the actual name of the key in the format. Returns ------- meta : dict An IO compatible meta dictionary according to ``valid_keys`` and ``key_mappings``. """ meta = {} for key in shape_meta: if key in valid_keys: meta[key_mappings.get(key, key)] = shape_meta[key] return meta astropy-regions-f4139aa/regions/io/crtf/read.py000066400000000000000000000460211521703212700215740ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import copy import itertools import re from warnings import warn import astropy.units as u from astropy.coordinates import Angle, frame_transform_graph from astropy.utils.data import get_readable_fileobj from regions.core import Regions from regions.core.registry import RegionsRegistry from regions.io.crtf.core import (CRTFRegionParserError, CRTFRegionParserWarning, valid_symbols) from regions.io.crtf.io_core import _Shape, _ShapeList, reg_mapping __all__ = [] # All CASA files start with '#CRTF' . It may also include the version # number like '#CRTFv0' . regex_begin = re.compile(r'^#CRTFv?[\d]?') # Comment format regex_comment = re.compile(r'^#.*$') # Identifies the global attributes Format regex_global = re.compile(r'^global\s+(?P.*)?') # Coordinate Format : '[x, y]' regex_coordinate = re.compile(r'\[([\w.+\-:]*?)\s*[,]\s*([\w.+\-:]*?)\]') # Single length format, e.g., helps extract the radius of a circle regex_length = re.compile(r'(?:\[[^=\]]*\])+[,]\s*([^\[]*)\]') # Extracts each 'parameter=value' pair regex_meta = re.compile(r'(?:(\w+)\s*=[\s\'\"]*([^,\[\]]+?)[\'\",]+)|(?:(\w+)\s*=\s*\[(.*?)\])') # noqa: E501 # Region format which segregates the include ('+'|'-') parameter, the # kind of definition ('ann' for annotations or '' for regions) and region # type. regex_region = re.compile(r'(?P[+-])?(?Pann(?=\s))?\s*(?P[a-z]*?)\s?\[[^=]*]') # noqa: E501 # Line format which checks the validity of the line and segregates the # meta attributes from the region format. regex_line = re.compile(r'(?P[+-]?(?:ann(?=\s))?\s*[a-z]+?\s?\[[^=]+\])(?:\s*,?\s*(?P.*))?') # noqa: E501 @RegionsRegistry.register(Regions, 'read', 'crtf') def _read_crtf(filename, errors='strict', cache=False): """ Read a CRTF region file and return a list of region objects. Parameters ---------- filename : str The filename of the file to access. errors : {'strict', 'warn', 'ignore'}, optional The error handling scheme to use for handling parsing errors. The default is 'strict', which will raise a `~regions.CRTFRegionParserError`. 'warn' will raise a `~regions.CRTFRegionParserWarning`, and 'ignore' will do nothing (i.e., be silent). cache : bool or 'update', optional Whether to cache the contents of remote URLs. If 'update', check the remote URL for a new version but store the result in the cache. Returns ------- regions : list A list of `~regions.Region` objects. """ with get_readable_fileobj(filename) as fh: if regex_begin.search(fh.readline()): region_string = fh.read() return _parse_crtf(region_string, errors=errors) else: raise CRTFRegionParserError('Every CRTF Region must start with ' '"#CRTF"') @RegionsRegistry.register(Regions, 'parse', 'crtf') def _parse_crtf(region_string, errors='strict'): parser = _CRTFParser(region_string, errors=errors) return Regions(parser.shapes.to_regions()) class _CRTFParser: """ Parse a CRTF string. This class transforms a CRTF string to a `~regions.io.core.ShapeList`. The result is stored in the ``shapes`` attribute. Parameters ---------- region_string : str A CRTF region string. errors : {'strict', 'warn', 'ignore'}, optional The error handling scheme to use for handling parsing errors. The default is 'strict', which will raise a `~regions.CRTFRegionParserError`. 'warn' will raise a `~regions.CRTFRegionParserWarning`, and 'ignore' will do nothing (i.e., be silent). """ # Each line is tested for either containing a region with meta # attributes or global parameters. If meta attributes or global # parameters are found, they are stored in the ``global_meta`` # attribute. If a region is found, then ``_CRTFRegionParser`` # is invoked to transform the line into a `~regions.io.core.Shape` # object, which is stored in the ``shapes`` attribute. # valid definition (region or annotation) types valid_definition = ('box', 'centerbox', 'rotbox', 'poly', 'circle', 'annulus', 'ellipse', 'line', 'vector', 'text', 'symbol') # valid parameters (attributes) valid_global_keys = ('coord', 'frame', 'corr', 'veltype', 'restfreq', 'linewidth', 'linestyle', 'symsize', 'symthick', 'color', 'font', 'fontsize', 'fontstyle', 'usetex', 'labelpos', 'labelcolor', 'labeloff', 'range') def __init__(self, region_string, errors='strict'): if errors not in ('strict', 'ignore', 'warn'): raise ValueError('errors must be one of "strict", "ignore", or ' '"warn"') self.region_string = region_string self.errors = errors self.global_meta = {} # global states self.shapes = _ShapeList() self.run() def __str__(self): ss = self.__class__.__name__ ss += f'\nErrors: {self.errors}' ss += f'\nGlobal meta: {self.global_meta}' ss += f'\nShapes: {self.shapes}' ss += '\n' return ss def parse_line(self, line): """ Parse a single line. """ # Skip blanks if line == '': return # Skip comments if regex_comment.search(line): return # Special case / header: parse global parameters into metadata global_parameters = regex_global.search(line) if global_parameters: self.parse_global_meta(global_parameters.group('parameters')) return # Tries to check the validity of the line. crtf_line = regex_line.search(line) if crtf_line: # Tries to parse the line. # Finds info about the region. region = regex_region.search(crtf_line.group('region')) type_ = region.group('type') or 'reg' include = region.group('include') or '+' region_type = region.group('regiontype').lower() if region_type in self.valid_definition: helper = _CRTFRegionParser( self.global_meta, include, type_, region_type, *crtf_line.group('region', 'parameters')) self.shapes.append(helper.shape) else: self._raise_error('Not a valid CRTF Region type: ' f'"{region_type}".') else: self._raise_error(f'Not a valid CRTF line: "{line}".') return def _raise_error(self, msg): if self.errors == 'warn': warn(msg, CRTFRegionParserWarning) elif self.errors == 'strict': raise CRTFRegionParserError(msg) def run(self): """ Run all the steps. This function splits the regions into lines and calls ``parse_line`` for each line. """ for line in self.region_string.split('\n'): self.parse_line(line) def parse_global_meta(self, global_meta_str): """ Parse the line starting with global to extract all the valid meta key/value pair. """ if global_meta_str: global_meta_str = regex_meta.findall(global_meta_str + ',') if global_meta_str: for par in global_meta_str: if par[0] != '': val1 = par[0].lower() val2 = par[1] else: val1 = par[2].lower() val2 = par[3] val1 = val1.strip() val2 = val2.strip() if val1 in self.valid_global_keys: if val1 in ('range', 'corr', 'labeloff'): val2 = val2.split(',') val2 = [x.strip() for x in val2 if x] self.global_meta[val1] = val2 else: self._raise_error(f'"{val1}" is not a valid global meta ' 'key') class _CRTFRegionParser: """ Parse a CRTF region string. This will turn a line containing a CRTF region into a `~regions.Shape` object. Parameters ---------- global_meta : dict Global meta data of the CRTF (CASA Region Text Format) file which is used as default meta values for regions. include : {'+', '-'} Flag at the beginning of the line. type_ : {'reg', 'ann'} Kind of the region definition. region_type : str Region type. reg_str : str Region string to parse. meta_str : str Meta string to parse. errors : {'strict', 'warn', 'ignore'}, optional The error handling scheme to use for handling parsing errors. The default is 'strict', which will raise a `~regions.CRTFRegionParserError`. 'warn' will raise a `~regions.CRTFRegionParserWarning`, and 'ignore' will do nothing (i.e., be silent). """ # List of valid coordinate system # TODO : There are still many reference systems to support coordinate_systems = ['j2000', 'icrs', 'galactic', 'supergal', 'image', 'ecliptic'] # Maps CASA coordinate frame to appropriate astropy coordinate frames. coordsys_mapping = dict(zip(frame_transform_graph.get_names(), frame_transform_graph.get_names(), strict=True)) coordsys_mapping['j2000'] = 'fk5' coordsys_mapping['b1950'] = 'fk4' coordsys_mapping['supergal'] = 'supergalactic' coordsys_mapping['ecliptic'] = 'geocentrictrueecliptic' # CRTF Format specifications defining how a region is read: # 'c' denotes a coordinates # 'l' denotes a length # 'pl' denotes a pair of lengths # 's' denotes a string (generally text or a symbol) language_spec = {'circle': ['c', 'l'], 'box': ['c', 'c'], 'centerbox': ['c', 'pl'], 'rotbox': ['c', 'pl', 'l'], 'poly': itertools.cycle('c'), 'annulus': ['c', 'pl'], 'ellipse': ['c', 'pl', 'l'], 'line': ['c', 'c'], 'vector': ['c', 'c'], 'symbol': ['c', 's'], 'text': ['c', 's']} def __init__(self, global_meta, include, type_, region_type, reg_str, meta_str, errors='strict'): self.global_meta = global_meta self.reg_str = reg_str self.meta_str = meta_str self.errors = errors self.coord = None self.coordsys = None self.coord_str = None self.type_ = type_ self.region_type = region_type self.meta = copy.deepcopy(global_meta) self.shape = None self.include = include or '+' self.parse() def _raise_error(self, msg): if self.errors == 'warn': warn(msg, CRTFRegionParserWarning) elif self.errors == 'strict': raise CRTFRegionParserError(msg) def parse(self): """ Parse the CRTF region string. """ self.convert_meta() self.coordsys = self.meta.get('coord', 'image').lower() self.set_coordsys() self.convert_coordinates() self.make_shape() def set_coordsys(self): """ Map to astropy's coordinate system frame name. """ # TODO: needs expert attention (most reference systems are not # mapped) if self.coordsys.lower() in self.coordsys_mapping: self.coordsys = self.coordsys_mapping[self.coordsys.lower()] def convert_coordinates(self): """ Convert coordinate string to `~astropy.coordinates.Angle` or `~astropy.units.quantity.Quantity` objects. """ coord_list_str = (regex_coordinate.findall(self.reg_str) + regex_length.findall(self.reg_str)) coord_list = [] if self.region_type == 'poly': if len(coord_list_str) < 3: self._raise_error(f'Not in proper format: {self.reg_str} ' 'polygon should have >= 3 coordinates') else: expected_nparam = len(self.language_spec[self.region_type]) if len(coord_list_str) != expected_nparam: self._raise_error(f'Not in proper format: "{self.reg_str}". ' 'Does not contain expected number of ' 'parameters for the region ' f'"{self.region_type}"') # TODO: check zip strict=True for attr_spec, val_str in zip(self.language_spec[self.region_type], coord_list_str): if attr_spec == 'c': if len(val_str) == 2 and val_str[1] != '': coord_list.append( _CRTFCoordinateParser.parse_coordinate(val_str[0])) coord_list.append( _CRTFCoordinateParser.parse_coordinate(val_str[1])) else: self._raise_error(f'Not in proper format: {val_str} ' 'should be a coordinate') if attr_spec == 'pl': if len(val_str) == 2 and val_str[1] != '': coord_list.append( _CRTFCoordinateParser.parse_angular_length_quantity( val_str[0])) coord_list.append( _CRTFCoordinateParser.parse_angular_length_quantity( val_str[1])) else: self._raise_error(f'Not in proper format: {val_str} ' 'should be a pair of lengths') if attr_spec == 'l': if isinstance(val_str, str): coord_list.append( _CRTFCoordinateParser.parse_angular_length_quantity( val_str)) else: self._raise_error(f'Not in proper format: {val_str} ' 'should be a single length') if attr_spec == 's': if self.region_type == 'symbol': if val_str in valid_symbols: self.meta['symbol'] = val_str else: self._raise_error(f'Not in proper format: "{val_str}" ' 'should be a symbol') elif self.region_type == 'text': self.meta['text'] = val_str[1:-1] self.coord = coord_list def convert_meta(self): """ Parse the meta_str to a dictionary and store in the ``meta`` attribute. """ if self.meta_str: self.meta_str = regex_meta.findall(self.meta_str + ',') if self.meta_str: for par in self.meta_str: if par[0] != '': val1 = par[0] val2 = par[1] else: val1 = par[2] val2 = par[3] val1 = val1.strip() val2 = val2.strip() if val1 in _CRTFParser.valid_global_keys or val1 == 'label': if val1 in ('range', 'corr', 'labeloff'): val2 = val2.split(',') val2 = [x.strip() for x in val2] self.meta[val1] = val2 else: self._raise_error(f'"{val1}" is not a valid meta key') self.meta['include'] = self.include != '-' self.include = self.meta['include'] if 'range' in self.meta: self.meta['range'] = [u.Quantity(x) for x in self.meta['range']] self.meta['type'] = self.type_ def make_shape(self): """ Make a `~regions.Shape` object. """ if self.region_type == 'ellipse': self.coord[2:] = [x * 2 for x in self.coord[2:]] # Map major and minor axis to height and width respectively self.coord[2], self.coord[3] = self.coord[3], self.coord[2] if len(self.coord) % 2 == 1: # check if angle is present self.coord[-1] /= 2 if self.region_type == 'box': x = (self.coord[0] + self.coord[2]) / 2 y = (self.coord[1] + self.coord[3]) / 2 w = u.Quantity(self.coord[0] - self.coord[2]) h = u.Quantity(self.coord[1] - self.coord[3]) self.coord = [x, y, abs(w), abs(h)] self.meta.pop('coord', None) self.shape = _Shape(coordsys=self.coordsys, region_type=reg_mapping['CRTF'][self.region_type], coord=self.coord, meta=self.meta, composite=False, include=self.include) class _CRTFCoordinateParser: """ Helper class to structure coordinate parser. """ @staticmethod def parse_coordinate(string_rep): """ Parse a single coordinate. """ # Any CRTF coordinate representation (sexagesimal or degrees) if 'pix' in string_rep: return u.Quantity(string_rep[:-3], u.dimensionless_unscaled) if 'h' in string_rep or 'rad' in string_rep: return Angle(string_rep) # CRTF format apparently implicitly defines # 18:20:30.12 # strings as hour units, and # 10.11.54.69 # as degrees. # I can't find this definition in documentation anywhere, though. unit = u.deg if len(string_rep.split('.')) >= 3: string_rep = string_rep.replace('.', ':', 2) elif string_rep.count(':') == 2: unit = u.hour return Angle(string_rep, unit) @staticmethod def parse_angular_length_quantity(string_rep): """ Parse a string into a Quantity object. Given a string that is a number and a unit, return a Quantity of that string. An error is raised if there is no unit, e.g.: * 50" -> 50 * u.arcsec * 50 -> CRTFRegionParserError : Units must be specified for 50 """ unit_mapping = {'deg': u.deg, 'rad': u.rad, 'arcmin': u.arcmin, 'arcsec': u.arcsec, 'pix': u.dimensionless_unscaled, '"': u.arcsec, "'": u.arcmin} regex_str = re.compile(r'([0-9+,-.]*)(.*)') restr = regex_str.search(string_rep) unit = restr.group(2) if unit: if unit in unit_mapping: return u.Quantity(restr.group(1), unit=unit_mapping[unit]) return u.Quantity(restr.group(1)) else: raise CRTFRegionParserError('Units must be specified for ' f'{string_rep}') astropy-regions-f4139aa/regions/io/crtf/tests/000077500000000000000000000000001521703212700214465ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/crtf/tests/__init__.py000066400000000000000000000000001521703212700235450ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/crtf/tests/data/000077500000000000000000000000001521703212700223575ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/crtf/tests/data/CRTF_CARTA.crtf000066400000000000000000000021061521703212700247060ustar00rootroot00000000000000#CRTFv0 CASA Region Text Format version 0 poly [[262.60121842deg, -34.70428113deg], [262.59389200deg, -34.69913763deg], [262.58849307deg, -34.69879109deg], [262.58379743deg, -34.70008642deg], [262.58402556deg, -34.70267217deg], [262.58933290deg, -34.70223072deg], [262.59020415deg, -34.70337542deg], [262.58862973deg, -34.71055795deg], [262.59172148deg, -34.70870333deg], [262.59556206deg, -34.71123464deg], [262.61080572deg, -34.71136497deg], [262.61942435deg, -34.71027497deg], [262.62265070deg, -34.70362260deg], [262.61872411deg, -34.70362154deg], [262.60975102deg, -34.70997555deg]] coord=ICRS, corr=[I], linewidth=1, linestyle=-, symsize=1, symthick=1, color=green, font=Helvetica, fontsize=10, fontstyle=bold, usetex=false poly [[262.60718383deg, -34.67844254deg], [262.58802310deg, -34.67884924deg], [262.58679959deg, -34.68238917deg], [262.60805460deg, -34.68206155deg], [262.61615892deg, -34.68021601deg], [262.61706427deg, -34.67894794deg]] coord=ICRS, corr=[I], linewidth=1, linestyle=-, symsize=1, symthick=1, color=green, font=Helvetica, fontsize=10, fontstyle=bold, usetex=false astropy-regions-f4139aa/regions/io/crtf/tests/data/CRTF_labelcolor.crtf000066400000000000000000000016351521703212700262000ustar00rootroot00000000000000#CRTFv0 CASA Region Text Format version 0 poly [[238.29839664deg, -54.60647979deg], [238.29935543deg, -54.60610099deg], [238.30011816deg, -54.60598728deg], [238.30035808deg, -54.60646691deg], [238.29933401deg, -54.60708556deg], [238.29868031deg, -54.60743908deg], [238.29820094deg, -54.60771684deg], [238.29728571deg, -54.60814611deg], [238.29626149deg, -54.60858802deg], [238.29523727deg, -54.60908040deg], [238.29423473deg, -54.60920672deg], [238.29399489deg, -54.60885331deg], [238.29440888deg, -54.60841148deg], [238.29510618deg, -54.60793175deg], [238.29560731deg, -54.60741417deg], [238.29589059deg, -54.60726267deg], [238.29669691deg, -54.60707325deg], [238.29721997deg, -54.60704794deg], [238.29783007deg, -54.60665657deg]] coord=ICRS, corr=[I], linewidth=2, linestyle=-, symsize=1, symthick=1, color=2ee6d6, font=Helvetica, fontsize=10, fontstyle=bold, usetex=false, label="region18", labelcolor=green, labelpos=top astropy-regions-f4139aa/regions/io/crtf/tests/data/CRTF_labelcolor_output.crtf000066400000000000000000000014441521703212700276160ustar00rootroot00000000000000#CRTFv0 global coord=J2000 poly[[238.298410deg, -54.606476deg], [238.299369deg, -54.606098deg], [238.300132deg, -54.605984deg], [238.300372deg, -54.606464deg], [238.299347deg, -54.607082deg], [238.298694deg, -54.607436deg], [238.298214deg, -54.607713deg], [238.297299deg, -54.608143deg], [238.296275deg, -54.608585deg], [238.295251deg, -54.609077deg], [238.294248deg, -54.609203deg], [238.294008deg, -54.608850deg], [238.294422deg, -54.608408deg], [238.295120deg, -54.607928deg], [238.295621deg, -54.607411deg], [238.295904deg, -54.607259deg], [238.296710deg, -54.607070deg], [238.297233deg, -54.607045deg], [238.297844deg, -54.606653deg]], label='region18', linewidth=2, linestyle=-, symsize=1, symthick=1, color=2ee6d6, font=Helvetica, fontsize=10, fontstyle=bold, usetex=false, labelpos=top, corr=[I] astropy-regions-f4139aa/regions/io/crtf/tests/data/CRTFgeneral.crtf000066400000000000000000000020431521703212700253320ustar00rootroot00000000000000#CRTFv0 global coord=B1950, frame=BARY, corr=[I, Q], color=blue # A simple circle region: ann circle[[18h12m24s, -23d11m00s], 2.3arcsec] # A box region, this one only for annotation: (Not Implemented ) # ann box[[140.0342deg, -12.34243deg], [140.0360deg, -12.34320deg]] # A rotated box region, for a particular range of velocities: rotbox[[12h01m34.1s, 12d23m32s], [3arcmin, 1arcmin], 12deg], range=[-1240km/s, 1240km/s] # An annular region, overriding some of the global defaults: annulus[[17h51m03.2s, -45d17m50s], [0.10deg, 4.12deg]], corr=[I,Q,U,V], color=red, label='My label here' # Cuts an ellipse out of the previous regions, but only for Q and a particular frequency range: -ellipse[[12deg, +45deg], [0.25deg, 1.34deg], 45rad], range=[1.420GHz, 1.421GHz], corr=[Q], color=green, label='Removed this' # A diamond marker, in J2000 coordinates: symbol[[32.1423deg, 12.1412deg], D], linewidth=2, coord=J2000, symsize=2 # Text region text[[32.1423deg, 12.1412deg], 'my text'], linewidth=2, coord=J2000 # Box region box[[1deg, 2deg], [3deg, 4deg]] astropy-regions-f4139aa/regions/io/crtf/tests/data/CRTFgeneraloutput.crtf000066400000000000000000000014421521703212700266150ustar00rootroot00000000000000#CRTFv0 global coord=B1950 ann circle[[273.100deg, -23.183deg], 0.001deg], frame=BARY, color=blue, corr=[I, Q] rotbox[[180.392deg, 12.392deg], [0.050deg, 0.017deg], 12.000deg], frame=BARY, color=blue, range=[-1240.0km/s, 1240.0km/s], corr=[I, Q] annulus[[267.763deg, -45.297deg], [0.100deg, 4.120deg]], label='My label here', frame=BARY, color=red, corr=[I, Q, U, V] -ellipse[[12.000deg, 45.000deg], [0.250deg, 1.340deg], 2578.310deg], label='Removed this', frame=BARY, color=green, range=[1.42GHz, 1.421GHz], corr=[Q] symbol[[31.470deg, 11.905deg], D], frame=BARY, color=blue, linewidth=2, symsize=2, corr=[I, Q] text[[31.470deg, 11.905deg], 'my text'], frame=BARY, color=blue, linewidth=2, corr=[I, Q] rotbox[[2.000deg, 3.000deg], [2.000deg, 2.000deg], 0.000deg], frame=BARY, color=blue, corr=[I, Q] astropy-regions-f4139aa/regions/io/crtf/tests/data/binary_mask.pkl000066400000000000000000011541211521703212700253730ustar00rootroot00000000000000cnumpy.core.multiarray _reconstruct p0 (cnumpy ndarray p1 (I0 tp2 S'b' p3 tp4 Rp5 (I1 (I100 I100 I1 I1 tp6 cnumpy dtype p7 (S'f8' p8 I0 I1 tp9 Rp10 (I3 S'<' p11 NNNI-1 I-1 I0 tp12 bI01 S'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' p13 tp14 b.astropy-regions-f4139aa/regions/io/crtf/tests/data/crtf_carta_sexagesimal.crtf000066400000000000000000000014421521703212700277320ustar00rootroot00000000000000#CRTFv0 CASA Region Text Format version 0 symbol [[18:47:44.02103, -001.54.58.0916], .] coord=J2000, corr=[I], linewidth=2, linestyle=-, symsize=1, symthick=1, color=2ee6d6, font=Helvetica, fontsize=10, fontstyle=bold, usetex=false symbol [[18:47:44.04110, -001.54.56.5869], .] coord=J2000, corr=[I], linewidth=2, linestyle=-, symsize=1, symthick=1, color=2ee6d6, font=Helvetica, fontsize=10, fontstyle=bold, usetex=false symbol [[18:47:44.06118, -001.54.55.0070], .] coord=J2000, corr=[I], linewidth=2, linestyle=-, symsize=1, symthick=1, color=2ee6d6, font=Helvetica, fontsize=10, fontstyle=bold, usetex=false symbol [[18:47:44.10133, -001.54.53.7280], .] coord=J2000, corr=[I], linewidth=2, linestyle=-, symsize=1, symthick=1, color=2ee6d6, font=Helvetica, fontsize=10, fontstyle=bold, usetex=false astropy-regions-f4139aa/regions/io/crtf/tests/test_casa_mask.py000066400000000000000000000055701521703212700250100ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import pickle import tempfile import astropy.units as u import pytest from astropy.coordinates import SkyCoord from regions.io.crtf.write import _write_crtf from regions.shapes.ellipse import EllipseSkyRegion try: from casatasks import tclean from casatools import image from casatools import measures as me from casatools import simulator HAS_CASATOOLS = True except ImportError: HAS_CASATOOLS = False @pytest.mark.skipif(not HAS_CASATOOLS, reason='casatools is required') def test_casa_masking(): with tempfile.TemporaryDirectory() as tmpdir: # SIMULATE SOME DATA SET # Define antennas diam = [25, 25, 25, 25, 25] xx = [50, 100, 150, 200, 250] yy = [2, -5, -20, -50, -100] zz = [-0.5, -1.0, -1.5, -2.0, -2.5] sm = simulator() sm.open(tmpdir + '/SIM.ms') # do configuration posvla = me.observatory('VLA') sm.setconfig(telescopename='VLA', x=xx, y=yy, z=zz, dishdiameter=diam, mount='alt-az', antname='VLA', coordsystem='local', referencelocation=posvla) # Initialize the spectral windows sm.setspwindow(spwname='CBand', freq='5GHz', deltafreq='50MHz', freqresolution='50MHz', nchannels=1, stokes='RR RL LR LL') # Initialize the source and calibrater sm.setfield(sourcename='My cal', sourcedirection=['J2000', '00h0m0.0', '+45.0.0.000'], calcode='A') sm.setfield(sourcename='My source', sourcedirection=['J2000', '01h0m0.0', '+47.0.0.000']) sm.setlimits(shadowlimit=0.001, elevationlimit='8.0deg') sm.setauto(autocorrwt=0.0) sm.settimes(integrationtime='10s', usehourangle=False, referencetime=me.epoch('utc', 'today')) sm.observe('My cal', 'CBand', starttime='720s', stoptime='1020s') sm.observe('My source', 'CBand', starttime='1030s', stoptime='1500s') sm.close() # Create mask to use during clean reg = EllipseSkyRegion(SkyCoord(0.0 * u.deg, 45.0 * u.deg, frame='fk5'), width=1.0 * u.arcmin, height=2.0 * u.arcmin, angle=45 * u.deg) _write_crtf([reg], tmpdir + '/SIM.crtf', 'fk5', '.6f', 'deg') # Image the dataset tclean(vis=tmpdir + '/SIM.ms', imagename=tmpdir + '/SIM', imsize=100, cell='5arcsec', niter=1, mask=tmpdir + '/SIM.crtf', interactive=False) ia = image() ia.open(tmpdir + '/SIM.mask') mask_array = ia.getregion() ia.close() with open('data/binary_mask.pkl', 'rb') as f: ref_mask = pickle.load(f) assert all(mask_array == ref_mask) astropy-regions-f4139aa/regions/io/crtf/tests/test_crtf.py000066400000000000000000000176701521703212700240300ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the crtf subpackage. """ import re import astropy.units as u import pytest from astropy.coordinates import Angle, SkyCoord from astropy.tests.helper import assert_quantity_allclose from astropy.utils.data import get_pkg_data_filename from regions.core import Regions from regions.io.crtf.core import CRTFRegionParserError from regions.io.crtf.read import _CRTFParser from regions.shapes.circle import CircleSkyRegion from regions.shapes.ellipse import EllipseSkyRegion implemented_region_types = ('ellipse', 'circle', 'rectangle', 'poly', 'point', 'text', 'symbol') def test_global_parser(): """ Checks that the global_parser does what's expected. """ global_test_str = ('global coord=B1950_VLA, frame=BARY, corr=[I, Q], ' 'color=blue') global_parser = _CRTFParser(global_test_str) expected = {'coord': 'B1950_VLA', 'frame': 'BARY', 'corr': ['I', 'Q'], 'color': 'blue'} assert dict(global_parser.global_meta) == expected def test_valid_crtf_line(): """ Checks whether a the line is valid CRTF format. """ line_str = 'coord=B1950_VLA, frame=BARY, corr=[I, Q], color=blue' match = 'Not a valid CRTF line:' with pytest.raises(CRTFRegionParserError, match=match): Regions.parse(line_str, format='crtf') def test_valid_region_type(): """ Checks whether the region type is valid in CRTF format. """ reg_str = 'hyperbola[[18h12m24s, -23d11m00s], 2.3arcsec]' match = 'Not a valid CRTF Region type: "hyperbola"' with pytest.raises(CRTFRegionParserError, match=match): Regions.parse(reg_str, format='crtf') def test_valid_global_meta_key(): """ Checks whether the global key is valid or not. """ global_test_str = ('global label=B1950_VLA, frame=BARY, corr=[I, Q], ' 'color=blue') match = '"label" is not a valid global meta key' with pytest.raises(CRTFRegionParserError, match=match): Regions.parse(global_test_str, format='crtf') def test_valid_meta_key(): """ Checks whether the key is valid or not. """ meta_test_str = ('annulus[[17h51m03.2s, -45d17m50s], [0.10deg, 4.12deg]], ' 'hello="My label here"') match = '"hello" is not a valid meta key' with pytest.raises(CRTFRegionParserError, match=match): Regions.parse(meta_test_str, format='crtf') def test_valid_region_syntax(): """ Checks whether the region has valid parameters. """ reg_str1 = 'circle[[18h12m24s, -23d11m00s], [2.3arcsec,4.5arcsec]' match = re.escape("Not in proper format: ('2.3arcsec', '4.5arcsec') " 'should be a single length') with pytest.raises(CRTFRegionParserError, match=match): Regions.parse(reg_str1, format='crtf') reg_str2 = ('symbol[[32.1423deg, 12.1412deg], 12deg], linewidth=2, ' 'coord=J2000, symsize=2') match = 'Not in proper format: "12deg" should be a symbol' with pytest.raises(CRTFRegionParserError, match=match): Regions.parse(reg_str2, format='crtf') reg_str3 = 'circle[[18h12m24s, -23d11m00s]' match = ('Does not contain expected number of parameters for the region ' '"circle"') with pytest.raises(CRTFRegionParserError, match=match): Regions.parse(reg_str3, format='crtf') reg_str4 = 'poly[[1, 2], [4, 5]]' match = 'polygon should have >= 3 coordinates' with pytest.raises(CRTFRegionParserError, match=match): Regions.parse(reg_str4, format='crtf') reg_str6 = 'rotbox[[12h01m34.1s, 12d23m33s], [3arcmin,], 12deg]' match = re.escape("('3arcmin', '') should be a pair of lengths") with pytest.raises(CRTFRegionParserError, match=match): Regions.parse(reg_str6, format='crtf') def test_issue_312_regression(): """ Make sure there is no trailing comma when writing a CRTF string with no metadata. """ reg = EllipseSkyRegion(center=SkyCoord(279.174990 * u.deg, -21.257123 * u.deg, frame='fk5'), width=0.001571 * u.deg, height=0.001973 * u.deg, angle=111.273322 * u.deg) crtfstr = reg.serialize(format='crtf', coordsys='fk5', fmt='.6f', radunit='deg') assert crtfstr.strip()[-1] != ',' @pytest.mark.parametrize(('filename', 'outname', 'coordsys', 'fmt'), [('data/CRTFgeneral.crtf', 'data/CRTFgeneraloutput.crtf', 'fk4', '.3f'), ('data/CRTFgeneraloutput.crtf', 'data/CRTFgeneraloutput.crtf', 'fk4', '.3f'), ('data/CRTF_labelcolor.crtf', 'data/CRTF_labelcolor_output.crtf', 'fk5', '.6f')]) def test_file_crtf(filename, outname, coordsys, fmt): """ The "labelcolor" example is a regression test for Issue 405 The others are just a general serialization self-consistency check. """ filename = get_pkg_data_filename(filename) regs = Regions.read(filename, errors='warn', format='crtf') actual_output = regs.serialize(format='crtf', coordsys=coordsys, fmt=fmt).strip() with open(get_pkg_data_filename(outname)) as fh: ref_output = fh.read().strip() # since metadata is not required to preserve order, we have to do a more # complex comparison desired_lines = [set(line.split(',')) for line in ref_output.split('\n')] actual_lines = [set(line.split(',')) for line in actual_output.split('\n')] for split_line in actual_lines: assert split_line in desired_lines for split_line in desired_lines: assert split_line in actual_lines def test_crtf_header(): """ Regression test for issue #239. """ crtf_str = ('#CRTFv0 CASA Region Text Format version 0\n' 'circle[[42deg, 43deg], 3deg], coord=J2000, color=green') reg = Regions.parse(crtf_str, format='crtf')[0] assert isinstance(reg, CircleSkyRegion) assert reg.center.ra.value == 42.0 assert reg.center.ra.unit == 'deg' assert reg.center.dec.value == 43.0 assert reg.center.dec.unit == 'deg' assert reg.radius.value == 3.0 assert reg.radius.unit == 'deg' def test_space_after_regname(): """ Regression test for #271: space is allowed. """ reg_str = 'circle [[42deg, 43deg], 3deg], coord=J2000, color=green' reg = Regions.parse(reg_str, format='crtf')[0] assert isinstance(reg, CircleSkyRegion) def test_no_comma_after_region(): reg_str = 'circle [[42deg, 43deg], 3deg] coord=J2000, color=green' reg = Regions.parse(reg_str, format='crtf')[0] assert isinstance(reg, CircleSkyRegion) assert reg.center.ra.value == 42.0 assert reg.center.ra.unit == 'deg' assert reg.center.dec.value == 43.0 assert reg.center.dec.unit == 'deg' assert reg.radius.value == 3.0 assert reg.radius.unit == 'deg' def test_casa_file_crtf(): filename = get_pkg_data_filename('data/CRTF_CARTA.crtf') regions = Regions.read(filename, format='crtf') assert len(regions) == 2 def test_angle_serialization(): """ Regression test for issue #223 to ensure Angle arcsec inputs are correctly converted to degrees. """ reg = Regions([CircleSkyRegion(SkyCoord(10, 20, unit='deg'), Angle(1, 'arcsec'))]) regstr = reg.serialize(format='crtf') expected = ('#CRTFv0\nglobal coord=J2000\ncircle[[10.000009deg, ' '20.000002deg], 0.000278deg]\n') assert regstr == expected def test_read_sexagesimal_regression407(): filename = 'data/crtf_carta_sexagesimal.crtf' filename = get_pkg_data_filename(filename) regs = Regions.read(filename, errors='warn', format='crtf') assert_quantity_allclose( regs[0].center.ra, 281.93342096 * u.deg, rtol=1e-9) astropy-regions-f4139aa/regions/io/crtf/tests/test_io_core.py000066400000000000000000000046321521703212700245030ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import pytest from astropy.tests.helper import assert_quantity_allclose from astropy.units.quantity import Quantity from regions.io.crtf.io_core import _to_shape_list from regions.io.crtf.read import _CRTFParser def test_shape_crtf(): reg_str = ('circle[[18h12m24s, -23d11m00s], 2.3arcsec], linewidth=2, ' 'coord=J2000, symsize=2') parser = _CRTFParser(reg_str) shape1 = parser.shapes[0] region = parser.shapes.to_regions() shape2 = _to_shape_list(region, 'fk5')[0] assert shape1.coordsys == shape2.coordsys assert shape1.region_type == shape2.region_type assert shape1.include == shape2.include assert_quantity_allclose(shape1.coord, shape2.coord) # assert dict(shape1.meta) == dict(shape2.meta) assert shape1.composite == shape2.composite # Pixel origin is assumed to be 0 origin. Needs to be checked though. reg_str = 'circle[[1pix, 2pix], 3pix], linewidth=2, coord=image, symsize=2' parser = _CRTFParser(reg_str) shape = parser.shapes[0] assert_quantity_allclose(shape.coord, [Quantity('1'), Quantity('2'), Quantity('3')]) def test_valid_shape(): reg_str = ('circle[[18h12m24s, -23d11m00s], 2.3arcsec], linewidth=2, ' 'coord=J2000, symsize=2') shape = _CRTFParser(reg_str).shapes[0] match = '"box" is not a valid region type' with pytest.raises(ValueError, match=match): shape.region_type = 'box' shape = _CRTFParser(reg_str).shapes[0] match = '"hello" is not a valid coordinate reference frame' with pytest.raises(ValueError, match=match): shape.coordsys = 'hello' def test_valid_ellipse_crtf(): reg_str1 = 'ellipse[[1pix, 2pix], [3pix, 4pix], 5deg]' reg_str2 = 'ellipse[[1deg, 2deg], [3deg, 4deg], 5deg], coord=fk5' shape1 = _CRTFParser(reg_str1, 'warn').shapes[0] shape2 = _CRTFParser(reg_str2, 'warn').shapes[0] assert_quantity_allclose(shape1.coord[:2], [1, 2]) assert_quantity_allclose(shape2.coord[:2], [Quantity('1deg'), Quantity('2deg')]) assert_quantity_allclose(shape1.coord[2:-1], [8, 6]) assert_quantity_allclose(shape2.coord[2:-1], [Quantity('8deg'), Quantity('6deg')]) assert_quantity_allclose(shape1.coord[-1], Quantity('5deg')) assert_quantity_allclose(shape2.coord[-1], Quantity('5deg')) astropy-regions-f4139aa/regions/io/crtf/write.py000066400000000000000000000034101521703212700220060ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import os from regions.core import Region, Regions from regions.core.registry import RegionsRegistry from regions.io.crtf.io_core import _to_shape_list __all__ = [] @RegionsRegistry.register(Region, 'serialize', 'crtf') @RegionsRegistry.register(Regions, 'serialize', 'crtf') def _serialize_crtf(regions, coordsys='fk5', fmt='.6f', radunit='deg'): shapelist = _to_shape_list(regions, coordsys) return shapelist.to_crtf(coordsys, fmt, radunit) @RegionsRegistry.register(Region, 'write', 'crtf') @RegionsRegistry.register(Regions, 'write', 'crtf') def _write_crtf(regions, filename, coordsys='fk5', fmt='.6f', radunit='deg', overwrite=False): """ Convert a list of `~regions.Region` to a CRTF string and write to a file. Parameters ---------- regions : list A list of `~regions.Region` objects. filename : str The filename in which the string is to be written. coordsys : str, optional An Astropy coordinate system that overrides the coordinate frames of all regions. fmt : str, optional A python string format defining the output precision. Default is '.6f', which is accurate to 0.0036 arcseconds. radunit : str, optional The unit of the radius. overwrite : bool, optional If True, overwrite the output file if it exists. Raises an `OSError` if False and the output file exists. Default is False. """ if os.path.lexists(filename) and not overwrite: raise OSError(f'{filename} already exists') output = _serialize_crtf(regions, coordsys=coordsys, fmt=fmt, radunit=radunit) with open(filename, 'w') as fh: fh.write(output) astropy-regions-f4139aa/regions/io/ds9/000077500000000000000000000000001521703212700200455ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/ds9/__init__.py000066400000000000000000000002211521703212700221510ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage provides tools for reading and writing DS9 region files. """ astropy-regions-f4139aa/regions/io/ds9/connect.py000066400000000000000000000026331521703212700220540ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst from astropy.utils.data import get_readable_fileobj from regions.core import Region, Regions from regions.core.registry import RegionsRegistry __all__ = [] @RegionsRegistry.register(Region, 'identify', 'ds9') @RegionsRegistry.register(Regions, 'identify', 'ds9') def is_ds9(methodname, filepath): """ Identify a DS9 region file. Parameters ---------- methodname : {'read', 'write'} The method name called that needs auto-identification. filepath : str The path to the file. Returns ------- result : bool Returns `True` if the given file is a DS9 region file. """ all_exten = ('.ds9', '.reg', '.ds9.gz', '.reg.gz') exten = {'read': all_exten, 'write': all_exten[0:2]} if methodname == 'write': return filepath.lower().endswith(exten[methodname]) elif methodname == 'read': if (isinstance(filepath, str) and filepath.lower().endswith(exten[methodname])): return True else: with get_readable_fileobj(filepath, encoding='binary') as fileobj: signature = '# Region file format: DS9' pos = fileobj.tell() sig = fileobj.read(len(signature)) fileobj.seek(pos) return sig == signature or sig == signature.encode() else: return False astropy-regions-f4139aa/regions/io/ds9/core.py000066400000000000000000000136361521703212700213600ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import itertools from regions._utils.optional_deps import HAS_MATPLOTLIB from regions.shapes import (CircleAnnulusPixelRegion, CircleAnnulusSkyRegion, CirclePixelRegion, CircleSkyRegion, EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion, EllipsePixelRegion, EllipseSkyRegion, LinePixelRegion, LineSkyRegion, PointPixelRegion, PointSkyRegion, PolygonPixelRegion, PolygonSkyRegion, RectangleAnnulusPixelRegion, RectangleAnnulusSkyRegion, RectanglePixelRegion, RectangleSkyRegion, TextPixelRegion, TextSkyRegion) __all__ = [] def make_region_template(): return itertools.chain(('coord', 'coord'), itertools.cycle(('length',))) def make_polygon_template(): return itertools.cycle(('coord',)) # mappings from DS9 frames to astropy coordinates frames ds9_frame_map = {'image': 'image', 'icrs': 'icrs', 'fk5': 'fk5', 'j2000': 'fk5', 'fk4': 'fk4', 'b1950': 'fk4', 'galactic': 'galactic', 'ecliptic': 'barycentricmeanecliptic'} # mappings from DS9 shape to region class pixel_map = {'circle': CirclePixelRegion, 'ellipse': EllipsePixelRegion, 'box': RectanglePixelRegion, 'polygon': PolygonPixelRegion, 'annulus': CircleAnnulusPixelRegion, 'ellipse_annulus': EllipseAnnulusPixelRegion, 'rectangle_annulus': RectangleAnnulusPixelRegion, 'line': LinePixelRegion, 'point': PointPixelRegion, 'text': TextPixelRegion} sky_map = {'circle': CircleSkyRegion, 'ellipse': EllipseSkyRegion, 'box': RectangleSkyRegion, 'polygon': PolygonSkyRegion, 'annulus': CircleAnnulusSkyRegion, 'ellipse_annulus': EllipseAnnulusSkyRegion, 'rectangle_annulus': RectangleAnnulusSkyRegion, 'line': LineSkyRegion, 'point': PointSkyRegion, 'text': TextSkyRegion} ds9_shape_to_region = {} ds9_shape_to_region['pixel'] = pixel_map ds9_shape_to_region['sky'] = sky_map ds9_params_template = {'point': ('coord', 'coord'), 'text': ('coord', 'coord'), 'circle': ('coord', 'coord', 'length'), 'line': ('coord', 'coord', 'coord', 'coord'), 'ellipse': make_region_template(), 'annulus': make_region_template(), 'box': make_region_template(), 'polygon': make_polygon_template()} # mapping from regions shapes to ds9 shape formats # unsupported ds9 shapes: # vector, ruler, compass, projection, panda, epanda, bpanda ds9_shape_templates = {'circle': ('circle', '{center},{radius}'), 'ellipse': ('ellipse', '{center},{width},{height}' ',{angle}'), 'rectangle': ('box', '{center},{width},{height},{angle}'), 'circleannulus': ('annulus', '{center},{inner_radius},' '{outer_radius}'), 'ellipseannulus': ('ellipse', '{center},{inner_width},' '{inner_height},' '{outer_width},' '{outer_height},{angle}'), 'rectangleannulus': ('box', '{center},{inner_width},' '{inner_height},{outer_width},' '{outer_height},{angle}'), 'polygon': ('polygon', '{vertices}'), 'line': ('line', '{start},{end}'), 'point': ('point', '{center}'), 'text': ('text', '{center}')} if not HAS_MATPLOTLIB: boxcircle = '8' # octagon arrow = '^' # triangle else: import matplotlib.path as mpath vertices = [[0., -1.], [0.2652031, -1.], [0.51957987, -0.89463369], [0.70710678, -0.70710678], [0.89463369, -0.51957987], [1., -0.2652031], [1., 0.], [1., 0.2652031], [0.89463369, 0.51957987], [0.70710678, 0.70710678], [0.51957987, 0.89463369], [0.2652031, 1.], [0., 1.], [-0.2652031, 1.], [-0.51957987, 0.89463369], [-0.70710678, 0.70710678], [-0.89463369, 0.51957987], [-1., 0.2652031], [-1., 0.], [-1., -0.2652031], [-0.89463369, -0.51957987], [-0.70710678, -0.70710678], [-0.51957987, -0.89463369], [-0.2652031, -1.], [0., -1.], [0., -1.], [0., -1.], [1., -1.], [1., 1.], [-1., 1.], [-1., -1.], [0., -1.]] codes = [1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 79, 1, 2, 2, 2, 2, 79] boxcircle = mpath.Path(vertices, codes) arrow_verts = [[-1, 0], [0, 0], [-1, 1], [0, 0], [0, 1]] arrow = mpath.Path(arrow_verts, codes=None) # mapping from ds9 point symbols to matplotlib marker symbols ds9_valid_symbols = {'circle': 'o', 'box': 's', 'diamond': 'D', 'x': 'x', 'cross': '+', 'arrow': arrow, 'boxcircle': boxcircle} class DS9ParserError(Exception): """ A custom exception for DS9 parsing errors. """ astropy-regions-f4139aa/regions/io/ds9/meta.py000066400000000000000000000212051521703212700213450ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import warnings from astropy.utils.exceptions import AstropyUserWarning from regions.io.ds9.core import DS9ParserError, ds9_valid_symbols __all__ = [] def _split_raw_metadata(raw_metadata): """ Split the raw metadata into meta and visual dictionaries. The raw metadata is not changed, except unsupported metadata is ignored. Parameters ---------- raw_metadata : dict The raw metadata as read from the DS9 region file/str. Returns ------- meta, visual : dict The raw metadata and visual metadata dictionaries. """ # not currently supported by regions unsupported_meta = ('line', 'vector', 'ruler', 'compass') # TODO: include text in visual keys to support text annotations for # all DS9 regions? ds9_visual_keys = ('color', 'dash', 'dashlist', 'fill', 'font', 'point', 'textangle', 'textrotate', 'width') meta = {} visual = {} for key, value in raw_metadata.items(): if key in unsupported_meta: # don't warn for "line=0 0" (no arrows) because it works, # but skip adding it to metadata if key != 'line' or '1' in value: warnings.warn(f'DS9 meta "{key}={value}" is unsupported and ' 'will be ignored', AstropyUserWarning) continue if key in ds9_visual_keys: visual[key] = value else: meta[key] = value # set the default plotting style to 'ds9' when parsing ds9 data visual['default_style'] = 'ds9' return meta, visual def _translate_ds9_to_visual(shape, visual_meta): """ Translate ds9 visual metadata to dictionary of matplotlib keywords. Parameters ---------- shape : str The DS9 region shape (e.g., 'circle'). visual_meta : dict The visual metadata. Returns ------- visual : dict The visual dictionaries of region metadata translated to matplotlib keywords. """ meta = visual_meta.copy() fill = meta.pop('fill', 0) # fill=1 is supported in DS9 only for the circle, ellipse, box, and # polygon shapes if fill == 1 and shape in ('circle', 'ellipse', 'box', 'polygon'): meta['fill'] = True dash = meta.pop('dash', 0) dashlist = meta.pop('dashlist', None) if int(dash) == 1: if dashlist is not None: dashes = tuple(int(i) for i in dashlist.split()) meta['linestyle'] = (0, dashes) else: meta['linestyle'] = 'dashed' if shape == 'point': warnings.warn('dashed lines are unsupported for DS9 point ' 'regions and will be ignored', AstropyUserWarning) meta.pop('linestyle', None) # "point=symbol [size]"; size is optional, symbol is not point = meta.pop('point', None) if point is not None: point_ = point.split() if len(point_) == 1: ds9_marker = point_[0] elif len(point_) == 2: ds9_marker, meta['markersize'] = point_ else: raise ValueError(f'invalid point data "{point}"') meta['marker'] = ds9_valid_symbols[ds9_marker] font = meta.pop('font', None) if font is not None: vals = font.split() if len(vals) == 4: (meta['fontname'], meta['fontsize'], meta['fontweight'], meta['fontstyle']) = vals if len(vals) == 3: meta['fontname'], meta['fontsize'], meta['fontweight'] = vals if len(vals) == 2: meta['fontname'], meta['fontsize'] = vals if len(vals) == 1: meta['fontname'] = vals[0] # define default font values (helvetica 10 normal roman) if 'fontsize' not in meta: meta['fontsize'] = '10' if 'fontweight' not in meta: meta['fontweight'] = 'normal' if 'fontstyle' not in meta: meta['fontstyle'] = 'normal' # fontsize is a string try: meta['fontsize'] = int(meta['fontsize']) except ValueError: raise DS9ParserError('font size must be an integer, got ' f'{meta["fontsize"]}') from None meta['fontstyle'] = meta['fontstyle'].replace('roman', 'normal') if shape == 'point': width = meta.pop('width', None) if width is not None: meta['markeredgewidth'] = width else: invalid = ('marker', 'markersize') for key in invalid: meta.pop(key, None) if shape == 'text': textangle = meta.pop('textangle', None) if textangle is not None: textrotate = meta.pop('textrotate', None) if textrotate != 0: # rotate if None or 1 meta['rotation'] = textangle # remove invalid mpl kwargs for matplotlib.text.Text invalid = ('linestyle', 'linewidth', 'fill') for key in invalid: meta.pop(key, None) else: # these kwarg are valid only for matplotlib.text.Text invalid = ('textangle', 'textrotate') for key in invalid: meta.pop(key, None) if shape not in ('point', 'line', 'text'): color = meta.pop('color', None) if color is not None: meta['facecolor'] = color meta['edgecolor'] = color return meta def _remove_invalid_keys(region_meta, valid_keys): # TODO: instead of new dict, del region_meta in-place? meta = {} for key in region_meta: if key in valid_keys: meta[key] = region_meta[key] return meta def _translate_metadata_to_ds9(region, shape): """ Translate region metadata to valid ds9 meta keys. """ meta = {**region.meta, **region.visual} # special case for Text regions if 'text' in region._params: meta = {'text': region.text, **meta} if 'annulus' in shape: # ds9 does not allow fill for annulus regions meta.pop('fill', None) fill = meta.pop('fill', None) if fill is not None: meta['fill'] = int(fill) if 'text' in meta: meta['text'] = f'{{{meta["text"]}}}' edgecolor = meta.pop('edgecolor', None) facecolor = meta.pop('facecolor', None) color = None if edgecolor is not None: color = edgecolor if facecolor is not None: if facecolor != color: warnings.warn('facecolor and edgecolor are different, edgecolor ' 'will be used', AstropyUserWarning) if color is None: color = facecolor if color is not None: meta['color'] = color linewidth = meta.pop('linewidth', None) if linewidth is not None: meta['width'] = linewidth # point region marker width markeredgewidth = meta.pop('markeredgewidth', None) if markeredgewidth is not None: meta['width'] = markeredgewidth marker = meta.pop('marker', None) if marker is not None: symbol_map = {y: x for x, y in ds9_valid_symbols.items()} if marker in symbol_map: markersize = meta.pop('markersize', None) msize = '' if markersize is not None: msize = f' {markersize}' meta['point'] = f'{symbol_map[marker]}{msize}' else: warnings.warn(f'Unable to serialize marker "{marker}"', AstropyUserWarning) fontname = meta.pop('fontname', None) if fontname is not None: fontsize = meta.pop('fontsize', 10) # default 10 fontweight = meta.pop('fontweight', 'normal') # default normal # default roman fontstyle = meta.pop('fontstyle', 'roman').replace('normal', 'roman') meta['font'] = f'"{fontname} {fontsize} {fontweight} {fontstyle}"' linestyle = meta.pop('linestyle', None) if linestyle is not None: meta['dash'] = 1 # if linestyle in ('dashed', '--'): if isinstance(linestyle, tuple): meta['dashlist'] = f'{linestyle[1][0]} {linestyle[1][1]}' # dashes = meta.pop('dashes', None) # if dashes is not None: # meta['dashlist'] = f'{dashes[0]} {dashes[1]}' rotation = meta.pop('rotation', None) if rotation is not None: meta['textangle'] = rotation # meta['textrotate'] = 1 # ds9 meta keys meta_keys = ['background', 'delete', 'edit', 'fixed', 'highlite', 'include', 'move', 'rotate', 'select', 'source', 'tag', 'text'] visual_keys = ['color', 'dash', 'dashlist', 'fill', 'font', 'point', 'textangle', 'textrotate', 'width'] valid_keys = meta_keys + visual_keys meta = _remove_invalid_keys(meta, valid_keys) return meta astropy-regions-f4139aa/regions/io/ds9/read.py000066400000000000000000000577221521703212700213470ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import re import string import warnings from dataclasses import dataclass import astropy.units as u from astropy.coordinates import Angle, SkyCoord from astropy.utils.data import get_readable_fileobj from astropy.utils.exceptions import AstropyUserWarning from regions.core import PixCoord, RegionMeta, Regions, RegionVisual from regions.core.registry import RegionsRegistry from regions.io.ds9.core import (DS9ParserError, ds9_frame_map, ds9_params_template, ds9_shape_to_region, make_region_template) from regions.io.ds9.meta import _split_raw_metadata, _translate_ds9_to_visual __all__ = [] @RegionsRegistry.register(Regions, 'read', 'ds9') def _read_ds9(filename, cache=False): """ Read a DS9 region file in as a list of `~regions.Region` objects. Parameters ---------- filename : str The filename of the file to access. cache : bool or 'update', optional Whether to cache the contents of remote URLs. If 'update', check the remote URL for a new version but store the result in the cache. Returns ------- regions : `regions.Regions` A `Regions` object containing a list of `~regions.Region` objects. """ with get_readable_fileobj(filename, cache=cache) as fh: region_string = fh.read() return _parse_ds9(region_string) @RegionsRegistry.register(Regions, 'parse', 'ds9') def _parse_ds9(region_str): """ Parse a DS9 region string. Parameters ---------- region_str : str The string contents of a DS9 region file. Returns ------- regions : `regions.Regions` A `Regions` object containing a list of `~regions.Region` objects. """ # first parse the input string to generate the raw region data region_data = _parse_raw_data(region_str) # now parse the raw region data into region object(s) regions = [] for region_data_ in region_data: region = _make_region(region_data_) if region is not None: # skip region if error during parsing regions.extend(region) return Regions(regions) @dataclass class _RegionData: """ Class to hold data used to initialize a Region object. Data for multi-annulus regions is stored in a single object. """ frame: str region_type: str shape: str shape_params: str raw_meta: dict region_str: str def _split_lines(region_str): """ Split a region string on newlines and all semicolons, except those between {} braces ({} contains ds9 text strings). Parameters ---------- region_str : str The string contents of a DS9 region file. Returns ------- lines : list of str A list of strings. """ return [line_.strip() for line in region_str.split('\n') for line_ in _split_semicolon(line)] def _parse_raw_data(region_str): """ Parse the raw data to extract the region data. Parameters ---------- region_str : str The string contents of a DS9 region file. Returns ------- lines : list of `_RegionData` A list of `_RegionData` objects. Note that at this stage, region data for multi-annulus regions is stored in a single object. """ global_meta = {} composite_meta = '' frame = None region_data = [] supported_frames = ['image', 'icrs', 'fk5', 'j2000', 'fk4', 'b1950', 'galactic', 'ecliptic'] unsupported_frames = ['linear', 'amplifier', 'detector', 'physical', 'tile'] wcs_frames = ['wcs', 'wcs0'] + [f'wcs{letter}' for letter in string.ascii_lowercase] unsupported_frames += wcs_frames supported_shapes = ['circle', 'ellipse', 'box', 'annulus', 'polygon', 'line', 'point', 'text', 'composite'] unsupported_shapes = ['vector', 'ruler', 'compass', 'projection', 'panda', 'epanda', 'bpanda'] supported_frames_shapes = supported_frames + supported_shapes unsupported_frames_shapes = unsupported_frames + unsupported_shapes valid_frames_shapes = supported_frames_shapes + unsupported_frames_shapes regex_frame_or_shape = re.compile('^#? *([+-]?)([a-zA-Z0-9]+)') for line in _split_lines(region_str): # split on semicolons & newlines # skip blank lines if not line: continue # skip comments if (line.startswith('#') and not line.startswith(('# text(', '# composite('))): continue original_line = line # used to parse text and tag fields (keep case) line = line.lower() # ds9 region files can have multiple (including successive) # global lines if line.startswith('global'): global_meta.update(_parse_metadata(original_line[7:])) continue match = regex_frame_or_shape.search(line) if match is None: raise ValueError(f'Unable to parse line "{line}".') include_symbol = match.groups()[0] frame_or_shape = match.groups()[1] if frame_or_shape not in valid_frames_shapes: warnings.warn(f'"{frame_or_shape}" frame or shape is not a valid ' 'frame or region shape; unable to parse line ' f'"{line}", skipping.', AstropyUserWarning) continue if frame_or_shape in unsupported_frames_shapes: warnings.warn(f'"{frame_or_shape}" frame or shape is not ' 'supported by the regions package, skipping.', AstropyUserWarning) if frame_or_shape in unsupported_frames: frame = None continue if frame_or_shape in supported_frames: # NOTE: frame value persists for subsequent regions until changed frame = frame_or_shape continue if frame_or_shape in supported_shapes: shape = frame_or_shape # a frame must be defined before any shape(s) if frame is None: warnings.warn('A coordinate frame was not found for region: ' f'"{line}", skipping.', AstropyUserWarning) continue if shape == 'composite': idx = line.find('||') if idx == -1: raise ValueError('unable to parse line with composite ' f'shape: "{line}"') # composite metadata applies to all regions within the # composite shape composite_meta = _parse_metadata(line[idx + 2:].strip()) # remove "composite=1" since we split the composite composite_meta.pop('composite', None) # NOTE: include=1/0 in metadata overrides the leading # "-/+" symbol; -: include=0;, + or '': include=1 include = 0 if include_symbol == '-' else 1 include_meta = {'include': include} params_str, meta_str = _parse_shape_line(shape, original_line, match.span()) region_meta = _parse_metadata(meta_str) raw_meta = _define_raw_metadata(global_meta, composite_meta, include_meta, region_meta) region_type = 'sky' if frame == 'image': region_type = 'pixel' # composite shape is used only to extract metadata if shape != 'composite': region_data.append(_RegionData(frame, region_type, shape, params_str, raw_meta, line)) # reset composite metadata after the composite region ends if '||' not in line and composite_meta: composite_meta = {} return region_data def _parse_shape_line(shape, line, span): """ Parse a line of a DS9 file containing a shape. Parameters ---------- shape : str The DS9 region shape (e.g., 'circle'). line : str A line defining a DS9 shape. span : 2 tuple A tuple containing the (start, end) positions of the match defining the shape. Returns ------- shape_params_str : str The region shape parameters as a string. meta_str : str The region metadata as a string. """ full_line = line line = full_line[span[1]:] # starts with the shape parameters # ds9 writes out text regions in this odd (undocumented) format if shape == 'text' and full_line.lower().startswith('# text'): idx = line.find(' ') if idx == -1: raise ValueError(f'unable to parse line "{line}"') meta_str = line[idx + 1:] shape_params_str = line[:idx] else: # split line into shape parameters and metadata parts = line.split('#', 1) # color value can contain a # if len(parts) == 1: shape_params_str = line meta_str = '' # no metadata found else: shape_params_str, meta_str = parts # strip trailing space and | chars shape_params_str = shape_params_str.strip(' |') shape_params_str = re.sub('[()]', '', shape_params_str).lower() meta_str = meta_str.strip() return shape_params_str, meta_str def _parse_metadata(metadata_str): """ Parse metadata for a single DS9 region. Parameters ---------- metadata_str : str The region metadata (e.g., everything after the "#"). Returns ------- metadata : dict The region metadata as a dictionary. """ # {.*?} # all chars in curly braces # \'.*?\' # all chars in single quotes # \".*?\" # all chars in double quotes # [-?\d+\.?\d*\s]+\s? # ([-/+]floats [whitespace]) incl. repeats # (e.g., dashlist=8 3, width=3, textangle=18.35) # [^=\s]+\s*[-?\d+\.?\d*]* # (all chars (e.g., point=diamond) or # (all chars [whitespace] digits) # (e.g., point=diamond 42) metadata_regex = re.compile(r'([a-zA-Z]+)\s*=\s*({.*?}|\'.*?\'|\".*?\"|' r'[-?\d+\.?\d*\s]+\s?|' r'[^=\s]+\s*[-?\d+\.?\d*]*)') metadata = {} for key, val in metadata_regex.findall(metadata_str): key = key.lower() val = val.strip().strip("'").strip('"').lstrip('{').rstrip('}') if key not in metadata: if key == 'tag': val = [val] # tag value is always a list metadata[key] = val elif key == 'tag': metadata[key].append(val) else: warnings.warn(f'Found duplicate metadata for "{key}", ' 'skipping', AstropyUserWarning) return metadata def _define_raw_metadata(global_meta, composite_meta, include_meta, region_meta): """ Define the raw metadata dictionary for the region. The raw metadata is not modified, except that invalid metadata is ignored. Parameters ---------- global_meta : dict The global metadata. composite_meta : dict The metadata for a composite region. include_meta : dict The include/exclude metadata. region_meta : dict The region metadata. Returns ------- meta : tuple of dict The (valid) raw metadata extracted from the region file. """ all_meta = global_meta.copy() all_meta.update(composite_meta) all_meta.update(include_meta) # region_meta must come after include_meta because include=1/0 in # metadata overrides the leading "-/+" include symbol all_meta.update(region_meta) # valid DS9 point symbols valid_points = ('circle', 'box', 'diamond', 'cross', 'x', 'arrow', 'boxcircle') # valid DS9 line values valid_lines = ('0 0', '0 1', '1 0', '1 1') # binary keys must have values of 0 or 1 # NOTE: textrotate is not documented in the DS9 region file spec binary_keys = ('dash', 'select', 'highlite', 'fixed', 'edit', 'move', 'rotate', 'delete', 'include', 'source', 'background', 'fill', 'vector', 'textrotate') metadata = {} for key, value in all_meta.items(): try: value = float(value) if value.is_integer(): value = int(value) except (ValueError, TypeError): pass is_invalid = False # point value can either be ["symbol int"] or ["symbol"] if key == 'point': val = value.split() if val[0] not in valid_points: is_invalid = True if len(val) == 2 and not float(val[1]).is_integer(): is_invalid = True if key == 'line' and value not in valid_lines: is_invalid = True if key in binary_keys and value not in (0, 1): is_invalid = True if is_invalid: warnings.warn(f'DS9 "{key}={value}" is invalid and will be ' 'ignored', AstropyUserWarning) continue metadata[key] = value return metadata def _parse_pixel_coord(param_str): invalid_chars = ('d', 'r', 'p', ':', 'h', 'm', 's') for char in invalid_chars: if char in param_str: raise DS9ParserError('Cannot parse pixel region position ' 'coordinates') if param_str[-1] == 'i': param_str = param_str[:-1] # DS9 uses 1-indexed pixels return float(param_str) - 1 def _parse_sky_coord(param_str, frame, index): invalid_chars = ('i', 'p') for char in invalid_chars: if char in param_str: raise DS9ParserError('Cannot parse sky region position ' 'coordinates') if param_str[-1] == 'r': return Angle(param_str[:-1], unit=u.radian) elif param_str[-1] == 'd': return Angle(param_str[:-1], unit=u.degree) elif 'd' in param_str or 'h' in param_str: return Angle(param_str) elif ':' in param_str: if index % 2 == 0 and frame not in ('galactic', 'ecliptic'): return Angle(param_str, u.hourangle) else: return Angle(param_str, u.degree) else: # unit not specified return Angle(float(param_str), unit=u.degree) def _parse_coord(region_type, param_str, frame, index): if region_type == 'pixel': return _parse_pixel_coord(param_str) else: return _parse_sky_coord(param_str, frame, index) def _parse_angle(param_str): invalid_chars = ('p', 'i') for char in invalid_chars: if char in param_str: raise DS9ParserError('Cannot parse sky region angle parameter') unit_mapping = {'"': u.arcsec, "'": u.arcmin, 'd': u.deg, 'r': u.rad} if param_str[-1] not in string.digits: unit = unit_mapping[param_str[-1]] return u.Quantity(float(param_str[:-1]), unit=unit) else: return u.Quantity(float(param_str), unit=u.degree) def _parse_size(region_type, param_str): if region_type == 'pixel': invalid_chars = ('"', "'", 'd', 'r', 'p') for char in invalid_chars: if char in param_str: raise DS9ParserError('Cannot parse pixel region size ' 'parameters - must not be in angular ' 'units') if param_str[-1] == 'i': param_str = param_str[:-1] return float(param_str) else: # size in angular units return _parse_angle(param_str) def _parse_shape_params(region_data): """ Parse the shape parameters for a region line. Parameters ---------- region_data : `_RegionData` instance A `_RegionData` instance containing the data for a region line. Data for multi-annulus regions is stored in a single object. Returns ------- shape_params : list of list(s) The shape parameters for each region(s). ``shape_params`` is a list of lists of shape parameters. A separate shape list is returned for multiple annulus/box/ellipse regions. Otherwise, the output list contains only one list. """ region_type = region_data.region_type shape = region_data.shape frame = region_data.frame params = [val for val in re.split(r'\s|\,', region_data.shape_params) if val] # split values on space or comma nparams = len(params) n_annulus = 0 if shape in ('ellipse', 'box') and nparams > 5: if nparams % 2 != 1: raise ValueError(f'incorrect number of parameters ({nparams}) ' f'for shape "{shape}"') n_annulus = ((nparams - 3) // 2) - 1 if shape in ('annulus',): n_annulus = nparams - 3 if shape in ('ellipse', 'box', 'annulus'): # reset the cycle iterators shape_template = make_region_template() else: shape_template = ds9_params_template[shape] shape_params = [] # TODO: check zip strict=True for idx, (param_type, value) in enumerate(zip(shape_template, params)): if shape in ('ellipse', 'box') and idx == nparams - 1: param_type = 'angle' # last parameter is always an angle if param_type == 'coord': param = _parse_coord(region_type, value, frame, idx) elif param_type in ('length',): param = _parse_size(region_type, value) if shape == 'ellipse': param *= 2.0 # ds9 uses semi-axis lengths elif param_type in ('angle',): param = _parse_angle(value) else: raise ValueError('cannot parse shape parameters') shape_params.append(param) if n_annulus > 1: tmp_params = [] for i in range(n_annulus): if shape in ('ellipse', 'box'): idx = (i + 1) * 2 params = [shape_params[0], shape_params[1], shape_params[idx], shape_params[idx + 1], shape_params[idx + 2], shape_params[idx + 3], shape_params[-1]] else: idx = i + 2 params = [shape_params[0], shape_params[1], shape_params[idx], shape_params[idx + 1]] tmp_params.append(params) shape_params = tmp_params else: shape_params = [shape_params] if n_annulus > 0: if shape == 'ellipse': shape = 'ellipse_annulus' elif shape == 'box': shape = 'rectangle_annulus' return shape, shape_params def _define_coords(region_type, params, frame=None): if region_type == 'pixel': coords = PixCoord(*params) else: coords = SkyCoord(*params, frame=frame) return coords def _define_region_params(region_type, shape, shape_params, frame=None): if frame is not None: frame = ds9_frame_map[frame] if shape == 'polygon': coord_params = (shape_params[0::2], shape_params[1::2]) params = [_define_coords(region_type, coord_params, frame=frame)] elif shape == 'line': params = [_define_coords(region_type, shape_params[0:2], frame=frame), _define_coords(region_type, shape_params[2:4], frame=frame)] elif shape in ('ellipse_annulus', 'rectangle_annulus'): size_params = shape_params[2:-1] tmp = [size_params[0::2], size_params[1::2]] tmp_flat = [item for sublist in tmp for item in sublist] params = [_define_coords(region_type, shape_params[0:2], frame=frame), *tmp_flat, shape_params[-1]] else: params = ([_define_coords(region_type, shape_params[0:2], frame=frame)] + shape_params[2:]) return params def _make_region(region_data): """ Make a region object from the region data. Parameters ---------- region_data : `_RegionData` instance A `_RegionData` instance. """ try: # NOTE: returned shape can be different from region_data.shape shape, shape_params_list = _parse_shape_params(region_data) except DS9ParserError as err: # raise a warning and skip the region msg = f'{str(err)}: {region_data.region_str}' warnings.warn(msg, AstropyUserWarning) return None # define the parameters to initialize a Region # NOTE: region_params can be longer than region_data for # multi-annulus regions region_type = region_data.region_type region_params = [] for shape_params in shape_params_list: region_params.extend([_define_region_params(region_type, shape, shape_params, region_data.frame)]) # separate the metadata and visual metadata and then translate the # visual metadata to valid mpl kwargs for the particular region meta, visual = _split_raw_metadata(region_data.raw_meta) visual = _translate_ds9_to_visual(shape, visual) regions = [] for shape_params in region_params: # for Text region, we need to add meta['text'] to params and # remove it from meta; set to '' if the text meta value was not # specified if shape == 'text': shape_params.append(region_data.raw_meta.get('text', '')) meta.pop('text', None) region = ds9_shape_to_region[region_type][shape](*shape_params) region.meta = RegionMeta(meta) region.visual = RegionVisual(visual) region._raw_meta = region_data.raw_meta regions.append(region) return regions def _find_text_delim_idx(region_str): """ Find the indices of the DS9 text field delimiters ({}, '', or "") in a string. """ pattern = re.compile(r'(text\s*=\s*[{\'"])') idx0 = [] delim = [] start_idx = [] for match in pattern.finditer(region_str): idx0.append(match.start()) end_delim = match.group()[-1] if end_delim == '{': end_delim = '}' delim.append(end_delim) start_idx.append(match.span()[1]) idx1 = [] for sidx, char in zip(start_idx, delim, strict=True): idx1.append(region_str.find(char, sidx)) return idx0, idx1 def _split_semicolon(region_str): r""" Split a DS9 region string on semicolons. The line is not split on semicolons found in a text field. This turned out to be a very tricky problem (attempts with regex failed): * text fields are delimited by {}, '', or "" * the text delimiters do not have to be consistent within a file * region strings can have unpaired ' (arcmin) or " (arcsec) * region strings can have "#" in a color value (cannot split on #) * the text field can contain "text={str}", "text='str'", 'test="str"' etc., e.g., text={text="hello"} * escaped delimiter characters used by the text field are not allowed within the text field, e.g., "text={my field\{test\}}" and "text='my field \'test\''" are invalid. However, "text={my field, 'test'}" is valid This code finds the text field delimiters and then finds the indices of the opening and closing delimiters. Text fields that contain "text={str}", etc. are included (as a smaller range between the larger text field range), but this is fine because all we need are index ranges where to exclude semicolons (for splitting). Semicolons found at indices between the open/close delimiter indices are excluded from splitting. """ idx0, idx1 = _find_text_delim_idx(region_str) semi_idx = [pos for pos, char in enumerate(region_str) if char == ';'] fidx = [] for i in semi_idx: for i0, i1 in zip(idx0, idx1, strict=True): if i0 <= i <= i1: break else: fidx.append(i + 1) fidx.insert(0, 0) return [region_str[i:j].rstrip(';') for i, j in zip(fidx, fidx[1:] + [None], strict=True)] astropy-regions-f4139aa/regions/io/ds9/tests/000077500000000000000000000000001521703212700212075ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/ds9/tests/__init__.py000066400000000000000000000000001521703212700233060ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/ds9/tests/data/000077500000000000000000000000001521703212700221205ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.color.reg000066400000000000000000000010661521703212700244360ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 fk5 circle(13:29:52.675,+47:11:45.02,1") # color=blue circle(13:29:52.675,+47:11:45.02,2") # color=#800 circle(13:29:52.675,+47:11:45.02,3") # color=#0a0 circle(13:29:52.675,+47:11:45.02,4") # color=#880000 circle(13:29:52.675,+47:11:45.02,5") # color=#00aa00 circle(13:29:52.675,+47:11:45.02,6") # color=#888800000000 circle(13:29:52.675,+47:11:45.02,7") # color=#0000aaaa0000 astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.color.spaces.reg000066400000000000000000000011041521703212700257040ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 fk5 circle(13:29:52.675, +47:11:45.02, 1") # color=blue circle(13:29:52.675, +47:11:45.02, 2") # color=#800 circle(13:29:52.675, +47:11:45.02, 3") # color=#0a0 circle(13:29:52.675, +47:11:45.02, 4") # color=#880000 circle(13:29:52.675, +47:11:45.02, 5") # color=#00aa00 circle(13:29:52.675, +47:11:45.02, 6") # color=#888800000000 circle(13:29:52.675, +47:11:45.02, 7") # color=#0000aaaa0000 astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.composite.reg000066400000000000000000000035321521703212700253220ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 fk5 # composite(202.48165,47.1931,317.39831) || composite=1 circle(202.48705,47.208237,3.9640007") || # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(202.48309,47.204492,7.9280014",3.9640007",2.3983109) || # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(202.47783,47.201057,15.856003",7.9280014",2.3983109) || # color=yellow font="helvetica 10 bold roman" text={Box} polygon(202.47309,47.198922,202.46877,47.199044,202.46859,47.196108,202.47291,47.195985) || # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(202.4684,47.194116,202.46202,47.193946) || # line=1 1 color=cyan text={Line} # text(202.45907,47.188886) || color=magenta font="helvetica 14 bold roman" text={Region} annulus(202.49319,47.203697,1.9820003",3.9640007",5.946001") || # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(202.48834,47.200368,2.9730005",1.4865003",5.946001",2.9730005",2.3983109) || # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(202.4832,47.197085,7.9280014",3.9640007",11.892002",5.946001",2.3983109) || # font="helvetica 10 bold roman" text={Box Annulus} point(202.49871,47.200189) || # point=circle text={Circle Point} point(202.49351,47.196791) || # point=box color=red width=3 text={Box Point} point(202.48857,47.193854) || # point=diamond text={Diamond Point} point(202.5005,47.197643) || # point=cross color=blue text={Cross Point} point(202.49591,47.194852) || # point=x text={X Point} point(202.49148,47.191862) || # point=arrow color=magenta text={Arrow Point} point(202.49864,47.19267) || # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.ecliptic.hms.reg000066400000000000000000000035551521703212700257070ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 ecliptic circle(+175:06:44.660,+50:57:09.921,3.964") # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(+175:06:44.660,+50:56:53.323,7.928",3.964",326.717) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(+175:06:39.530,+50:56:35.776,15.856",7.928",326.717) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(+175:06:31.722,+50:56:22.779,+175:06:17.685,+50:56:16.970,+175:06:26.903,+50:56:08.125,+175:06:40.939,+50:56:13.934) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(+175:06:32.927,+50:56:02.026,+175:06:13.393,+50:55:52.432) # line=1 1 color=cyan text={Line} # text(+175:06:20.956,+50:55:33.430) color=magenta font="helvetica 14 bold roman" text={Region} annulus(+175:07:19.163,+50:57:05.414,1.982",3.964",5.946") # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(+175:07:14.964,+50:56:48.757,2.973",1.4865",5.946",2.973",326.717) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(+175:07:09.715,+50:56:31.833,7.928",3.964",11.892",5.946",326.717) # font="helvetica 10 bold roman" text={Box Annulus} point(+175:07:48.259,+50:57:03.028) # point=circle text={Circle Point} point(+175:07:43.186,+50:56:45.675) # point=box color=red width=3 text={Box Point} point(+175:07:37.393,+50:56:30.037) # point=diamond text={Diamond Point} point(+175:08:02.394,+50:56:58.143) # point=cross color=blue text={Cross Point} point(+175:07:57.227,+50:56:43.435) # point=x text={X Point} point(+175:07:53.228,+50:56:28.371) # point=arrow color=magenta text={Arrow Point} point(+175:08:13.115,+50:56:40.951) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.ecliptic.reg000066400000000000000000000033701521703212700251140ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 ecliptic circle(175.11241,50.952756,3.9640007") # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(175.11241,50.948145,7.9280014",3.9640007",326.71725) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(175.11098,50.943271,15.856003",7.9280014",326.71725) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(175.10881,50.939661,175.10491,50.938047,175.10747,50.93559,175.11137,50.937204) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(175.10915,50.933896,175.10372,50.931231) # line=1 1 color=cyan text={Line} # text(175.10582,50.925953) color=magenta font="helvetica 14 bold roman" text={Region} annulus(175.12199,50.951504,1.9820003",3.9640007",5.946001") # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(175.12082,50.946877,2.9730005",1.4865003",5.946001",2.9730005",326.71725) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(175.11937,50.942176,7.9280014",3.9640007",11.892002",5.946001",326.71725) # font="helvetica 10 bold roman" text={Box Annulus} point(175.13007,50.950841) # point=circle text={Circle Point} point(175.12866,50.946021) # point=box color=red width=3 text={Box Point} point(175.12705,50.941677) # point=diamond text={Diamond Point} point(175.134,50.949484) # point=cross color=blue text={Cross Point} point(175.13256,50.945398) # point=x text={X Point} point(175.13145,50.941214) # point=arrow color=magenta text={Arrow Point} point(175.13698,50.944709) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.fits.reg000066400000000000000000000006651521703212700242710ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 image circle(383,1153,40) box(679,1149,160,80,0) -box(1057,1155,160,80,30) box(675,979,160,80,0) box(1065,979,160,80,30) -ellipse(399,777,80,40,30) point(687,773) # point=boxcircle annulus(685,561,20,60) polygon(1014,805,1151,805,1151,668,1051,685) astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.fk4.hms.reg000066400000000000000000000034541521703212700245750ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 fk4 circle(13:27:50.384,+47:27:57.76,3.964") # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(13:27:49.426,+47:27:44.31,7.928",3.964",2.55413) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(13:27:48.154,+47:27:31.98,15.856",7.928",2.55413) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(13:27:47.011,+47:27:24.32,13:27:45.968,+47:27:24.79,13:27:45.922,+47:27:14.22,13:27:46.964,+47:27:13.75) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(13:27:45.873,+47:27:07.05,13:27:44.336,+47:27:06.48) # line=1 1 color=cyan text={Line} # text(13:27:43.620,+47:26:48.29) color=magenta font="helvetica 14 bold roman" text={Region} annulus(13:27:51.861,+47:27:41.38,1.982",3.964",5.946") # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(13:27:50.688,+47:27:29.43,2.973",1.4865",5.946",2.973",2.55413) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(13:27:49.447,+47:27:17.65,7.928",3.964",11.892",5.946",2.55413) # font="helvetica 10 bold roman" text={Box Annulus} point(13:27:53.189,+47:27:28.71) # point=circle text={Circle Point} point(13:27:51.931,+47:27:16.52) # point=box color=red width=3 text={Box Point} point(13:27:50.737,+47:27:05.98) # point=diamond text={Diamond Point} point(13:27:53.618,+47:27:19.54) # point=cross color=blue text={Cross Point} point(13:27:52.509,+47:27:09.52) # point=x text={X Point} point(13:27:51.438,+47:26:58.79) # point=arrow color=magenta text={Arrow Point} point(13:27:53.166,+47:27:01.65) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.fk4.reg000066400000000000000000000033621521703212700240050ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 fk4 circle(201.95993,47.466046,3.9640007") # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(201.95594,47.462308,7.9280014",3.9640007",2.5541322) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(201.95064,47.458883,15.856003",7.9280014",2.5541322) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(201.94588,47.456757,201.94153,47.456887,201.94134,47.453951,201.94568,47.45382) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(201.94114,47.45196,201.93473,47.451801) # line=1 1 color=cyan text={Line} # text(201.93175,47.446747) color=magenta font="helvetica 14 bold roman" text={Region} annulus(201.96609,47.461495,1.9820003",3.9640007",5.946001") # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(201.9612,47.458175,2.9730005",1.4865003",5.946001",2.9730005",2.5541322) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(201.95603,47.454902,7.9280014",3.9640007",11.892002",5.946001",2.5541322) # font="helvetica 10 bold roman" text={Box Annulus} point(201.97162,47.457976) # point=circle text={Circle Point} point(201.96638,47.454589) # point=box color=red width=3 text={Box Point} point(201.9614,47.451661) # point=diamond text={Diamond Point} point(201.97341,47.455428) # point=cross color=blue text={Cross Point} point(201.96879,47.452645) # point=x text={X Point} point(201.96433,47.449663) # point=arrow color=magenta text={Arrow Point} point(201.97152,47.450458) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.fk5.hms.reg000066400000000000000000000034541521703212700245760ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 fk5 circle(13:29:56.743,+47:12:30.42,3.964") # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(13:29:55.792,+47:12:16.94,7.928",3.964",2.39831) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(13:29:54.530,+47:12:04.57,15.856",7.928",2.39831) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(13:29:53.394,+47:11:56.88,13:29:52.357,+47:11:57.32,13:29:52.314,+47:11:46.75,13:29:53.351,+47:11:46.31) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(13:29:52.267,+47:11:39.58,13:29:50.737,+47:11:38.97) # line=1 1 color=cyan text={Line} # text(13:29:50.030,+47:11:20.75) color=magenta font="helvetica 14 bold roman" text={Region} annulus(13:29:58.217,+47:12:14.07,1.982",3.964",5.946") # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(13:29:57.053,+47:12:02.09,2.973",1.4865",5.946",2.973",2.39831) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(13:29:55.820,+47:11:50.27,7.928",3.964",11.892",5.946",2.39831) # font="helvetica 10 bold roman" text={Box Annulus} point(13:29:59.541,+47:12:01.44) # point=circle text={Circle Point} point(13:29:58.293,+47:11:49.21) # point=box color=red width=3 text={Box Point} point(13:29:57.107,+47:11:38.64) # point=diamond text={Diamond Point} point(13:29:59.971,+47:11:52.28) # point=cross color=blue text={Cross Point} point(13:29:58.870,+47:11:42.23) # point=x text={X Point} point(13:29:57.807,+47:11:31.47) # point=arrow color=magenta text={Arrow Point} point(13:29:59.526,+47:11:34.38) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.fk5.reg000066400000000000000000000033611521703212700240050ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 fk5 circle(202.48643,47.208449,3.9640007") # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(202.48247,47.204704,7.9280014",3.9640007",2.3983109) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(202.47721,47.201269,15.856003",7.9280014",2.3983109) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(202.47248,47.199134,202.46815,47.199257,202.46797,47.19632,202.4723,47.196197) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(202.46778,47.194329,202.4614,47.194158) # line=1 1 color=cyan text={Line} # text(202.45846,47.189099) color=magenta font="helvetica 14 bold roman" text={Region} annulus(202.49257,47.20391,1.9820003",3.9640007",5.946001") # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(202.48772,47.20058,2.9730005",1.4865003",5.946001",2.9730005",2.3983109) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(202.48258,47.197298,7.9280014",3.9640007",11.892002",5.946001",2.3983109) # font="helvetica 10 bold roman" text={Box Annulus} point(202.49809,47.200401) # point=circle text={Circle Point} point(202.49289,47.197004) # point=box color=red width=3 text={Box Point} point(202.48795,47.194067) # point=diamond text={Diamond Point} point(202.49988,47.197856) # point=cross color=blue text={Cross Point} point(202.49529,47.195065) # point=x text={X Point} point(202.49086,47.192074) # point=arrow color=magenta text={Arrow Point} point(202.49802,47.192883) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.galactic.hms.reg000066400000000000000000000035551521703212700256620ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 galactic circle(+104:50:15.532,+68:32:38.380,3.964") # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(+104:50:24.720,+68:32:54.634,7.928",3.964",158.389) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(+104:50:43.097,+68:33:11.163,15.856",7.928",158.389) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(+104:51:03.480,+68:33:22.893,+104:51:30.395,+68:33:26.787,+104:51:19.743,+68:33:36.626,+104:50:52.826,+68:33:32.732) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(+104:51:12.957,+68:33:43.369,+104:51:51.267,+68:33:50.265) # line=1 1 color=cyan text={Line} # text(+104:51:49.050,+68:34:09.839) color=magenta font="helvetica 14 bold roman" text={Region} annulus(+104:49:19.827,+68:32:47.191,1.982",3.964",5.946") # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(+104:49:36.118,+68:33:02.969,2.973",1.4865",5.946",2.973",158.389) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(+104:49:54.339,+68:33:18.875,7.928",3.964",11.892",5.946",158.389) # font="helvetica 10 bold roman" text={Box Annulus} point(+104:48:32.059,+68:32:53.230) # point=circle text={Circle Point} point(+104:48:50.197,+68:33:09.580) # point=box color=red width=3 text={Box Point} point(+104:49:08.613,+68:33:24.159) # point=diamond text={Diamond Point} point(+104:48:10.904,+68:32:59.810) # point=cross color=blue text={Cross Point} point(+104:48:27.737,+68:33:13.560) # point=x text={X Point} point(+104:48:42.801,+68:33:27.806) # point=arrow color=magenta text={Arrow Point} point(+104:48:02.289,+68:33:18.011) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.galactic.reg000066400000000000000000000033631521703212700250710ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 galactic circle(104.83765,68.543994,3.9640007") # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(104.8402,68.54851,7.9280014",3.9640007",158.38935) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(104.8453,68.553101,15.856003",7.9280014",158.38935) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(104.85097,68.556359,104.85844,68.557441,104.85548,68.560174,104.84801,68.559092) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(104.8536,68.562047,104.86424,68.563963) # line=1 1 color=cyan text={Line} # text(104.86363,68.5694) color=magenta font="helvetica 14 bold roman" text={Region} annulus(104.82217,68.546442,1.9820003",3.9640007",5.946001") # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(104.8267,68.550825,2.9730005",1.4865003",5.946001",2.9730005",158.38935) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(104.83176,68.555243,7.9280014",3.9640007",11.892002",5.946001",158.38935) # font="helvetica 10 bold roman" text={Box Annulus} point(104.80891,68.548119) # point=circle text={Circle Point} point(104.81394,68.552661) # point=box color=red width=3 text={Box Point} point(104.81906,68.556711) # point=diamond text={Diamond Point} point(104.80303,68.549947) # point=cross color=blue text={Cross Point} point(104.8077,68.553767) # point=x text={X Point} point(104.81189,68.557724) # point=arrow color=magenta text={Arrow Point} point(104.80064,68.555003) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.icrs.hms.reg000066400000000000000000000034551521703212700250520ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 icrs circle(13:29:56.743,+47:12:30.42,3.964") # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(13:29:55.792,+47:12:16.94,7.928",3.964",2.39832) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(13:29:54.530,+47:12:04.57,15.856",7.928",2.39832) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(13:29:53.394,+47:11:56.88,13:29:52.357,+47:11:57.33,13:29:52.314,+47:11:46.75,13:29:53.351,+47:11:46.31) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(13:29:52.267,+47:11:39.58,13:29:50.737,+47:11:38.97) # line=1 1 color=cyan text={Line} # text(13:29:50.030,+47:11:20.76) color=magenta font="helvetica 14 bold roman" text={Region} annulus(13:29:58.217,+47:12:14.08,1.982",3.964",5.946") # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(13:29:57.053,+47:12:02.09,2.973",1.4865",5.946",2.973",2.39832) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(13:29:55.820,+47:11:50.27,7.928",3.964",11.892",5.946",2.39832) # font="helvetica 10 bold roman" text={Box Annulus} point(13:29:59.541,+47:12:01.45) # point=circle text={Circle Point} point(13:29:58.293,+47:11:49.22) # point=box color=red width=3 text={Box Point} point(13:29:57.108,+47:11:38.64) # point=diamond text={Diamond Point} point(13:29:59.971,+47:11:52.28) # point=cross color=blue text={Cross Point} point(13:29:58.870,+47:11:42.24) # point=x text={X Point} point(13:29:57.807,+47:11:31.47) # point=arrow color=magenta text={Arrow Point} point(13:29:59.526,+47:11:34.38) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.icrs.oneline.reg000066400000000000000000000013011521703212700257000ustar00rootroot00000000000000icrs;circle(202.48643,47.208449,3.9640007");-ellipse(202.48247,47.204705,7.9280014",3.9640007",2.3983198);-box(202.47721,47.20127,15.856003",7.9280014",2.3983198);polygon(202.47248,47.199135,202.46815,47.199257,202.46797,47.19632,202.4723,47.196198);-line(202.46778,47.194329,202.4614,47.194159);annulus(202.49257,47.20391,1.9820003",3.9640007",5.946001");ellipse(202.48772,47.200581,2.9730005",1.4865003",5.946001",2.9730005",2.3983198);box(202.48259,47.197298,7.9280014",3.9640007",11.892002",5.946001",2.3983198);point(202.49809,47.200401);point(202.49289,47.197004);point(202.48795,47.194067);point(202.49988,47.197856);point(202.49529,47.195065);point(202.49086,47.192075);point(202.49802,47.192883) astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.icrs.reg000066400000000000000000000033621521703212700242610ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 icrs circle(202.48643,47.208449,3.9640007") # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(202.48247,47.204705,7.9280014",3.9640007",2.3983198) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(202.47721,47.20127,15.856003",7.9280014",2.3983198) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(202.47248,47.199135,202.46815,47.199257,202.46797,47.19632,202.4723,47.196198) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(202.46778,47.194329,202.4614,47.194159) # line=1 1 color=cyan text={Line} # text(202.45846,47.189099) color=magenta font="helvetica 14 bold roman" text={Region} annulus(202.49257,47.20391,1.9820003",3.9640007",5.946001") # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(202.48772,47.200581,2.9730005",1.4865003",5.946001",2.9730005",2.3983198) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(202.48259,47.197298,7.9280014",3.9640007",11.892002",5.946001",2.3983198) # font="helvetica 10 bold roman" text={Box Annulus} point(202.49809,47.200401) # point=circle text={Circle Point} point(202.49289,47.197004) # point=box color=red width=3 text={Box Point} point(202.48795,47.194067) # point=diamond text={Diamond Point} point(202.49988,47.197856) # point=cross color=blue text={Cross Point} point(202.49529,47.195065) # point=x text={X Point} point(202.49086,47.192075) # point=arrow color=magenta text={Arrow Point} point(202.49802,47.192883) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.image.oneline.reg000066400000000000000000000016321521703212700260310ustar00rootroot00000000000000image;circle(166,546,20);-ellipse(248,529,40,20,45);-box(338,527,80,40,45);polygon(407.24723,538,445,575.75275,482.75277,538,445,500.24723);-line(509,513,569,564);annulus(166,434,10,20,30);ellipse(251,430,15,7.5,30,15,45);box(338,429,40,20,60,30,45);point(159,341);point(248,339);point(329,341);point(174,292);point(250,293);point(327,290);point(252,241);panda(428,341,0,270,3,0,30,2);panda(430,250,51.5819,90,1,0,15,1);panda(430,250,51.5819,90,1,15,30,1);panda(430,250,90,180,1,0,15,1);panda(430,250,90,180,1,15,30,1);panda(430,250,180,270,1,0,15,1);panda(430,250,180,270,1,15,30,1);epanda(510,333,0,270,3,15,7.5,30,15,1,45);epanda(513,244,45,90,1,15,7.5,30,15,1,45);epanda(513,244,90,180,1,15,7.5,30,15,1,45);epanda(513,244,180,270,1,15,7.5,30,15,1,45);bpanda(597,333,0,270,3,40,20,60,30,1,45);bpanda(593,245,45,90,1,40,20,60,30,1,45);bpanda(593,245,90,180,1,40,20,60,30,1,45);bpanda(593,245,180,270,1,40,20,60,30,1,45);astropy-regions-f4139aa/regions/io/ds9/tests/data/ds9.image.reg000066400000000000000000000026101521703212700243760ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 image circle(166,546,20) # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment -ellipse(248,529,40,20,45) # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background -box(338,527,80,40,45) # color=yellow font="helvetica 10 bold roman" text={Box} polygon(407.24723,538,445,575.75275,482.75277,538,445,500.24723) # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 -line(509,513,569,564) # line=1 1 color=cyan text={Line} # text(658,521) color=magenta font="helvetica 14 bold roman" text={Region} annulus(166,434,10,20,30) # color=magenta font="helvetica 10 bold roman" text={Annulus} ellipse(251,430,15,7.5,30,15,45) # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} box(338,429,40,20,60,30,45) # font="helvetica 10 bold roman" text={Box Annulus} point(159,341) # point=circle text={Circle Point} point(248,339) # point=box color=red width=3 text={Box Point} point(329,341) # point=diamond text={Diamond Point} point(174,292) # point=cross color=blue text={Cross Point} point(250,293) # point=x text={X Point} point(327,290) # point=arrow color=magenta text={Arrow Point} point(252,241) # point=boxcircle text={BoxCircle Point} astropy-regions-f4139aa/regions/io/ds9/tests/data/fk5_reference.reg000066400000000000000000000047521521703212700253320ustar00rootroot00000000000000# Region file format: DS9 astropy/regions fk5 circle(202.49,47.21,3.96") # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Circle} tag={foo} tag={foo bar} This is a Comment color=pink width=3 font="times 10 normal roman" -ellipse(202.48,47.20,7.93",3.96",2.40) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Ellipse} background color=#0ff width=1 font="helvetica 10 normal italic" -box(202.48,47.20,15.86",7.93",2.40) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Box} color=yellow width=1 font="helvetica 10 bold roman" polygon(202.47,47.20,202.47,47.20,202.47,47.20,202.47,47.20) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=0 move=1 delete=1 source=1 text={Polygon} rotate=0 color=green width=1 font="courier 10 normal roman" -line(202.47,47.19,202.46,47.19) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 line=1 1 text={Line} color=cyan width=1 font="helvetica 10 normal roman" box(202.48,47.20,7.93",3.96",0.00) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Box Annulus} color=green width=1 font="helvetica 10 bold roman" point(202.50,47.20) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Circle Point} color=green width=1 font="helvetica 10 normal roman" point=circle point(202.49,47.20) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Box Point} color=red width=3 font="helvetica 10 normal roman" point=box point(202.49,47.19) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Diamond Point} color=green width=1 font="helvetica 10 normal roman" point=diamond point(202.50,47.20) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Cross Point} color=blue width=1 font="helvetica 10 normal roman" point=cross point(202.50,47.20) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={X Point} color=green width=1 font="helvetica 10 normal roman" point=x point(202.49,47.19) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Arrow Point} color=magenta width=1 font="helvetica 10 normal roman" point=arrow point(202.50,47.19) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={BoxCircle Point} color=green width=1 font="helvetica 10 normal roman" point=boxcircle astropy-regions-f4139aa/regions/io/ds9/tests/data/galactic_reference.reg000066400000000000000000000047631521703212700264160ustar00rootroot00000000000000# Region file format: DS9 astropy/regions galactic circle(104.84,68.54,3.96") # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Circle} tag={foo} tag={foo bar} This is a Comment color=pink width=3 font="times 10 normal roman" -ellipse(104.84,68.55,7.93",3.96",158.39) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Ellipse} background color=#0ff width=1 font="helvetica 10 normal italic" -box(104.85,68.55,15.86",7.93",158.39) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Box} color=yellow width=1 font="helvetica 10 bold roman" polygon(104.85,68.56,104.86,68.56,104.86,68.56,104.85,68.56) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=0 move=1 delete=1 source=1 text={Polygon} rotate=0 color=green width=1 font="courier 10 normal roman" -line(104.85,68.56,104.86,68.56) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 line=1 1 text={Line} color=cyan width=1 font="helvetica 10 normal roman" box(104.83,68.56,7.93",3.96",0.00) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Box Annulus} color=green width=1 font="helvetica 10 bold roman" point(104.81,68.55) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Circle Point} color=green width=1 font="helvetica 10 normal roman" point=circle point(104.81,68.55) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Box Point} color=red width=3 font="helvetica 10 normal roman" point=box point(104.82,68.56) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Diamond Point} color=green width=1 font="helvetica 10 normal roman" point=diamond point(104.80,68.55) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Cross Point} color=blue width=1 font="helvetica 10 normal roman" point=cross point(104.81,68.55) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={X Point} color=green width=1 font="helvetica 10 normal roman" point=x point(104.81,68.56) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={Arrow Point} color=magenta width=1 font="helvetica 10 normal roman" point=arrow point(104.80,68.56) # dashlist=8 3 select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 source=1 text={BoxCircle Point} color=green width=1 font="helvetica 10 normal roman" point=boxcircle astropy-regions-f4139aa/regions/io/ds9/tests/data/plot_image.reg000066400000000000000000000016331521703212700247420ustar00rootroot00000000000000# Region file format: DS9 version 4.1 global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 image point(101,101) # point=circle color=red point(101,201) # point=box point(101,301) # point=diamond color=#bad8ff point(101,401) # point=cross color=cyan point(201,101) # point=x color=yellow point(201,201) # point=arrow color=magenta point(201,301) # point=boxcircle color=orange # text(201,401) textangle=45 color=pink font="helvetica 12 normal roman" text={Text} circle(501,701,75) # color=red annulus(701,701,60,90) # color=violet ellipse(501,501,75,40,70) # color=#4c90ff ellipse(701,501,30,60,40,80,70) # color=cyan box(501,301,150,80,20) # color=orange box(701,301,50,75,100,150,20) # color=magenta polygon(601,164,668,41,370,61,500,161) # color=yellow line(807.67432,202.77662,720.05219,55.496868) # line=0 0 color=#00ff7f astropy-regions-f4139aa/regions/io/ds9/tests/test_ds9.py000066400000000000000000000625161521703212700233310ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the ds9 subpackage. """ import os import warnings import astropy.units as u import numpy as np import pytest from astropy.coordinates import Angle, SkyCoord from astropy.io import fits from astropy.tests.helper import assert_quantity_allclose from astropy.utils.data import get_pkg_data_filename, get_pkg_data_filenames from astropy.utils.exceptions import AstropyUserWarning from numpy.testing import assert_allclose, assert_equal from regions._utils.optional_deps import HAS_MATPLOTLIB from regions.core import PixCoord, Regions, RegionVisual from regions.shapes import (CirclePixelRegion, CircleSkyRegion, PointPixelRegion, RegularPolygonPixelRegion, TextPixelRegion) from regions.tests.helpers import assert_region_allclose @pytest.mark.remote_data @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_example_plot(): import matplotlib.pyplot as plt image_file = get_pkg_data_filename('tutorials/FITS-images/HorseHead.fits') data = fits.getdata(image_file, ext=0, memmap=False) data = np.rot90(data, k=-1) _, ax = plt.subplots(figsize=(8, 8)) ax.imshow(data, cmap='gray', origin='lower') region_file = get_pkg_data_filename('data/plot_image.reg', package='regions.io.ds9.tests') regions = Regions.read(region_file, format='ds9') for _i, region in enumerate(regions): region.plot(ax=ax) plt.close() def test_roundtrip(tmpdir): filenames = get_pkg_data_filenames('data', pattern='*.reg') # AstropyUserWarning will be emitted only for some of the files with warnings.catch_warnings(): warnings.simplefilter('ignore', AstropyUserWarning) # Check that all test files are readable for filename in filenames: regions = Regions.read(filename, format='ds9') assert len(regions) > 0 tempfile = tmpdir.join('tmp.ds9').strpath regions.write(tempfile, format='ds9', overwrite=True, precision=20) regions2 = Regions.read(tempfile, format='ds9') assert len(regions2) > 0 for reg1, reg2 in zip(regions, regions2, strict=True): assert_region_allclose(reg1, reg2) def test_serialize(): """ Simple test for serialize. """ center = SkyCoord(42, 43, unit='deg') radius = Angle(3, 'deg') region = CircleSkyRegion(center, radius) expected = ('# Region file format: DS9 astropy/regions\nicrs\n' 'circle(42.0000,43.0000,3.0000)\n') actual = region.serialize(format='ds9', precision=4) assert actual == expected def test_serialize_parse_text(): """ Test serialization of Text region. """ text = 'Example Text' region = TextPixelRegion(PixCoord(42, 43), text=text) expected = ('# Region file format: DS9 astropy/regions\nglobal ' f'text={{{text}}}\nimage\ntext(43.0000,44.0000)\n') actual = region.serialize(format='ds9', precision=4) assert actual == expected reg = Regions.parse(actual, format='ds9')[0] assert reg.text == text assert 'text' not in reg.meta def test_parse(): """ Simple test for parse. """ region_str = ('# Region file format: DS9 astropy/regions\nfk5\n' 'circle(42.0000,43.0000,3.0000)\n') region = Regions.parse(region_str, format='ds9')[0] assert_allclose(region.center.ra.deg, 42) assert_allclose(region.center.dec.deg, 43) assert_quantity_allclose(region.radius, 3 * u.deg) def test_exclude(): """ Simple parse test for an excluded region. """ region_str = ('# Region file format: DS9 astropy/regions\nfk5\n' ' -circle(42.0000,43.0000,3.0000)\n') region = Regions.parse(region_str, format='ds9')[0] assert not region.meta['include'] def test_read_write(tmpdir): """ Simple test for write and read. """ center = SkyCoord(42, 43, unit='deg', frame='fk5') radius = Angle(3, 'deg') region = CircleSkyRegion(center, radius) region.meta['text'] = 'ExampleText' filename = os.path.join(str(tmpdir), 'ds9.reg') region.write(filename, format='ds9') region2 = Regions.read(filename, format='ds9')[0] assert_quantity_allclose(region.center.ra, 42 * u.deg) assert_quantity_allclose(region.center.dec, 43 * u.deg) assert_quantity_allclose(region.radius, 3 * u.deg) assert 'text' in region2.meta assert region2.meta['text'] == 'ExampleText' def test_invalid_region_warns(): ds9_str = ('# Region file format: DS9 astropy/regions\nfk5\n' 'circle(42.0000,43.0000,3.0000)\ninvalidregion(blah)') match = 'frame or shape is not a valid frame or region shape' with pytest.warns(AstropyUserWarning, match=match): Regions.parse(ds9_str, format='ds9') def test_global_parser(): """ Test parsing global metadata. """ region_str = ('global color=green dashlist=8 3 width=1 ' 'font="helvetica 10 normal roman" select=1 highlite=1 ' 'dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1\n' 'icrs;circle(42.0000,43.0000,3.0000)') region = Regions.parse(region_str, format='ds9')[0] meta_expected = {'select': 1, 'highlite': 1, 'fixed': 0, 'edit': 1, 'move': 1, 'delete': 1, 'include': 1, 'source': 1} visual_expected = {'linewidth': 1, 'fontname': 'helvetica', 'fontsize': 10, 'fontweight': 'normal', 'fontstyle': 'normal', 'facecolor': 'green', 'edgecolor': 'green', 'default_style': 'ds9'} assert region.meta == meta_expected assert region.visual == visual_expected def test_meta_color(): region_str = ('# Region file format: DS9 astropy/regions\nicrs\n' 'circle(42.0000,43.0000,3.0000) # color=green\n' 'circle(43.0000,43.0000,3.0000) # color=orange\n') regions = Regions.parse(region_str, format='ds9') assert regions[0].visual['facecolor'] == 'green' assert regions[0].visual['edgecolor'] == 'green' assert regions[1].visual['facecolor'] == 'orange' assert regions[1].visual['edgecolor'] == 'orange' def test_meta_color_override_global(): """ Color parsing test in the presence of a global color. """ region_str = ('# Region file format: DS9 astropy/regions\n' 'global color=blue dashlist=8 3 width=1 ' 'font="helvetica 10 normal roman" select=1 highlite=1 ' 'dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1\n' 'icrs\ncircle(42.0000,43.0000,3.0000) # color=green\n' 'circle(42.0000,43.0000,5.0000) # color=orange\n' 'circle(42.0000,43.0000,7.0000)') regions = Regions.parse(region_str, format='ds9') assert regions[0].visual['facecolor'] == 'green' assert regions[1].visual['facecolor'] == 'orange' assert regions[2].visual['facecolor'] == 'blue' def test_issue134_regression(): region_str = 'galactic; circle(+0:14:26.064,+0:00:45.206,30.400")' region = Regions.parse(region_str, format='ds9')[0] assert_quantity_allclose(region.radius, 30.4 * u.arcsec) def test_issue65_regression(): region_str = 'J2000; circle 188.5557102 12.0314056 1" # color=red' region = Regions.parse(region_str, format='ds9')[0] assert_quantity_allclose(region.center.ra, 188.5557102 * u.deg) assert_quantity_allclose(region.center.dec, 12.0314056 * u.deg) assert_quantity_allclose(region.radius, 1 * u.arcsec) def test_frame_uppercase(): """ Regression test for issue #236 (PR #237). """ region_str = ('GALACTIC\ncircle(188.5557102,12.0314056,7.1245) # ' 'text={This message has both a " and : in it} ' 'textangle=30') region = Regions.parse(region_str, format='ds9')[0] assert_quantity_allclose(region.center.l, 188.5557102 * u.deg) assert_quantity_allclose(region.center.b, 12.0314056 * u.deg) assert_quantity_allclose(region.radius, 7.1245 * u.deg) def test_pixel_angle(): """ Check whether angle in PixelRegions is a u.Quantity object. """ region_str = 'image\nbox(1.5,2,2,1,0)' region = Regions.parse(region_str, format='ds9')[0] assert isinstance(region.angle, u.Quantity) assert_quantity_allclose(region.angle, 0. * u.deg) def test_parse_formats(): region1_str = 'image\ncircle(1.5, 2, 2)' region1 = Regions.parse(region1_str, format='ds9')[0] region2_str = 'image\ncircle(1.5i, 2i, 2i)' region2 = Regions.parse(region2_str, format='ds9')[0] assert region1 == region2 region3_str = 'icrs\ncircle(1.5, 2, 2)' region3 = Regions.parse(region3_str, format='ds9')[0] region4_str = 'icrs\ncircle(1.5d, 2d, 2d)' region4 = Regions.parse(region4_str, format='ds9')[0] assert region3 == region4 region5_str = 'icrs\ncircle(1.5r, 1r, 2r)' region5 = Regions.parse(region5_str, format='ds9')[0] assert_quantity_allclose(region5.center.ra, 1.5 * u.radian) assert_quantity_allclose(region5.center.dec, 1.0 * u.radian) assert_quantity_allclose(region5.radius, 2.0 * u.radian) region6_str = 'icrs\ncircle(1:20:30, 2:3:7, 2)' region6 = Regions.parse(region6_str, format='ds9')[0] assert_quantity_allclose(region6.center.ra, 20.124999999999996 * u.deg) assert_quantity_allclose(region6.center.dec, 2.051944444444444 * u.deg) assert_quantity_allclose(region6.radius, 2. * u.deg) region7_str = 'icrs\ncircle(1h20m30s, 2d3m7s, 2d)' region7 = Regions.parse(region7_str, format='ds9')[0] assert_quantity_allclose(region7.center.ra, 20.124999999999996 * u.deg) assert_quantity_allclose(region7.center.dec, 2.051944444444444 * u.deg) assert_quantity_allclose(region7.radius, 2. * u.deg) def test_angle_serialization(): """ Regression test for issue #223 to ensure Angle arcsec inputs are correctly converted to degrees. """ region = CircleSkyRegion(SkyCoord(10, 20, unit='deg'), Angle(1, 'arcsec')) region_str = region.serialize(format='ds9', precision=6) expected = ('# Region file format: DS9 astropy/regions\nicrs\n' 'circle(10.000000,20.000000,0.000278)\n') assert region_str == expected def test_semicolon(): """ Regression test for issue #238 to allow semicolons in the text field. """ region_str = ('galactic\n' 'circle(0.003, 0.1, 206.696") # text={S17; test} color=red\n' 'circle(0.1, -0.5, 360.148") # text={S19} color=red') regions = Regions.parse(region_str, format='ds9') assert len(regions) == 2 regstr = ('galactic;' 'circle(202.4,47.2,10.9) # text={test; test; A1} color=red;' 'circle(103.8,40.6,3.6) # text="A, \'B\', ; {C}, D"' ' color=orange;' 'circle(103.8,30.6,3.6) # text="A, \'B, ; C}, D" color=orange;' 'circle(303.8,40.6,3.6) # text=\'A, "B", ; {C}, D\' color=cyan;' 'circle(303.8,40.6,3.11);' 'circle(303.8,30.6,3.6) # text=\'A, "B, ; {C, D\' color=cyan;' 'circle(503.8,40.6,3.6) # text={A, "B", ; \'C\', D}' ' color=yellow\n' 'circle(503.8,30.6,3.6) # text={A, "B, ; \'C, D} color=yellow;' '-ellipse(104.84,68.55,7.93",3.96",158.39) # dashlist=8 3' ' select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1' ' source=1 text={Ellipse;Text} background color=#0ff width=1' ' font="helvetica 10 normal italic";' "ellipse(104.84,68.55,7.93',3.96',158.39) # dashlist=8 3" ' text="hello; world;";' 'circle(1.5, 2, 2) # text={this_is_text};' 'text(151.1,51.2) # text={text ; hi ; there;} textangle=45' ' color=orange font="helvetica 12 normal roman";' 'text(151.1,51.2) # text = {text ; hi ; there;} textangle=45' ' color=orange font="helvetica 12 normal roman";' 'circle(1.5, 2, 2) # text={text="hi;world";text;"tes\'t};' "circle(1.5, 2, 2) # text={text='hi;world';text;\"tes't};") regions = Regions.parse(regstr, format='ds9') assert len(regions) == 15 def test_parser_no_metadata(): """ Regression test for issue #259 to ensure regions without metadata are parsed correctly. """ region1_str = 'galactic;circle(42,43,3)' region2_str = 'galactic;circle 42 43 3' region1 = Regions.parse(region1_str, format='ds9')[0] region2 = Regions.parse(region2_str, format='ds9')[0] assert isinstance(region1, CircleSkyRegion) assert isinstance(region2, CircleSkyRegion) assert_quantity_allclose(region1.radius, 3.0 * u.deg) assert_quantity_allclose(region2.radius, 3.0 * u.deg) def test_spaces_metadata(): """ Regression test for spaces before or after "=" in region metadata. """ regstr = ('galactic;' 'circle(202.4,47.2,10.9) # text={a; test; test; A1} color=red;' 'circle(202.4,47.2,10.9) # text ={a; test; test; A1} color= red;' 'circle(202.4,47.2,10.9) # text= {a; test; test; A1} color =red;' 'circle(202.4,47.2,10.9) # text = {a; test; test; A1}' ' color = red;') regions = Regions.parse(regstr, format='ds9') assert len(regions) == 4 for i in range(1, len(regions)): assert regions[0].visual == regions[i].visual assert regions[0].meta == regions[i].meta regstr2 = ('galactic;' 'text(151.1,51.2) # text={text ; hi ; there;} textangle= 45' ' color=orange font="helvetica 12 normal roman";' 'text(151.1,51.2) # text ={text ; hi ; there;} textangle =45' ' color=orange font="helvetica 12 normal roman";' 'text(151.1,51.2) # text= {text ; hi ; there;} textangle =45' ' color = orange font="helvetica 12 normal roman";' 'text(151.1,51.2) # text = {text ; hi ; there;} textangle=45' ' color =orange font="helvetica 12 normal roman";') regions = Regions.parse(regstr2, format='ds9') assert len(regions) == 4 for i in range(1, len(regions)): assert regions[0].visual == regions[i].visual assert regions[0].meta == regions[i].meta @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_point_boxcircle(): import matplotlib.path as mpath region_str = ('# Region file format: DS9 astropy/regions\n' 'image\n' 'point(101.000000,101.000000) # color=red\n' 'point(101.000000,201.000000) # point=boxcircle color=blue\n' 'point(101.000000,301.000000)') regions = Regions.parse(region_str, format='ds9') assert isinstance(regions[0].as_artist().get_marker(), mpath.Path) assert regions[0].as_artist().get_markeredgecolor() == 'red' assert isinstance(regions[1].visual['marker'], mpath.Path) assert regions[1].as_artist().get_markeredgecolor() == 'blue' assert isinstance(regions[2].as_artist().get_marker(), mpath.Path) assert regions[2].as_artist().get_markeredgecolor() == '#00ff00' @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_compound_color(): region_str = ('# Region file format: DS9 astropy/regions\n' 'image\n' 'annulus(651.0,301.0,60.0,90.0) # color=red') regions = Regions.parse(region_str, format='ds9') assert regions[0].as_artist().get_edgecolor() == (1., 0., 0., 1.) @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_default_mpl_kwargs(): region_str = ('# Region file format: DS9 astropy/regions\n' 'image\n' 'circle(101.0,101.0,3.0) # color=red\n' 'circle(101.0,301.0,3.0)\n' 'point(101.0,101.0) # color=red\n' 'point(101.0,301.0)\n' 'text(101.0,101.0) # text={Text} color=red\n' 'text(101.0,101.0) # text={Text}') regions = Regions.parse(region_str, format='ds9') # Patch assert regions[0].visual['default_style'] == 'ds9' assert regions[0].as_artist().get_edgecolor() == (1, 0, 0, 1) assert regions[1].as_artist().get_edgecolor() == (0, 1, 0, 1) for i in (0, 1): regions[i].visual['default_style'] = None assert regions[0].as_artist().get_edgecolor() == (1, 0, 0, 1) assert regions[1].as_artist().get_edgecolor() == (0, 0, 0, 1) # Line2D assert regions[2].visual['default_style'] == 'ds9' assert regions[2].as_artist().get_markeredgecolor() == 'red' assert regions[3].as_artist().get_markeredgecolor() == '#00ff00' for i in (2, 3): regions[i].visual['default_style'] = None assert regions[2].as_artist().get_markeredgecolor() == 'red' assert regions[3].as_artist().get_markeredgecolor() == 'C0' # Text assert regions[4].visual['default_style'] == 'ds9' assert regions[4].as_artist().get_color() == 'red' assert regions[5].as_artist().get_color() == '#00ff00' for i in (4, 5): assert regions[i].as_artist().get_va() == 'center' assert regions[i].as_artist().get_ha() == 'center' for i in (4, 5): regions[i].visual['default_style'] = None assert regions[i].as_artist().get_va() == 'baseline' assert regions[i].as_artist().get_ha() == 'left' assert regions[4].as_artist().get_color() == 'red' assert regions[5].as_artist().get_color() == 'black' def test_annulus(): region1_str = 'image\nannulus(1, 2, 3, 4)' regions1 = Regions.parse(region1_str, format='ds9') assert len(regions1) == 1 assert_equal(regions1[0].center.xy, (0, 1)) assert_equal(regions1[0].inner_radius, 3.0) assert_equal(regions1[0].outer_radius, 4.0) # multiple annuli region2_str = 'image\nannulus(1, 2, 3, 4, 5, 6)' regions2 = Regions.parse(region2_str, format='ds9') assert len(regions2) == 3 assert_equal(regions2[0].center.xy, (0, 1)) assert_equal(regions2[0].inner_radius, 3.0) assert_equal(regions2[0].outer_radius, 4.0) assert_equal(regions2[1].center.xy, (0, 1)) assert_equal(regions2[1].inner_radius, 4.0) assert_equal(regions2[1].outer_radius, 5.0) assert_equal(regions2[2].center.xy, (0, 1)) assert_equal(regions2[2].inner_radius, 5.0) assert_equal(regions2[2].outer_radius, 6.0) def test_ellipse(): region1_str = 'image\nellipse (1, 2, 3, 4, 5)' region1 = Regions.parse(region1_str, format='ds9')[0] assert_equal(region1.center.xy, (0, 1)) assert_equal(region1.width, 6.0) assert_equal(region1.height, 8.0) assert_equal(region1.angle, 5.0 * u.deg) region2_str = 'icrs\nellipse (1, 2, 3, 4, 5)' region2 = Regions.parse(region2_str, format='ds9')[0] assert_equal(region2.center.ra, 1. * u.deg) assert_equal(region2.center.dec, 2. * u.deg) assert_equal(region2.width, 6.0 * u.deg) assert_equal(region2.height, 8.0 * u.deg) assert_equal(region2.angle, 5.0 * u.deg) # elliptical annulus region3_str = 'image\nellipse (1, 2, 3, 4, 5, 6, 7)' region3 = Regions.parse(region3_str, format='ds9')[0] assert_equal(region3.center.xy, (0, 1)) assert_equal(region3.inner_width, 6.0) assert_equal(region3.inner_height, 8.0) assert_equal(region3.outer_width, 10.0) assert_equal(region3.outer_height, 12.0) assert_equal(region3.angle, 7.0 * u.deg) # multiple elliptical annuli region4_str = 'image\nellipse (1, 2, 3, 4, 5, 6, 7, 8, 9)' region4 = Regions.parse(region4_str, format='ds9') assert len(region4) == 2 assert_equal(region4[0].center.xy, (0, 1)) assert_equal(region4[0].inner_width, 6.0) assert_equal(region4[0].inner_height, 8.0) assert_equal(region4[0].outer_width, 10.0) assert_equal(region4[0].outer_height, 12.0) assert_equal(region4[0].angle, 9.0 * u.deg) assert_equal(region4[1].center.xy, (0, 1)) assert_equal(region4[1].inner_width, 10.0) assert_equal(region4[1].inner_height, 12.0) assert_equal(region4[1].outer_width, 14.0) assert_equal(region4[1].outer_height, 16.0) assert_equal(region4[1].angle, 9.0 * u.deg) def test_box(): region1_str = 'image\nbox (1, 2, 3, 4, 5)' region1 = Regions.parse(region1_str, format='ds9')[0] assert_equal(region1.center.xy, (0, 1)) assert_equal(region1.width, 3.0) assert_equal(region1.height, 4.0) assert_equal(region1.angle, 5.0 * u.deg) region2_str = 'icrs\nbox (1, 2, 3, 4, 5)' region2 = Regions.parse(region2_str, format='ds9')[0] assert_equal(region2.center.ra, 1. * u.deg) assert_equal(region2.center.dec, 2. * u.deg) assert_equal(region2.width, 3.0 * u.deg) assert_equal(region2.height, 4.0 * u.deg) assert_equal(region2.angle, 5.0 * u.deg) # box (rectangle) annulus region3_str = 'image\nbox (1, 2, 3, 4, 5, 6, 7)' region3 = Regions.parse(region3_str, format='ds9')[0] assert_equal(region3.center.xy, (0, 1)) assert_equal(region3.inner_width, 3.0) assert_equal(region3.inner_height, 4.0) assert_equal(region3.outer_width, 5.0) assert_equal(region3.outer_height, 6.0) assert_equal(region3.angle, 7.0 * u.deg) # multiple box (rectangle) annuli region4_str = 'image\nbox (1, 2, 3, 4, 5, 6, 7, 8, 9)' region4 = Regions.parse(region4_str, format='ds9') assert len(region4) == 2 assert_equal(region4[0].center.xy, (0, 1)) assert_equal(region4[0].inner_width, 3.0) assert_equal(region4[0].inner_height, 4.0) assert_equal(region4[0].outer_width, 5.0) assert_equal(region4[0].outer_height, 6.0) assert_equal(region4[0].angle, 9.0 * u.deg) assert_equal(region4[1].center.xy, (0, 1)) assert_equal(region4[1].inner_width, 5.0) assert_equal(region4[1].inner_height, 6.0) assert_equal(region4[1].outer_width, 7.0) assert_equal(region4[1].outer_height, 8.0) assert_equal(region4[1].angle, 9.0 * u.deg) def test_invalid_metadata(): # test that invalid ds9 metadata raises warnings regstr = ('# Region file format: DS9 version 4.1\n' 'global color=green dashlist=8 3 width=1 ' 'font="helvetica 10 normal roman" select=1 highlite=1 dash=1 ' 'fixed=0 edit=1 move=1 rotate=1 delete=1 include=1 source=1.1 ' 'background=0 text={hello world} fill=1.8 point=diamond 11.2 ' 'textrotate=1 textangle=34 tag={Tag 1} tag={Tag 2} line=3 1\n' 'image; circle(100.4,47.2,10.9) # color=red\n' 'image; circle(202.4,47.2,10.9) # color=blue point=junk 10\n' 'image; circle(302.4,147.2,10.9) # color=cyan point=invalid\n') match = 'is invalid and will be ignored' with pytest.warns(AstropyUserWarning, match=match) as record: regs = Regions.parse(regstr, format='ds9') assert len(record) == 12 assert 'source=1.1' in record[0].message.args[0] assert 'fill=1.8' in record[1].message.args[0] assert 'point=diamond 11.2' in record[2].message.args[0] assert 'line=3 1' in record[3].message.args[0] assert 'point=junk 10' in record[6].message.args[0] assert 'point=invalid' in record[10].message.args[0] assert len(regs) == 3 def test_unsupported_metadata(): regstr = ('image; point(335.5,415.6) # point=diamond 11 color=yellow ' 'width=2 text={example} dash=1 tag={Tag 1} tag={Tag 2}') match = 'dashed lines are unsupported' with pytest.warns(AstropyUserWarning, match=match): Regions.parse(regstr, format='ds9') def test_text_metadata(): # Regression test for issue #233: make sure that text metadata is # parsed and stored appropriately. region_str = 'image\ncircle(1.5, 2, 2) # text={this_is_text}' regions = Regions.parse(region_str, format='ds9') assert len(regions) == 1 assert regions[0].meta['text'] == 'this_is_text' rstr = 'image; circle(503.6,490.6,31.1) # color=blue text={A, {B}, C}' reg = Regions.parse(rstr, format='ds9')[0] assert reg.meta['text'] == 'A, {B' rstr = 'image; circle(503.6,490.6,31.1) # color=blue text={A, {{B}}, C}' reg = Regions.parse(rstr, format='ds9')[0] assert reg.meta['text'] == 'A, {{B' rstr = "image; circle(503.6,490.6,31.1) # color=blue text='A, 'B', C'" reg = Regions.parse(rstr, format='ds9')[0] assert reg.meta['text'] == 'A, ' rstr = 'image; circle(503.6,490.6,31.1) # color=blue text="A, "B", C"' reg = Regions.parse(rstr, format='ds9')[0] assert reg.meta['text'] == 'A, ' def test_mixed_coord(): with pytest.raises(ValueError): CirclePixelRegion(PixCoord(10, 20), Angle(1, 'arcsec')) def test_unsupported_marker(): """ Test that warning is issued when serializing a valid matplotlib marker, but unsupported by DS9. """ region = PointPixelRegion(PixCoord(2, 2), visual=RegionVisual(marker='Z')) match = 'Unable to serialize marker' with pytest.warns(AstropyUserWarning, match=match): region.serialize(format='ds9') region = PointPixelRegion(PixCoord(2, 2), visual=RegionVisual(marker='$f_{init}$')) with pytest.warns(AstropyUserWarning, match=match): region.serialize(format='ds9') def test_serialize_regularpolygon(): region = RegularPolygonPixelRegion(PixCoord(10, 10), 4, 20) poly_region = region.to_polygon() result1 = region.serialize(format='ds9') result2 = poly_region.serialize(format='ds9') assert result1 == result2 def test_serialize_empty_list(): regions = Regions([]) assert regions.serialize(format='ds9') == '' astropy-regions-f4139aa/regions/io/ds9/write.py000066400000000000000000000172101521703212700215520ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import os import warnings from copy import deepcopy from astropy.coordinates import Angle, SkyCoord from astropy.units import Quantity from astropy.utils.exceptions import AstropyUserWarning from regions.core import (CompoundPixelRegion, CompoundSkyRegion, PixCoord, PixelRegion, Region, Regions) from regions.core.registry import RegionsRegistry from regions.io.ds9.core import ds9_frame_map, ds9_shape_templates from regions.io.ds9.meta import _translate_metadata_to_ds9 from regions.shapes import RegularPolygonPixelRegion __all__ = [] @RegionsRegistry.register(Region, 'serialize', 'ds9') @RegionsRegistry.register(Regions, 'serialize', 'ds9') def _serialize_ds9(regions, precision=8): if not regions: return '' region_data = [] for region in regions: if isinstance(region, (CompoundPixelRegion, CompoundSkyRegion)): warnings.warn('Cannot serialize a compound region, skipping', AstropyUserWarning) if isinstance(region, RegularPolygonPixelRegion): region = region.to_polygon() region_data.append(_serialize_region_ds9(region, precision=precision)) # ds9 file header output = '# Region file format: DS9 astropy/regions\n' # extract common region metadata and place in the global metadata all_meta = [] for region in region_data: region_meta = deepcopy(region['meta']) region_meta.pop('tag', None) # "tag" cannot be in global metadata all_meta.append(region_meta) global_meta = dict(set.intersection(*[set(meta_dict.items()) for meta_dict in all_meta])) if global_meta: output += f'global {_make_meta_str(global_meta)}\n' # define region frame and metadata (removing items that are # defined in global metadata) frames = [] metadata = [] for region in region_data: frames.append(region['frame']) meta_tmp = region['meta'] for key in global_meta: meta_tmp.pop(key) metadata.append(meta_tmp) # extract the coordinate frame if it is identical for all regions # TODO: extract common coord frame(s) for block(s) of consecutive regions frames = set(frames) global_frame = None if len(frames) == 1: global_frame = frames.pop() output += f'{global_frame}\n' # add line for each region for region, region_meta in zip(region_data, metadata, strict=True): if global_frame is None: output += f'{region["frame"]}; ' meta_str = _make_meta_str(region_meta) if meta_str: meta_str = f' # {meta_str}' output += f'{region["region"]}{meta_str}\n' return output @RegionsRegistry.register(Region, 'write', 'ds9') @RegionsRegistry.register(Regions, 'write', 'ds9') def _write_ds9(regions, filename, *, precision=8, overwrite=False): """ Convert a list of `~regions.Region` to a DS9 string and write to a file. Parameters ---------- regions : list A list of `~regions.Region` objects. filename : str The output region filename. precision : int, optional The level of decimal precision given as the number of decimal places. overwrite : bool, optional If `True`, overwrite the output file if it exists. If `False` (default) and the output file exists, an `OSError` is raised. """ if os.path.lexists(filename) and not overwrite: raise OSError(f'{filename} already exists') output = _serialize_ds9(regions, precision=precision) with open(filename, 'w') as fh: fh.write(output) def _get_region_shape(region): shape = region.__class__.__name__.lower().replace('skyregion', '') shape = shape.replace('pixelregion', '') return shape def _get_frame_name(region, mapping): if isinstance(region, PixelRegion): frame = 'image' elif 'center' in region._params: frame = region.center.frame.name elif 'vertices' in region._params: frame = region.vertices.frame.name elif 'start' in region._params: frame = region.start.frame.name else: raise ValueError(f'Unable to get coordinate frame for {region!r}') if frame not in mapping: warnings.warn(f'Cannot serialize region with frame={frame}, skipping', AstropyUserWarning) return mapping[frame] def _make_meta_str(meta): metalist = [] for key, val in meta.items(): if key == 'tag': # can have multiple tags; value is always a list metalist.append(' '.join([f'tag={{{val}}}' for val in val])) else: metalist.append(f'{key}={val}') return ' '.join(metalist) def _get_region_params(region, shape_template, precision=8): ellipse_axes = ('width', 'height', 'inner_width', 'inner_height', 'outer_width', 'outer_height') ellipse_names = ('ellipse', 'ellipseannulus') param = {} for param_name in region._params: if param_name in ('text',): continue value = getattr(region, param_name) # DS9 ellipse parameters are serialized as semi-axis lengths, but # ellipse region is defined by full axis lengths is_ellipse = (shape_template[0] in ellipse_names and param_name in ellipse_axes) if not isinstance(value, (PixCoord, SkyCoord)) and is_ellipse: # deepcopy to prevent changing value in memory value = deepcopy(value) / 2.0 # semi-axis lengths if isinstance(value, PixCoord): # pixels; ds9's origin is (1, 1) if value.isscalar: value = (f'{value.x + 1:0.{precision}f},' f'{value.y + 1:0.{precision}f}') else: value_str = '' for val in value: value_str += (f'{val.x + 1:0.{precision}f},' f'{val.y + 1:0.{precision}f},') value = value_str[:-1] elif isinstance(value, SkyCoord): val = value.to_string(precision=precision) # polygon region has multiple SkyCoord value = ' '.join(val) if not value.isscalar else val value = value.replace(' ', ',') elif isinstance(value, Angle): value = value.to_string(unit='deg', decimal=True, precision=precision) elif isinstance(value, Quantity): # [:-4] to trim ' deg' from string end value = value.to_string(unit='deg', precision=precision)[:-4] else: value = f'{value:.{precision}f}' param[param_name] = value try: param_str = shape_template[1].format(**param) except KeyError as err: raise ValueError( f'Unable to get shape parameters for {region!r}') from err return param_str def _serialize_region_ds9(region, precision=8): frame_mapping = {v: k for k, v in ds9_frame_map.items()} frame = _get_frame_name(region, mapping=frame_mapping) shape = _get_region_shape(region) if shape not in ds9_shape_templates: warnings.warn(f'Cannot serialize region shape "{shape}", ' 'skipping', AstropyUserWarning) region_params = _get_region_params(region, ds9_shape_templates[shape], precision=precision) region_type = ds9_shape_templates[shape][0] region_str = f'{region_type}({region_params})' region_meta = _translate_metadata_to_ds9(region, shape) return {'frame': frame, 'region': region_str, 'meta': region_meta} astropy-regions-f4139aa/regions/io/fits/000077500000000000000000000000001521703212700203135ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/fits/__init__.py000066400000000000000000000002371521703212700224260ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage provides tools for reading and writing FITS Region Binary Table files. """ astropy-regions-f4139aa/regions/io/fits/connect.py000066400000000000000000000025501521703212700223200ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import warnings from astropy.io import fits from regions.core import Region, Regions from regions.core.registry import RegionsRegistry __all__ = [] @RegionsRegistry.register(Region, 'identify', 'fits') @RegionsRegistry.register(Regions, 'identify', 'fits') def is_fits(methodname, filepath): """ Identify a FITS region file. Parameters ---------- methodname : {'read', 'write'} The method name called that needs auto-identification. filepath : str The path to the file. Returns ------- result : bool Returns `True` if the given file is a FITS region file. """ all_exten = ('.fits', '.fit', '.fts', '.fits.gz', '.fit.gz', '.fts.gz') exten = {'read': all_exten, 'write': all_exten[0:3]} if methodname == 'write': return filepath.lower().endswith(exten[methodname]) elif methodname == 'read': if (isinstance(filepath, str) and filepath.lower().endswith(exten[methodname])): return True else: try: with warnings.catch_warnings(): warnings.simplefilter('ignore') with fits.open(filepath): return True except OSError: return False else: return False astropy-regions-f4139aa/regions/io/fits/core.py000066400000000000000000000026161521703212700216220ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst from regions.shapes import (CircleAnnulusPixelRegion, CirclePixelRegion, EllipseAnnulusPixelRegion, EllipsePixelRegion, PointPixelRegion, PolygonPixelRegion, RectanglePixelRegion) __all__ = [] # mappings from FITS shape to region class and column names/indices shape_map = {'point': (PointPixelRegion, ('X0', 'Y0')), 'circle': (CirclePixelRegion, ('X0', 'Y0', 'R0')), 'ellipse': (EllipsePixelRegion, ('X0', 'Y0', 'R0', 'R1', 'ROTANG0')), 'annulus': (CircleAnnulusPixelRegion, ('X0', 'Y0', 'R0', 'R1')), 'elliptannulus': (EllipseAnnulusPixelRegion, ('X0', 'Y0', 'R0', 'R1', 'R2', 'R3', 'ROTANG0')), 'box': (RectanglePixelRegion, ('X0', 'Y0', 'R0', 'R1')), 'rotbox': (RectanglePixelRegion, ('X0', 'Y0', 'R0', 'R1', 'ROTANG0')), 'rectangle': (RectanglePixelRegion, ('X0', 'X1', 'Y0', 'Y1')), 'rotrectangle': (RectanglePixelRegion, ('X0', 'X1', 'Y0', 'Y1', 'ROTANG0')), 'polygon': (PolygonPixelRegion, ('X', 'Y'))} class FITSParserError(Exception): """ A custom exception for FITS parsing errors. """ astropy-regions-f4139aa/regions/io/fits/read.py000066400000000000000000000140301521703212700215760ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import warnings import astropy.units as u import numpy as np from astropy.io import fits from astropy.table import QTable from astropy.utils.exceptions import AstropyUserWarning from regions.core import PixCoord, RegionMeta, Regions from regions.core.registry import RegionsRegistry from regions.io.fits.core import FITSParserError, shape_map __all__ = [] @RegionsRegistry.register(Regions, 'parse', 'fits') def _parse_fits(region_table): """ Parse a FITS region table. Parameters ---------- region_table : `~astropy.table.Table` The table contents of a FITS region file. Returns ------- regions : `regions.Regions` A `Regions` object containing a list of `~regions.Region` objects. """ regions = parse_table(region_table) return Regions(regions) @RegionsRegistry.register(Regions, 'read', 'fits') def _read_fits(filename, cache=False): """ Read a FITS region file, converting a FITS regions table to a list of `~regions.Region` objects. Parameters ---------- filename : str The FITS region filename. The first "REGION" FITS extension will be used. cache : bool or 'update', optional Whether to cache the contents of remote URLs. If 'update', check the remote URL for a new version but store the result in the cache. Returns ------- regions : list A list of `~regions.Region` objects. """ with fits.open(filename, cache=cache) as hdul: for hdu in hdul: # use the first 'REGION' HDU if hdu.name == 'REGION': region_table = QTable.read(hdu) regions = _parse_fits(region_table) return regions raise FITSParserError('An extension with name (EXTNAME) "REGION" ' 'was not found') def get_shape(region_row): include = 1 shape_key = 'SHAPE' if shape_key not in region_row.colnames: shape = 'point' return shape, include # Strip surrounding whitespace before matching the shape name. FITS # character columns are fixed-width and space-padded, and some writers # (or cross-platform/encoding quirks) leave trailing whitespace on the # SHAPE value, which would otherwise fail the ``valid_shapes`` check # below and raise a spurious FITSParserError (see GH #637). Using # ``startswith`` instead of indexing also avoids an IndexError when the # value is empty after stripping. shape = region_row[shape_key].strip().lower() if shape.startswith('!'): include = 0 shape = shape[1:] supported_shapes = list(shape_map.keys()) unsupported_shapes = ['pie', 'sector', 'diamond', 'rhombus', 'rotdiamond', 'rotrhombus'] valid_shapes = supported_shapes + unsupported_shapes if shape not in valid_shapes: raise FITSParserError(f'{shape!r} is not a valid FITS region shape') if shape not in supported_shapes: warnings.warn(f'{shape!r} is not supported by the regions package, ' 'skipping.', AstropyUserWarning) shape = None return shape, include def get_column_values(region_row, colname): index = None if colname[-1].isdigit(): index = int(colname[-1]) colname = colname[:-1] value = np.atleast_1d(region_row[colname]) if isinstance(value, u.Quantity) and value.unit == u.pixel: value = value.value # remove pixel units if index is None: # polygon uses all values return value try: return value[index] except IndexError as exc: raise FITSParserError(f'The {colname!r} column must have more ' f'than {index!r} values for the ' 'region.') from exc def get_shape_params(shape, region_row, shape_columns): values = [get_column_values(region_row, column) for column in shape_columns] if 'rectangle' in shape: (xmin, xmax, ymin, ymax) = values[0:4] xcenter = 0.5 * (xmin + xmax) ycenter = 0.5 * (ymin + ymax) xsize = xmax - xmin ysize = ymax - ymin shape_params = [PixCoord(xcenter, ycenter), xsize, ysize] if shape == 'rotrectangle': shape_params.append(values[-1]) # angle return shape_params # center (or polygon) coordinates for all other regions shape_params = [PixCoord(values[0], values[1])] # shape params if shape == 'ellipse': # FITS uses semi-axis lengths; # the last value is always the rotation angle values[2:-1] = list(np.array(values[2:-1]) * 2.) shape_params.extend(values[2:]) return shape_params def parse_row(region_row): shape, include = get_shape(region_row) if shape is None: return None region_cls, shape_columns = shape_map[shape] for column in shape_columns: if column[-1].isdigit(): column = column[:-1] if column not in region_row.colnames: warnings.warn(f'Table columns are missing for {shape!r} shape, ' 'skipping.', AstropyUserWarning) return None shape_params = get_shape_params(shape, region_row, shape_columns) region = region_cls(*shape_params) meta = {} if include == 0: meta = {'include': include} region.meta = RegionMeta(meta) shape_key = 'COMPONENT' if shape_key in region_row.colnames: component = int(region_row[shape_key]) meta = {'component': component} if meta: region.meta = RegionMeta(meta) return region def parse_table(region_table): valid_columns = ('X', 'Y', 'SHAPE', 'R', 'ROTANG', 'COMPONENT') for column in region_table.colnames: if column not in valid_columns: raise FITSParserError(f'{column!r} is not a valid column name') regions = [] for row in region_table: region = parse_row(row) if region is not None: regions.append(region) return regions astropy-regions-f4139aa/regions/io/fits/tests/000077500000000000000000000000001521703212700214555ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/fits/tests/__init__.py000066400000000000000000000000001521703212700235540ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/fits/tests/data/000077500000000000000000000000001521703212700223665ustar00rootroot00000000000000astropy-regions-f4139aa/regions/io/fits/tests/data/regions_nowcs.fits000066400000000000000000000264001521703212700261360ustar00rootroot00000000000000SIMPLE = T / conforms to FITS standard BITPIX = 8 / array data type NAXIS = 0 / number of array dimensions EXTEND = T END XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / array data type NAXIS = 2 / number of array dimensions NAXIS1 = 149 / length of dimension 1 NAXIS2 = 11 / length of dimension 2 PCOUNT = 0 / number of group parameters GCOUNT = 1 / number of groups TFIELDS = 5 / number of table fields EXTNAME = 'REGION ' EXTVER = 1 EXTLEVEL= 1 HDUNAME = 'REGION ' HDUCLASS= 'ASC ' HDUCLAS1= 'REGION ' HDUCLAS2= 'STANDARD' HDUVERS = '1.2.0 ' HDUDOC = 'ASC-FITS-REGION-1.2: Rots, McDowell: FITS REGION Binary Table Design'CONTENT = 'REGION ' ORIGIN = 'astropy/regions' TTYPE1 = 'SHAPE ' TFORM1 = '13A ' TTYPE2 = 'X ' TFORM2 = '6D ' TUNIT2 = 'pix ' TDIM2 = '(6) ' TTYPE3 = 'Y ' TFORM3 = '6D ' TUNIT3 = 'pix ' TDIM3 = '(6) ' TTYPE4 = 'R ' TFORM4 = '4D ' TUNIT4 = 'pix ' TDIM4 = '(4) ' TTYPE5 = 'ROTANG ' TFORM5 = 'D ' TUNIT5 = 'deg ' END circle@n@`@Opoint@X@ellipse@rfffff@C33333@B@@Rffffff@.333333annulus@y33333@}@5fffff@OLelliptannulus@k33333@@E33333@U@Z@j@ufffffbox@c@ @b@Rrotbox@y33333@~@b@R@C33333rectangle@Vffffff@m@lL@rrotrectangle@w(@:fffff@mvfffff@sa@=fffffpolygon@ @@@@fffff@ @s33333@vK33333@s@m<@h@n)!circle@n@`@Oastropy-regions-f4139aa/regions/io/fits/tests/data/regions_wcs.fits000066400000000000000000000264001521703212700256010ustar00rootroot00000000000000SIMPLE = T / conforms to FITS standard BITPIX = 8 / array data type NAXIS = 0 / number of array dimensions EXTEND = T END XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / array data type NAXIS = 2 / number of array dimensions NAXIS1 = 119 / length of dimension 1 NAXIS2 = 9 / length of dimension 2 PCOUNT = 0 / number of group parameters GCOUNT = 1 / number of groups TFIELDS = 6 / number of table fields EXTNAME = 'REGION ' EXTVER = 1 EXTLEVEL= 1 HDUNAME = 'REGION ' HDUCLASS= 'ASC ' HDUCLAS1= 'REGION ' HDUCLAS2= 'STANDARD' HDUVERS = '1.2.0 ' HDUDOC = 'ASC-FITS-REGION-1.2: Rots, McDowell: FITS REGION Binary Table Design'CONTENT = 'REGION ' TELESCOP= 'CHANDRA ' DATAMODE= 'FAINT ' INSTRUME= 'ACIS ' OBJECT = 'M101 ' ONTIME = 99514.55727793276 DEADC = 0.98733741 DEADAPP = F EXPOSURE= 98254.44528487051 LIVETIME= 98254.44528487051 MJDREF = 50814.0 TIMEREF = 'LOCAL ' USER = 'pence ' FILIN001= 'acisf00934N002_evt2.fits' CREATOR = 'extractor v4.84' DATE = '2008-06-20T16:49:07' ORIGIN = 'NASA/GSFC' MTYPE1 = 'sky ' MFORM1 = 'x,y ' MTYPE2 = 'EQPOS ' MFORM2 = 'RA,DEC ' TCTYP1 = 'RA---TAN' TCRPX1 = 4096.5 TDRPX1 = 4096.5 TCRVL1 = 210.7801525309 TCDLT1 = -0.00013666666666667 TDDLT1 = -0.00013666666666667 TCTYP2 = 'DEC--TAN' TCRPX2 = 4096.5 TDRPX2 = 4096.5 TCRVL2 = 54.366791304488 TCDLT2 = 0.00013666666666667 TDDLT2 = 0.00013666666666667 TCROT2 = 0.0 CHECKSUM= 'ZmaGdkTDZkYDdkYD' DATASUM = '3203022982' TTYPE1 = 'X ' TFORM1 = '4D ' TUNIT1 = 'pix ' TTYPE2 = 'Y ' TFORM2 = '4D ' TUNIT2 = 'pix ' TTYPE3 = 'SHAPE ' TFORM3 = '7A ' TTYPE4 = 'R ' TFORM4 = '4D ' TUNIT4 = 'pix ' TTYPE5 = 'ROTANG ' TFORM5 = 'D ' TUNIT5 = 'deg ' TTYPE6 = 'COMPONENT' TFORM6 = 'K ' DATEREF = '1998-01-01' END @@circle@wߋq @ b@eROTBOX@]ֻ@x!@ 0 tempfile = tmpdir.join('tmp.fits').strpath regions.write(tempfile, format='fits', overwrite=True) regions2 = Regions.read(tempfile, format='fits') for reg1, reg2 in zip(regions, regions2, strict=True): assert_region_allclose(reg1, reg2) def test_only_pixel_regions(): reg_sky = CircleSkyRegion(SkyCoord(1, 2, unit='deg'), 5 * u.deg) reg = Regions([reg_sky]) match = 'Sky regions cannot be serialized' with pytest.warns(AstropyUserWarning, match=match): result = reg.serialize(format='fits') assert len(result) == 0 reg_pix = CirclePixelRegion(PixCoord(10, 10), 5) reg = Regions([reg_sky, reg_pix]) match = 'Sky regions cannot be serialized' with pytest.warns(AstropyUserWarning, match=match): result = reg.serialize(format='fits') assert len(result) == 1 def test_valid_columns(): t = QTable([[1, 2, 3]], names=('a')) match = "'a' is not a valid column name" with pytest.raises(FITSParserError, match=match): Regions.parse(t, format='fits') def test_invalid_regions(): center = PixCoord(42, 43) region1 = RectangleAnnulusPixelRegion(center, 4.2, 5.2, 7.2, 8.2, angle=15 * u.deg) region2 = LinePixelRegion(start=center, end=PixCoord(x=52, y=53)) region3 = TextPixelRegion(center, text='Example Text') regions = (region1, region2, region3) match = 'cannot be serialized using the FITS format' for reg in regions: with pytest.warns(AstropyUserWarning, match=match): _ = reg.serialize(format='fits') def test_valid_row(): x = [1] * u.pix y = [2] * u.pix shapes = ['CIRCLE'] tbl = QTable([x, y, shapes], names=('X', 'Y', 'SHAPE')) match = 'Table columns are missing' with pytest.warns(AstropyUserWarning, match=match): Regions.parse(tbl, format='fits') # test invalid shape shapes = ['INVALID'] tbl2 = QTable([x, y, shapes], names=('X', 'Y', 'SHAPE')) match = "'invalid' is not a valid FITS region shape" with pytest.raises(FITSParserError, match=match): Regions.parse(tbl2, format='fits') shapes = ['PIE'] rotang = [[20, 30]] * u.deg tbl3 = QTable([x, y, shapes, rotang], names=('X', 'Y', 'SHAPE', 'ROTANG')) match = "'pie' is not supported" with pytest.warns(AstropyUserWarning, match=match): Regions.parse(tbl3, format='fits') def test_shape_with_whitespace(): # FITS character columns are fixed-width and space-padded; some writers # (e.g. CSCView output, or cross-platform/encoding quirks) leave trailing # whitespace on the SHAPE value. Such a value must still be recognized # instead of raising a spurious FITSParserError (see GH #637). x = [1] * u.pix y = [2] * u.pix r = [3] * u.pix tbl = QTable([x, y, r, ['circle ']], names=('X', 'Y', 'R', 'SHAPE')) regions = Regions.parse(tbl, format='fits') assert len(regions) == 1 assert isinstance(regions[0], CirclePixelRegion) assert_equal(regions[0].center.x, 1) assert_equal(regions[0].radius, 3) # The exclude prefix ('!') must still be honored after stripping. tbl_excl = QTable([x, y, r, [' !circle ']], names=('X', 'Y', 'R', 'SHAPE')) regions = Regions.parse(tbl_excl, format='fits') assert len(regions) == 1 assert regions[0].meta.get('include') == 0 def test_components(): center = PixCoord(10, 10) regions = Regions((CirclePixelRegion(center, 3), CirclePixelRegion(center, 5), CirclePixelRegion(center, 7), CirclePixelRegion(center, 9))) tbl1 = regions.serialize(format='fits') assert 'COMPONENT' not in tbl1.colnames components = [None, None, None, None] for region, component in zip(regions, components, strict=True): region.meta = RegionMeta({'component': component}) tbl2 = regions.serialize(format='fits') assert 'COMPONENT' not in tbl2.colnames components = np.arange(4) for region, component in zip(regions, components, strict=True): region.meta = RegionMeta({'component': component}) tbl3 = regions.serialize(format='fits') assert 'COMPONENT' in tbl3.colnames assert_equal(tbl3['COMPONENT'], components) components = [1, 2, None, 4] for region, component in zip(regions, components, strict=True): region.meta = RegionMeta({'component': component}) tbl4 = regions.serialize(format='fits') assert 'COMPONENT' in tbl4.colnames assert_equal(tbl4['COMPONENT'], [1, 2, 5, 4]) astropy-regions-f4139aa/regions/io/fits/write.py000066400000000000000000000137021521703212700220220ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import warnings from dataclasses import dataclass import astropy.units as u import numpy as np from astropy.io import fits from astropy.table import QTable from astropy.utils.exceptions import AstropyUserWarning from regions.core import Region, Regions, SkyRegion from regions.core.registry import RegionsRegistry from regions.shapes import RegularPolygonPixelRegion __all__ = [] @RegionsRegistry.register(Region, 'serialize', 'fits') @RegionsRegistry.register(Regions, 'serialize', 'fits') def _serialize_fits(regions): region_data = [] for region in regions: if isinstance(region, SkyRegion): warnings.warn('Sky regions cannot be serialized to the FITS ' 'region format, skipping.', AstropyUserWarning) continue regdata = _serialize_region_fits(region) if regdata is not None: region_data.append(regdata) if region_data: fits_table = _make_table(region_data) return fits_table return QTable() @RegionsRegistry.register(Region, 'write', 'fits') @RegionsRegistry.register(Regions, 'write', 'fits') def _write_fits(regions, filename, header=None, overwrite=False): """ Convert a list of `~regions.Region` to a FITS region table and write to a file. Parameters ---------- regions : list A list of `~regions.Region` objects. filename : str The filename in which the table is to be written. header : `~astropy.io.fits.Header`, optional The FITS header. overwrite : bool, optional If True, overwrite the output file if it exists. Raises an `OSError` if False and the output file exists. Default is False. """ output = _serialize_fits(regions) if header is None: hdudoc = ('ASC-FITS-REGION-1.2: Rots, McDowell: FITS REGION ' 'Binary Table Design') header = dict([('EXTNAME', 'REGION'), ('EXTVER', 1), ('EXTLEVEL', 1), ('HDUNAME', 'REGION'), ('HDUCLASS', 'ASC'), ('HDUCLAS1', 'REGION'), ('HDUCLAS2', 'STANDARD'), ('HDUVERS', '1.2.0'), ('HDUDOC', hdudoc), ('CONTENT', 'REGION'), ('ORIGIN', 'astropy/regions')]) bin_table = fits.BinTableHDU(data=output, header=header) bin_table.writeto(filename, overwrite=overwrite) @dataclass class _RegionData: """ Class to hold region data. """ shape: str x: np.ndarray y: np.ndarray r: np.ndarray rotang: np.ndarray component: int def _serialize_region_fits(region): if isinstance(region, RegularPolygonPixelRegion): region = region.to_polygon() region_clsname = region.__class__.__name__ unsupported_regions = ('RectangleAnnulusPixelRegion', 'LinePixelRegion', 'TextPixelRegion', 'CompoundPixelRegion') if region_clsname in unsupported_regions: warnings.warn(f'({region_clsname} cannot be serialized using the ' 'FITS format, skipping.', AstropyUserWarning) return None # translate region class to FITS shape name shape = region_clsname.lower().replace('pixelregion', '') if region.meta.get('include', None) == 0: shape = f'!{shape}' region_map = {'circleannulus': 'annulus', 'ellipseannulus': 'elliptannulus', 'rectangle': 'rotbox'} if shape in region_map: shape = region_map[shape] shape_params = [] rotang = None for param in region._params: value = getattr(region, param) if param in ('center', 'vertices'): x, y = value.xy elif param == 'angle': rotang = value else: # ellipse region is defined by full axis lengths, but # FITS regions file uses semi-axis lengths if shape == 'ellipse': value /= 2.0 shape_params.append(value) if not shape_params: shape_params = 0 if rotang is None: rotang = u.Quantity(0, 'deg') component = region.meta.get('component', None) return _RegionData(shape, np.atleast_1d(x), np.atleast_1d(y), np.atleast_1d(shape_params), np.atleast_1d(rotang), component) def _make_column(arrays): arr_sizes = [arr.size for arr in arrays] arr_size = np.max(arr_sizes) data = [] for (arr, size) in zip(arrays, arr_sizes, strict=True): pad_width = arr_size - size if pad_width != 0: arr = np.pad(arr, (0, pad_width), mode='constant') if not isinstance(arr[0], u.Quantity): arr <<= u.pix if arr_size == 1 and pad_width == 0: arr = arr[0] # use scalars instead of len-1 arrays data.append(arr) data = u.Quantity(data) return data def _define_components(region_data): components = np.array([regdata.component for regdata in region_data]) none_idx = [i for i, item in enumerate(components) if item is None] if not none_idx: return components comps = [i for i in components if i is not None] if comps: start_component = np.max(comps) + 1 components[none_idx] = np.arange(len(none_idx)) + start_component else: # all components are set to None - do not write a COMPONENT # column components = None return components def _make_table(region_data): tbl = QTable() attrs = ('shape', 'x', 'y', 'r', 'rotang') for attr in attrs: arrays = [getattr(data, attr) for data in region_data] if attr not in ('shape',): arrays = _make_column(arrays) tbl[attr.upper()] = arrays components = _define_components(region_data) if components is not None: tbl['COMPONENT'] = components return tbl astropy-regions-f4139aa/regions/shapes/000077500000000000000000000000001521703212700202225ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/__init__.py000066400000000000000000000010221521703212700223260ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage defines region shapes. """ from .annulus import * # noqa: F401, F403 from .circle import * # noqa: F401, F403 from .ellipse import * # noqa: F401, F403 from .line import * # noqa: F401, F403 from .lune import * # noqa: F401, F403 from .point import * # noqa: F401, F403 from .polygon import * # noqa: F401, F403 from .range import * # noqa: F401, F403 from .rectangle import * # noqa: F401, F403 from .text import * # noqa: F401, F403 astropy-regions-f4139aa/regions/shapes/annulus.py000066400000000000000000001356611521703212700222750ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines annulus regions in both pixel and sky coordinates. """ import abc import math import operator import astropy.units as u import numpy as np from regions._utils.wcs_helpers import (pixel_shape_to_sky_svd, pixel_to_sky_mean_scale, pixel_to_sky_svd_scales, sky_shape_to_pixel_svd, sky_to_pixel_mean_scale, sky_to_pixel_svd_scales) from regions.core.attributes import (PositiveScalar, PositiveScalarAngle, RegionMetaDescr, RegionVisualDescr, ScalarAngle, ScalarPixCoord, ScalarSkyCoord) from regions.core.compound import (CompoundPixelRegion, CompoundSphericalSkyRegion) from regions.core.core import PixelRegion, SkyRegion, SphericalSkyRegion from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord from regions.shapes.circle import CirclePixelRegion, CircleSphericalSkyRegion from regions.shapes.ellipse import EllipsePixelRegion, EllipseSkyRegion from regions.shapes.rectangle import RectanglePixelRegion, RectangleSkyRegion __all__ = ['AnnulusPixelRegion', 'AnnulusSphericalSkyRegion', 'AsymmetricAnnulusPixelRegion', 'AsymmetricAnnulusSkyRegion', 'CircleAnnulusPixelRegion', 'CircleAnnulusSkyRegion', 'CircleAnnulusSphericalSkyRegion', 'EllipseAnnulusPixelRegion', 'EllipseAnnulusSkyRegion', 'RectangleAnnulusPixelRegion', 'RectangleAnnulusSkyRegion'] class AnnulusPixelRegion(PixelRegion, abc.ABC): """ A base class for annulus pixel regions. """ @property def _compound_region(self): return CompoundPixelRegion(self._inner_region, self._outer_region, operator.xor, self.meta, self.visual) @property def area(self): return self._outer_region.area - self._inner_region.area @property def bounding_box(self): return self._outer_region.bounding_box def contains(self, pixcoord): return self._compound_region.contains(pixcoord) def covers(self, pixcoord): outer_cov = self._outer_region.covers(pixcoord) inner_con = self._inner_region.contains(pixcoord) return np.logical_and(outer_cov, np.logical_not(inner_con)) def as_artist(self, origin=(0, 0), **kwargs): return self._compound_region.as_artist(origin, **kwargs) def to_mask(self, mode='center', subpixels=5): return self._compound_region.to_mask(mode, subpixels) def rotate(self, center, angle): """ Rotate the region. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `~regions.PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- region : `~regions.AnnulusPixelRegion` The rotated region (which is an independent copy). """ changes = {} changes['center'] = self.center.rotate(center, angle) if hasattr(self, 'angle'): changes['angle'] = self.angle + angle return self.copy(**changes) class AnnulusSphericalSkyRegion(SphericalSkyRegion, abc.ABC): """ A base class for spherical sky annulus regions. """ @property def _compound_region(self): return CompoundSphericalSkyRegion( self._inner_region, self._outer_region, operator.xor, self.meta, self.visual) @property def frame(self): return self._inner_region.frame @property def bounding_circle(self): return self._outer_region.bounding_circle @property def bounding_lonlat(self): # Bounding lonlat of inner, outer regions # Check of inner region goes over the pole so # a more limited lat range is needed lons_arr, lats_arr = self._outer_region.bounding_lonlat # Check if shape covers either pole & modify lats arr accordingly, # accounting for annular geometry: lons_arr, lats_arr = self._validate_lonlat_bounds( lons_arr, lats_arr, inner_region=self._inner_region, ) return lons_arr, lats_arr def contains(self, coord): return self._compound_region.contains(coord) def covers(self, coord): return self._compound_region.covers(coord) class CircleAnnulusPixelRegion(AnnulusPixelRegion): """ A circular annulus in pixel coordinates. Parameters ---------- center : `~regions.PixCoord` The position of the center of the annulus. inner_radius : float The inner radius of the annulus in pixels. outer_radius : float The outer radius of the annulus in pixels. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from regions import CircleAnnulusPixelRegion, PixCoord fig, ax = plt.subplots() reg = CircleAnnulusPixelRegion(PixCoord(x=6, y=6), inner_radius=5.5, outer_radius=8.0) patch = reg.plot(ax=ax, facecolor='none', edgecolor='red', lw=2, label='Circle Annulus') ax.legend(handles=(patch,), loc='upper center') ax.set_xlim(-5, 20) ax.set_ylim(-5, 20) ax.set_aspect('equal') """ _component_class = CirclePixelRegion _params = ('center', 'inner_radius', 'outer_radius') center = ScalarPixCoord('The center pixel position as a |PixCoord|.') inner_radius = PositiveScalar('The inner radius in pixels as a float.') outer_radius = PositiveScalar('The outer radius in pixels as a float.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, inner_radius, outer_radius, meta=None, visual=None): self.center = center self.inner_radius = inner_radius self.outer_radius = outer_radius self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() if inner_radius >= outer_radius: raise ValueError('outer_radius must be greater than inner_radius') @property def _inner_region(self): return self._component_class(self.center, self.inner_radius, self.meta, self.visual) @property def _outer_region(self): return self._component_class(self.center, self.outer_radius, self.meta, self.visual) def to_sky(self, wcs, *, as_ellipse=False): """ Return a sky region from this pixel region. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`). as_ellipse : bool, optional If `True`, return an `~regions.EllipseAnnulusSkyRegion` instead of a `~regions.CircleAnnulusSkyRegion`. An ellipse annulus is generally a better approximation when the WCS has distortions or different pixel scales along different axes. Default is `False`. Returns ------- region : `~regions.CircleAnnulusSkyRegion` or \ `~regions.EllipseAnnulusSkyRegion` The sky region. An ellipse annulus is returned if ``as_ellipse`` is `True`. """ if as_ellipse: center, scale_major, scale_minor, angle = pixel_to_sky_svd_scales( (self.center.x, self.center.y), wcs) inner_width = 2 * self.inner_radius * scale_major * u.arcsec outer_width = 2 * self.outer_radius * scale_major * u.arcsec inner_height = 2 * self.inner_radius * scale_minor * u.arcsec outer_height = 2 * self.outer_radius * scale_minor * u.arcsec # The helper returns a position angle (PA) from North; # regions measures the angle from the RA axis (90 deg # offset). angle = (angle + 90 * u.deg).wrap_at(360 * u.deg) return EllipseAnnulusSkyRegion( center, inner_width, outer_width, inner_height, outer_height, angle=angle, meta=self.meta.copy(), visual=self.visual.copy()) center, mean_scale = pixel_to_sky_mean_scale( (self.center.x, self.center.y), wcs) inner_radius = self.inner_radius * mean_scale * u.arcsec outer_radius = self.outer_radius * mean_scale * u.arcsec return CircleAnnulusSkyRegion(center, inner_radius, outer_radius, self.meta.copy(), self.visual.copy()) def to_polygon(self, *, n_vertices=100): """ Return a `~regions.CompoundPixelRegion` of two `~regions.PolygonPixelRegion` objects that approximates this annulus. Parameters ---------- n_vertices : int, optional The number of polygon vertices for each circle. Default is 100. Returns ------- polygon : `~regions.CompoundPixelRegion` A compound region of two polygon regions approximating the annulus. """ inner_polygon = self._inner_region.to_polygon(n_vertices=n_vertices) outer_polygon = self._outer_region.to_polygon(n_vertices=n_vertices) return CompoundPixelRegion(inner_polygon, outer_polygon, operator.xor, self.meta.copy(), self.visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform( wcs, boundary_distortions) if boundary_distortions: # Requires planar to spherical projection (using WCS) # and discretization # Will require implementing discretization in pixel space # to get correct handling of distortions. raise NotImplementedError # ### Potential solution: # # Leverage polygon class to_spherical_sky() functionality without # # distortions, as the distortions were already # # computed in creating # # that polygon approximation # return self.discretize_boundary( # n_vertices=n_vertices).to_spherical_sky( # wcs=wcs, boundary_distortions=False # ) return self.to_sky(wcs).to_spherical_sky() class CircleAnnulusSkyRegion(SkyRegion): """ A circular annulus in sky coordinates. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The position of the center of the annulus. inner_radius : `~astropy.units.Quantity` The inner radius of the annulus in angular units. outer_radius : `~astropy.units.Quantity` The outer radius of the annulus in angular units. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('center', 'inner_radius', 'outer_radius') center = ScalarSkyCoord('The center position as a |SkyCoord|.') inner_radius = PositiveScalarAngle('The inner radius as a |Quantity| ' 'angle.') outer_radius = PositiveScalarAngle('The outer radius as a |Quantity| ' 'angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, inner_radius, outer_radius, meta=None, visual=None): self.center = center self.inner_radius = inner_radius self.outer_radius = outer_radius self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() if inner_radius >= outer_radius: raise ValueError('outer_radius must be greater than inner_radius') def to_pixel(self, wcs, *, as_ellipse=False): """ Return a pixel region from this sky region. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`). as_ellipse : bool, optional If `True`, return an `~regions.EllipseAnnulusPixelRegion` instead of a `~regions.CircleAnnulusPixelRegion`. An ellipse annulus is generally a better approximation when the WCS has distortions or different pixel scales along different axes. Default is `False`. Returns ------- region : `~regions.CircleAnnulusPixelRegion` or \ `~regions.EllipseAnnulusPixelRegion` The pixel region. An ellipse annulus is returned if ``as_ellipse`` is `True`. """ if as_ellipse: center, scale_major, scale_minor, angle = sky_to_pixel_svd_scales( self.center, wcs) inner_radius_arcsec = self.inner_radius.to_value(u.arcsec) outer_radius_arcsec = self.outer_radius.to_value(u.arcsec) inner_width = 2 * inner_radius_arcsec * scale_major outer_width = 2 * outer_radius_arcsec * scale_major inner_height = 2 * inner_radius_arcsec * scale_minor outer_height = 2 * outer_radius_arcsec * scale_minor return EllipseAnnulusPixelRegion( PixCoord(*center), inner_width, outer_width, inner_height, outer_height, angle=angle, meta=self.meta.copy(), visual=self.visual.copy()) center, mean_scale = sky_to_pixel_mean_scale(self.center, wcs) inner_radius = self.inner_radius.to_value(u.arcsec) * mean_scale outer_radius = self.outer_radius.to_value(u.arcsec) * mean_scale return CircleAnnulusPixelRegion(PixCoord(*center), inner_radius, outer_radius, meta=self.meta.copy(), visual=self.visual.copy()) def to_polygon(self, wcs, *, n_vertices=100): """ Return a `~regions.CompoundSkyRegion` of two `~regions.PolygonSkyRegion` objects that approximates this annulus. Parameters ---------- wcs : `~astropy.wcs.WCS` The WCS to use for the sky-to-pixel-to-sky conversion. n_vertices : int, optional The number of polygon vertices for each circle. Default is 100. Returns ------- polygon : `~regions.CompoundSkyRegion` A compound region of two polygon regions approximating the annulus. """ return self.to_pixel(wcs).to_polygon(n_vertices=n_vertices).to_sky(wcs) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform( wcs, boundary_distortions) if boundary_distortions: # Requires planar to spherical projection (using WCS) # and discretization # Will require implementing discretization in pixel space # to get correct handling of distortions. raise NotImplementedError # ### Potential solution: # # Leverage polygon class to_spherical_sky() functionality without # # distortions, as the distortions were already # # computed in creating # # that polygon approximation # return self.to_pixel(wcs) # .discretize_boundary(n_vertices=n_vertices) # .to_spherical_sky( # wcs=wcs, boundary_distortions=False # ) return CircleAnnulusSphericalSkyRegion( self.center, self.inner_radius, self.outer_radius, self.meta.copy(), self.visual.copy(), ) class CircleAnnulusSphericalSkyRegion(AnnulusSphericalSkyRegion): """ Class for a circular annulus sky region, where the circular annulus is interpreted within a spherical geometry reference frame. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The center position. inner_radius : `~astropy.units.Quantity` The inner radius in angular units. outer_radius : `~astropy.units.Quantity` The outer radius in angular units. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _component_class = CircleSphericalSkyRegion _params = ('center', 'inner_radius', 'outer_radius') center = ScalarSkyCoord('The center position as a |SkyCoord|.') inner_radius = PositiveScalarAngle('The inner radius as a |Quantity| ' 'angle.') outer_radius = PositiveScalarAngle('The outer radius as a |Quantity| ' 'angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, inner_radius, outer_radius, meta=None, visual=None): self.center = center self.inner_radius = inner_radius self.outer_radius = outer_radius self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() if inner_radius >= outer_radius: raise ValueError('outer_radius must be greater than inner_radius') @property def _inner_region(self): return self._component_class(self.center, self.inner_radius, self.meta, self.visual) @property def _outer_region(self): return self._component_class(self.center, self.outer_radius, self.meta, self.visual) def transform_to(self, frame, merge_attributes=True): frame = self._validate_frame_transformation(frame) # Only center transforms, radii preserved center_transf = self.center.transform_to( frame, merge_attributes=merge_attributes) return CircleAnnulusSphericalSkyRegion( center_transf, self.inner_radius.copy(), self.outer_radius.copy(), self.meta.copy(), self.visual.copy(), ) def discretize_boundary(self, *, n_vertices=100): return CompoundSphericalSkyRegion( self._inner_region.discretize_boundary(n_vertices=n_vertices), self._outer_region.discretize_boundary(n_vertices=n_vertices), operator=operator.xor, meta=self.meta.copy(), visual=self.visual.copy(), ) def to_polygon(self, *, n_vertices=100): """ Return a `~regions.CompoundSphericalSkyRegion` of two `~regions.PolygonSphericalSkyRegion` objects that approximates this annulus. Parameters ---------- n_vertices : int, optional The number of polygon vertices for each circle. Default is 100. Returns ------- polygon : `~regions.CompoundSphericalSkyRegion` A compound region of two polygon regions approximating the annulus. """ return self.discretize_boundary(n_vertices=n_vertices) def to_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform( wcs, boundary_distortions) if boundary_distortions: # Requires spherical to planar projection (from WCS) # and discretization # Use to_pixel(), then apply "small angle approx" to get planar # sky. return self.to_pixel( boundary_distortions=boundary_distortions, wcs=wcs, n_vertices=n_vertices, ).to_sky(wcs) return CircleAnnulusSkyRegion( self.center.copy(), self.inner_radius.copy(), self.outer_radius.copy(), meta=self.meta.copy(), visual=self.visual.copy(), ) def to_pixel(self, wcs, *, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform( wcs, boundary_distortions) if boundary_distortions: from .polygon import PolygonPixelRegion # Requires spherical to planar projection (from WCS) and # discretization disc_kwargs = ( {} if n_vertices is None else {'n_vertices': n_vertices}) polygonized = self.discretize_boundary(**disc_kwargs) inner_vertices = wcs.world_to_pixel(polygonized.region1.vertices) outer_vertices = wcs.world_to_pixel(polygonized.region2.vertices) return CompoundPixelRegion( PolygonPixelRegion(PixCoord(*inner_vertices)), PolygonPixelRegion(PixCoord(*outer_vertices)), operator=operator.xor, meta=self.meta.copy(), visual=self.visual.copy(), ) return self.to_sky().to_pixel(wcs) class AsymmetricAnnulusPixelRegion(AnnulusPixelRegion): """ Helper class for asymmetric annuli sky regions. Used for ellipse and rectangle pixel annulus regions. Parameters ---------- center : `~regions.PixCoord` The position of the center of the annulus. center : `~regions.PixCoord` The position of the center of the annulus. inner_width : float The inner width of the annulus (before rotation) in pixels. outer_width : float The outer width of the annulus (before rotation) in pixels. inner_height : float The inner height of the annulus (before rotation) in pixels. outer_height : float The outer height of the annulus (before rotation) in pixels. angle : `~astropy.units.Quantity`, optional The rotation angle of the annulus, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the x axis. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('center', 'inner_width', 'outer_width', 'inner_height', 'outer_height', 'angle') center = ScalarPixCoord('The center pixel position as a |PixCoord|.') inner_width = PositiveScalar('The inner width (before rotation) in ' 'pixels as a float.') outer_width = PositiveScalar('The outer width (before rotation) in ' 'pixels as a float.') inner_height = PositiveScalar('The inner height (before rotation) in ' 'pixels as a float.') outer_height = PositiveScalar('The outer height (before rotation) in ' 'pixels as a float.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a ' '|Quantity| angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, inner_width, outer_width, inner_height, outer_height, angle=0 * u.deg, meta=None, visual=None): self.center = center self.inner_width = inner_width self.outer_width = outer_width self.inner_height = inner_height self.outer_height = outer_height self.angle = angle self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() if inner_width >= outer_width: raise ValueError('outer_width must be greater than inner_width') if inner_height >= outer_height: raise ValueError('outer_height must be greater than inner_height') @property def _inner_region(self): return self._component_class(self.center, self.inner_width, self.inner_height, self.angle, self.meta, self.visual) @property def _outer_region(self): return self._component_class(self.center, self.outer_width, self.outer_height, self.angle, self.meta, self.visual) def to_sky_args(self, wcs): # The photutils helpers measure the sky rotation as a position # angle (PA) from North; regions measures it from the RA axis. # Convert between them with a 90 deg offset. center, outer_width, outer_height, angle = pixel_shape_to_sky_svd( (self.center.x, self.center.y), wcs, self.outer_width, self.outer_height, self.angle.to_value(u.radian)) _, inner_width, inner_height, _ = pixel_shape_to_sky_svd( (self.center.x, self.center.y), wcs, self.inner_width, self.inner_height, self.angle.to_value(u.radian)) angle = (angle + 90 * u.deg).wrap_at(360 * u.deg) return (center, inner_width * u.arcsec, outer_width * u.arcsec, inner_height * u.arcsec, outer_height * u.arcsec, angle) class AsymmetricAnnulusSkyRegion(SkyRegion): """ Helper class for asymmetric annuli sky regions. Used for ellipse and rectangle sky annulus regions. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The position of the center of the annulus. inner_width : `~astropy.units.Quantity` The inner width of the annulus (before rotation) as an angle. outer_width : `~astropy.units.Quantity` The outer width of the annulus (before rotation) as an angle. inner_height : `~astropy.units.Quantity` The inner height of the annulus (before rotation) as an angle. outer_height : `~astropy.units.Quantity` The outer height of the annulus (before rotation) as an angle. angle : `~astropy.units.Quantity`, optional The rotation angle of the annulus, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the longitude axis of the celestial coordinates. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('center', 'inner_width', 'outer_width', 'inner_height', 'outer_height', 'angle') center = ScalarSkyCoord('The center position as a |SkyCoord|.') inner_width = PositiveScalarAngle('The inner width (before rotation) as ' 'a |Quantity| angle.') outer_width = PositiveScalarAngle('The outer width (before rotation) as ' 'a |Quantity| angle.') inner_height = PositiveScalarAngle('The inner height (before rotation) ' 'as a |Quantity| angle.') outer_height = PositiveScalarAngle('The outer height (before rotation) ' 'as a |Quantity| angle.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a' '|Quantity| angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, inner_width, outer_width, inner_height, outer_height, angle=0 * u.deg, meta=None, visual=None): self.center = center self.inner_width = inner_width self.outer_width = outer_width self.inner_height = inner_height self.outer_height = outer_height self.angle = angle self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() if inner_width >= outer_width: raise ValueError('outer_width must be greater than inner_width') if inner_height >= outer_height: raise ValueError('outer_height must be greater than inner_height') def to_pixel_args(self, wcs): # Convert regions sky angle (from RA axis) to photutils PA (from # North) by subtracting 90 deg. sky_angle_rad = self.angle.to_value(u.radian) - math.pi / 2 center, outer_width, outer_height, angle = sky_shape_to_pixel_svd( self.center, wcs, self.outer_width.to_value(u.arcsec), self.outer_height.to_value(u.arcsec), sky_angle_rad) _, inner_width, inner_height, _ = sky_shape_to_pixel_svd( self.center, wcs, self.inner_width.to_value(u.arcsec), self.inner_height.to_value(u.arcsec), sky_angle_rad) return (PixCoord(*center), inner_width, outer_width, inner_height, outer_height, angle) class EllipseAnnulusPixelRegion(AsymmetricAnnulusPixelRegion): """ A elliptical annulus in pixel coordinates. Parameters ---------- center : `~regions.PixCoord` The position of the center of the elliptical annulus. inner_width : float The inner width of the elliptical annulus (before rotation) in pixels. outer_width : float The outer width of the elliptical annulus (before rotation) in pixels. inner_height : float The inner height of the elliptical annulus (before rotation) in pixels. outer_height : float The outer height of the elliptical annulus (before rotation) in pixels. angle : `~astropy.units.Quantity`, optional The rotation angle of the elliptical annulus, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the x axis. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from astropy.coordinates import Angle from regions import EllipseAnnulusPixelRegion, PixCoord fig, ax = plt.subplots() reg = EllipseAnnulusPixelRegion(PixCoord(6, 6), inner_width=5.5, outer_width=8.5, inner_height=3.5, outer_height=6.5, angle=Angle('45deg')) patch = reg.plot(ax=ax, facecolor='none', edgecolor='red', lw=2, label='Ellipse Annulus') ax.legend(handles=(patch,), loc='upper center') ax.set_xlim(-5, 20) ax.set_ylim(-5, 20) ax.set_aspect('equal') """ # duplicated from AsymmetricAnnulusPixelRegion because otherwise Sphinx # ignores the docstrings in the parent class center = ScalarPixCoord('The center pixel position as a |PixCoord|.') inner_width = PositiveScalar('The inner width (before rotation) in ' 'pixels as a float.') outer_width = PositiveScalar('The outer width (before rotation) in ' 'pixels as a float.') inner_height = PositiveScalar('The inner height (before rotation) in ' 'pixels as a float.') outer_height = PositiveScalar('The outer height (before rotation) in ' 'pixels as a float.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a ' '|Quantity| angle.') _component_class = EllipsePixelRegion def __init__(self, center, inner_width, outer_width, inner_height, outer_height, angle=0 * u.deg, meta=None, visual=None): super().__init__(center, inner_width, outer_width, inner_height, outer_height, angle, meta, visual) def to_polygon(self, *, n_vertices=100): """ Return a `~regions.CompoundPixelRegion` of two `~regions.PolygonPixelRegion` objects that approximates this annulus. Parameters ---------- n_vertices : int, optional The number of polygon vertices for each ellipse. Default is 100. Returns ------- polygon : `~regions.CompoundPixelRegion` A compound region of two polygon regions approximating the annulus. """ inner_polygon = self._inner_region.to_polygon(n_vertices=n_vertices) outer_polygon = self._outer_region.to_polygon(n_vertices=n_vertices) return CompoundPixelRegion(inner_polygon, outer_polygon, operator.xor, self.meta.copy(), self.visual.copy()) def to_sky(self, wcs): return EllipseAnnulusSkyRegion(*self.to_sky_args(wcs), meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): raise NotImplementedError class EllipseAnnulusSkyRegion(AsymmetricAnnulusSkyRegion): """ A elliptical annulus in `~astropy.coordinates.SkyCoord` coordinates. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The position of the center of the elliptical annulus. inner_width : `~astropy.units.Quantity` The inner width of the elliptical annulus (before rotation) as an angle. outer_width : `~astropy.units.Quantity` The outer width of the elliptical annulus (before rotation) as an angle. inner_height : `~astropy.units.Quantity` The inner height of the elliptical annulus (before rotation) as an angle. outer_height : `~astropy.units.Quantity` The outer height of the elliptical annulus (before rotation) as an angle. angle : `~astropy.units.Quantity`, optional The rotation angle of the elliptical annulus, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the longitude axis of the celestial coordinates. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ # duplicated from AsymmetricAnnulusSkyRegion because otherwise Sphinx # ignores the docstrings in the parent class center = ScalarSkyCoord('The center position as a |SkyCoord|.') inner_width = PositiveScalarAngle('The inner width (before rotation) as ' 'a |Quantity| angle.') outer_width = PositiveScalarAngle('The outer width (before rotation) as ' 'a |Quantity| angle.') inner_height = PositiveScalarAngle('The inner height (before rotation) ' 'as a |Quantity| angle.') outer_height = PositiveScalarAngle('The outer height (before rotation) ' 'as a |Quantity| angle.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a ' '|Quantity| angle.') _component_class = EllipseSkyRegion def __init__(self, center, inner_width, outer_width, inner_height, outer_height, angle=0 * u.deg, meta=None, visual=None): super().__init__(center, inner_width, outer_width, inner_height, outer_height, angle, meta, visual) def to_polygon(self, wcs, *, n_vertices=100): """ Return a `~regions.CompoundSkyRegion` of two `~regions.PolygonSkyRegion` objects that approximates this annulus. Parameters ---------- wcs : `~astropy.wcs.WCS` The WCS to use for the sky-to-pixel-to-sky conversion. n_vertices : int, optional The number of polygon vertices for each ellipse. Default is 100. Returns ------- polygon : `~regions.CompoundSkyRegion` A compound region of two polygon regions approximating the annulus. """ return self.to_pixel(wcs).to_polygon(n_vertices=n_vertices).to_sky(wcs) def to_pixel(self, wcs): return EllipseAnnulusPixelRegion(*self.to_pixel_args(wcs), meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): raise NotImplementedError class RectangleAnnulusPixelRegion(AsymmetricAnnulusPixelRegion): """ A rectangular annulus in pixel coordinates. Parameters ---------- center : `~regions.PixCoord` The position of the center of the rectangular annulus. inner_width : float The inner width of the rectangular annulus (before rotation) in pixels. outer_width : float The outer width of the rectangular annulus (before rotation) in pixels. inner_height : float The inner height of the rectangular annulus (before rotation) in pixels. outer_height : float The outer height of the rectangular annulus (before rotation) in pixels. angle : `~astropy.units.Quantity`, optional The rotation angle of the rectangular annulus, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the x axis. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from astropy.coordinates import Angle from regions import PixCoord, RectangleAnnulusPixelRegion fig, ax = plt.subplots() reg = RectangleAnnulusPixelRegion(PixCoord(x=6, y=6), inner_width=5.5, outer_width=8.5, inner_height=3.5, outer_height=6.5, angle=Angle('45deg')) patch = reg.plot(ax=ax, facecolor='none', edgecolor='red', lw=2, label='Rectangle Annulus') ax.legend(handles=(patch,), loc='upper center') ax.set_xlim(-5, 20) ax.set_ylim(-5, 20) ax.set_aspect('equal') """ # duplicated from AsymmetricAnnulusPixelRegion because otherwise Sphinx # ignores the docstrings in the parent class center = ScalarPixCoord('The center pixel position as a |PixCoord|.') inner_width = PositiveScalar('The inner width (before rotation) in ' 'pixels as a float.') outer_width = PositiveScalar('The outer width (before rotation) in ' 'pixels as a float.') inner_height = PositiveScalar('The inner height (before rotation) in ' 'pixels as a float.') outer_height = PositiveScalar('The outer height (before rotation) in ' 'pixels as a float.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a ' '|Quantity| angle.') _component_class = RectanglePixelRegion def __init__(self, center, inner_width, outer_width, inner_height, outer_height, angle=0 * u.deg, meta=None, visual=None): super().__init__(center, inner_width, outer_width, inner_height, outer_height, angle, meta, visual) def to_sky_args(self, wcs): # The photutils SVD helpers measure the sky rotation as a # position angle (PA) from North; regions measures it from the # RA axis. Convert between them with a 90 deg offset. center, outer_width, outer_height, angle = pixel_shape_to_sky_svd( (self.center.x, self.center.y), wcs, self.outer_width, self.outer_height, self.angle.to_value(u.radian)) _, inner_width, inner_height, _ = pixel_shape_to_sky_svd( (self.center.x, self.center.y), wcs, self.inner_width, self.inner_height, self.angle.to_value(u.radian)) angle = (angle + 90 * u.deg).wrap_at(360 * u.deg) return (center, inner_width * u.arcsec, outer_width * u.arcsec, inner_height * u.arcsec, outer_height * u.arcsec, angle) def to_polygon(self): """ Return a `~regions.CompoundPixelRegion` of two `~regions.PolygonPixelRegion` objects equivalent to this annulus. Returns ------- polygon : `~regions.CompoundPixelRegion` A compound region of two polygon regions equivalent to the annulus. """ inner_polygon = self._inner_region.to_polygon() outer_polygon = self._outer_region.to_polygon() return CompoundPixelRegion(inner_polygon, outer_polygon, operator.xor, self.meta.copy(), self.visual.copy()) def to_sky(self, wcs): return RectangleAnnulusSkyRegion(*self.to_sky_args(wcs), meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): raise NotImplementedError class RectangleAnnulusSkyRegion(AsymmetricAnnulusSkyRegion): """ A rectangular annulus in `~astropy.coordinates.SkyCoord` coordinates. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The position of the center of the rectangular annulus. inner_width : `~astropy.units.Quantity` The inner width of the rectangular annulus (before rotation) as an angle. outer_width : `~astropy.units.Quantity` The outer width of the rectangular annulus (before rotation) as an angle. inner_height : `~astropy.units.Quantity` The inner height of the rectangular annulus (before rotation) as an angle. outer_height : `~astropy.units.Quantity` The outer height of the rectangular annulus (before rotation) as an angle. angle : `~astropy.units.Quantity`, optional The rotation angle of the rectangular annulus, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the longitude axis of the celestial coordinates. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ # duplicated from AsymmetricAnnulusSkyRegion because otherwise Sphinx # ignores the docstrings in the parent class center = ScalarSkyCoord('The center position as a |SkyCoord|.') inner_width = PositiveScalarAngle('The inner width (before rotation) as ' 'a |Quantity| angle.') outer_width = PositiveScalarAngle('The outer width (before rotation) as ' 'a |Quantity| angle.') inner_height = PositiveScalarAngle('The inner height (before rotation) ' 'as a |Quantity| angle.') outer_height = PositiveScalarAngle('The outer height (before rotation) ' 'as a |Quantity| angle.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a ' '|Quantity| angle.') _component_class = RectangleSkyRegion def __init__(self, center, inner_width, outer_width, inner_height, outer_height, angle=0 * u.deg, meta=None, visual=None): super().__init__(center, inner_width, outer_width, inner_height, outer_height, angle, meta, visual) def to_pixel_args(self, wcs): # Convert regions sky angle (from RA axis) to photutils PA (from # North) by subtracting 90 deg. sky_angle_rad = self.angle.to_value(u.radian) - math.pi / 2 center, outer_width, outer_height, angle = sky_shape_to_pixel_svd( self.center, wcs, self.outer_width.to_value(u.arcsec), self.outer_height.to_value(u.arcsec), sky_angle_rad) _, inner_width, inner_height, _ = sky_shape_to_pixel_svd( self.center, wcs, self.inner_width.to_value(u.arcsec), self.inner_height.to_value(u.arcsec), sky_angle_rad) return (PixCoord(*center), inner_width, outer_width, inner_height, outer_height, angle) def to_polygon(self, wcs): """ Return a `~regions.CompoundSkyRegion` of two `~regions.PolygonSkyRegion` objects equivalent to this annulus. Parameters ---------- wcs : `~astropy.wcs.WCS` The WCS to use for the sky-to-pixel-to-sky conversion. Returns ------- polygon : `~regions.CompoundSkyRegion` A compound region of two polygon regions equivalent to the annulus. """ return self.to_pixel(wcs).to_polygon().to_sky(wcs) def to_pixel(self, wcs): return RectangleAnnulusPixelRegion(*self.to_pixel_args(wcs), meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): raise NotImplementedError astropy-regions-f4139aa/regions/shapes/circle.py000066400000000000000000000463061521703212700220460ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines circular regions in both pixel and sky coordinates. """ import math import astropy.units as u import numpy as np from astropy.coordinates import Angle from regions._geometry import circle_overlap_grid from regions._utils.spherical_helpers import ( get_circle_latitude_tangent_limits, get_circle_longitude_tangent_limits) from regions._utils.wcs_helpers import (pixel_to_sky_mean_scale, pixel_to_sky_svd_scales, sky_to_pixel_mean_scale, sky_to_pixel_svd_scales) from regions.core.attributes import (PositiveScalar, PositiveScalarAngle, RegionMetaDescr, RegionVisualDescr, ScalarPixCoord, ScalarSkyCoord) from regions.core.bounding_box import RegionBoundingBox from regions.core.core import PixelRegion, SkyRegion, SphericalSkyRegion from regions.core.mask import RegionMask from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord from regions.shapes.ellipse import EllipsePixelRegion, EllipseSkyRegion from regions.shapes.polygon import PolygonPixelRegion __all__ = ['CirclePixelRegion', 'CircleSkyRegion', 'CircleSphericalSkyRegion'] class CirclePixelRegion(PixelRegion): """ A circle defined using pixel coordinates. Parameters ---------- center : `~regions.PixCoord` The center position. radius : float The radius in pixels. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from regions import CirclePixelRegion, PixCoord fig, ax = plt.subplots() reg = CirclePixelRegion(PixCoord(x=8, y=7), radius=3.5) patch = reg.plot(ax=ax, facecolor='none', edgecolor='red', lw=2, label='Circle') ax.legend(handles=(patch,), loc='upper center') ax.set_xlim(0, 15) ax.set_ylim(0, 15) ax.set_aspect('equal') """ _params = ('center', 'radius') _mpl_artist = 'Patch' center = ScalarPixCoord('The center pixel position as a |PixCoord|.') radius = PositiveScalar('The radius in pixels as a float.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, radius, meta=None, visual=None): self.center = center self.radius = radius self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() @property def area(self): return math.pi * self.radius ** 2 def _containment(self, pixcoord, covers=False): """ Helper method for circle containment checks. """ pixcoord = PixCoord._validate(pixcoord, name='pixcoord') sep = self.center.separation(pixcoord) if covers: return sep <= self.radius return sep < self.radius def contains(self, pixcoord): return self._containment(pixcoord, covers=False) def covers(self, pixcoord): return self._containment(pixcoord, covers=True) def to_sky(self, wcs, *, as_ellipse=False): """ Return a sky region from this pixel region. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`). as_ellipse : bool, optional If `True`, return an `~regions.EllipseSkyRegion` instead of a `~regions.CircleSkyRegion`. An ellipse is generally a better approximation when the WCS has distortions or different pixel scales along different axes. Default is `False`. Returns ------- region : `~regions.CircleSkyRegion` or `~regions.EllipseSkyRegion` The sky region. An ellipse is returned if ``as_ellipse`` is `True`. """ if as_ellipse: center, scale_major, scale_minor, angle = pixel_to_sky_svd_scales( (self.center.x, self.center.y), wcs) width = Angle(2 * self.radius * scale_major, 'arcsec') height = Angle(2 * self.radius * scale_minor, 'arcsec') # The helper returns a position angle (PA) from North; # regions measures the angle from the RA axis (90 deg # offset). angle = (angle + 90 * u.deg).wrap_at(360 * u.deg) return EllipseSkyRegion(center, width, height, angle=angle, meta=self.meta.copy(), visual=self.visual.copy()) center, mean_scale = pixel_to_sky_mean_scale( (self.center.x, self.center.y), wcs) radius = Angle(self.radius * mean_scale, 'arcsec') return CircleSkyRegion(center, radius, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform(wcs, boundary_distortions) if boundary_distortions: # Requires planar to spherical projection (using WCS) and # discretization. # Will require implementing discretization in pixel space # to get correct handling of distortions. raise NotImplementedError # ### Potential solution: # Leverage polygon class to_spherical_sky() functionality # without distortions, as the distortions were already # computed in creating that polygon approximation # return self.discretize_boundary( # n_vertices=n_vertices).to_spherical_sky( # wcs=wcs, boundary_distortions=False # ) return self.to_sky(wcs).to_spherical_sky() @property def bounding_box(self): """ Bounding box (`~regions.RegionBoundingBox`). """ xmin = self.center.x - self.radius xmax = self.center.x + self.radius ymin = self.center.y - self.radius ymax = self.center.y + self.radius return RegionBoundingBox.from_float(xmin, xmax, ymin, ymax) def to_mask(self, mode='center', subpixels=1): self._validate_mode(mode, subpixels) if mode == 'center': mode = 'subpixels' subpixels = 1 # Find bounding box and mask size bbox = self.bounding_box ny, nx = bbox.shape # Find position of pixel edges and recenter so that circle is at # origin xmin = float(bbox.ixmin) - 0.5 - self.center.x xmax = float(bbox.ixmax) - 0.5 - self.center.x ymin = float(bbox.iymin) - 0.5 - self.center.y ymax = float(bbox.iymax) - 0.5 - self.center.y use_exact = 0 if mode == 'subpixels' else 1 fraction = circle_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, self.radius, use_exact, subpixels) return RegionMask(fraction, bbox=bbox) def as_artist(self, origin=(0, 0), **kwargs): """ Return a matplotlib patch object for this region (`matplotlib.patches.Circle`). Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. **kwargs : dict Any keyword arguments accepted by `~matplotlib.patches.Circle`. These keywords will override any visual meta attributes of this region. Returns ------- artist : `~matplotlib.patches.Circle` A matplotlib circle patch. """ from matplotlib.patches import Circle xy = self.center.x - origin[0], self.center.y - origin[1] radius = self.radius mpl_kwargs = self.visual.define_mpl_kwargs(self._mpl_artist) mpl_kwargs.update(kwargs) return Circle(xy=xy, radius=radius, **mpl_kwargs) def rotate(self, center, angle): """ Rotate the region. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `~regions.PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- region : `CirclePixelRegion` The rotated region (which is an independent copy). """ center = self.center.rotate(center, angle) return self.copy(center=center) def to_polygon(self, *, n_vertices=100): """ Return a `~regions.PolygonPixelRegion` that approximates this circle. Parameters ---------- n_vertices : int, optional The number of polygon vertices. Default is 100. Returns ------- polygon : `~regions.PolygonPixelRegion` A polygon region approximating the circle. """ theta = np.linspace(0, 2 * np.pi, n_vertices, endpoint=False) x = self.radius * np.cos(theta) + self.center.x y = self.radius * np.sin(theta) + self.center.y vertices = PixCoord(x=x, y=y) return PolygonPixelRegion(vertices=vertices, meta=self.meta.copy(), visual=self.visual.copy()) class CircleSkyRegion(SkyRegion): """ A circle defined using sky coordinates. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The center position. radius : `~astropy.units.Quantity` The radius in angular units. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('center', 'radius') center = ScalarSkyCoord('The center position as a |SkyCoord|.') radius = PositiveScalarAngle('The radius as a |Quantity| angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, radius, meta=None, visual=None): self.center = center self.radius = radius self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() def to_pixel(self, wcs, *, as_ellipse=False): """ Return a pixel region from this sky region. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`). as_ellipse : bool, optional If `True`, return an `~regions.EllipsePixelRegion` instead of a `~regions.CirclePixelRegion`. An ellipse is generally a better approximation when the WCS has distortions or different pixel scales along different axes. Default is `False`. Returns ------- region : `~regions.CirclePixelRegion` or `~regions.EllipsePixelRegion` The pixel region. An ellipse is returned if ``as_ellipse`` is `True`. """ if as_ellipse: center, scale_major, scale_minor, angle = sky_to_pixel_svd_scales( self.center, wcs) radius_arcsec = self.radius.to_value(u.arcsec) width = 2 * radius_arcsec * scale_major height = 2 * radius_arcsec * scale_minor return EllipsePixelRegion(PixCoord(*center), width, height, angle=angle, meta=self.meta.copy(), visual=self.visual.copy()) center, mean_scale = sky_to_pixel_mean_scale(self.center, wcs) radius = self.radius.to_value(u.arcsec) * mean_scale return CirclePixelRegion(PixCoord(*center), radius, meta=self.meta.copy(), visual=self.visual.copy()) def to_polygon(self, wcs, *, n_vertices=100): """ Return a `~regions.PolygonSkyRegion` that approximates this circle. Parameters ---------- wcs : `~astropy.wcs.WCS` The WCS to use for the sky-to-pixel-to-sky conversion. n_vertices : int, optional The number of polygon vertices. Default is 100. Returns ------- polygon : `~regions.PolygonSkyRegion` A polygon region approximating the circle. """ return self.to_pixel(wcs).to_polygon(n_vertices=n_vertices).to_sky(wcs) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform(wcs, boundary_distortions) if boundary_distortions: # Requires planar to spherical projection (using WCS) # and discretization # Will require implementing discretization in pixel space # to get correct handling of distortions. raise NotImplementedError # ### Potential solution: # # Leverage polygon class to_spherical_sky() functionality without # # distortions, as the distortions were already # # computed in creating # # that polygon approximation # return self.to_pixel(wcs) # .discretize_boundary(n_vertices=n_vertices) # .to_spherical_sky( # wcs=wcs, boundary_distortions=False # ) return CircleSphericalSkyRegion( self.center, self.radius, meta=self.meta, visual=self.visual, ) class CircleSphericalSkyRegion(SphericalSkyRegion): """ Class for a circular sky region, where the circle is interpreted within a spherical geometry reference frame. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The center position. radius : `~astropy.units.Quantity` The radius in angular units. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('center', 'radius') center = ScalarSkyCoord('The center position as a |SkyCoord|.') radius = PositiveScalarAngle('The radius as a |Quantity| angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, radius, meta=None, visual=None): self.center = center self.radius = radius self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() def _containment(self, coord, covers=False): sep = self.center.separation(coord) if covers: return sep <= self.radius return sep < self.radius def contains(self, coord): return self._containment(coord, covers=False) def covers(self, coord): return self._containment(coord, covers=True) @property def bounding_circle(self): return self.copy() @property def bounding_lonlat(self): lons_arr = get_circle_longitude_tangent_limits(self.center, self.radius) lats_arr = get_circle_latitude_tangent_limits(self.center, self.radius) lons_arr, lats_arr = self._validate_lonlat_bounds(lons_arr, lats_arr) return lons_arr, lats_arr def transform_to(self, frame, merge_attributes=True): frame = self._validate_frame_transformation(frame) center_transf = self.center.transform_to( frame, merge_attributes=merge_attributes, ) return CircleSphericalSkyRegion( center_transf, self.radius.copy(), self.meta.copy(), self.visual.copy(), ) def discretize_boundary(self, *, n_vertices=100): # Avoid circular imports: from .polygon import PolygonSphericalSkyRegion theta = np.linspace(0, 1, num=n_vertices, endpoint=False) * 360 * u.deg # Need to invert order because of CW convention: bound_verts = self.center.directional_offset_by(theta[::-1], self.radius) return PolygonSphericalSkyRegion(bound_verts, meta=self.meta.copy(), visual=self.visual.copy()) def to_polygon(self, *, n_vertices=100): """ Return a `~regions.PolygonSphericalSkyRegion` that approximates this circle. Parameters ---------- n_vertices : int, optional The number of polygon vertices. Default is 100. Returns ------- polygon : `~regions.PolygonSphericalSkyRegion` A polygon region approximating the circle. """ return self.discretize_boundary(n_vertices=n_vertices) def to_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform(wcs, boundary_distortions) if boundary_distortions: # Requires spherical to planar projection (from WCS) # and discretization # Use to_pixel(), then apply "small angle approx" to get planar # sky. return self.to_pixel( boundary_distortions=boundary_distortions, wcs=wcs, n_vertices=n_vertices, ).to_sky(wcs) return CircleSkyRegion( self.center, self.radius, meta=self.meta, visual=self.visual, ) def to_pixel(self, wcs, *, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform(wcs, boundary_distortions) if boundary_distortions: from .polygon import PolygonPixelRegion disc_kwargs = ({} if n_vertices is None else {'n_vertices': n_vertices}) # Requires spherical to planar projection (from WCS) and # discretization verts = wcs.world_to_pixel( self.discretize_boundary(**disc_kwargs).vertices, ) return PolygonPixelRegion( PixCoord(*verts), meta=self.meta.copy(), visual=self.visual.copy(), ) return self.to_sky().to_pixel(wcs) astropy-regions-f4139aa/regions/shapes/ellipse.py000066400000000000000000000411041521703212700222310ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines elliptical regions in both pixel and sky coordinates. """ import math import astropy.units as u import numpy as np from astropy.coordinates import Angle from regions._geometry import ellipse_overlap_grid from regions._utils.wcs_helpers import (pixel_shape_to_sky_svd, sky_shape_to_pixel_svd) from regions.core.attributes import (PositiveScalar, PositiveScalarAngle, RegionMetaDescr, RegionVisualDescr, ScalarAngle, ScalarPixCoord, ScalarSkyCoord) from regions.core.bounding_box import RegionBoundingBox from regions.core.core import PixelRegion, SkyRegion from regions.core.mask import RegionMask from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord from regions.shapes.polygon import PolygonPixelRegion __all__ = ['EllipsePixelRegion', 'EllipseSkyRegion'] class EllipsePixelRegion(PixelRegion): """ An ellipse in pixel coordinates. Parameters ---------- center : `~regions.PixCoord` The position of the center of the ellipse. width : float The width of the ellipse (before rotation) in pixels. height : float The height of the ellipse (before rotation) in pixels. angle : `~astropy.units.Quantity`, optional The rotation angle of the ellipse, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the x axis. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from astropy.coordinates import Angle from regions import EllipsePixelRegion, PixCoord fig, ax = plt.subplots() reg = EllipsePixelRegion(PixCoord(15, 10), width=16, height=10, angle=Angle(30, 'deg')) patch = reg.plot(ax=ax, facecolor='none', edgecolor='red', lw=2, label='Ellipse') ax.legend(handles=(patch,), loc='upper center') ax.set_xlim(0, 30) ax.set_ylim(0, 20) ax.set_aspect('equal') """ _params = ('center', 'width', 'height', 'angle') _mpl_artist = 'Patch' center = ScalarPixCoord('The center pixel position as a |PixCoord|.') width = PositiveScalar('The width of the ellipse (before rotation) in ' 'pixels as a float.') height = PositiveScalar('The height of the ellipse (before rotation) in ' 'pixels as a float.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a ' '|Quantity| angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, width, height, angle=0. * u.deg, meta=None, visual=None): self.center = center self.width = width self.height = height self.angle = angle self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() @property def area(self): return math.pi / 4 * self.width * self.height def _containment(self, pixcoord, covers=False): pixcoord = PixCoord._validate(pixcoord, name='pixcoord') cos_angle = np.cos(self.angle) sin_angle = np.sin(self.angle) dx = pixcoord.x - self.center.x dy = pixcoord.y - self.center.y value = ((2 * (cos_angle * dx + sin_angle * dy) / self.width) ** 2 + (2 * (sin_angle * dx - cos_angle * dy) / self.height) ** 2) if covers: return value <= 1.0 return value < 1.0 def contains(self, pixcoord): return self._containment(pixcoord, covers=False) def covers(self, pixcoord): return self._containment(pixcoord, covers=True) def to_sky(self, wcs): # The photutils helpers measure the sky rotation as a position # angle (PA) from North; regions measures it from the RA axis. # Convert between them with a 90 deg offset. center, sky_width, sky_height, angle = pixel_shape_to_sky_svd( (self.center.x, self.center.y), wcs, self.width, self.height, self.angle.to_value(u.radian)) angle = (angle + 90 * u.deg).wrap_at(360 * u.deg) width = Angle(sky_width, 'arcsec') height = Angle(sky_height, 'arcsec') return EllipseSkyRegion(center, width, height, angle=angle, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): raise NotImplementedError @property def bounding_box(self): """ The minimal bounding box (`~regions.RegionBoundingBox`) enclosing the exact elliptical region. """ # We use the solution described in # https://stackoverflow.com/a/14163413 cos_theta = np.cos(self.angle) sin_theta = np.sin(self.angle) width_x = 0.5 * self.width * cos_theta width_y = 0.5 * self.width * sin_theta height_x = 0.5 * self.height * -sin_theta height_y = 0.5 * self.height * cos_theta dx = np.sqrt(width_x**2 + height_x**2) dy = np.sqrt(width_y**2 + height_y**2) xmin = self.center.x - dx xmax = self.center.x + dx ymin = self.center.y - dy ymax = self.center.y + dy return RegionBoundingBox.from_float(xmin, xmax, ymin, ymax) def to_mask(self, mode='center', subpixels=5): # NOTE: assumes this class represents a single circle self._validate_mode(mode, subpixels) if mode == 'center': mode = 'subpixels' subpixels = 1 # Find bounding box and mask size bbox = self.bounding_box ny, nx = bbox.shape # Find position of pixel edges and recenter so that ellipse is # at origin xmin = float(bbox.ixmin) - 0.5 - self.center.x xmax = float(bbox.ixmax) - 0.5 - self.center.x ymin = float(bbox.iymin) - 0.5 - self.center.y ymax = float(bbox.iymax) - 0.5 - self.center.y use_exact = 0 if mode == 'subpixels' else 1 fraction = ellipse_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, 0.5 * self.width, 0.5 * self.height, self.angle.to_value(u.radian), use_exact, subpixels) return RegionMask(fraction, bbox=bbox) def as_artist(self, origin=(0, 0), **kwargs): """ Return a matplotlib patch object for the region (`matplotlib.patches.Ellipse`). Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. **kwargs : dict Any keyword arguments accepted by `~matplotlib.patches.Ellipse`. These keywords will override any visual meta attributes of this region. Returns ------- artist : `~matplotlib.patches.Ellipse` A matplotlib ellipse patch. """ from matplotlib.patches import Ellipse xy = self.center.x - origin[0], self.center.y - origin[1] width = self.width height = self.height # matplotlib expects rotation in degrees (anti-clockwise) angle = self.angle.to_value(u.deg) mpl_kwargs = self.visual.define_mpl_kwargs(self._mpl_artist) mpl_kwargs.update(kwargs) return Ellipse(xy=xy, width=width, height=height, angle=angle, **mpl_kwargs) def _update_from_mpl_selector(self, *args, **kwargs): xmin, xmax, ymin, ymax = self._mpl_selector.extents self.center = PixCoord(x=0.5 * (xmin + xmax), y=0.5 * (ymin + ymax)) self.width = (xmax - xmin) self.height = (ymax - ymin) self.angle = 0. * u.deg if self._mpl_selector_callback is not None: self._mpl_selector_callback(self) def as_mpl_selector(self, ax, active=True, sync=True, callback=None, drag_from_anywhere=False, **kwargs): """ Return a matplotlib editable widget for this region (`matplotlib.widgets.EllipseSelector`). Parameters ---------- ax : `~matplotlib.axes.Axes` The matplotlib axes to add the selector to. active : bool, optional Whether the selector should be active by default. sync : bool, optional If `True` (the default), the region will be kept in sync with the selector. Otherwise, the selector will be initialized with the values from the region but the two will then be disconnected. callback : callable, optional If specified, this function will be called every time the region is updated. This only has an effect if ``sync`` is `True`. If a callback is set, it is called for the first time once the selector has been created. drag_from_anywhere : bool, optional If `True`, the selector can be moved by clicking anywhere within its bounds, else only at the central anchor (only available with matplotlib 3.5 upwards; default: `False`). **kwargs : dict Additional keyword arguments that are passed to `matplotlib.widgets.EllipseSelector`. Returns ------- selector : `matplotlib.widgets.EllipseSelector` The matplotlib selector. Notes ----- Once a selector has been created, you will need to keep a reference to it until you no longer need it. In addition, you can enable/disable the selector at any point by calling ``selector.set_active(True)`` or ``selector.set_active(False)``. """ from matplotlib.widgets import EllipseSelector if hasattr(self, '_mpl_selector'): raise AttributeError( 'Cannot attach more than one selector to a region.') if self.angle.value != 0: raise NotImplementedError( 'Cannot create matplotlib selector for rotated ellipse.') if sync: sync_callback = self._update_from_mpl_selector else: def sync_callback(*args, **kwargs): pass rectprops = {'edgecolor': self.visual.get('color', 'black'), 'facecolor': 'none', 'linewidth': self.visual.get('linewidth', 1), 'linestyle': self.visual.get('linestyle', 'solid')} rectprops.update(kwargs.pop('props', dict())) kwargs.update({'props': rectprops}) self._mpl_selector = EllipseSelector( ax, sync_callback, interactive=True, drag_from_anywhere=drag_from_anywhere, **kwargs) self._mpl_selector.extents = (self.center.x - self.width / 2, self.center.x + self.width / 2, self.center.y - self.height / 2, self.center.y + self.height / 2) self._mpl_selector.set_active(active) self._mpl_selector_callback = callback if sync and self._mpl_selector_callback is not None: self._mpl_selector_callback(self) return self._mpl_selector def rotate(self, center, angle): """ Rotate the region. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `~regions.PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- region : `EllipsePixelRegion` The rotated region (which is an independent copy). """ center = self.center.rotate(center, angle) angle = self.angle + angle return self.copy(center=center, angle=angle) def to_polygon(self, *, n_vertices=100): """ Return a `~regions.PolygonPixelRegion` that approximates this ellipse. Parameters ---------- n_vertices : int, optional The number of polygon vertices. Default is 100. Returns ------- polygon : `~regions.PolygonPixelRegion` A polygon region approximating the ellipse. """ theta = np.linspace(0, 2 * np.pi, n_vertices, endpoint=False) x = 0.5 * self.width * np.cos(theta) y = 0.5 * self.height * np.sin(theta) cos_angle = np.cos(self.angle) sin_angle = np.sin(self.angle) x_rot = x * cos_angle - y * sin_angle + self.center.x y_rot = x * sin_angle + y * cos_angle + self.center.y vertices = PixCoord(x=x_rot, y=y_rot) return PolygonPixelRegion(vertices=vertices, meta=self.meta.copy(), visual=self.visual.copy()) class EllipseSkyRegion(SkyRegion): """ An ellipse defined using sky coordinates. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The position of the center of the ellipse. width : `~astropy.units.Quantity` The width of the ellipse (before rotation) as an angle. height : `~astropy.units.Quantity` The height of the ellipse (before rotation) as an angle. angle : `~astropy.units.Quantity`, optional The rotation angle of the ellipse, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the longitude axis of the celestial coordinates. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('center', 'width', 'height', 'angle') center = ScalarSkyCoord('The center position as a |SkyCoord|.') width = PositiveScalarAngle('The width of the ellipse (before rotation) ' 'as a |Quantity| angle.') height = PositiveScalarAngle('The height of the ellipse (before rotation) ' 'as a |Quantity| angle.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a ' '|Quantity| angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, width, height, angle=0. * u.deg, meta=None, visual=None): self.center = center self.width = width self.height = height self.angle = angle self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() def to_pixel(self, wcs): # Convert regions sky angle (from RA axis) to photutils PA (from # North) by subtracting 90 deg. center, pix_width, pix_height, angle = sky_shape_to_pixel_svd( self.center, wcs, self.width.to_value(u.arcsec), self.height.to_value(u.arcsec), self.angle.to_value(u.radian) - math.pi / 2) return EllipsePixelRegion(PixCoord(*center), pix_width, pix_height, angle=angle, meta=self.meta.copy(), visual=self.visual.copy()) def to_polygon(self, wcs, *, n_vertices=100): """ Return a `~regions.PolygonSkyRegion` that approximates this ellipse. Parameters ---------- wcs : `~astropy.wcs.WCS` The WCS to use for the sky-to-pixel-to-sky conversion. n_vertices : int, optional The number of polygon vertices. Default is 100. Returns ------- polygon : `~regions.PolygonSkyRegion` A polygon region approximating the ellipse. """ return self.to_pixel(wcs).to_polygon(n_vertices=n_vertices).to_sky(wcs) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): raise NotImplementedError astropy-regions-f4139aa/regions/shapes/line.py000066400000000000000000000152641521703212700215330ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines line regions in both pixel and sky coordinates. """ import numpy as np from regions.core.attributes import (RegionMetaDescr, RegionVisualDescr, ScalarPixCoord, ScalarSkyCoord) from regions.core.bounding_box import RegionBoundingBox from regions.core.core import PixelRegion, SkyRegion from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord __all__ = ['LinePixelRegion', 'LineSkyRegion'] class LinePixelRegion(PixelRegion): """ A line in pixel coordinates. Parameters ---------- start : `~regions.PixCoord` The start position. end : `~regions.PixCoord` The end position. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from regions import LinePixelRegion, PixCoord fig, ax = plt.subplots() start = PixCoord(x=5, y=8) end = PixCoord(x=25, y=25) reg = LinePixelRegion(start=start, end=end) patch = reg.plot(ax=ax, color='red', lw=2, label='Line') ax.legend(handles=(patch,), loc='upper center') ax.set_xlim(0, 30) ax.set_ylim(0, 30) ax.set_aspect('equal') """ _params = ('start', 'end') _mpl_artist = 'Line2D' start = ScalarPixCoord('The start pixel position as a |PixCoord|.') end = ScalarPixCoord('The end pixel position as a |PixCoord|.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, start, end, meta=None, visual=None): self.start = start self.end = end self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() @property def area(self): return 0 def contains(self, pixcoord): # Lines never contain anything return (False if pixcoord.isscalar else np.zeros(pixcoord.x.shape, dtype=bool)) def covers(self, pixcoord): # Lines never cover anything return (False if pixcoord.isscalar else np.zeros(pixcoord.x.shape, dtype=bool)) def to_sky(self, wcs): start = wcs.pixel_to_world(self.start.x, self.start.y) end = wcs.pixel_to_world(self.end.x, self.end.y) return LineSkyRegion(start, end, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): raise NotImplementedError @property def bounding_box(self): xmin = min(self.start.x, self.end.x) xmax = max(self.start.x, self.end.x) ymin = min(self.start.y, self.end.y) ymax = max(self.start.y, self.end.y) return RegionBoundingBox.from_float(xmin, xmax, ymin, ymax) def to_mask(self, mode='center', subpixels=5): # TODO: needs to be implemented raise NotImplementedError def as_artist(self, origin=(0, 0), **kwargs): """ Return a matplotlib patch object for this region (`matplotlib.patches.Arrow`). Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. **kwargs : dict Any keyword arguments accepted by `~matplotlib.patches.Arrow`. These keywords will override any visual meta attributes of this region. Returns ------- artist : `~matplotlib.patches.Arrow` A matplotlib line patch. """ from matplotlib.lines import Line2D mpl_kwargs = self.visual.define_mpl_kwargs(self._mpl_artist) mpl_kwargs.update(kwargs) return Line2D([self.start.x, self.end.x], [self.start.y, self.end.y], **mpl_kwargs) def rotate(self, center, angle): """ Rotate the region. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `~regions.PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- region : `LinePixelRegion` The rotated region (which is an independent copy). """ start = self.start.rotate(center, angle) end = self.end.rotate(center, angle) return self.copy(start=start, end=end) class LineSkyRegion(SkyRegion): """ A line in sky coordinates. Parameters ---------- start : `~astropy.coordinates.SkyCoord` The start position. end : `~astropy.coordinates.SkyCoord` The end position. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('start', 'end') start = ScalarSkyCoord('The start position as a |SkyCoord|.') end = ScalarSkyCoord('The end position as a |SkyCoord|.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, start, end, meta=None, visual=None): self.start = start self.end = end self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() def contains(self, skycoord, wcs): # pylint: disable=unused-argument # Lines never contain anything return (False if skycoord.isscalar else np.zeros(skycoord.shape, dtype=bool)) def covers(self, skycoord, wcs): # pylint: disable=unused-argument # Lines never cover anything return (False if skycoord.isscalar else np.zeros(skycoord.shape, dtype=bool)) def to_pixel(self, wcs): start_x, start_y = wcs.world_to_pixel(self.start) start = PixCoord(start_x, start_y) end_x, end_y = wcs.world_to_pixel(self.end) end = PixCoord(end_x, end_y) return LinePixelRegion(start, end, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): raise NotImplementedError astropy-regions-f4139aa/regions/shapes/lune.py000066400000000000000000000207501521703212700215430ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines a lune (a "wedge" bounded by two great circles) spherical sky region. """ import operator import astropy.units as u from astropy.coordinates import SkyCoord from regions._utils.spherical_helpers import ( cross_product_skycoord2skycoord, cross_product_sum_skycoord2skycoord, discretize_all_edge_boundaries, get_edge_raw_lonlat_bounds_circ_edges) from regions.core.attributes import (RegionMetaDescr, RegionVisualDescr, ScalarSkyCoord) from regions.core.compound import CompoundSphericalSkyRegion from regions.core.core import SphericalSkyRegion from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord from regions.shapes.circle import CircleSphericalSkyRegion from regions.shapes.polygon import (PolygonPixelRegion, PolygonSphericalSkyRegion) __all__ = ['LuneSphericalSkyRegion'] class LuneSphericalSkyRegion(SphericalSkyRegion): """ Class for a spherical "lune", the intersection between two great circles in spherical geometry. Composed of two CircleSphericalSkyRegions. Parameters ---------- center_gc1 : `~astropy.coordinates.SkyCoord` The center position of the first great circle. center_gc2 : `~astropy.coordinates.SkyCoord` The center position of the second great circle. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('center_gc1', 'center_gc2') center_gc1 = ScalarSkyCoord( 'The center position of the first great circle as a |SkyCoord|.') center_gc2 = ScalarSkyCoord( 'The center position of the second great circle as a |SkyCoord|.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center_gc1, center_gc2, meta=None, visual=None): self.center_gc1 = center_gc1 self.center_gc2 = center_gc2 self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() @property def _circle_1(self): return CircleSphericalSkyRegion( self.center_gc1, 90 * u.deg, self.meta, self.visual, ) @property def _circle_2(self): return CircleSphericalSkyRegion( self.center_gc2, 90 * u.deg, self.meta, self.visual, ) @property def _edge_circs(self): """ Get list of the great circles defining the lune boundaries. """ return [self._circle_1, self._circle_2] @property def _compound_region(self): return CompoundSphericalSkyRegion( self._circle_1, self._circle_2, operator.and_, self.meta, self.visual) @property def frame(self): return self.center_gc1.frame @property def centroid(self): """ Region centroid. """ # Cross product to get poles c_pole = cross_product_skycoord2skycoord( self.center_gc2, self.center_gc1) c_pole_a = cross_product_skycoord2skycoord( self.center_gc1, self.center_gc2) # Use centers + poles to get centroid from cross products of vertices # with cartesian representation # Assume CW order is gc1, c_pole, gc2, c_pole_a c_gc1_sph = self.center_gc1.represent_as('spherical') c_gc2_sph = self.center_gc2.represent_as('spherical') c_p_sph = c_pole.represent_as('spherical') c_p_a_sph = c_pole_a.represent_as('spherical') verts = SkyCoord([c_gc2_sph.lon, c_p_a_sph.lon, c_gc1_sph.lon, c_p_sph.lon], [c_gc2_sph.lat, c_p_a_sph.lat, c_gc1_sph.lat, c_p_sph.lat], frame=self.frame) return cross_product_sum_skycoord2skycoord(verts) @property def vertices(self): """ Region vertices. For a lune, these are the intersections of the two bounding great circles, and are antipodes. """ # Cross product to get poles c_pole = cross_product_skycoord2skycoord( self.center_gc2, self.center_gc1) c_pole_a = cross_product_skycoord2skycoord( self.center_gc1, self.center_gc2) c_p_sph = c_pole.represent_as('spherical') c_p_a_sph = c_pole_a.represent_as('spherical') verts = SkyCoord([c_p_sph.lon, c_p_a_sph.lon], [c_p_sph.lat, c_p_a_sph.lat], frame=self.frame) return verts @property def bounding_circle(self): return CircleSphericalSkyRegion(self.centroid, 90 * u.deg) @property def bounding_lonlat(self): lons_arr, lats_arr = get_edge_raw_lonlat_bounds_circ_edges( self.vertices, self.centroid, self._edge_circs, ) lons_arr, lats_arr = self._validate_lonlat_bounds(lons_arr, lats_arr) return lons_arr, lats_arr def contains(self, coord): return self._compound_region.contains(coord) def covers(self, coord): return self._compound_region.covers(coord) def transform_to(self, frame, merge_attributes=True): frame = self._validate_frame_transformation(frame) center_gc1_transf = self.center_gc1.transform_to( frame, merge_attributes=merge_attributes) center_gc2_transf = self.center_gc2.transform_to( frame, merge_attributes=merge_attributes) return LuneSphericalSkyRegion( center_gc1_transf, center_gc2_transf, self.meta.copy(), self.visual.copy(), ) def discretize_boundary(self, *, n_vertices=100): bound_verts = discretize_all_edge_boundaries( self.vertices, self._edge_circs, n_vertices=n_vertices, )[::-1] # inverted for lune # TODO: properly check for CW order even for a nverts=2 spherical # polygon (lune). return PolygonSphericalSkyRegion(bound_verts, meta=self.meta.copy(), visual=self.visual.copy()) def to_polygon(self, *, n_vertices=100): """ Return a `~regions.PolygonSphericalSkyRegion` that approximates this lune. Parameters ---------- n_vertices : int, optional The number of polygon vertices. Default is 100. Returns ------- polygon : `~regions.PolygonSphericalSkyRegion` A polygon region approximating the lune. """ return self.discretize_boundary(n_vertices=n_vertices) def to_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): if not boundary_distortions: raise ValueError( 'Invalid parameter: `boundary_distortions=False`!\n' 'Transforming lune to planar sky region is only possible by ' 'including boundary distortions, as there is no ' 'analogous sky region.', ) self._validate_planar_spherical_transform( wcs, boundary_distortions) # Requires spherical to planar projection (from WCS) and # discretization. Use to_pixel(), then apply "small angle # approx" to get planar sky. return self.to_pixel( boundary_distortions=boundary_distortions, wcs=wcs, n_vertices=n_vertices, ).to_sky(wcs) def to_pixel(self, wcs, *, boundary_distortions=False, n_vertices=None): if not boundary_distortions: raise ValueError( 'Invalid parameter: `boundary_distortions=False`!\n' 'Transforming lune to planar pixel region is only possible by ' 'including boundary distortions, as there is no ' 'analogous pixel region.', ) self._validate_planar_spherical_transform( wcs, boundary_distortions) disc_kwargs = {} if n_vertices is None else {'n_vertices': n_vertices} # Requires spherical to planar projection (from WCS) and discretization disc_bound = self.discretize_boundary(**disc_kwargs) verts = wcs.world_to_pixel(disc_bound.vertices) return PolygonPixelRegion( PixCoord(*verts), meta=self.meta.copy(), visual=self.visual.copy(), ) astropy-regions-f4139aa/regions/shapes/point.py000066400000000000000000000152751521703212700217370ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines point regions in both pixel and sky coordinates. """ import numpy as np from regions.core.attributes import (RegionMetaDescr, RegionVisualDescr, ScalarPixCoord, ScalarSkyCoord) from regions.core.bounding_box import RegionBoundingBox from regions.core.core import PixelRegion, SkyRegion from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord __all__ = ['PointPixelRegion', 'PointSkyRegion'] class PointPixelRegion(PixelRegion): """ A point position in pixel coordinates. Parameters ---------- center : `~regions.PixCoord` The position of the point. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from regions import PixCoord, PointPixelRegion, RegionVisual fig, ax = plt.subplots() regs = [] regs.append(PointPixelRegion(PixCoord(2, 2), visual=RegionVisual(marker='D'))) regs.append(PointPixelRegion(PixCoord(2, 3), visual=RegionVisual(marker='+'))) regs.append(PointPixelRegion(PixCoord(3, 3), visual=RegionVisual(marker='^'))) regs.append(PointPixelRegion(PixCoord(3, 2), visual=RegionVisual(marker='*'))) regs.append(PointPixelRegion(PixCoord(2, 4), visual=RegionVisual(marker='x'))) regs.append(PointPixelRegion(PixCoord(4, 2))) for reg in regs: reg.plot(ax=ax) ax.set_xlim(0, 6) ax.set_ylim(0, 6) ax.set_aspect('equal') """ _params = ('center',) _mpl_artist = 'point' # custom Line2D with marker only center = ScalarPixCoord('The point pixel position as a |PixCoord|.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, meta=None, visual=None): self.center = center self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() @property def area(self): return 0.0 def contains(self, pixcoord): # Points never contain anything return (False if pixcoord.isscalar else np.zeros(pixcoord.x.shape, dtype=bool)) def covers(self, pixcoord): # Points never cover anything return (False if pixcoord.isscalar else np.zeros(pixcoord.x.shape, dtype=bool)) def to_sky(self, wcs): center = wcs.pixel_to_world(self.center.x, self.center.y) return PointSkyRegion(center, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): raise NotImplementedError @property def bounding_box(self): return RegionBoundingBox.from_float(self.center.x, self.center.x, self.center.y, self.center.y) def to_mask(self, mode='center', subpixels=5): # TODO: needs to be implemented raise NotImplementedError def as_artist(self, origin=(0, 0), **kwargs): """ Return a matplotlib Line2D object for this region (`matplotlib.lines.Line2D`). Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `~matplotlib.lines.Line2D`. These keywords will override any visual meta attributes of this region. Returns ------- artist : `~matplotlib.lines.Line2D` A matplotlib Line2D object. """ # matplotlib does not have a "point" artist, so we use a Line2D # with marker only from matplotlib.lines import Line2D mpl_kwargs = self.visual.define_mpl_kwargs(self._mpl_artist) mpl_kwargs.update(kwargs) return Line2D([self.center.x - origin[0]], [self.center.y - origin[1]], **mpl_kwargs) def rotate(self, center, angle): """ Rotate the region. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `~regions.PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- region : `PointPixelRegion` The rotated region (which is an independent copy). """ center = self.center.rotate(center, angle) return self.copy(center=center) class PointSkyRegion(SkyRegion): """ A pixel region in sky coordinates. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The position of the point. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('center',) center = ScalarSkyCoord('The point position as a |SkyCoord|.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, meta=None, visual=None): self.center = center self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() def contains(self, skycoord, wcs): # pylint: disable=unused-argument # Points never contain anything return (False if skycoord.isscalar else np.zeros(skycoord.shape, dtype=bool)) def covers(self, skycoord, wcs): # pylint: disable=unused-argument # Points never cover anything return (False if skycoord.isscalar else np.zeros(skycoord.shape, dtype=bool)) def to_pixel(self, wcs): center_x, center_y = wcs.world_to_pixel(self.center) center = PixCoord(center_x, center_y) return PointPixelRegion(center, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): raise NotImplementedError astropy-regions-f4139aa/regions/shapes/polygon.py000066400000000000000000000556661521703212700223050ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines polygon regions in both pixel and sky coordinates. """ import operator import astropy.units as u import numpy as np from astropy.coordinates import SkyCoord from astropy.stats import circmean from regions._geometry import polygon_overlap_grid from regions._geometry.polygon_contains import (points_in_polygon, points_in_polygon_covers) from regions._utils.spherical_helpers import ( cross_product_skycoord2skycoord, cross_product_sum_skycoord2skycoord, discretize_all_edge_boundaries, get_edge_raw_lonlat_bounds_circ_edges) from regions.core.attributes import (OneDPixCoord, OneDSkyCoord, PositiveScalar, RegionMetaDescr, RegionVisualDescr, ScalarAngle, ScalarPixCoord) from regions.core.bounding_box import RegionBoundingBox from regions.core.compound import CompoundSphericalSkyRegion from regions.core.core import PixelRegion, SkyRegion, SphericalSkyRegion from regions.core.mask import RegionMask from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord __all__ = ['PolygonPixelRegion', 'RegularPolygonPixelRegion', 'PolygonSkyRegion', 'PolygonSphericalSkyRegion'] class PolygonPixelRegion(PixelRegion): """ A polygon in pixel coordinates. Parameters ---------- vertices : `~regions.PixCoord` The vertices of the polygon. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. origin : `~regions.PixCoord`, optional The origin for polynomial vertices. Using this keyword allows ``vertices`` to be specified relative to an origin pixel coordinate. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from regions import PixCoord, PolygonPixelRegion fig, ax = plt.subplots() x, y = [45, 45, 55, 60], [75, 70, 65, 75] vertices = PixCoord(x=x, y=y) reg = PolygonPixelRegion(vertices=vertices) patch = reg.plot(ax=ax, facecolor='none', edgecolor='red', lw=2, label='Polygon') ax.legend(handles=(patch,), loc='upper center') ax.set_xlim(30, 80) ax.set_ylim(50, 80) ax.set_aspect('equal') """ _params = ('vertices',) _mpl_artist = 'Patch' vertices = OneDPixCoord('The vertices of the polygon as a |PixCoord| ' 'array.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, vertices, meta=None, visual=None, origin=None): self._vertices = vertices self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() if origin is None: origin = PixCoord(0, 0) self.origin = origin self.vertices = vertices + origin @property def area(self): # See https://stackoverflow.com/questions/24467972 # Use offsets to improve numerical precision x_ = self.vertices.x - self.vertices.x.mean() y_ = self.vertices.y - self.vertices.y.mean() # Shoelace formula, for our case where the start vertex # isn't duplicated at the end, written to avoid an array copy area_main = np.dot(x_[:-1], y_[1:]) - np.dot(y_[:-1], x_[1:]) area_last = x_[-1] * y_[0] - y_[-1] * x_[0] return 0.5 * np.abs(area_main + area_last) def _containment(self, pixcoord, covers=False): pixcoord = PixCoord._validate(pixcoord, name='pixcoord') x = np.atleast_1d(np.asarray(pixcoord.x, dtype=float)) y = np.atleast_1d(np.asarray(pixcoord.y, dtype=float)) vx = np.asarray(self.vertices.x, dtype=float) vy = np.asarray(self.vertices.y, dtype=float) shape = x.shape if covers: mask = points_in_polygon_covers(x.flatten(), y.flatten(), vx, vy).astype(bool) else: mask = points_in_polygon(x.flatten(), y.flatten(), vx, vy).astype(bool) return mask.reshape(shape) def contains(self, pixcoord): return self._containment(pixcoord, covers=False) def covers(self, pixcoord): return self._containment(pixcoord, covers=True) def to_sky(self, wcs): vertices_sky = wcs.pixel_to_world(self.vertices.x, self.vertices.y) return PolygonSkyRegion(vertices=vertices_sky, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform( wcs, boundary_distortions) if boundary_distortions: # Requires planar to spherical projection (using WCS) # and discretization # Will require implementing discretization in pixel space # to get correct handling of distortions. raise NotImplementedError # ### Potential solution: # # Leverage polygon class to_spherical_sky() functionality without # # distortions, as the distortions were already # # computed in creating # # that polygon approximation # return self.to_pixel(wcs) # .discretize_boundary(n_vertices=n_vertices) # .to_spherical_sky( # wcs=wcs, boundary_distortions=False # ) # TODO: ensure vertices are in CW order, # as implicitly a planar -> spherical polygon will not be the # "large" complement polygon on the sphere? return self.to_sky(wcs).to_spherical_sky() @property def bounding_box(self): xmin = self.vertices.x.min() xmax = self.vertices.x.max() ymin = self.vertices.y.min() ymax = self.vertices.y.max() return RegionBoundingBox.from_float(xmin, xmax, ymin, ymax) def to_mask(self, mode='center', subpixels=5): self._validate_mode(mode, subpixels) if mode == 'center': mode = 'subpixels' subpixels = 1 use_exact = 0 if mode == 'subpixels' else 1 # Find bounding box and mask size bbox = self.bounding_box ny, nx = bbox.shape # Find position of pixel edges and recenter so that circle is at # origin xmin = float(bbox.ixmin) - 0.5 xmax = float(bbox.ixmax) - 0.5 ymin = float(bbox.iymin) - 0.5 ymax = float(bbox.iymax) - 0.5 vx = np.asarray(self.vertices.x, dtype=float) vy = np.asarray(self.vertices.y, dtype=float) fraction = polygon_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, vx, vy, use_exact, subpixels) return RegionMask(fraction, bbox=bbox) def as_artist(self, origin=(0, 0), **kwargs): """ Return a matplotlib patch object for this region (`matplotlib.patches.Polygon`). Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. **kwargs : dict Any keyword arguments accepted by `~matplotlib.patches.Polygon`. These keywords will override any visual meta attributes of this region. Returns ------- artist : `~matplotlib.patches.Polygon` A matplotlib polygon patch. """ from matplotlib.patches import Polygon xy = np.vstack([self.vertices.x - origin[0], self.vertices.y - origin[1]]).transpose() mpl_kwargs = self.visual.define_mpl_kwargs(self._mpl_artist) mpl_kwargs.update(kwargs) return Polygon(xy=xy, **mpl_kwargs) def rotate(self, center, angle): """ Rotate the region. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `~regions.PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- region : `PolygonPixelRegion` The rotated region (which is an independent copy). """ vertices = self.vertices.rotate(center, angle) return self.copy(vertices=vertices) class RegularPolygonPixelRegion(PolygonPixelRegion): """ A regular polygon in pixel coordinates. .. note:: This class will be serialized as a generic polygon region, thus when read back in it will produce a `~regions.PolygonPixelRegion` object instead of a `~regions.RegularPolygonPixelRegion` object. Parameters ---------- center : `~regions.PixCoord` The position of the center of the polygon. nvertices : `~regions.PixCoord` The number of polygon vertices (or sides). radius : float The distance from the center to any vertex. This is also known as the circumradius. angle : `~astropy.units.Quantity`, optional The rotation angle of the polygon, measured anti-clockwise. If set to zero (the default), the polygon will point "up" following the `matplotlib.patches.RegularPolygon` convention. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. Attributes ---------- side_length : float The side length. inradius : float The radius of the largest circle contained entirely within the polygon. This value is identical to the length of a line segment from the polygon center to the midpoint of one of its sides (known as as the apothem). perimeter : float The polygon perimeter. interior_angle : float The polygon interior angle, which is the angle at each vertex on the inside of the polygon. exterior_angle : float The polygon exterior angle, which is an angle at each vertex on the outside of the polygon. Examples -------- .. plot:: :include-source: import astropy.units as u import matplotlib.pyplot as plt from regions import PixCoord, RegularPolygonPixelRegion fig, ax = plt.subplots() center = PixCoord(x=50, y=50) reg1 = RegularPolygonPixelRegion(center, 6, 15) reg1.plot(edgecolor='red', lw=2) center = PixCoord(x=25, y=25) reg2 = RegularPolygonPixelRegion(center, 3, 15) reg2.plot(edgecolor='green', lw=2) center = PixCoord(x=25, y=75) reg3 = RegularPolygonPixelRegion(center, 3, 15, angle=25 * u.deg) reg3.plot(edgecolor='orange', lw=2) center = PixCoord(x=75, y=75) reg4 = RegularPolygonPixelRegion(center, 8, 15) reg4.plot(edgecolor='blue', lw=2) center = PixCoord(x=75, y=25) reg5 = RegularPolygonPixelRegion(center, 5, 15) reg5.plot(edgecolor='magenta', lw=2) ax.set_xlim(0, 100) ax.set_ylim(0, 100) ax.set_aspect('equal') """ _params = ('center', 'nvertices', 'radius', 'angle') center = ScalarPixCoord('The center pixel position as a |PixCoord|.') nvertices = PositiveScalar('The number of polygon vertices.') radius = PositiveScalar('The distance from the center to any vertex in ' 'pixels as a float.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a ' '|Quantity| angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, nvertices, radius, angle=0. * u.deg, meta=None, visual=None): if nvertices < 3: raise ValueError('nvertices must be >= 3') self.center = center self.nvertices = nvertices self.radius = radius self.angle = angle super().__init__(self._calc_vertices(), meta=meta, visual=visual) self.side_length = 2. * self.radius * np.sin(np.pi / self.nvertices) self.inradius = self.radius * np.cos(np.pi / self.nvertices) self.perimeter = self.side_length * self.nvertices self.interior_angle = ((self.nvertices - 2) / self.nvertices * 180) << u.deg self.exterior_angle = 360. / self.nvertices << u.deg def _calc_vertices(self): # uses the matplotlib convention that the polygon always points "up" theta = ((2. * np.pi / self.nvertices * np.arange(self.nvertices) + (np.pi / 2)) << u.radian) + self.angle xvert = self.radius * np.cos(theta) yvert = self.radius * np.sin(theta) return self.center + PixCoord(xvert, yvert) def rotate(self, center, angle): """ Rotate the region. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `~regions.PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- region : `PolygonPixelRegion` The rotated region (which is an independent copy). """ center = self.center.rotate(center, angle) angle = self.angle + angle return self.copy(center=center, angle=angle) def to_polygon(self): """ Return a `PolygonPixelRegion` of this region. """ return PolygonPixelRegion(self.vertices, origin=PixCoord(0, 0), meta=self.meta.copy(), visual=self.visual.copy()) class PolygonSkyRegion(SkyRegion): """ A polygon defined using vertices in sky coordinates. Parameters ---------- vertices : `~astropy.coordinates.SkyCoord` The vertices of the polygon. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('vertices',) vertices = OneDSkyCoord('The vertices of the polygon as a |SkyCoord| ' 'array.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, vertices, meta=None, visual=None): self.vertices = vertices self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() def to_pixel(self, wcs): x, y = wcs.world_to_pixel(self.vertices) vertices_pix = PixCoord(x, y) return PolygonPixelRegion(vertices_pix, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform( wcs, boundary_distortions) if boundary_distortions: # Requires planar to spherical projection (using WCS) # and discretization # Will require implementing discretization in pixel space # to get correct handling of distortions. raise NotImplementedError # ### Potential solution: # # Leverage polygon class to_spherical_sky() functionality without # # distortions, as the distortions were already # # computed in creating # # that polygon approximation # return self.to_pixel(wcs) # .discretize_boundary(n_vertices=n_vertices) # .to_spherical_sky( # wcs=wcs, boundary_distortions=False # ) # TODO: ensure vertices are in CW order, # as implicitly a planar -> spherical polygon will not be the # "large" complement polygon on the sphere? return PolygonSphericalSkyRegion( self.vertices, self.meta.copy(), self.visual.copy(), ) class PolygonSphericalSkyRegion(SphericalSkyRegion): """ A spherical polygon defined using vertices in sky coordinates. Currently, this class only supports convex spherical polygons. (Note that non-convex polygons will have incorrect "contains" logic.) Parameters ---------- vertices : `~astropy.coordinates.SkyCoord` The vertices of the polygon. Assumed to be in CW order. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('vertices',) vertices = OneDSkyCoord('The vertices of the polygon as a |SkyCoord| ' 'array.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, vertices, meta=None, visual=None): self.vertices = vertices self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() @property def _edge_circs(self): """ Get list of the great circles defining the polygon boundaries. """ from .circle import CircleSphericalSkyRegion gcs = [] for i in range(len(self.vertices)): # verts are in CW order: Cross product to get bounding # great circle centers # Compute GCs and stack into a compound set: c_gc = cross_product_skycoord2skycoord( self.vertices[i - 1], self.vertices[i]) gcs.append(CircleSphericalSkyRegion(c_gc, 90 * u.deg)) return gcs @property def _compound_region(self): # Need N great circles to define boundaries for an N-sided # polygon. # verts are in CW order: Cross product to get bounding great # circle centers. # Compute GCs and stack into a compound set. compreg = None gcs = self._edge_circs for gc in gcs: if compreg is None: compreg = gc else: compreg = CompoundSphericalSkyRegion( compreg, gc, operator.and_, self.meta, self.visual, ) return compreg @property def centroid(self): """ Region centroid. Defined as the point equidistant from all vertices. However, if this is not contained within the polygon, instead use the average of the vertices' positions. """ # Calculate from cross products of vertices with cartesian # representation verts are in CW order centroid_mindist = self.centroid_mindist if not self.contains(centroid_mindist): return self.centroid_avg return centroid_mindist @property def centroid_mindist(self): """ Region centroid, defined as the point equidistant from all vertices (minimum distance). """ # Calculate from cross products of vertices with cartesian # representation verts are in CW order return cross_product_sum_skycoord2skycoord(self.vertices) @property def centroid_avg(self): """ Region centroid, taking the average of the vertices' coordinates. """ verts_sph = self.vertices.represent_as('spherical') lons = verts_sph.lon lats = verts_sph.lat lon = circmean(lons) lat = np.average(lats) return SkyCoord(lon, lat, frame=self.frame) @property def bounding_circle(self): from .circle import CircleSphericalSkyRegion cent = self.centroid seps = cent.separation(self.vertices) return CircleSphericalSkyRegion(center=cent, radius=np.max(seps)) @property def bounding_lonlat(self): lons_arr, lats_arr = get_edge_raw_lonlat_bounds_circ_edges( self.vertices, self.centroid, self._edge_circs, ) lons_arr, lats_arr = self._validate_lonlat_bounds(lons_arr, lats_arr) return lons_arr, lats_arr def contains(self, coord): return self._compound_region.contains(coord) def covers(self, coord): return self._compound_region.covers(coord) def transform_to(self, frame, merge_attributes=True): frame = self._validate_frame_transformation(frame) # Only center transforms, radii preserved verts_transf = self.vertices.transform_to( frame, merge_attributes=merge_attributes) return PolygonSphericalSkyRegion( verts_transf, self.meta.copy(), self.visual.copy(), ) def discretize_boundary(self, *, n_vertices=100): bound_verts = discretize_all_edge_boundaries( self.vertices, self._edge_circs, n_vertices=n_vertices, ) return PolygonSphericalSkyRegion(bound_verts, meta=self.meta.copy(), visual=self.visual.copy()) def to_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform( wcs, boundary_distortions) if boundary_distortions: # Requires spherical to planar projection (from WCS) # and discretization # Use to_pixel(), then apply "small angle approx" to get planar # sky. return self.to_pixel( boundary_distortions=boundary_distortions, wcs=wcs, n_vertices=n_vertices, ).to_sky(wcs) return PolygonSkyRegion( self.vertices, meta=self.meta, visual=self.visual, ) def to_pixel(self, wcs, *, boundary_distortions=False, n_vertices=None): self._validate_planar_spherical_transform( wcs, boundary_distortions) if boundary_distortions: # Requires spherical to planar projection (from WCS) and # discretization disc_kwargs = ( {} if n_vertices is None else {'n_vertices': n_vertices}) verts = wcs.world_to_pixel( self.discretize_boundary(**disc_kwargs).vertices, ) return PolygonPixelRegion( PixCoord(*verts), meta=self.meta.copy(), visual=self.visual.copy(), ) return self.to_sky().to_pixel(wcs) astropy-regions-f4139aa/regions/shapes/range.py000066400000000000000000001070261521703212700216760ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines a range spherical sky region (bounded by longitude and/or latitude ranges). """ import copy import operator import astropy.units as u import numpy as np from astropy.coordinates import Angle, SkyCoord from astropy.stats import circmean from regions._utils.spherical_helpers import ( cross_product_skycoord2skycoord, cross_product_sum_skycoord2skycoord, discretize_all_edge_boundaries, get_edge_raw_lonlat_bounds_circ_edges) from regions.core.attributes import TwoValAngleorNone from regions.core.compound import CompoundSphericalSkyRegion from regions.core.core import ComplexSphericalSkyRegion from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord from regions.shapes.annulus import CircleAnnulusSphericalSkyRegion from regions.shapes.circle import CircleSphericalSkyRegion from regions.shapes.lune import LuneSphericalSkyRegion from regions.shapes.polygon import (PolygonPixelRegion, PolygonSphericalSkyRegion) from regions.shapes.whole_sky import WholeSphericalSkyRegion __all__ = ['RangeSphericalSkyRegion'] class RangeSphericalSkyRegion(ComplexSphericalSkyRegion): """ Sky Region defined within a range of longitude and/or latitudes. At least one set of longitude or latitude bounds must be set. If latitude_range[0] > latitude_range[1], will wrap over the poles (eg, will *exclude* the central latitude range.) Parameters ---------- frame : `~astropy.coordinates.BaseCoordinateFrame` The coordinate frame for the specified range region. longitude_range : length 2 list-like of `~astropy.coordinates.Angle` or `~astropy.units.Quantity` or None Longitude range in region. Spans from first to second entries (in increasing longitude value), so inverting the order will select the complement longitude range. Longitude values must be within [0, 360] degrees (or equivalent). latitude_range : length 2 list-like of `~astropy.coordinates.Angle` or `~astropy.units.Quantity` or None Latitude range in region. Spans from first to second entries (in increasing latitude value), so inverting the order will select the complement latitude range. I.e., if latitude_range[0] > latitude_range[1], will wrap over the poles, and will instead exclude the central latitude range. Longitude values must be within [-90, 90] degrees (or equivalent). meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. **kwargs : Keyword arguments Additional keyword arguments passed when created new instances after coordinate transformations. """ _params = ('frame', 'longitude_range', 'latitude_range') _boundaries = ('longitude_bounds', 'latitude_bounds') longitude_range = TwoValAngleorNone( 'The range of longitude values, as a length-2 |Quantity|' ' angle or list or as None', ) latitude_range = TwoValAngleorNone( 'The range of latitude values, as a length-2 |Quantity|' ' angle or list or as None', ) def __init__(self, frame=None, longitude_range=None, latitude_range=None, meta=None, visual=None, **kwargs): # Validate inputs if ((longitude_range is None) & (latitude_range is None) & (kwargs.get('_longitude_bounds', None) is None) & (kwargs.get('_latitude_bounds', None) is None)): # Non-frame transformation instantiation is only # intended to be used # by setting longitude_range and latitude_range, so note those in # the error message raise ValueError( 'A range for at least one of longitude and' ' latitude must be set') self.longitude_range = longitude_range self.latitude_range = latitude_range # Validate lon/lat range inputs, to ensure ranges have # expected definitions. # Run this after setting the attribute, which performs # initial validation using # TwoValAngleorNone attribute class self._validate_longitude_range(self.longitude_range) self._validate_latitude_range(self.latitude_range) self._longitude_bounds = None self._latitude_bounds = None self._vertices = None self._is_original_frame = kwargs.pop('_is_original_frame', True) # Check if "_frame" is passed as a kwarg, from a copy _frame = None if kwargs.get('_frame', 'missing') != 'missing': _frame = kwargs.pop('_frame', None) # Passing frame overrides any value passed in "_frame" in the kwargs, # so only if "frame"=None is "_frame" used if frame is None: frame = _frame # Get frame into standard format instance frame = self._standardize_frame(frame) if ( ((longitude_range is None) & (latitude_range is None)) & ((kwargs.get('_longitude_bounds', None) is not None) | (kwargs.get('_latitude_bounds', None) is not None)) ): # Create class by directly passing long/lat bounds # -- for cases when creating transformed # RangeSphericalSkyRegion instances self._longitude_bounds = kwargs.pop('_longitude_bounds', None) self._latitude_bounds = kwargs.pop('_latitude_bounds', None) # Also directly input _vertices, as these are only computed in the # original frame self._vertices = kwargs.pop('_vertices', None) # Also set _params to to just ("frame") for this transformed # instance. self._params = ('frame',) # Validate direct bounds, vertices inputs self._validate_inputs_lonlat_bounds_vertices() # Stash frame in private attribute to access when building on-the-fly # derived attributes & bounds self._frame = frame self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() def __eq__(self, other): # Equality operator for RangeSphericalSkyRegion. # Modified from default, because the lon/lat ranges can be none, # requiring comparison of boundaries. # All Region properties are compared for strict equality except # for Quantity parameters, which allow for different units if they # are directly convertible. if not isinstance(other, self.__class__): return False meta_params = ['meta', 'visual'] intern_params = ['_frame', '_vertices', '_is_original_frame', '_params'] params_list = ( list(self._params) + [f"_{bn}" for bn in list(self._boundaries)] + intern_params + meta_params ) # now check the parameter values # Note that Quantity comparisons allow for different units # if they directly convertible (e.g., 1. * u.deg == 60. * u.arcmin) for param in params_list: # np.any is used for SkyCoord array comparisons if np.any(getattr(self, param) != getattr(other, param)): return False # Note frame comparisons are done directly, so no # catching of TypeError is necessary return True def _validate_longitude_range(self, longitude_range): # Specifying longitude_range as an TwoValAngleorNone # attribute already validates # for input as either length-2 list-like with angular units, or None # Here, only check that if not None, # the values are within [0,360] deg or equivalent. if longitude_range is not None: for i in range(2): if not ((longitude_range[i].to_value(u.deg) >= 0) & (longitude_range[i].to_value(u.deg) <= 360)): raise ValueError( 'Longitude values must be within' ' [0, 360] degrees or equivalent!') def _validate_latitude_range(self, latitude_range): # Specifying latitude_range as an TwoValAngleorNone # attribute already validates # for input as either length-2 list-like with angular units, or None # Here, only check that if not None, # the values are within [-90,90] deg or equivalent. if latitude_range is not None: for i in range(2): if not ((latitude_range[i].to_value(u.deg) >= -90) & (latitude_range[i].to_value(u.deg) <= 90)): raise ValueError( 'Latitude values must be within' ' [-90, 90] degrees or equivalent!') def _validate_inputs_lonlat_bounds_vertices(self): # Validate direct bounds, vertices inputs # Lon bounds can be lune or None if not (isinstance(self._longitude_bounds, LuneSphericalSkyRegion) | (self._longitude_bounds is None)): raise ValueError('Invalid direct longitude bounds input! Must be ' 'LuneSphericalSkyRegion or None.') # Lat bounds can be circle, annulus, compound xor of whole sky and # annulus, or None lat_bounds = self._latitude_bounds valid_lat = ( isinstance(lat_bounds, (CircleAnnulusSphericalSkyRegion, CircleSphericalSkyRegion)) | (isinstance(lat_bounds, CompoundSphericalSkyRegion) & isinstance(getattr(lat_bounds, 'region1', None), WholeSphericalSkyRegion) & isinstance(getattr(lat_bounds, 'region2', None), CircleAnnulusSphericalSkyRegion) & (getattr(lat_bounds, 'operator', None) == operator.xor)) | (lat_bounds is None)) if not valid_lat: raise ValueError('Invalid direct latitude bounds input!' ' Must be one of: CircleSphericalSkyRegion,' ' CircleAnnulusSphericalSkyRegion,' ' CompoundSphericalSkyRegion (as' ' WholeSphericalSkyRegion' ' ^ CircleAnnulusSphericalSkyRegion), or None.') # Vertices can be Skycoord of specified length or None, depending on # bounds if self._bound_nverts > 0: if (isinstance(self._vertices, SkyCoord) & (len(self._vertices) != self._bound_nverts)): raise ValueError( 'Invalid vertices direct input! Inconsistent with bounds.') elif (self._vertices is not None) & (self._bound_nverts == 0): raise ValueError( 'Invalid vertices direct input! Inconsistent with bounds.') # ALWAYS derive boundaries on the fly, IF all _params are not None. # Only a concern for RangeSphericalSkyRegion. def _derive_longitude_bounds(self): # Internal, on-the-fly boundaries determination. # A concern if range values change after original _longitude_bounds # are computed, if these were "static" for all cases if self.longitude_range is None: return None # Define GC centers based on longitude range c1 = SkyCoord(self.longitude_range[0], 0 * u.deg, frame=self.frame) c2 = SkyCoord(self.longitude_range[1], 0 * u.deg, frame=self.frame) # Cross product to get pole. # Invert order if lon_range[1] > lon_range[0] if self.longitude_range[0] > self.longitude_range[1]: c_pole = cross_product_skycoord2skycoord(c2, c1) else: c_pole = cross_product_skycoord2skycoord(c1, c2) # GC centers from cross products center_gc1 = cross_product_skycoord2skycoord(c_pole, c1) center_gc2 = cross_product_skycoord2skycoord(c2, c_pole) c_gc1_sph = center_gc1.represent_as('spherical') c_gc2_sph = center_gc2.represent_as('spherical') # Squash small machine-rounding problems in cross product # latitude should be 0 for these longitude-describing GCs, # as they pass through the poles. # Only possible in the original frame, when defining from # longitude_range center_gc1 = SkyCoord(c_gc1_sph.lon, 0 * u.deg, frame=self.frame) center_gc2 = SkyCoord(c_gc2_sph.lon, 0 * u.deg, frame=self.frame) return LuneSphericalSkyRegion( center_gc1, center_gc2, self.meta, self.visual, ) def _derive_latitude_bounds(self): # Internal, on-the-fly boundaries determination. # A concern if range values change after original _latitude_bounds # are computed, if these were "static" for all cases if self.latitude_range is None: return None # Define north pole: 0 lon, 90 lat c_pole = SkyCoord(0 * u.deg, 90 * u.deg, frame=self.frame) # Define south pole c_pole_s = SkyCoord(0 * u.deg, -90 * u.deg, frame=self.frame) lat_arr = self.latitude_range.copy() # Logic if this is a "across-polar" region is handled by swapping # the logic of the latitude boundary itself _wraps_poles = False if lat_arr[0] > lat_arr[1]: _wraps_poles = True lat_arr = lat_arr[::-1] inner_rad = 90.0 * u.deg - lat_arr[1] outer_rad = 90.0 * u.deg - lat_arr[0] # Special handling: check if either edge of the range is -90 or 90 if ( (lat_arr[1] == Angle(90 * u.deg)) & (lat_arr[0] == Angle(-90 * u.deg)) ): # Whole sky: no constraint return None elif lat_arr[1] == Angle(90 * u.deg): if _wraps_poles: # Check if it wrapped pole, with input [90*u.deg, X*u.deg] # equivalent to [-90*u.deg, X*u.deg], and is a # simple circle centered at S pole. lat_bounds = CircleSphericalSkyRegion( c_pole_s, lat_arr[0] + 90.0 * u.deg, self.meta, self.visual, ) else: # Simple circle case lat_bounds = CircleSphericalSkyRegion( c_pole, outer_rad, self.meta, self.visual, ) return lat_bounds elif lat_arr[0] == Angle(-90 * u.deg): if _wraps_poles: # Check if it wrapped pole, with input [X*u.deg, -90*u.deg], # equivalent to [X*u.deg, 90*u.deg], and is a simple circle # Radius from input X*u.deg constraint is in inner_rad after # swap lat_bounds = CircleSphericalSkyRegion( c_pole, inner_rad, self.meta, self.visual, ) else: # Simple circle centered at S pole lat_bounds = CircleSphericalSkyRegion( c_pole_s, lat_arr[1] + 90.0 * u.deg, self.meta, self.visual, ) return lat_bounds # True annular range if _wraps_poles: # Negate by passing as xor with whole sky return CompoundSphericalSkyRegion( WholeSphericalSkyRegion(), CircleAnnulusSphericalSkyRegion( c_pole, inner_rad, outer_rad, self.meta, self.visual, ), operator.xor, self.meta, self.visual, ) # Not wrapping poles return CircleAnnulusSphericalSkyRegion( c_pole, inner_rad, outer_rad, self.meta, self.visual, ) @property def _bound_nverts(self): # Number of vertices for boundary # lat-only: 0 (annulus) # lon-only: 2 (lune) # both: 4 (range), unless one touches the pole: 3 # Only compute params once lon_bounds = self.longitude_bounds lat_bounds = self.latitude_bounds if (lon_bounds is not None) & (lat_bounds is not None): if isinstance(lat_bounds, CompoundSphericalSkyRegion): nverts = 6 # pole-wrapping gives 2 disjoint triangles elif isinstance(lat_bounds, CircleAnnulusSphericalSkyRegion): # 4 vertices nverts = 4 elif isinstance(lat_bounds, CircleSphericalSkyRegion): nverts = 3 elif (lon_bounds is not None) & (lat_bounds is None): # 2 vertices nverts = 2 else: # Only lat_bounds: 0 vertices nverts = 0 return nverts @property def _edge_circs(self): """ Get list of the circles defining the range boundaries. Only used if vertices=4, otherwise lune/annulus properties used. """ bound_nverts = self._bound_nverts if bound_nverts == 6: raise NotImplementedError if bound_nverts == 4: return [ self.longitude_bounds._circle_1, self.latitude_bounds._outer_region, self.longitude_bounds._circle_2, self.latitude_bounds._inner_region, ] if bound_nverts == 3: return [ self.longitude_bounds._circle_1, self.latitude_bounds, self.longitude_bounds._circle_2, ] @property def _compound_region(self): if self.longitude_bounds is None: return self.latitude_bounds if self.latitude_bounds is None: return self.longitude_bounds return CompoundSphericalSkyRegion( self.longitude_bounds, self.latitude_bounds, operator.and_, self.meta, self.visual, ) @property def longitude_bounds(self): """ Longitude bounds of the range spherical sky region. Defined as a spherical lune, or None if no longitude bound set. """ # If set, get bounds from internal attribute if getattr(self, '_longitude_bounds', None) is not None: return self._longitude_bounds # Otherwise, check whether range is set. # If set, derive bounds on-the-fly elif self.longitude_range is not None: return self._derive_longitude_bounds() # If both are set to None, then the bounds are intended to be not set else: return None @property def latitude_bounds(self): """ Latitude bounds of the range spherical sky region. Defined as a spherical circular annulus, a circle (if ending at a pole), or None if no latitude bound set. """ # If set, get bounds from internal attribute if getattr(self, '_latitude_bounds', None) is not None: return self._latitude_bounds # Otherwise, check whether range is set. # If set, derive bounds on-the-fly elif self.latitude_range is not None: return self._derive_latitude_bounds() # If both are set to None, then the bounds are intended to be not set else: return None @property def centroid(self): """ Region centroid. Defined as the point equidistant from all vertices, if both longitude and latitude bounds are set. However, if this point falls outside of the region (as in cases very "long and narrow" ranges), the centroid is instead approximated as the average of the longitude and latitudes of all vertices. If only longitude or only latitude bounds are set, the centroid is taken as the centroid of that boundary shape (i.e., the lune centroid or annulus center). """ return self.centroid_mindist @property def centroid_avg(self): """ An approximate "centroid", taking the average of the vertices longitude / latitudes, and invert as needed to define an "inside" point for a wide angle longitude range. Will behave poorly for regions crossing the poles. """ bound_nverts = self._bound_nverts if bound_nverts == 6: raise NotImplementedError if (bound_nverts == 4) | (bound_nverts == 3): # Both lon and lat bounds verts_sph = self.vertices.represent_as('spherical') lons = verts_sph.lon lats = verts_sph.lat lon = circmean(lons) lat = np.average(lats) centroid = SkyCoord(lon, lat, frame=self.frame) if ( (not self.contains(centroid)) and ( not self.longitude_bounds.contains(centroid) ) ): # If centroid not contained in range, or in subset # longitude bounds, modify longitude by adding 180 deg centroid = SkyCoord(lon + 180 * u.deg, lat, frame=self.frame) return centroid elif bound_nverts == 2: # Only lon bounds; just get centroid of lune return self.longitude_bounds.centroid elif bound_nverts == 0: # Only lat_bounds: just get center of annulus return self.latitude_bounds.center @property def centroid_mindist(self): """ Region centroid. Defined as the point equidistant from all vertices. """ bound_nverts = self._bound_nverts if bound_nverts == 6: raise NotImplementedError if (bound_nverts == 4) | (bound_nverts == 3): # If both lon/lat bounds, calculate from sum of cross # products of vertices with cartesian representation; verts # are in CW order centroid_mindist = cross_product_sum_skycoord2skycoord( self.vertices) if ((not self.contains(centroid_mindist)) and (not self.latitude_bounds.contains(centroid_mindist))): # If it's not contained, check if it's because it's # outside the latitude range; if so, instead just use avg # centroid. # Problem of very elongated bounds at high latitude -- # end up outside range. If so, fall back to average. return self.centroid_avg return centroid_mindist elif bound_nverts == 2: # Only lon bounds; just get centroid of lune return self.longitude_bounds.centroid elif bound_nverts == 0: # Only lat_bounds; just get center of annulus return self.latitude_bounds.center @property def vertices(self): """ Region vertices. The result depends on which ranges are set. If both longitude and latitude ranges are set, returns 4 vertices (the "corners" of the composite range) as a SkyCoord. If only a longitude range is set, returns 2 vertices (the vertices of the longitude spherical lune boundary) as a SkyCoord. If only latitude ranges are set, returns None (no vertices for the spherical circular annulus latitude bounds). """ if getattr(self, '_vertices', None) is not None: return self._vertices else: bound_nverts = self._bound_nverts if bound_nverts == 6: raise NotImplementedError if bound_nverts == 4: # Both lon/lat bounds: on the fly construct from # the lon/lat range attrib # Longitude min is on the right, so derive CW from lower right # corner. # Note this is never called for transformed instances, # which have vertices info stashed in _vertices. verts_lon = [ self.longitude_range[0], self.longitude_range[1], self.longitude_range[1], self.longitude_range[0], ] verts_lat = [ self.latitude_range[0], self.latitude_range[0], self.latitude_range[1], self.latitude_range[1], ] return SkyCoord(verts_lon, verts_lat, frame=self.frame) elif bound_nverts == 3: # Both lon/lat bounds: on the fly construct from # the lon/lat range attrib # Longitude min is on the right, so derive CW # from lower right corner. # However, ONE of the vertices is on a pole. # Note this is never called for transformed instances, # which have vertices info stashed in _vertices. if np.abs(self.latitude_range[0].to_value(u.deg)) == 90: verts_lon = [ self.longitude_range[0], self.longitude_range[1], self.longitude_range[0], ] verts_lat = [ self.latitude_range[0], self.latitude_range[1], self.latitude_range[1], ] elif np.abs(self.latitude_range[1].to_value(u.deg)) == 90: verts_lon = [ self.longitude_range[0], self.longitude_range[1], self.longitude_range[0], ] verts_lat = [ self.latitude_range[0], self.latitude_range[0], self.latitude_range[1], ] return SkyCoord(verts_lon, verts_lat, frame=self.frame) elif bound_nverts == 2: # 2 vertices: directly return from lune itself. return self.longitude_bounds.vertices elif bound_nverts == 0: # Only lat_bounds: no vertices return None @property def bounding_circle(self): bound_nverts = self._bound_nverts if bound_nverts == 0: # No vertices: only an annulus # Use the annulus bounding circle. return self.latitude_bounds.bounding_circle elif bound_nverts == 2: # Lune: get lune bounding circle return self.longitude_bounds.bounding_circle elif bound_nverts == 4: # Quadrilateral: use max of seps to vertices cent = self.centroid_mindist seps = cent.separation(self.vertices) return CircleSphericalSkyRegion(center=cent, radius=np.max(seps)) elif bound_nverts == 6: raise NotImplementedError @property def bounding_lonlat(self): bound_nverts = self._bound_nverts if bound_nverts == 0: # Only an annulus: Use the annulus bounding lonlat. return self.latitude_bounds.bounding_lonlat if bound_nverts == 2: # Lune: get lune bounding lonlat return self.longitude_bounds.bounding_lonlat if (bound_nverts == 4) | (bound_nverts == 3): # Only used for both lon+lat bounds, # otherwise directly calls lon or lat bounding_lonlat() lons_arr, lats_arr = get_edge_raw_lonlat_bounds_circ_edges( self.vertices, self.centroid, self._edge_circs, ) # If original frame: keep lon bounds if touching a pole # Only validate and change if not the original frame # (eg, when the region could cap the pole) if not self._is_original_frame: lons_arr, lats_arr = self._validate_lonlat_bounds( lons_arr, lats_arr) return lons_arr, lats_arr if bound_nverts == 6: raise NotImplementedError def _containment(self, coord, covers=False): if self._is_original_frame: # Just compute directly from lon/lat ranges # Ensure coordinate is in same frame c_repr = coord.represent_as('spherical') # Longitude range constraints if self.longitude_range is not None: # Ensure lon range is wrapped at 360, matching internal # SkyCoord convention. To handle possible cases of lon # ranges "wrapping around" across the standard 360->0 # wrap, wrap lon ranges + coord values around the first # entry angle wrap_ang = self.longitude_range[0] coord_lons = c_repr.lon.wrap_at(wrap_ang) in_range_lon = ( (coord_lons >= Angle( self.longitude_range[0]).wrap_at(wrap_ang)) & (coord_lons <= Angle( self.longitude_range[1]).wrap_at(wrap_ang)) ) # No constraints elif coord.isscalar: in_range_lon = True else: in_range_lon = np.ones(coord.shape, dtype=bool) # Latitude range constraints if self.latitude_range is not None: coord_lat = c_repr.lat # Check if latitude ranges are "simpler". # Equivalent to no constraints, or simpler circles. if ( (self.latitude_range[1] == Angle(90 * u.deg)) & (self.latitude_range[0] == Angle(-90 * u.deg)) ): # Whole sky if coord.isscalar: in_range_lat = True else: in_range_lat = np.ones(coord.shape, dtype=bool) elif ( self.latitude_range[1] == Angle(90 * u.deg) ): # Range includes latitude up to N pole in_range_lat = coord_lat >= self.latitude_range[0] elif ( self.latitude_range[0] == Angle(-90 * u.deg) ): # Range includes latitudes down to S pole in_range_lat = coord_lat <= self.latitude_range[1] else: # Actual annulus in_range_lat = ( (coord_lat >= self.latitude_range[0]) & (coord_lat <= self.latitude_range[1]) ) # No constraints elif coord.isscalar: in_range_lat = True else: in_range_lat = np.ones(coord.shape, dtype=bool) return in_range_lon & in_range_lat # If transformed: must use boundary compound region logic if covers: return self._compound_region.covers(coord) return self._compound_region.contains(coord) def contains(self, coord): return self._containment(coord, covers=False) def covers(self, coord): return self._containment(coord, covers=True) def transform_to(self, frame, merge_attributes=True): frame = self._validate_frame_transformation(frame) transformed = {} # Internal attribute noting this is a transformed frame. transformed['_is_original_frame'] = False fields = list(self._params) for field in fields: # High-level lon/lat attributes will not transform: set to None transformed[field] = None boundaries = list(self._boundaries) for bound in boundaries: # Will be passing directly into internal attributes # _longitude_bounds, _latitude_bounds bound_orig = getattr(self, bound) if bound_orig is not None: transformed[f"_{bound}"] = bound_orig.transform_to( frame, merge_attributes=merge_attributes, ) else: transformed[f"_{bound}"] = bound_orig # Also transform vertices for field in ['_vertices']: verts = copy.deepcopy(getattr(self, field)) if (verts is None) and self._is_original_frame: # If internal attribute not set, and original frame, # get the on-the-fly-derived verts. verts = self.vertices if verts is not None: transformed[field] = verts.transform_to( frame, merge_attributes=merge_attributes, ) else: transformed[field] = None # Add transformed frame to transformed dict transformed['frame'] = frame return self.__class__(**transformed) def discretize_boundary(self, *, n_vertices=100): bound_nverts = self._bound_nverts if bound_nverts == 0: return self.latitude_bounds.discretize_boundary( n_vertices=n_vertices) elif bound_nverts == 2: return self.longitude_bounds.discretize_boundary( n_vertices=n_vertices) elif (bound_nverts == 4) | (bound_nverts == 3): bound_verts = discretize_all_edge_boundaries( self.vertices, self._edge_circs, n_vertices=n_vertices, ) return PolygonSphericalSkyRegion( bound_verts, meta=self.meta.copy(), visual=self.visual.copy()) elif bound_nverts == 6: raise NotImplementedError def to_polygon(self, *, n_vertices=100): """ Return a `~regions.PolygonSphericalSkyRegion` that approximates this longitude/latitude range. Parameters ---------- n_vertices : int, optional The number of polygon vertices. Default is 100. Returns ------- polygon : `~regions.PolygonSphericalSkyRegion` A polygon region approximating the range. """ return self.discretize_boundary(n_vertices=n_vertices) def to_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): if not boundary_distortions: raise ValueError( 'Invalid parameter: `boundary_distortions=False`!\n' 'Transforming range to planar sky region is only possible by ' 'including boundary distortions.', ) self._validate_planar_spherical_transform( wcs, boundary_distortions) # Requires spherical to planar projection (from WCS) and discretization # Use to_pixel(), then apply "small angle approx" to get planar sky. return self.to_pixel( boundary_distortions=boundary_distortions, wcs=wcs, n_vertices=n_vertices, ).to_sky(wcs) def to_pixel(self, wcs, *, boundary_distortions=False, n_vertices=None): if not boundary_distortions: raise ValueError( 'Invalid parameter: `boundary_distortions=False`!\n' 'Transforming range to planar pixel region is' ' only possible by ' 'including boundary distortions.', ) self._validate_planar_spherical_transform( wcs, boundary_distortions) disc_kwargs = {} if n_vertices is None else {'n_vertices': n_vertices} # Requires spherical to planar projection (from WCS) and discretization disc_bound = self.discretize_boundary(**disc_kwargs) # Anticipating complex, wrapped over the poles case if isinstance(disc_bound, CompoundSphericalSkyRegion): return disc_bound.to_pixel(wcs) verts = wcs.world_to_pixel(disc_bound.vertices) return PolygonPixelRegion( PixCoord(*verts), meta=self.meta.copy(), visual=self.visual.copy(), ) astropy-regions-f4139aa/regions/shapes/rectangle.py000066400000000000000000000420771521703212700225520ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines rectangular regions in both pixel and sky coordinates. """ import astropy.units as u import numpy as np from astropy.coordinates import Angle from regions._geometry import rectangle_overlap_grid from regions._utils.wcs_helpers import (pixel_shape_to_sky_svd, sky_shape_to_pixel_svd) from regions.core.attributes import (PositiveScalar, PositiveScalarAngle, RegionMetaDescr, RegionVisualDescr, ScalarAngle, ScalarPixCoord, ScalarSkyCoord) from regions.core.bounding_box import RegionBoundingBox from regions.core.core import PixelRegion, SkyRegion from regions.core.mask import RegionMask from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord from regions.shapes.polygon import PolygonPixelRegion __all__ = ['RectanglePixelRegion', 'RectangleSkyRegion'] class RectanglePixelRegion(PixelRegion): """ A rectangle in pixel coordinates. Parameters ---------- center : `~regions.PixCoord` The position of the center of the rectangle. width : float The width of the rectangle (before rotation) in pixels. height : float The height of the rectangle (before rotation) in pixels. angle : `~astropy.units.Quantity`, optional The rotation angle of the rectangle, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the x axis. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from astropy.coordinates import Angle from regions import PixCoord, RectanglePixelRegion fig, ax = plt.subplots() reg = RectanglePixelRegion(PixCoord(x=15, y=10), width=8, height=5, angle=Angle(30, 'deg')) patch = reg.plot(ax=ax, facecolor='none', edgecolor='red', lw=2, label='Rectangle') ax.legend(handles=(patch,), loc='upper center') ax.set_xlim(0, 30) ax.set_ylim(0, 20) ax.set_aspect('equal') """ _params = ('center', 'width', 'height', 'angle') _mpl_artist = 'Patch' center = ScalarPixCoord('The center pixel position as a |PixCoord|.') width = PositiveScalar('The width of the rectangle (before rotation) in ' 'pixels as a float.') height = PositiveScalar('The height of the rectangle (before rotation) ' 'in pixels as a float.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a ' '|Quantity| angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, width, height, angle=0 * u.deg, meta=None, visual=None): self.center = center self.width = width self.height = height self.angle = angle self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() @property def area(self): return self.width * self.height def _containment(self, pixcoord, covers=False): pixcoord = PixCoord._validate(pixcoord, name='pixcoord') cos_angle = np.cos(self.angle) sin_angle = np.sin(self.angle) dx = pixcoord.x - self.center.x dy = pixcoord.y - self.center.y dx_rot = cos_angle * dx + sin_angle * dy dy_rot = sin_angle * dx - cos_angle * dy if covers: return ((np.abs(dx_rot) <= self.width * 0.5) & (np.abs(dy_rot) <= self.height * 0.5)) return ((np.abs(dx_rot) < self.width * 0.5) & (np.abs(dy_rot) < self.height * 0.5)) def contains(self, pixcoord): return self._containment(pixcoord, covers=False) def covers(self, pixcoord): return self._containment(pixcoord, covers=True) def to_sky(self, wcs): # The photutils SVD helpers measure the sky rotation as a # position angle (PA) from North; regions measures it from the # RA axis. Convert between them with a 90 deg offset. center, sky_width, sky_height, angle = pixel_shape_to_sky_svd( (self.center.x, self.center.y), wcs, self.width, self.height, self.angle.to_value(u.radian)) angle = (angle + 90 * u.deg).wrap_at(360 * u.deg) width = Angle(sky_width, 'arcsec') height = Angle(sky_height, 'arcsec') return RectangleSkyRegion(center, width, height, angle=angle, meta=self.meta.copy(), visual=self.visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): raise NotImplementedError @property def bounding_box(self): w2 = self.width / 2. h2 = self.height / 2. cos_angle = np.cos(self.angle) # self.angle is a Quantity sin_angle = np.sin(self.angle) # self.angle is a Quantity dx1 = abs(w2 * cos_angle - h2 * sin_angle) dy1 = abs(w2 * sin_angle + h2 * cos_angle) dx2 = abs(w2 * cos_angle + h2 * sin_angle) dy2 = abs(w2 * sin_angle - h2 * cos_angle) dx = max(dx1, dx2) dy = max(dy1, dy2) xmin = self.center.x - dx xmax = self.center.x + dx ymin = self.center.y - dy ymax = self.center.y + dy return RegionBoundingBox.from_float(xmin, xmax, ymin, ymax) def to_mask(self, mode='center', subpixels=5): # NOTE: assumes this class represents a single rectangle self._validate_mode(mode, subpixels) if mode == 'center': mode = 'subpixels' subpixels = 1 # Find bounding box and mask size bbox = self.bounding_box ny, nx = bbox.shape # Find position of pixel edges and recenter so that circle is at # origin xmin = float(bbox.ixmin) - 0.5 - self.center.x xmax = float(bbox.ixmax) - 0.5 - self.center.x ymin = float(bbox.iymin) - 0.5 - self.center.y ymax = float(bbox.iymax) - 0.5 - self.center.y use_exact = 0 if mode == 'subpixels' else 1 fraction = rectangle_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, self.width, self.height, self.angle.to_value(u.radian), use_exact, subpixels) return RegionMask(fraction, bbox=bbox) def as_artist(self, origin=(0, 0), **kwargs): """ Return a matplotlib patch object for this region (`matplotlib.patches.Rectangle`). Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. **kwargs : dict Any keyword arguments accepted by `~matplotlib.patches.Rectangle`. These keywords will override any visual meta attributes of this region. Returns ------- artist : `~matplotlib.patches.Rectangle` A matplotlib rectangle patch. """ from matplotlib.patches import Rectangle xy = self._lower_left_xy() xy = xy[0] - origin[0], xy[1] - origin[1] width = self.width height = self.height # matplotlib expects rotation in degrees (anti-clockwise) angle = self.angle.to_value(u.deg) mpl_kwargs = self.visual.define_mpl_kwargs(self._mpl_artist) mpl_kwargs.update(kwargs) return Rectangle(xy=xy, width=width, height=height, angle=angle, **mpl_kwargs) def _update_from_mpl_selector(self, *args, **kwargs): xmin, xmax, ymin, ymax = self._mpl_selector.extents self.center = PixCoord(x=0.5 * (xmin + xmax), y=0.5 * (ymin + ymax)) self.width = (xmax - xmin) self.height = (ymax - ymin) self.angle = 0. * u.deg if self._mpl_selector_callback is not None: self._mpl_selector_callback(self) def as_mpl_selector(self, ax, active=True, sync=True, callback=None, drag_from_anywhere=False, **kwargs): """ Return a matplotlib editable widget for the region (`matplotlib.widgets.RectangleSelector`). Parameters ---------- ax : `~matplotlib.axes.Axes` The matplotlib axes to add the selector to. active : bool, optional Whether the selector should be active by default. sync : bool, optional If `True` (the default), the region will be kept in sync with the selector. Otherwise, the selector will be initialized with the values from the region but the two will then be disconnected. callback : callable, optional If specified, this function will be called every time the region is updated. This only has an effect if ``sync`` is `True`. If a callback is set, it is called for the first time once the selector has been created. drag_from_anywhere : bool, optional If `True`, the selector can be moved by clicking anywhere within its bounds, else only at the central anchor (only available with matplotlib 3.5 upwards; default: `False`). **kwargs : dict Additional keyword arguments are passed to `matplotlib.widgets.RectangleSelector`. Returns ------- selector : `matplotlib.widgets.RectangleSelector` The matplotlib selector. Notes ----- Once a selector has been created, you will need to keep a reference to it until you no longer need it. In addition, you can enable/disable the selector at any point by calling ``selector.set_active(True)`` or ``selector.set_active(False)``. """ from matplotlib.widgets import RectangleSelector if hasattr(self, '_mpl_selector'): raise AttributeError( 'Cannot attach more than one selector to a region.') if self.angle.value != 0: raise NotImplementedError('Cannot create matplotlib selector for ' 'rotated rectangle.') if sync: sync_callback = self._update_from_mpl_selector else: def sync_callback(*args, **kwargs): pass rectprops = {'edgecolor': self.visual.get('color', 'black'), 'facecolor': 'none', 'linewidth': self.visual.get('linewidth', 1), 'linestyle': self.visual.get('linestyle', 'solid')} rectprops.update(kwargs.pop('props', dict())) kwargs.update({'props': rectprops}) self._mpl_selector = RectangleSelector( ax, sync_callback, interactive=True, drag_from_anywhere=drag_from_anywhere, **kwargs) self._mpl_selector.extents = (self.center.x - self.width / 2, self.center.x + self.width / 2, self.center.y - self.height / 2, self.center.y + self.height / 2) self._mpl_selector.set_active(active) self._mpl_selector_callback = callback if sync and self._mpl_selector_callback is not None: self._mpl_selector_callback(self) return self._mpl_selector @property def corners(self): """ Return the x, y coordinate pairs that define the corners. """ corners = [(-self.width / 2, -self.height / 2), (self.width / 2, -self.height / 2), (self.width / 2, self.height / 2), (-self.width / 2, self.height / 2), ] rotmat = [[np.cos(self.angle), np.sin(self.angle)], [-np.sin(self.angle), np.cos(self.angle)]] return np.dot(corners, rotmat) + np.array([self.center.x, self.center.y]) def to_polygon(self): """ Return a 4-sided polygon equivalent to this rectangle. """ x, y = self.corners.T vertices = PixCoord(x=x, y=y) return PolygonPixelRegion(vertices=vertices, meta=self.meta.copy(), visual=self.visual.copy()) def _lower_left_xy(self): """ Compute lower left ``xy`` pixel position. This is used for the conversion to matplotlib in ``as_artist``. Taken from http://photutils.readthedocs.io/en/latest/_modules/photutils/ape rture/rectangle.html#RectangularAperture.plot. """ hw = self.width / 2. hh = self.height / 2. sint = np.sin(self.angle) cost = np.cos(self.angle) dx = (hh * sint) - (hw * cost) dy = -(hh * cost) - (hw * sint) x = self.center.x + dx y = self.center.y + dy return x, y def rotate(self, center, angle): """ Rotate the region. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `~regions.PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- region : `RectanglePixelRegion` The rotated region (which is an independent copy). """ center = self.center.rotate(center, angle) angle = self.angle + angle return self.copy(center=center, angle=angle) class RectangleSkyRegion(SkyRegion): """ A rectangle in sky coordinates. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The position of the center of the rectangle. width : `~astropy.units.Quantity` The width of the rectangle (before rotation) as an angle. height : `~astropy.units.Quantity` The height of the rectangle (before rotation) as an angle. angle : `~astropy.units.Quantity`, optional The rotation angle of the rectangle, measured anti-clockwise. If set to zero (the default), the width axis is lined up with the longitude axis of the celestial coordinates. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('center', 'width', 'height', 'angle') center = ScalarSkyCoord('The center position as a |SkyCoord|.') width = PositiveScalarAngle('The width of the rectangle (before rotation) ' 'as a |Quantity| angle.') height = PositiveScalarAngle('The height of the rectangle (before ' 'rotation) as a |Quantity| angle.') angle = ScalarAngle('The rotation angle measured anti-clockwise as a ' '|Quantity| angle.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, width, height, angle=0 * u.deg, meta=None, visual=None): self.center = center self.width = width self.height = height self.angle = angle self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() def to_pixel(self, wcs): # Convert regions sky angle (from RA axis) to photutils PA (from # North) by subtracting 90 deg. center, pix_width, pix_height, angle = sky_shape_to_pixel_svd( self.center, wcs, self.width.to_value(u.arcsec), self.height.to_value(u.arcsec), self.angle.to_value(u.radian) - np.pi / 2) return RectanglePixelRegion(PixCoord(*center), pix_width, pix_height, angle=angle, meta=self.meta.copy(), visual=self.visual.copy()) def to_polygon(self, wcs): """ Return a `~regions.PolygonSkyRegion` equivalent to this rectangle. Parameters ---------- wcs : `~astropy.wcs.WCS` The WCS to use for the sky-to-pixel-to-sky conversion. Returns ------- polygon : `~regions.PolygonSkyRegion` A polygon region equivalent to the rectangle. """ return self.to_pixel(wcs).to_polygon().to_sky(wcs) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): raise NotImplementedError astropy-regions-f4139aa/regions/shapes/tests/000077500000000000000000000000001521703212700213645ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/__init__.py000066400000000000000000000000001521703212700234630ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/data/000077500000000000000000000000001521703212700222755ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/data/example_header.fits000066400000000000000000000132001521703212700261230ustar00rootroot00000000000000SIMPLE = T / conforms to FITS standard BITPIX = 64 / array data type NAXIS = 2 / number of array dimensions NAXIS1 = 1 NAXIS2 = 1 WCSAXES = 2 / Number of coordinate axes CRPIX1 = 100.5 / Pixel coordinate of reference point CRPIX2 = 100.5 / Pixel coordinate of reference point CDELT1 = -0.02 / [deg] Coordinate increment at reference point CDELT2 = 0.02 / [deg] Coordinate increment at reference point CUNIT1 = 'deg ' / Units of coordinate increment and value CUNIT2 = 'deg ' / Units of coordinate increment and value CTYPE1 = 'GLON-CAR' / galactic longitude, plate caree projection CTYPE2 = 'GLAT-CAR' / galactic latitude, plate caree projection CRVAL1 = 0 / [deg] Coordinate value at reference point CRVAL2 = 0 / [deg] Coordinate value at reference point LONPOLE = 0.0 / [deg] Native longitude of celestial pole LATPOLE = 90.0 / [deg] Native latitude of celestial pole BUNIT = '' END astropy-regions-f4139aa/regions/shapes/tests/reference/000077500000000000000000000000001521703212700233225ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference/test_to_mask_circ-mode_center.txt000066400000000000000000000013371521703212700320450ustar00rootroot000000000000000.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 astropy-regions-f4139aa/regions/shapes/tests/reference/test_to_mask_circ-mode_exact.txt000066400000000000000000000013371521703212700316710ustar00rootroot000000000000000.00000000e+00 1.12637824e-01 5.47795293e-01 6.97159714e-01 5.36338125e-01 9.98826171e-02 0.00000000e+00 1.41612171e-01 9.21332849e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.06385082e-01 1.20533937e-01 6.46660408e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 6.10634408e-01 8.43986340e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 8.07960340e-01 7.30305012e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 6.94279012e-01 2.73584439e-01 9.91864462e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.86382743e-01 2.43040158e-01 0.00000000e+00 3.04862210e-01 8.10551293e-01 9.59915714e-01 7.99094125e-01 2.82640956e-01 0.00000000e+00 test_to_mask_circ-mode_subpixels-subpixels_1.txt000066400000000000000000000013371521703212700347600ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 test_to_mask_circ-mode_subpixels-subpixels_18.txt000066400000000000000000000013371521703212700350500ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference0.00000000e+00 1.11111111e-01 5.52469136e-01 7.00617284e-01 5.33950617e-01 9.87654321e-02 0.00000000e+00 1.41975309e-01 9.16666667e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.10493827e-01 1.20370370e-01 6.48148148e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 6.04938272e-01 8.30246914e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 8.17901235e-01 7.31481481e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 6.94444444e-01 2.71604938e-01 9.90740741e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.87654321e-01 2.40740741e-01 0.00000000e+00 3.02469136e-01 8.11728395e-01 9.47530864e-01 7.99382716e-01 2.83950617e-01 0.00000000e+00 test_to_mask_circ-mode_subpixels-subpixels_5.txt000066400000000000000000000013371521703212700347640ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference0.00000000e+00 1.20000000e-01 5.20000000e-01 7.20000000e-01 5.20000000e-01 8.00000000e-02 0.00000000e+00 1.20000000e-01 9.20000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 8.80000000e-01 1.20000000e-01 6.40000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 6.00000000e-01 8.00000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 8.00000000e-01 7.20000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 7.20000000e-01 2.80000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 2.40000000e-01 0.00000000e+00 3.20000000e-01 8.00000000e-01 1.00000000e+00 8.00000000e-01 2.80000000e-01 0.00000000e+00 astropy-regions-f4139aa/regions/shapes/tests/reference/test_to_mask_elli-mode_center.txt000066400000000000000000000024501521703212700320470ustar00rootroot000000000000000.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 astropy-regions-f4139aa/regions/shapes/tests/reference/test_to_mask_elli-mode_exact.txt000066400000000000000000000024501521703212700316730ustar00rootroot000000000000000.00000000e+00 8.01667043e-02 4.87639631e-01 7.30817612e-01 8.06746395e-01 7.45670005e-01 5.55430691e-01 2.27566801e-01 1.38968315e-04 0.00000000e+00 0.00000000e+00 1.41676247e-01 9.08318173e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 7.30547011e-01 1.05109527e-01 0.00000000e+00 5.95752634e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 8.66894487e-01 6.73606729e-02 6.00269727e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 4.91612660e-01 1.87245096e-01 9.74610937e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 6.06039373e-01 0.00000000e+00 2.94605839e-01 9.33077841e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.83823931e-01 2.40883497e-01 0.00000000e+00 0.00000000e+00 8.19102899e-02 5.04782713e-01 8.27328098e-01 9.90006863e-01 1.00000000e+00 9.74313237e-01 7.37734089e-01 2.41844795e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 2.28614469e-02 6.92455973e-02 1.36850721e-02 0.00000000e+00 0.00000000e+00 0.00000000e+00 test_to_mask_elli-mode_subpixels-subpixels_1.txt000066400000000000000000000024501521703212700347620ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 test_to_mask_elli-mode_subpixels-subpixels_18.txt000066400000000000000000000024501521703212700350520ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference0.00000000e+00 8.33333333e-02 4.84567901e-01 7.31481481e-01 8.11728395e-01 7.43827160e-01 5.55555556e-01 2.25308642e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.41975309e-01 9.07407407e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 7.28395062e-01 1.01851852e-01 0.00000000e+00 5.95679012e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 8.61111111e-01 6.48148148e-02 6.01851852e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 4.90740741e-01 1.91358025e-01 9.75308642e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 5.95679012e-01 0.00000000e+00 2.96296296e-01 9.35185185e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.81481481e-01 2.43827160e-01 0.00000000e+00 0.00000000e+00 8.02469136e-02 5.03086420e-01 8.27160494e-01 9.90740741e-01 1.00000000e+00 9.72222222e-01 7.37654321e-01 2.37654321e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 2.46913580e-02 5.55555556e-02 1.54320988e-02 0.00000000e+00 0.00000000e+00 0.00000000e+00 test_to_mask_elli-mode_subpixels-subpixels_5.txt000066400000000000000000000024501521703212700347660ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference0.00000000e+00 8.00000000e-02 4.80000000e-01 7.60000000e-01 8.00000000e-01 7.60000000e-01 5.60000000e-01 2.40000000e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.20000000e-01 8.80000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 7.20000000e-01 1.20000000e-01 0.00000000e+00 5.60000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 8.80000000e-01 8.00000000e-02 5.60000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 5.20000000e-01 2.00000000e-01 9.60000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 6.00000000e-01 0.00000000e+00 2.40000000e-01 9.60000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 2.40000000e-01 0.00000000e+00 0.00000000e+00 8.00000000e-02 5.20000000e-01 8.40000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 7.20000000e-01 2.00000000e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 astropy-regions-f4139aa/regions/shapes/tests/reference/test_to_mask_poly-mode_center.txt000066400000000000000000000013371521703212700321100ustar00rootroot000000000000001.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 test_to_mask_poly-mode_subpixels-subpixels_1.txt000066400000000000000000000013371521703212700350230ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 test_to_mask_poly-mode_subpixels-subpixels_18.txt000066400000000000000000000013371521703212700351130ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference4.19753086e-01 3.98148148e-01 2.31481481e-01 6.17283951e-02 0.00000000e+00 0.00000000e+00 0.00000000e+00 6.04938272e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 8.88888889e-01 6.94444444e-01 1.85185185e-02 4.07407407e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 5.83333333e-01 0.00000000e+00 1.94444444e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.72222222e-01 1.11111111e-01 0.00000000e+00 2.16049383e-02 9.72222222e-01 1.00000000e+00 1.00000000e+00 6.01851852e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 3.91975309e-01 8.88888889e-01 9.81481481e-01 1.29629630e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 2.77777778e-02 2.56172840e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 test_to_mask_poly-mode_subpixels-subpixels_5.txt000066400000000000000000000013371521703212700350270ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference4.40000000e-01 4.00000000e-01 2.00000000e-01 4.00000000e-02 0.00000000e+00 0.00000000e+00 0.00000000e+00 6.00000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 8.80000000e-01 6.80000000e-01 0.00000000e+00 4.00000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 5.60000000e-01 0.00000000e+00 2.00000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.60000000e-01 8.00000000e-02 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 6.00000000e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 4.00000000e-01 9.20000000e-01 1.00000000e+00 1.60000000e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 4.00000000e-02 2.80000000e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 astropy-regions-f4139aa/regions/shapes/tests/reference/test_to_mask_rect-mode_center.txt000066400000000000000000000016611521703212700320620ustar00rootroot000000000000000.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 test_to_mask_rect-mode_subpixels-subpixels_1.txt000066400000000000000000000016611521703212700347750ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 test_to_mask_rect-mode_subpixels-subpixels_18.txt000066400000000000000000000016611521703212700350650ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference0.00000000e+00 1.11111111e-01 3.08641975e-02 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 7.56172840e-01 9.78395062e-01 7.99382716e-01 5.83333333e-01 3.70370370e-01 1.57407407e-01 6.17283951e-03 0.00000000e+00 1.23456790e-02 9.59876543e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.38271605e-01 3.14814815e-01 1.82098765e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 2.00617284e-01 3.95061728e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.66049383e-01 2.16049383e-02 2.77777778e-02 2.16049383e-01 4.29012346e-01 6.41975309e-01 8.54938272e-01 9.96913580e-01 1.00000000e+00 7.77777778e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 7.09876543e-02 2.77777778e-01 2.77777778e-01 0.00000000e+00 test_to_mask_rect-mode_subpixels-subpixels_5.txt000066400000000000000000000016611521703212700350010ustar00rootroot00000000000000astropy-regions-f4139aa/regions/shapes/tests/reference0.00000000e+00 1.20000000e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 7.60000000e-01 1.00000000e+00 8.00000000e-01 5.60000000e-01 3.60000000e-01 1.60000000e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 9.60000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 9.60000000e-01 3.20000000e-01 1.60000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 2.00000000e-01 4.00000000e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00 2.40000000e-01 4.40000000e-01 6.40000000e-01 8.40000000e-01 1.00000000e+00 1.00000000e+00 7.60000000e-01 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 8.00000000e-02 2.80000000e-01 2.40000000e-01 0.00000000e+00 astropy-regions-f4139aa/regions/shapes/tests/setup_package.py000066400000000000000000000002411521703212700245460ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst def get_package_data(): return {'regions.shapes.tests': ['reference/*.txt', 'data/*.fits']} astropy-regions-f4139aa/regions/shapes/tests/test_annulus.py000066400000000000000000000671171521703212700244760ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import numpy as np import pytest from astropy.coordinates import Latitude, Longitude, SkyCoord from astropy.io import fits from astropy.tests.helper import assert_quantity_allclose from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from numpy.testing import assert_allclose, assert_equal from regions.core import PixCoord, RegionMeta, RegionVisual from regions.core.compound import CompoundPixelRegion, CompoundSkyRegion from regions.shapes.annulus import (CircleAnnulusPixelRegion, CircleAnnulusSkyRegion, CircleAnnulusSphericalSkyRegion, EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion, RectangleAnnulusPixelRegion, RectangleAnnulusSkyRegion) from regions.shapes.circle import CircleSphericalSkyRegion from regions.shapes.tests.test_common import (BaseTestPixelRegion, BaseTestSkyRegion, BaseTestSphericalSkyRegion) from regions.tests.helpers import make_simple_wcs @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): filename = get_pkg_data_filename('data/example_header.fits') header = fits.getheader(filename) return WCS(header) class TestCircleAnnulusPixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = CircleAnnulusPixelRegion(PixCoord(3, 4), inner_radius=2, outer_radius=3, meta=meta, visual=visual) sample_box = [0, 6, 1, 7] inside = [(3, 2)] outside = [(3, 0)] expected_area = 5 * np.pi expected_repr = ('') expected_str = ('Region: CircleAnnulusPixelRegion\ncenter: ' 'PixCoord(x=3, y=4)\ninner_radius: 2\nouter_radius: 3') skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='icrs') wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) def test_init(self): assert_quantity_allclose(self.reg.center.x, 3) assert_quantity_allclose(self.reg.center.y, 4) assert_quantity_allclose(self.reg.inner_radius, 2) assert_quantity_allclose(self.reg.outer_radius, 3) def test_copy(self): reg = self.reg.copy() assert reg.center.xy == (3, 4) assert reg.inner_radius == 2 assert reg.outer_radius == 3 assert reg.meta == self.meta assert reg.visual == self.visual def test_pix_sky_roundtrip(self): wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) reg_new = self.reg.to_sky(wcs).to_pixel(wcs) assert_allclose(reg_new.center.x, self.reg.center.x) assert_allclose(reg_new.center.y, self.reg.center.y) assert_allclose(reg_new.inner_radius, self.reg.inner_radius) assert_allclose(reg_new.outer_radius, self.reg.outer_radius) assert reg_new.meta == self.reg.meta assert reg_new.visual == self.reg.visual # test that converted region meta and visual are copies and not views reg_new.meta['text'] = 'new' reg_new.visual['color'] = 'green' assert reg_new.meta['text'] != self.reg.meta['text'] assert reg_new.visual['color'] != self.reg.visual['color'] def test_transformation(self): skyannulus = self.reg.to_sky(wcs=self.wcs) assert isinstance(skyannulus, CircleAnnulusSkyRegion) def test_to_spherical_sky(self, wcs): sphskyann = self.reg.to_spherical_sky( wcs, boundary_distortions=False) assert isinstance(sphskyann, CircleAnnulusSphericalSkyRegion) with pytest.raises(NotImplementedError): _ = self.reg.to_spherical_sky(wcs, boundary_distortions=True) def test_to_spherical_sky_no_wcs(self): match = 'missing 1 required positional argument' with pytest.raises(TypeError, match=match): self.reg.to_spherical_sky() def test_rotate(self): reg = self.reg.rotate(PixCoord(2, 3), 90 * u.deg) assert_allclose(reg.center.xy, (1, 4)) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.inner_radius = 3 assert reg != self.reg def test_covers(self): # Point at center (inside inner) assert not self.reg.covers(PixCoord(3, 4)) # Point on inner boundary assert self.reg.covers(PixCoord(3, 2)) # Point in body of the annulus assert self.reg.covers(PixCoord(3, 1.5)) # Point on outer boundary assert self.reg.covers(PixCoord(3, 7)) # Point outside the annulus assert not self.reg.covers(PixCoord(3, 8)) # Multiple points check together pts = PixCoord([3, 3, 3, 3, 3], [4, 2, 1.5, 7, 8]) assert_equal(self.reg.covers(pts), [False, True, True, True, False]) class TestCircleAnnulusSkyRegion(BaseTestSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = CircleAnnulusSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), 20 * u.arcsec, 30 * u.arcsec, meta=meta, visual=visual) skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='icrs') wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) expected_repr = (', inner_radius=20.0 ' 'arcsec, outer_radius=30.0 arcsec)>') expected_str = ('Region: CircleAnnulusSkyRegion\ncenter: \ninner_radius: ' '20.0 arcsec\nouter_radius: 30.0 arcsec') def test_init(self): assert_quantity_allclose(self.reg.center.ra, self.skycoord.ra) assert_quantity_allclose(self.reg.inner_radius, 20 * u.arcsec) assert_quantity_allclose(self.reg.outer_radius, 30 * u.arcsec) def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center.ra.deg, 3) assert_allclose(reg.inner_radius.to_value('arcsec'), 20) assert_allclose(reg.outer_radius.to_value('arcsec'), 30) assert reg.meta == self.meta assert reg.visual == self.visual def test_contains(self): assert not self.reg.contains(self.skycoord, self.wcs) test_coord = SkyCoord(3 * u.deg, 10 * u.deg, frame='icrs') assert not self.reg.contains(test_coord, self.wcs) def test_transformation(self): pixannulus = self.reg.to_pixel(wcs=self.wcs) assert isinstance(pixannulus, CircleAnnulusPixelRegion) def test_to_spherical_sky(self, wcs): sphskyann = self.reg.to_spherical_sky( wcs=wcs, boundary_distortions=False) assert isinstance(sphskyann, CircleAnnulusSphericalSkyRegion) with pytest.raises(NotImplementedError): _ = self.reg.to_spherical_sky(wcs=wcs, boundary_distortions=True) def test_to_spherical_sky_no_wcs(self): match = "'wcs' must be set if `boundary_distortions=True`" with pytest.raises(ValueError, match=match): self.reg.to_spherical_sky(boundary_distortions=True) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.inner_radius = 10 * u.arcsec assert reg != self.reg class TestCircleAnnulusSphericalSkyRegion(BaseTestSphericalSkyRegion): inside = [(3 * u.deg, 4.006 * u.deg)] outside = [(3 * u.deg, 7 * u.deg)] meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = CircleAnnulusSphericalSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), 20 * u.arcsec, 30 * u.arcsec, meta=meta, visual=visual) skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='icrs') wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) expected_repr = (', inner_radius=20.0 ' 'arcsec, outer_radius=30.0 arcsec)>') expected_str = ('Region: CircleAnnulusSphericalSkyRegion\n' 'center: \ninner_radius: ' '20.0 arcsec\nouter_radius: 30.0 arcsec') def test_init(self): assert_quantity_allclose(self.reg.center.ra, self.skycoord.ra) assert_quantity_allclose(self.reg.inner_radius, 20 * u.arcsec) assert_quantity_allclose(self.reg.outer_radius, 30 * u.arcsec) def test_valid_radii(self): match = 'outer_radius must be greater than inner_radius' with pytest.raises(ValueError, match=match): CircleAnnulusSphericalSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), 30 * u.arcsec, 20 * u.arcsec) def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center.ra.deg, 3) assert_allclose(reg.inner_radius.to_value('arcsec'), 20) assert_allclose(reg.outer_radius.to_value('arcsec'), 30) assert reg.meta == self.meta assert reg.visual == self.visual def test_contains(self): assert not self.reg.contains(self.skycoord) test_coord = SkyCoord(3 * u.deg, 10 * u.deg, frame='icrs') assert not self.reg.contains(test_coord) def test_transformation(self): pixannulus = self.reg.to_pixel(self.wcs) assert isinstance(pixannulus, CircleAnnulusPixelRegion) skyannulus = self.reg.to_sky(wcs=self.wcs) assert isinstance(skyannulus, CircleAnnulusSkyRegion) polysky = self.reg.to_sky( wcs=self.wcs, boundary_distortions=True) assert isinstance(polysky, CompoundSkyRegion) polypix = self.reg.to_pixel( self.wcs, boundary_distortions=True) assert isinstance(polypix, CompoundPixelRegion) def test_transformation_no_wcs(self): match = "'wcs' must be set if `boundary_distortions=True`" with pytest.raises(ValueError, match=match): self.reg.to_sky(boundary_distortions=True) def test_frame_transformation(self): reg2 = self.reg.transform_to('galactic') assert reg2.center == self.reg.center.transform_to('galactic') assert_allclose(reg2.inner_radius.to_value('arcsec'), 20) assert isinstance(reg2, CircleAnnulusSphericalSkyRegion) assert reg2.frame.name == 'galactic' def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.inner_radius = 10 * u.arcsec assert reg != self.reg def test_bounding_circle(self): skycoord = SkyCoord(3 * u.deg, 4 * u.deg) circ = CircleSphericalSkyRegion(skycoord, 30 * u.arcsec, meta=self.meta, visual=self.visual) bounding_circle = self.reg.bounding_circle assert bounding_circle == circ def test_bounding_lonlat(self): skycoord = SkyCoord(3 * u.deg, 0 * u.deg) reg = CircleAnnulusSphericalSkyRegion(skycoord, 20 * u.arcsec, 30 * u.arcsec) bounding_lonlat = reg.bounding_lonlat assert_quantity_allclose(bounding_lonlat[0], Longitude([3. * u.deg - 30 * u.arcsec, 3. * u.deg + 30 * u.arcsec])) assert_quantity_allclose(bounding_lonlat[1], Latitude([-30 * u.arcsec, 30 * u.arcsec])) skycoord2 = SkyCoord(3 * u.deg, 90 * u.deg) reg2 = CircleAnnulusSphericalSkyRegion(skycoord2, 20 * u.arcsec, 30 * u.arcsec) bounding_lonlat2 = reg2.bounding_lonlat assert bounding_lonlat2[0] is None assert_quantity_allclose(bounding_lonlat2[1], Latitude([90 * u.deg - 30 * u.arcsec, 90 * u.deg - 20 * u.arcsec])) skycoord3 = SkyCoord(3 * u.deg, -90 * u.deg) reg3 = CircleAnnulusSphericalSkyRegion(skycoord3, 20 * u.arcsec, 30 * u.arcsec) bounding_lonlat3 = reg3.bounding_lonlat assert bounding_lonlat3[0] is None assert_quantity_allclose(bounding_lonlat3[1], Latitude([-90 * u.deg + 20 * u.arcsec, -90 * u.deg + 30 * u.arcsec])) class TestCircleAnnulusPixelRegionToSkyEllipse: """ Tests for CircleAnnulusPixelRegion.to_sky with as_ellipse=True. """ def setup_method(self): self.center = PixCoord(10, 10) self.inner_radius = 3.0 self.outer_radius = 5.0 self.meta = RegionMeta({'text': 'test'}) self.visual = RegionVisual({'color': 'blue'}) self.reg = CircleAnnulusPixelRegion( self.center, self.inner_radius, self.outer_radius, meta=self.meta, visual=self.visual) self.wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) def test_to_sky_as_ellipse(self): result = self.reg.to_sky(self.wcs, as_ellipse=True) assert isinstance(result, EllipseAnnulusSkyRegion) assert result.meta == self.meta assert result.visual == self.visual def test_to_sky_ellipse_roundtrip(self): sky_ellipse = self.reg.to_sky(self.wcs, as_ellipse=True) pix_ellipse = sky_ellipse.to_pixel(self.wcs) assert_allclose(pix_ellipse.center.x, self.center.x, rtol=1e-5) assert_allclose(pix_ellipse.center.y, self.center.y, rtol=1e-5) assert_allclose(pix_ellipse.inner_width, 2 * self.inner_radius, rtol=1e-4) assert_allclose(pix_ellipse.outer_width, 2 * self.outer_radius, rtol=1e-4) assert_allclose(pix_ellipse.inner_height, 2 * self.inner_radius, rtol=1e-4) assert_allclose(pix_ellipse.outer_height, 2 * self.outer_radius, rtol=1e-4) def test_to_sky_as_ellipse_meta_copies(self): result = self.reg.to_sky(self.wcs, as_ellipse=True) result.meta['text'] = 'new' result.visual['color'] = 'green' assert result.meta['text'] != self.reg.meta['text'] assert result.visual['color'] != self.reg.visual['color'] class TestCircleAnnulusSkyRegionToPixelEllipse: """ Tests for CircleAnnulusSkyRegion.to_pixel with as_ellipse=True. """ def setup_method(self): self.center = SkyCoord(3 * u.deg, 4 * u.deg) self.inner_radius = 20 * u.arcsec self.outer_radius = 30 * u.arcsec self.meta = RegionMeta({'text': 'test'}) self.visual = RegionVisual({'color': 'blue'}) self.reg = CircleAnnulusSkyRegion( self.center, self.inner_radius, self.outer_radius, meta=self.meta, visual=self.visual) self.wcs = make_simple_wcs(SkyCoord(3 * u.deg, 4 * u.deg), 5 * u.arcsec, 20) def test_to_pixel_as_ellipse(self): result = self.reg.to_pixel(self.wcs, as_ellipse=True) assert isinstance(result, EllipseAnnulusPixelRegion) assert result.meta == self.meta assert result.visual == self.visual def test_to_pixel_ellipse_roundtrip(self): pix_ellipse = self.reg.to_pixel(self.wcs, as_ellipse=True) sky_ellipse = pix_ellipse.to_sky(self.wcs) assert_quantity_allclose(sky_ellipse.inner_width, 2 * self.inner_radius, rtol=1e-4) assert_quantity_allclose(sky_ellipse.outer_width, 2 * self.outer_radius, rtol=1e-4) assert_quantity_allclose(sky_ellipse.inner_height, 2 * self.inner_radius, rtol=1e-4) assert_quantity_allclose(sky_ellipse.outer_height, 2 * self.outer_radius, rtol=1e-4) def test_to_pixel_as_ellipse_meta_copies(self): result = self.reg.to_pixel(self.wcs, as_ellipse=True) result.meta['text'] = 'new' result.visual['color'] = 'green' assert result.meta['text'] != self.reg.meta['text'] assert result.visual['color'] != self.reg.visual['color'] class TestEllipseAnnulusPixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = EllipseAnnulusPixelRegion(PixCoord(3, 4), 2, 5, 5, 8, meta=meta, visual=visual) sample_box = [1, 6, 0, 9] inside = [(3, 7)] outside = [(3, 4)] expected_area = 7.5 * np.pi expected_repr = ('') expected_str = ('Region: EllipseAnnulusPixelRegion\ncenter: ' 'PixCoord(x=3, y=4)\ninner_width: 2\nouter_width: 5\n' 'inner_height: 5\nouter_height: 8\nangle: 0.0 deg') skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='icrs') wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) def test_init(self): assert_quantity_allclose(self.reg.center.x, 3) assert_quantity_allclose(self.reg.center.y, 4) assert_quantity_allclose(self.reg.inner_width, 2) assert_quantity_allclose(self.reg.inner_height, 5) assert_quantity_allclose(self.reg.outer_width, 5) assert_quantity_allclose(self.reg.outer_height, 8) def test_copy(self): reg = self.reg.copy() assert reg.center.xy == (3, 4) assert reg.inner_width == 2 assert reg.inner_height == 5 assert reg.outer_width == 5 assert reg.outer_height == 8 assert_allclose(reg.angle.to_value('deg'), 0) assert reg.meta == self.meta assert reg.visual == self.visual def test_pix_sky_roundtrip(self): wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) reg_new = self.reg.to_sky(wcs).to_pixel(wcs) assert_allclose(reg_new.center.x, self.reg.center.x) assert_allclose(reg_new.center.y, self.reg.center.y) assert_allclose(reg_new.inner_width, self.reg.inner_width) assert_allclose(reg_new.outer_width, self.reg.outer_width) assert_allclose(reg_new.inner_height, self.reg.inner_height) assert_allclose(reg_new.outer_height, self.reg.outer_height) assert_quantity_allclose(reg_new.angle, self.reg.angle, atol=1e-10 * u.deg) assert reg_new.meta == self.reg.meta assert reg_new.visual == self.reg.visual # test that converted region meta and visual are copies and not views reg_new.meta['text'] = 'new' reg_new.visual['color'] = 'green' assert reg_new.meta['text'] != self.reg.meta['text'] assert reg_new.visual['color'] != self.reg.visual['color'] def test_transformation(self): skyannulus = self.reg.to_sky(wcs=self.wcs) assert isinstance(skyannulus, EllipseAnnulusSkyRegion) def test_rotate(self): reg = self.reg.rotate(PixCoord(2, 3), 90 * u.deg) assert_allclose(reg.center.xy, (1, 4)) assert_allclose(reg.angle.to_value('deg'), 90) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.outer_height = 10 assert reg != self.reg class TestEllipseAnnulusSkyRegion(BaseTestSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='icrs') reg = EllipseAnnulusSkyRegion(skycoord, 20 * u.arcsec, 50 * u.arcsec, 50 * u.arcsec, 80 * u.arcsec, meta=meta, visual=visual) wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) expected_repr = (', inner_width=20.0 ' 'arcsec, outer_width=50.0 arcsec, inner_height=50.0 ' 'arcsec, outer_height=80.0 arcsec, angle=0.0 deg)>') expected_str = ('Region: EllipseAnnulusSkyRegion\ncenter: \ninner_width: ' '20.0 arcsec\nouter_width: 50.0 arcsec\n' 'inner_height: 50.0 arcsec\nouter_height: ' '80.0 arcsec\nangle: 0.0 deg') def test_init(self): assert_quantity_allclose(self.reg.center.ra, self.skycoord.ra) assert_quantity_allclose(self.reg.inner_width, 20 * u.arcsec) assert_quantity_allclose(self.reg.inner_height, 50 * u.arcsec) def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center.ra.deg, 3) assert_allclose(reg.inner_width.to_value('arcsec'), 20) assert_allclose(reg.inner_height.to_value('arcsec'), 50) assert_allclose(reg.outer_width.to_value('arcsec'), 50) assert_allclose(reg.outer_height.to_value('arcsec'), 80) assert_allclose(reg.angle.to_value('deg'), 0) assert reg.meta == self.meta assert reg.visual == self.visual def test_contains(self): assert not self.reg.contains(self.skycoord, self.wcs) test_coord = SkyCoord(3 * u.deg, 10 * u.deg, frame='icrs') assert not self.reg.contains(test_coord, self.wcs) def test_transformation(self): pixannulus = self.reg.to_pixel(wcs=self.wcs) assert isinstance(pixannulus, EllipseAnnulusPixelRegion) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.outer_height = 85 * u.arcsec assert reg != self.reg class TestRectangleAnnulusPixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = RectangleAnnulusPixelRegion(PixCoord(3, 4), 2, 5, 5, 8, meta=meta, visual=visual) sample_box = [1, 6, 0, 9] inside = [(3, 7)] outside = [(3, 4)] expected_area = 30 expected_repr = ('') expected_str = ('Region: RectangleAnnulusPixelRegion\ncenter: ' 'PixCoord(x=3, y=4)\ninner_width: 2\nouter_width: 5\n' 'inner_height: 5\nouter_height: 8\nangle: 0.0 deg') skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='icrs') wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) def test_init(self): assert_quantity_allclose(self.reg.center.x, 3) assert_quantity_allclose(self.reg.center.y, 4) assert_quantity_allclose(self.reg.inner_width, 2) assert_quantity_allclose(self.reg.inner_height, 5) assert_quantity_allclose(self.reg.outer_width, 5) assert_quantity_allclose(self.reg.outer_height, 8) def test_copy(self): reg = self.reg.copy() assert_quantity_allclose(reg.center.x, 3) assert_quantity_allclose(reg.center.y, 4) assert_quantity_allclose(reg.inner_width, 2) assert_quantity_allclose(reg.inner_height, 5) assert_quantity_allclose(reg.outer_width, 5) assert_quantity_allclose(reg.outer_height, 8) assert reg.meta == self.meta assert reg.visual == self.visual def test_pix_sky_roundtrip(self): wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) reg_new = self.reg.to_sky(wcs).to_pixel(wcs) assert_allclose(reg_new.center.x, self.reg.center.x) assert_allclose(reg_new.center.y, self.reg.center.y) assert_allclose(reg_new.inner_width, self.reg.inner_width) assert_allclose(reg_new.outer_width, self.reg.outer_width) assert_allclose(reg_new.inner_height, self.reg.inner_height) assert_allclose(reg_new.outer_height, self.reg.outer_height) assert_quantity_allclose(reg_new.angle, self.reg.angle, atol=1e-10 * u.deg) assert reg_new.meta == self.reg.meta assert reg_new.visual == self.reg.visual # test that converted region meta and visual are copies and not views reg_new.meta['text'] = 'new' reg_new.visual['color'] = 'green' assert reg_new.meta['text'] != self.reg.meta['text'] assert reg_new.visual['color'] != self.reg.visual['color'] def test_transformation(self): skyannulus = self.reg.to_sky(wcs=self.wcs) assert isinstance(skyannulus, RectangleAnnulusSkyRegion) def test_rotate(self): reg = self.reg.rotate(PixCoord(2, 3), 90 * u.deg) assert_allclose(reg.center.xy, (1, 4)) assert_allclose(reg.angle.to_value('deg'), 90) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.outer_height = 10 assert reg != self.reg class TestRectangleAnnulusSkyRegion(BaseTestSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='icrs') reg = RectangleAnnulusSkyRegion(skycoord, 20 * u.arcsec, 50 * u.arcsec, 50 * u.arcsec, 80 * u.arcsec, meta=meta, visual=visual) wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) expected_repr = (', inner_width=20.0 ' 'arcsec, outer_width=50.0 arcsec, inner_height=50.0 ' 'arcsec, outer_height=80.0 arcsec, angle=0.0 deg)>') expected_str = ('Region: RectangleAnnulusSkyRegion\ncenter: \ninner_width: ' '20.0 arcsec\nouter_width: 50.0 arcsec\n' 'inner_height: 50.0 arcsec\nouter_height: 80.0 arcsec\n' 'angle: 0.0 deg') def test_init(self): assert_quantity_allclose(self.reg.center.ra, self.skycoord.ra) assert_quantity_allclose(self.reg.inner_width, 20 * u.arcsec) assert_quantity_allclose(self.reg.inner_height, 50 * u.arcsec) def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center.ra.deg, 3) assert_allclose(reg.inner_width.to_value('arcsec'), 20) assert_allclose(reg.inner_height.to_value('arcsec'), 50) assert_allclose(reg.outer_width.to_value('arcsec'), 50) assert_allclose(reg.outer_height.to_value('arcsec'), 80) assert_allclose(reg.angle.to_value('deg'), 0) assert reg.meta == self.meta assert reg.visual == self.visual def test_contains(self): assert not self.reg.contains(self.skycoord, self.wcs) test_coord = SkyCoord(3 * u.deg, 10 * u.deg, frame='icrs') assert not self.reg.contains(test_coord, self.wcs) def test_transformation(self): pixannulus = self.reg.to_pixel(wcs=self.wcs) assert isinstance(pixannulus, RectangleAnnulusPixelRegion) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.outer_height = 85 * u.arcsec assert reg != self.reg astropy-regions-f4139aa/regions/shapes/tests/test_api.py000066400000000000000000000414421521703212700235530ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ The tests in this file simply check what functionality is currently implemented and doesn't check anything about correctness. """ from collections import OrderedDict import astropy.units as u import pytest from astropy.coordinates import Angle, SkyCoord from astropy.wcs import WCS from regions.core.core import (PixelRegion, Region, SkyRegion, SphericalSkyRegion) from regions.core.mask import RegionMask from regions.core.metadata import RegionMeta, RegionVisual from regions.core.pixcoord import PixCoord from regions.shapes.annulus import (CircleAnnulusPixelRegion, CircleAnnulusSkyRegion, CircleAnnulusSphericalSkyRegion, EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion, RectangleAnnulusPixelRegion, RectangleAnnulusSkyRegion) from regions.shapes.circle import (CirclePixelRegion, CircleSkyRegion, CircleSphericalSkyRegion) from regions.shapes.ellipse import EllipsePixelRegion, EllipseSkyRegion from regions.shapes.lune import LuneSphericalSkyRegion from regions.shapes.point import PointPixelRegion, PointSkyRegion from regions.shapes.polygon import (PolygonPixelRegion, PolygonSkyRegion, PolygonSphericalSkyRegion) from regions.shapes.range import RangeSphericalSkyRegion from regions.shapes.rectangle import RectanglePixelRegion, RectangleSkyRegion from regions.shapes.whole_sky import WholeSphericalSkyRegion PIXEL_REGIONS = [ CirclePixelRegion(PixCoord(3, 4), radius=5), CircleAnnulusPixelRegion(PixCoord(3, 4), 5, 7), EllipsePixelRegion(PixCoord(3, 4), width=7, height=5, angle=3 * u.deg), EllipseAnnulusPixelRegion(PixCoord(3, 4), 7, 9, 8, 10, angle=3 * u.deg), PolygonPixelRegion(PixCoord([1, 4, 3], [2, 4, 4])), RectanglePixelRegion(PixCoord(6, 5), width=3, height=5), RectangleAnnulusPixelRegion(PixCoord(6, 5), 3, 6, 5, 7), PointPixelRegion(PixCoord(1, 2))] SKY_REGIONS = [ CircleSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), radius=5 * u.deg), CircleAnnulusSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), 5 * u.deg, 7 * u.deg), EllipseSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), width=7 * u.deg, height=5 * u.deg, angle=3 * u.deg), EllipseAnnulusSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), 7 * u.deg, 9 * u.deg, 5 * u.deg, 9 * u.deg, angle=3 * u.deg), PolygonSkyRegion(SkyCoord([1, 4, 3] * u.deg, [2, 4, 4] * u.deg)), RectangleSkyRegion(SkyCoord(6 * u.deg, 5 * u.deg), width=3 * u.deg, height=5 * u.deg), RectangleAnnulusSkyRegion(SkyCoord(6 * u.deg, 5 * u.deg), 3 * u.deg, 5 * u.deg, 5 * u.deg, 7 * u.deg), PointSkyRegion(SkyCoord(6 * u.deg, 5 * u.deg))] SPHERICAL_SKY_REGIONS = [ CircleSphericalSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), radius=5 * u.deg), CircleAnnulusSphericalSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), 5 * u.deg, 7 * u.deg), LuneSphericalSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), SkyCoord(153 * u.deg, 4 * u.deg)), PolygonSphericalSkyRegion(SkyCoord([1, 4, 3] * u.deg, [2, 4, 4] * u.deg)), RangeSphericalSkyRegion( longitude_range=[0, 10] * u.deg, latitude_range=[40, 60] * u.deg, frame='icrs'), WholeSphericalSkyRegion()] BOUNDARY_DISTORTIONS = [True, False] MASK_MODES = ['center', 'exact', 'subpixels'] COMMON_WCS = WCS(naxis=2) COMMON_WCS.wcs.ctype = 'RA---TAN', 'DEC--TAN' def ids_func(arg): if isinstance(arg, Region): return arg.__class__.__name__ else: return str(arg) @pytest.mark.parametrize('region', [PIXEL_REGIONS[i] for i in (0, 2, 5)], ids=ids_func) def test_pix_in(region): assert PixCoord(5, 4) in region @pytest.mark.parametrize('region', PIXEL_REGIONS, ids=ids_func) def test_pix_area(region): # TODO: remove the pytest.skip once polygon area is implemented if isinstance(region, PolygonPixelRegion): pytest.skip() area = region.area assert not isinstance(area, u.Quantity) @pytest.mark.parametrize('region', PIXEL_REGIONS, ids=ids_func) def test_pix_to_sky(region): try: sky_region = region.to_sky(COMMON_WCS) assert isinstance(sky_region, SkyRegion) except NotImplementedError: pytest.xfail() @pytest.mark.parametrize('region', PIXEL_REGIONS, ids=ids_func) @pytest.mark.parametrize('include_dist', BOUNDARY_DISTORTIONS, ids=ids_func) def test_pix_to_spherical_sky(region, include_dist): # TODO: remove expected failures when implemented # Expected failure: # No spherical Ellipse, EllipseAnnulus, Point, Rectangle, # RectangleAnnulus # Also expected failure: # Boundary distortions not yet implemented for # CirclePixelRegion, CircleAnnulusPixelRegion, PolygonPixelRegion if ( isinstance(region, (EllipsePixelRegion, EllipseAnnulusPixelRegion, PointPixelRegion, RectanglePixelRegion, RectangleAnnulusPixelRegion)) | ((isinstance(region, (CirclePixelRegion, CircleAnnulusPixelRegion, PolygonPixelRegion))) & include_dist) ): with pytest.raises(NotImplementedError): sph_sky_region = region.to_spherical_sky( COMMON_WCS, boundary_distortions=include_dist, ) assert isinstance(sph_sky_region, SphericalSkyRegion) else: sph_sky_region = region.to_spherical_sky( COMMON_WCS, boundary_distortions=include_dist, ) assert isinstance(sph_sky_region, SphericalSkyRegion) @pytest.mark.parametrize('region', PIXEL_REGIONS, ids=ids_func) @pytest.mark.parametrize('mode', MASK_MODES, ids=ids_func) def test_pix_to_mask(region, mode): try: mask = region.to_mask(mode=mode) assert isinstance(mask, RegionMask) except NotImplementedError: pytest.xfail() @pytest.mark.parametrize('region', SKY_REGIONS, ids=ids_func) def test_sky_in(region): region.contains(SkyCoord(1 * u.deg, 1 * u.deg, frame='icrs'), COMMON_WCS) @pytest.mark.parametrize('region', SKY_REGIONS, ids=ids_func) def test_sky_in_array(region): region.contains(SkyCoord([1, 2, 3] * u.deg, [3, 2, 1] * u.deg, frame='icrs'), COMMON_WCS) @pytest.mark.parametrize('region', SKY_REGIONS, ids=ids_func) def test_sky_to_pix(region): pix_region = region.to_pixel(wcs=COMMON_WCS) assert isinstance(pix_region, PixelRegion) @pytest.mark.parametrize('region', SKY_REGIONS, ids=ids_func) @pytest.mark.parametrize('include_dist', BOUNDARY_DISTORTIONS, ids=ids_func) def test_sky_to_spherical_sky(region, include_dist): # TODO: remove expected failures when implemented # Expected failure: # No spherical Ellipse, EllipseAnnulus, Point, Rectangle, # RectangleAnnulus # Also expected failure: # Boundary distortions not yet implemented for # CircleSkyRegion, CircleAnnulusSkyRegion, PolygonSkyRegion if ( isinstance(region, (EllipseSkyRegion, EllipseAnnulusSkyRegion, PointSkyRegion, RectangleSkyRegion, RectangleAnnulusSkyRegion)) | ((isinstance(region, (CircleSkyRegion, CircleAnnulusSkyRegion, PolygonSkyRegion))) & include_dist) ): with pytest.raises(NotImplementedError): sph_sky_region = region.to_spherical_sky( wcs=COMMON_WCS, boundary_distortions=include_dist, ) assert isinstance(sph_sky_region, SphericalSkyRegion) else: sph_sky_region = region.to_spherical_sky( wcs=COMMON_WCS, boundary_distortions=include_dist, ) assert isinstance(sph_sky_region, SphericalSkyRegion) @pytest.mark.parametrize('region', SPHERICAL_SKY_REGIONS, ids=ids_func) @pytest.mark.parametrize('include_dist', BOUNDARY_DISTORTIONS, ids=ids_func) def test_spherical_sky_to_sky(region, include_dist): if isinstance(region, WholeSphericalSkyRegion): with pytest.raises(ValueError): _ = region.to_sky( wcs=COMMON_WCS, boundary_distortions=include_dist, ) elif (isinstance(region, (LuneSphericalSkyRegion, RangeSphericalSkyRegion)) & (not include_dist)): # No distortions: not defined: match = 'Invalid parameter: `boundary_distortions=False`!' with pytest.raises(ValueError, match=match): region.to_sky( COMMON_WCS, boundary_distortions=include_dist, ) else: sky_region = region.to_sky( wcs=COMMON_WCS, boundary_distortions=include_dist, ) assert isinstance(sky_region, SkyRegion) @pytest.mark.parametrize('region', SPHERICAL_SKY_REGIONS, ids=ids_func) @pytest.mark.parametrize('include_dist', BOUNDARY_DISTORTIONS, ids=ids_func) def test_spherical_sky_to_pix(region, include_dist): if isinstance(region, WholeSphericalSkyRegion): with pytest.raises(ValueError): _ = region.to_pixel( COMMON_WCS, boundary_distortions=include_dist, ) elif (isinstance(region, (LuneSphericalSkyRegion, RangeSphericalSkyRegion)) & (not include_dist)): # No distortions: not defined: match = 'Invalid parameter: `boundary_distortions=False`!' with pytest.raises(ValueError, match=match): region.to_pixel( COMMON_WCS, boundary_distortions=include_dist, ) else: pixel_region = region.to_pixel( COMMON_WCS, boundary_distortions=include_dist, ) assert isinstance(pixel_region, PixelRegion) @pytest.mark.parametrize('region', PIXEL_REGIONS, ids=ids_func) def test_attribute_validation_pixel_regions(region): invalid_values = dict(center=[PixCoord([1, 2], [2, 3]), 1, SkyCoord(1 * u.deg, 2 * u.deg), (10, 10), (10 * u.deg, 10 * u.deg)], radius=[u.Quantity('1deg'), [1], PixCoord(1, 2), 3 * u.km, 0.0, -10.], angle=[u.Quantity([1 * u.deg, 2 * u.deg]), 2, PixCoord(1, 2), 3 * u.km], vertices=[u.Quantity('1'), 2, PixCoord(1, 2), PixCoord([[1, 2]], [[2, 3]]), 3 * u.km, (10, 10), (10 * u.deg, 10 * u.deg)]) invalid_values['width'] = invalid_values['radius'] invalid_values['height'] = invalid_values['radius'] invalid_values['inner_height'] = invalid_values['radius'] invalid_values['inner_width'] = invalid_values['radius'] invalid_values['outer_height'] = invalid_values['radius'] invalid_values['outer_width'] = invalid_values['radius'] invalid_values['inner_radius'] = invalid_values['radius'] invalid_values['outer_radius'] = invalid_values['radius'] invalid_values['start'] = invalid_values['center'] invalid_values['end'] = invalid_values['radius'] for attr in invalid_values: if hasattr(region, attr): for val in invalid_values.get(attr, None): match = f'{attr!r} must' with pytest.raises(ValueError, match=match): setattr(region, attr, val) @pytest.mark.parametrize('region', SKY_REGIONS, ids=ids_func) def test_attribute_validation_sky_regions(region): invalid_values = dict(center=[PixCoord([1, 2], [2, 3]), 1, SkyCoord([1 * u.deg], [2 * u.deg]), (10, 10), (10 * u.deg, 10 * u.deg)], radius=[u.Quantity([1 * u.deg, 5 * u.deg]), [1], SkyCoord(1 * u.deg, 2 * u.deg), 1, 3 * u.km, 0.0 * u.deg, -10. * u.deg], angle=[u.Quantity([1 * u.deg, 2 * u.deg]), 2, SkyCoord(1 * u.deg, 2 * u.deg), 3. * u.km], vertices=[u.Quantity('1deg'), 2, SkyCoord(1 * u.deg, 2 * u.deg), SkyCoord([[1 * u.deg, 2 * u.deg]], [[2 * u.deg, 3 * u.deg]]), 3 * u.km, (10, 10), (10 * u.deg, 10 * u.deg)]) invalid_values['width'] = invalid_values['radius'] invalid_values['height'] = invalid_values['radius'] invalid_values['inner_height'] = invalid_values['radius'] invalid_values['inner_width'] = invalid_values['radius'] invalid_values['outer_height'] = invalid_values['radius'] invalid_values['outer_width'] = invalid_values['radius'] invalid_values['inner_radius'] = invalid_values['radius'] invalid_values['outer_radius'] = invalid_values['radius'] invalid_values['start'] = invalid_values['center'] invalid_values['end'] = invalid_values['radius'] for attr in invalid_values: if hasattr(region, attr): for val in invalid_values.get(attr, None): match = f'{attr!r} must' with pytest.raises(ValueError, match=match): setattr(region, attr, val) @pytest.mark.parametrize('region', SPHERICAL_SKY_REGIONS, ids=ids_func) def test_attribute_validation_spherical_sky_regions(region): invalid_values = dict(center=[PixCoord([1, 2], [2, 3]), 1, SkyCoord([1 * u.deg], [2 * u.deg]), (10, 10), (10 * u.deg, 10 * u.deg)], radius=[u.Quantity([1 * u.deg, 5 * u.deg]), [1], SkyCoord(1 * u.deg, 2 * u.deg), 1, 3 * u.km, 0.0 * u.deg, -10. * u.deg], angle=[u.Quantity([1 * u.deg, 2 * u.deg]), 2, SkyCoord(1 * u.deg, 2 * u.deg), 3. * u.km], vertices=[u.Quantity('1deg'), 2, SkyCoord(1 * u.deg, 2 * u.deg), SkyCoord([[1 * u.deg, 2 * u.deg]], [[2 * u.deg, 3 * u.deg]]), 3 * u.km, (10, 10), (10 * u.deg, 10 * u.deg)], longitude_range=[1, [1], [1, 2], [1 * u.deg], [3 * u.km, 5 * u.km], u.Quantity([1 * u.deg]), u.Quantity([3 * u.km, 5 * u.km]), Angle([1 * u.deg])]) invalid_values['width'] = invalid_values['radius'] invalid_values['height'] = invalid_values['radius'] invalid_values['inner_height'] = invalid_values['radius'] invalid_values['inner_width'] = invalid_values['radius'] invalid_values['outer_height'] = invalid_values['radius'] invalid_values['outer_width'] = invalid_values['radius'] invalid_values['inner_radius'] = invalid_values['radius'] invalid_values['outer_radius'] = invalid_values['radius'] invalid_values['start'] = invalid_values['center'] invalid_values['end'] = invalid_values['radius'] invalid_values['latitude_range'] = invalid_values['longitude_range'] region_attrs = [attr for attr, _ in vars(region).items()] for attr in invalid_values: if hasattr(region, attr) & (attr in region_attrs): for val in invalid_values.get(attr, None): match = f'{attr!r} must' with pytest.raises(ValueError, match=match): setattr(region, attr, val) @pytest.mark.parametrize('region', PIXEL_REGIONS + SKY_REGIONS, ids=ids_func) def test_metadata(region): region.meta = {'text': 'hello'} region.visual = {'color': 'blue'} assert isinstance(region.meta, RegionMeta) assert isinstance(region.visual, RegionVisual) # dict subclasses are allowed region.meta = OrderedDict({'text': 'hello'}) region.visual = OrderedDict({'color': 'blue'}) assert isinstance(region.meta, RegionMeta) assert isinstance(region.visual, RegionVisual) with pytest.raises(ValueError): region.meta = 1 with pytest.raises(ValueError): region.visual = 'blue' astropy-regions-f4139aa/regions/shapes/tests/test_circle.py000066400000000000000000000461531521703212700242470ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import numpy as np import pytest from astropy.coordinates import Latitude, Longitude, SkyCoord from astropy.io import fits from astropy.tests.helper import assert_quantity_allclose from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from numpy.testing import assert_allclose, assert_equal from regions._utils.optional_deps import HAS_MATPLOTLIB, HAS_SHAPELY from regions.core import PixCoord, RegionMeta, RegionVisual from regions.shapes.circle import (CirclePixelRegion, CircleSkyRegion, CircleSphericalSkyRegion) from regions.shapes.ellipse import EllipsePixelRegion, EllipseSkyRegion from regions.shapes.polygon import PolygonPixelRegion, PolygonSkyRegion from regions.shapes.tests.test_common import (BaseTestPixelRegion, BaseTestSkyRegion, BaseTestSphericalSkyRegion) from regions.tests.helpers import make_simple_wcs @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): filename = get_pkg_data_filename('data/example_header.fits') header = fits.getheader(filename) return WCS(header) class TestCirclePixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = CirclePixelRegion(PixCoord(3, 4), radius=2, meta=meta, visual=visual) sample_box = [0, 6, 1, 7] inside = [(3, 4)] outside = [(3, 0)] expected_area = 4 * np.pi expected_repr = '' expected_str = ('Region: CirclePixelRegion\ncenter: PixCoord(x=3, y=4)' '\nradius: 2') def test_copy(self): reg = self.reg.copy() assert reg.center.xy == (3, 4) assert reg.radius == 2 assert reg.meta == self.meta assert reg.visual == self.visual def test_pix_sky_roundtrip(self): wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) reg_new = self.reg.to_sky(wcs).to_pixel(wcs) assert_allclose(reg_new.center.x, self.reg.center.x) assert_allclose(reg_new.center.y, self.reg.center.y) assert_allclose(reg_new.radius, self.reg.radius) assert reg_new.meta == self.reg.meta assert reg_new.visual == self.reg.visual # test that converted region meta and visual are copies and not views reg_new.meta['text'] = 'new' reg_new.visual['color'] = 'green' assert reg_new.meta['text'] != self.reg.meta['text'] assert reg_new.visual['color'] != self.reg.visual['color'] def test_to_spherical_sky(self, wcs): sphskycircle = self.reg.to_spherical_sky( wcs, boundary_distortions=False) assert isinstance(sphskycircle, CircleSphericalSkyRegion) with pytest.raises(NotImplementedError): _ = self.reg.to_spherical_sky(wcs, boundary_distortions=True) def test_to_spherical_sky_no_wcs(self): match = 'missing 1 required positional argument' with pytest.raises(TypeError, match=match): self.reg.to_spherical_sky() @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_as_artist(self): patch = self.reg.as_artist() assert_allclose(patch.center, (3, 4)) assert_allclose(patch.radius, 2) def test_rotate(self): reg = self.reg.rotate(PixCoord(2, 3), 90 * u.deg) assert_allclose(reg.center.xy, (1, 4)) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.radius = 3 assert reg != self.reg def test_zero_size(self): with pytest.raises(ValueError): CirclePixelRegion(PixCoord(50, 50), radius=0) def test_contains_covers_ignore_include_metadata(): """ Test that contains and covers ignore the 'include' metadata key, and depend strictly on the geometric shape. """ meta = RegionMeta({'include': False}) circle_exclude = CirclePixelRegion(PixCoord(10, 10), radius=5, meta=meta) circle_include = CirclePixelRegion(PixCoord(10, 10), radius=5) inside_pt = PixCoord(10, 10) boundary_pt = PixCoord(15, 10) outside_pt = PixCoord(20, 10) # Should be completely identical regardless of 'include' metadata assert_equal(circle_exclude.contains(inside_pt), circle_include.contains(inside_pt)) assert_equal(circle_exclude.contains(boundary_pt), circle_include.contains(boundary_pt)) assert_equal(circle_exclude.contains(outside_pt), circle_include.contains(outside_pt)) # Should be completely identical regardless of 'include' metadata assert_equal(circle_exclude.covers(inside_pt), circle_include.covers(inside_pt)) assert_equal(circle_exclude.covers(boundary_pt), circle_include.covers(boundary_pt)) assert_equal(circle_exclude.covers(outside_pt), circle_include.covers(outside_pt)) class TestCircleSkyRegion(BaseTestSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = CircleSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), 2 * u.arcsec, meta=meta, visual=visual) expected_repr = (', radius=2.0 arcsec)>') expected_str = ('Region: CircleSkyRegion\ncenter: \nradius: 2.0 ' 'arcsec') def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center.ra.deg, 3) assert_allclose(reg.radius.to_value('arcsec'), 2) assert reg.meta == self.meta assert reg.visual == self.visual def test_transformation(self, wcs): skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='galactic') skycircle = CircleSkyRegion(skycoord, 2 * u.arcsec) pixcircle = skycircle.to_pixel(wcs) assert_allclose(pixcircle.center.x, -50.5) assert_allclose(pixcircle.center.y, 299.5) assert_allclose(pixcircle.radius, 0.0278117, rtol=1e-6) skycircle2 = pixcircle.to_sky(wcs) assert_quantity_allclose(skycircle.center.data.lon, skycircle2.center.data.lon) assert_quantity_allclose(skycircle.center.data.lat, skycircle2.center.data.lat) assert_quantity_allclose(skycircle2.radius, skycircle.radius) sphskycircle = self.reg.to_spherical_sky( wcs=wcs, boundary_distortions=False) assert isinstance(sphskycircle, CircleSphericalSkyRegion) with pytest.raises(NotImplementedError): _ = self.reg.to_spherical_sky(wcs=wcs, boundary_distortions=True) def test_to_spherical_sky_no_wcs(self): match = "'wcs' must be set if `boundary_distortions=True`" with pytest.raises(ValueError, match=match): self.reg.to_spherical_sky(boundary_distortions=True) def test_dimension_center(self): center = SkyCoord([1, 2] * u.deg, [3, 4] * u.deg) radius = 2 * u.arcsec match = "'center' must be a scalar SkyCoord" with pytest.raises(ValueError, match=match): CircleSkyRegion(center, radius) def test_contains(self, wcs): position = SkyCoord([1, 3] * u.deg, [2, 4] * u.deg) # 1,2 is outside, 3,4 is the center and is inside assert all(self.reg.contains(position, wcs) == np.array([False, True], dtype='bool')) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.radius = 3 * u.arcsec assert reg != self.reg def test_zero_size(self): with pytest.raises(ValueError): CircleSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), 0. * u.arcsec) class TestCircleSphericalSkyRegion(BaseTestSphericalSkyRegion): inside = [(3 * u.deg, 4 * u.deg)] outside = [(3 * u.deg, 0 * u.deg)] meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = CircleSphericalSkyRegion( SkyCoord(3 * u.deg, 4 * u.deg), 2 * u.arcsec, meta=meta, visual=visual) expected_repr = (', radius=2.0 arcsec)>') expected_str = ('Region: CircleSphericalSkyRegion\n' 'center: \nradius: 2.0 arcsec') def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center.ra.deg, 3) assert_allclose(reg.radius.to_value('arcsec'), 2) assert reg.meta == self.meta assert reg.visual == self.visual def test_transformation(self, wcs): skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='galactic') sphskycircle = CircleSphericalSkyRegion(skycoord, 2 * u.arcsec) pixcircle = sphskycircle.to_pixel(wcs) assert_allclose(pixcircle.center.x, -50.5) assert_allclose(pixcircle.center.y, 299.5) assert_allclose(pixcircle.radius, 0.0278117, rtol=1e-6) skycircle = sphskycircle.to_sky(wcs=wcs) assert isinstance(skycircle, CircleSkyRegion) sphskycircle2 = pixcircle.to_spherical_sky(wcs) assert_quantity_allclose(sphskycircle.center.data.lon, sphskycircle2.center.data.lon) assert_quantity_allclose(sphskycircle.center.data.lat, sphskycircle2.center.data.lat) assert_quantity_allclose(sphskycircle2.radius, sphskycircle.radius) polysky = sphskycircle.to_sky( wcs=wcs, boundary_distortions=True) assert isinstance(polysky, PolygonSkyRegion) polypix = sphskycircle.to_pixel(wcs, boundary_distortions=True) assert isinstance(polypix, PolygonPixelRegion) def test_transformation_no_wcs(self): match = "'wcs' must be set if `boundary_distortions=True`" with pytest.raises(ValueError, match=match): self.reg.to_sky(boundary_distortions=True) def test_frame_transformation(self): skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='galactic') reg = CircleSphericalSkyRegion(skycoord, 2 * u.arcsec) reg2 = reg.transform_to('icrs') assert reg2.center == skycoord.transform_to('icrs') assert_allclose(reg2.radius.to_value('arcsec'), 2) assert isinstance(reg2, CircleSphericalSkyRegion) assert reg2.frame.name == 'icrs' def test_dimension_center(self): center = SkyCoord([1, 2] * u.deg, [3, 4] * u.deg) radius = 2 * u.arcsec match = "'center' must be a scalar SkyCoord" with pytest.raises(ValueError, match=match): CircleSphericalSkyRegion(center, radius) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.radius = 3 * u.arcsec assert reg != self.reg def test_zero_size(self): with pytest.raises(ValueError): CircleSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), 0. * u.arcsec) def test_bounding_circle(self): skycoord = SkyCoord(3 * u.deg, 4 * u.deg, frame='galactic') reg = CircleSphericalSkyRegion(skycoord, 2 * u.arcsec) bounding_circle = reg.bounding_circle assert bounding_circle == reg def test_bounding_lonlat(self): skycoord = SkyCoord(3 * u.deg, 0 * u.deg, frame='galactic') reg = CircleSphericalSkyRegion(skycoord, 2 * u.arcsec) bounding_lonlat = reg.bounding_lonlat assert_quantity_allclose(bounding_lonlat[0], Longitude([3. * u.deg - 2 * u.arcsec, 3. * u.deg + 2 * u.arcsec])) assert_quantity_allclose(bounding_lonlat[1], Latitude([-2 * u.arcsec, 2 * u.arcsec])) class TestCirclePixelRegionToSkyEllipse: """ Tests for CirclePixelRegion.to_sky with as_ellipse=True. """ def setup_method(self): self.center = PixCoord(10, 10) self.radius = 5.0 self.meta = RegionMeta({'text': 'test'}) self.visual = RegionVisual({'color': 'blue'}) self.reg = CirclePixelRegion(self.center, self.radius, meta=self.meta, visual=self.visual) self.wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) def test_to_sky_as_ellipse(self): result = self.reg.to_sky(self.wcs, as_ellipse=True) assert isinstance(result, EllipseSkyRegion) assert result.meta == self.meta assert result.visual == self.visual def test_to_sky_ellipse_roundtrip(self): sky_ellipse = self.reg.to_sky(self.wcs, as_ellipse=True) pix_ellipse = sky_ellipse.to_pixel(self.wcs) # For a simple WCS without distortion, the roundtrip should # recover the original diameter as width and height assert_allclose(pix_ellipse.width, 2 * self.radius, rtol=1e-4) assert_allclose(pix_ellipse.height, 2 * self.radius, rtol=1e-4) assert_allclose(pix_ellipse.center.x, self.center.x, rtol=1e-5) assert_allclose(pix_ellipse.center.y, self.center.y, rtol=1e-5) def test_to_sky_ellipse_center_matches_circle(self): sky_circle = self.reg.to_sky(self.wcs) sky_ellipse = self.reg.to_sky(self.wcs, as_ellipse=True) assert_quantity_allclose(sky_ellipse.center.ra, sky_circle.center.ra) assert_quantity_allclose(sky_ellipse.center.dec, sky_circle.center.dec) def test_to_sky_as_ellipse_meta_copies(self): result = self.reg.to_sky(self.wcs, as_ellipse=True) result.meta['text'] = 'new' result.visual['color'] = 'green' assert result.meta['text'] != self.reg.meta['text'] assert result.visual['color'] != self.reg.visual['color'] class TestCircleSkyRegionToPixelEllipse: """ Tests for CircleSkyRegion.to_pixel with as_ellipse=True. """ def setup_method(self): self.center = SkyCoord(3 * u.deg, 4 * u.deg) self.radius = 20 * u.arcsec self.meta = RegionMeta({'text': 'test'}) self.visual = RegionVisual({'color': 'blue'}) self.reg = CircleSkyRegion(self.center, self.radius, meta=self.meta, visual=self.visual) self.wcs = make_simple_wcs(SkyCoord(3 * u.deg, 4 * u.deg), 5 * u.arcsec, 20) def test_to_pixel_as_ellipse(self): result = self.reg.to_pixel(self.wcs, as_ellipse=True) assert isinstance(result, EllipsePixelRegion) assert result.meta == self.meta assert result.visual == self.visual def test_to_pixel_default_returns_circle(self): result = self.reg.to_pixel(self.wcs) assert isinstance(result, CirclePixelRegion) def test_to_pixel_ellipse_roundtrip(self): pix_ellipse = self.reg.to_pixel(self.wcs, as_ellipse=True) sky_ellipse = pix_ellipse.to_sky(self.wcs) # Roundtrip should recover the original diameter assert_quantity_allclose(sky_ellipse.width, 2 * self.radius, rtol=1e-4) assert_quantity_allclose(sky_ellipse.height, 2 * self.radius, rtol=1e-4) def test_to_pixel_ellipse_center_matches_circle(self): pix_circle = self.reg.to_pixel(self.wcs) pix_ellipse = self.reg.to_pixel(self.wcs, as_ellipse=True) assert_allclose(pix_ellipse.center.x, pix_circle.center.x) assert_allclose(pix_ellipse.center.y, pix_circle.center.y) def test_to_pixel_as_ellipse_meta_copies(self): result = self.reg.to_pixel(self.wcs, as_ellipse=True) result.meta['text'] = 'new' result.visual['color'] = 'green' assert result.meta['text'] != self.reg.meta['text'] assert result.visual['color'] != self.reg.visual['color'] class TestCircleCovers: """ Test CirclePixelRegion contains and covers boundary semantics using explicit expected values. """ @staticmethod def setup_circle(): """ Create a circle centered at (5, 5) with radius 3. """ return CirclePixelRegion(PixCoord(5, 5), radius=3) @pytest.mark.parametrize(('method', 'expected'), [('contains', [True, False, False, False, False]), ('covers', [True, True, False, True, True])]) def test_array(self, method, expected): """ Test contains/covers boundary semantics for an array mixing interior, boundary, and exterior points. """ reg = self.setup_circle() # Center (in), right boundary, beyond right (out), top boundary, # left boundary x = np.array([5, 8, 9, 5, 2]) y = np.array([5, 5, 5, 8, 5]) result = getattr(reg, method)(PixCoord(x, y)) assert_equal(result, np.array(expected)) @pytest.mark.skipif(not HAS_SHAPELY, reason='shapely is required') class TestCircleShapelyComparison: """ Test that CirclePixelRegion contains and covers match Shapely's contains and covers functions (DE-9IM semantics). """ # Test points grouped by location relative to the circle. # Shapely approximates a circle with a polygonal ``buffer``, whose # vertices land exactly on the cardinal axes. The boundary points # below are therefore restricted to the axes (distance 3 from the # center) so that they lie on both the true circle and Shapely's # polygonal approximation; off-axis boundary points would fall # slightly inside the inscribed polygon and disagree. boundary_points = [(8, 5), (2, 5), (5, 8), (5, 2)] interior_points = [(5, 5), (6, 5), (5, 6), (4, 4)] exterior_points = [(9, 5), (1, 5), (5, 9), (5, 1), (0, 0)] all_points = boundary_points + interior_points + exterior_points @staticmethod def setup_circle(): """ Create a circle centered at (5, 5) with radius 3. """ return CirclePixelRegion(PixCoord(5, 5), radius=3) @staticmethod def shapely_circle(): """ Create an equivalent Shapely circle (buffer around the center). """ from shapely.geometry import Point as ShapelyPoint return ShapelyPoint(5, 5).buffer(3) @pytest.mark.parametrize('method', ['contains', 'covers']) @pytest.mark.parametrize('point', all_points) def test_matches_shapely(self, method, point): """ Test that contains/covers match Shapely for points on the boundary, interior, and exterior of the circle. """ from shapely import Point, contains, covers shp_func = {'contains': contains, 'covers': covers}[method] x, y = point reg_result = bool(getattr(self.setup_circle(), method)(PixCoord(x, y))) shp_result = bool(shp_func(self.shapely_circle(), Point(x, y))) assert reg_result == shp_result astropy-regions-f4139aa/regions/shapes/tests/test_common.py000066400000000000000000000066121521703212700242720ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Base class for all shape tests. """ import numpy as np import pytest from astropy.coordinates import SkyCoord from numpy.testing import assert_allclose, assert_equal from regions._utils.optional_deps import HAS_MATPLOTLIB from regions.core.pixcoord import PixCoord class BaseTestRegion: def test_repr(self): assert repr(self.reg) == self.expected_repr def test_str(self): assert str(self.reg) == self.expected_str def test_del(self): # test that Region shape attributes (descriptors) cannot be # deleted) with pytest.raises(AttributeError): delattr(self.reg, self.reg._params[0]) class BaseTestPixelRegion(BaseTestRegion): def test_area(self): assert_allclose(self.reg.area, self.expected_area) def test_mask_area(self): try: mask = self.reg.to_mask(mode='exact') assert_allclose(np.sum(mask.data), self.expected_area) except NotImplementedError: try: mask = self.reg.to_mask(mode='subpixels', subpixels=30) assert_allclose(np.sum(mask.data), self.expected_area, rtol=0.005) except NotImplementedError: pytest.skip() def test_contains_scalar(self): if len(self.inside) > 0: pixcoord = PixCoord(*self.inside[0]) assert self.reg.contains(pixcoord) assert pixcoord in self.reg if len(self.outside) > 0: pixcoord = PixCoord(*self.outside[0]) assert not self.reg.contains(pixcoord) assert pixcoord not in self.reg def test_contains_array_1d(self): pixcoord = PixCoord(*zip(*(self.inside + self.outside), strict=True)) actual = self.reg.contains(pixcoord) assert_equal(actual[:len(self.inside)], True) assert_equal(actual[len(self.inside):], False) match = 'coord must be scalar' with pytest.raises(ValueError, match=match): assert pixcoord in self.reg def test_contains_array_2d(self): x, y = zip(*(self.inside + self.outside), strict=True) pixcoord = PixCoord([x] * 3, [y] * 3) actual = self.reg.contains(pixcoord) assert actual.shape == (3, len(x)) assert_equal(actual[:, :len(self.inside)], True) assert_equal(actual[:, len(self.inside):], False) @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_plot(self): from matplotlib.artist import Artist artist = self.reg.plot() assert isinstance(artist, Artist) class BaseTestSkyRegion(BaseTestRegion): # TODO: here we should add inside/outside tests as above pass class BaseTestSphericalSkyRegion(BaseTestRegion): def test_contains_scalar(self): if len(self.inside) > 0: skycoord = SkyCoord(*self.inside[0]) assert self.reg.contains(skycoord) if len(self.outside) > 0: skycoord = SkyCoord(*self.outside[0]) assert not self.reg.contains(skycoord) def test_contains_array_1d(self): coos = self.inside + self.outside lon, lat = zip(*coos) skycoord = SkyCoord(list(lon), list(lat)) actual = self.reg.contains(skycoord) assert_equal(actual[:len(self.inside)], True) assert_equal(actual[len(self.inside):], False) astropy-regions-f4139aa/regions/shapes/tests/test_ellipse.py000066400000000000000000000441221521703212700244350ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import numpy as np import pytest from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.tests.helper import assert_quantity_allclose from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from numpy.testing import assert_allclose, assert_equal from regions._utils.optional_deps import HAS_MATPLOTLIB, HAS_SHAPELY from regions.core import PixCoord, RegionMeta, RegionVisual from regions.shapes.ellipse import EllipsePixelRegion, EllipseSkyRegion from regions.shapes.tests.test_common import (BaseTestPixelRegion, BaseTestSkyRegion) from regions.tests.helpers import make_simple_wcs @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): filename = get_pkg_data_filename('data/example_header.fits') header = fits.getheader(filename) return WCS(header) class TestEllipsePixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = EllipsePixelRegion(center=PixCoord(3, 4), width=4, height=3, angle=5 * u.deg, meta=meta, visual=visual) sample_box = [-2, 8, -1, 9] inside = [(4.5, 4)] outside = [(5, 4)] expected_area = 3 * np.pi expected_repr = ('') expected_str = ('Region: EllipsePixelRegion\ncenter: PixCoord(x=3, y=4)\n' 'width: 4\nheight: 3\nangle: 5.0 deg') def test_copy(self): reg = self.reg.copy() assert reg.center.xy == (3, 4) assert reg.width == 4 assert reg.height == 3 assert_allclose(reg.angle.to_value('deg'), 5) assert reg.meta == self.meta assert reg.visual == self.visual def test_pix_sky_roundtrip(self): wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) reg_new = self.reg.to_sky(wcs).to_pixel(wcs) assert_allclose(reg_new.center.x, self.reg.center.x) assert_allclose(reg_new.center.y, self.reg.center.y) assert_allclose(reg_new.width, self.reg.width) assert_allclose(reg_new.height, self.reg.height) assert_quantity_allclose(reg_new.angle, self.reg.angle) assert reg_new.meta == self.reg.meta assert reg_new.visual == self.reg.visual # test that converted region meta and visual are copies and not views reg_new.meta['text'] = 'new' reg_new.visual['color'] = 'green' assert reg_new.meta['text'] != self.reg.meta['text'] assert reg_new.visual['color'] != self.reg.visual['color'] @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_as_artist(self): patch = self.reg.as_artist() assert_allclose(patch.center, (3, 4)) assert_allclose(patch.width, 4) assert_allclose(patch.height, 3) assert_allclose(patch.angle, 5) def test_rotate(self): reg = self.reg.rotate(PixCoord(2, 3), 90 * u.deg) assert_allclose(reg.center.xy, (1, 4)) assert_allclose(reg.angle.to_value('deg'), 95) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.width = 3 assert reg != self.reg def test_region_bbox(self): a = 7 b = 3 reg = EllipsePixelRegion(PixCoord(50, 50), width=a, height=b, angle=0. * u.deg) assert reg.bounding_box.shape == (b, a) reg = EllipsePixelRegion(PixCoord(50.5, 50.5), width=a, height=b, angle=0. * u.deg) assert reg.bounding_box.shape == (b + 1, a + 1) reg = EllipsePixelRegion(PixCoord(50, 50), width=a, height=b, angle=90. * u.deg) assert reg.bounding_box.shape == (a, b) def test_region_bbox_zero_size(self): with pytest.raises(ValueError): EllipsePixelRegion(PixCoord(50, 50), width=0, height=0, angle=0. * u.deg) with pytest.raises(ValueError): EllipsePixelRegion(PixCoord(50, 50), width=10, height=0, angle=0. * u.deg) @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') @pytest.mark.parametrize('sync', (False, True)) def test_as_mpl_selector(self, sync): import matplotlib.pyplot as plt rng = np.random.default_rng(0) data = rng.random((16, 16)) mask = np.zeros_like(data) _, ax = plt.subplots() ax.imshow(data) def update_mask(reg): mask[:] = reg.to_mask( mode='subpixels', subpixels=10).to_image(data.shape) # For now this will only work with unrotated ellipses. Once this # works with rotated ellipses, the following exception check can # be removed as well as the ``angle=0 * u.deg`` in the call to # copy() below. with pytest.raises(NotImplementedError, match=('Cannot create matplotlib selector' ' for rotated ellipse.')): self.reg.as_mpl_selector(ax) region = self.reg.copy(angle=0 * u.deg) selector = region.as_mpl_selector(ax, callback=update_mask, sync=sync) # noqa: E501, F841 from matplotlib.backend_bases import MouseEvent canvas = ax.figure.canvas evt = MouseEvent('button_press_event', canvas, *ax.transData.transform((7.3, 4.4)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('motion_notify_event', canvas, *ax.transData.transform((9.3, 5.4)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('button_release_event', canvas, *ax.transData.transform((9.3, 5.4)), button=1) canvas.callbacks.process(evt.name, evt) ax.figure.canvas.draw() if sync: assert_allclose(region.center.x, 8.3) assert_allclose(region.center.y, 4.9) assert_allclose(region.width, 2) assert_allclose(region.height, 1) assert_quantity_allclose(region.angle, 0 * u.deg) assert_equal(mask, region.to_mask( mode='subpixels', subpixels=10).to_image(data.shape)) else: assert_allclose(region.center.x, 3) assert_allclose(region.center.y, 4) assert_allclose(region.width, 4) assert_allclose(region.height, 3) assert_quantity_allclose(region.angle, 0 * u.deg) assert_equal(mask, 0) match = 'Cannot attach more than one selector to a reg' with pytest.raises(AttributeError, match=match): region.as_mpl_selector(ax) plt.close() @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') @pytest.mark.parametrize('anywhere', (False, True)) def test_mpl_selector_drag(self, anywhere): """ Test dragging of entire region from central handle and anywhere. """ import matplotlib.pyplot as plt rng = np.random.default_rng(0) data = rng.random((16, 16)) mask = np.zeros_like(data) _, ax = plt.subplots() ax.imshow(data) def update_mask(reg): mask[:] = reg.to_mask( mode='subpixels', subpixels=10).to_image(data.shape) region = self.reg.copy(angle=0 * u.deg) selector = region.as_mpl_selector(ax, callback=update_mask, drag_from_anywhere=anywhere) assert selector.drag_from_anywhere is anywhere assert region._mpl_selector.drag_from_anywhere is anywhere # click_and_drag(selector, start=(3, 4), end=(3.5, 4.5)) from matplotlib.backend_bases import MouseEvent canvas = ax.figure.canvas evt = MouseEvent('button_press_event', canvas, *ax.transData.transform((3, 4)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('motion_notify_event', canvas, *ax.transData.transform((3.5, 4.5)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('button_release_event', canvas, *ax.transData.transform((3.5, 4.5)), button=1) canvas.callbacks.process(evt.name, evt) ax.figure.canvas.draw() assert_allclose(region.center.x, 3.5) assert_allclose(region.center.y, 4.5) assert_allclose(region.width, 4) assert_allclose(region.height, 3) evt = MouseEvent('button_press_event', canvas, *ax.transData.transform((3.25, 4.25)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('motion_notify_event', canvas, *ax.transData.transform((4.25, 5.25)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('button_release_event', canvas, *ax.transData.transform((4.25, 5.25)), button=1) canvas.callbacks.process(evt.name, evt) ax.figure.canvas.draw() # For drag_from_anywhere=False this will have created a new 1x1 # rectangle. if anywhere: assert_allclose(region.center.x, 4.5) assert_allclose(region.center.y, 5.5) assert_allclose(region.width, 4) assert_allclose(region.height, 3) else: assert_allclose(region.center.x, 4.5) assert_allclose(region.center.y, 5.5) assert_equal(mask, region.to_mask( mode='subpixels', subpixels=10).to_image(data.shape)) plt.close() @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') @pytest.mark.parametrize('userargs', ({'useblit': True}, {'grab_range': 20, 'minspanx': 5, 'minspany': 4}, {'props': {'facecolor': 'blue', 'linewidth': 2}}, {'twit': 'gumby'})) def test_mpl_selector_kwargs(self, userargs): """ Test that additional kwargs are passed to selector. """ import matplotlib.pyplot as plt rng = np.random.default_rng(0) data = rng.random((16, 16)) mask = np.zeros_like(data) _, ax = plt.subplots() ax.imshow(data) def update_mask(reg): mask[:] = reg.to_mask( mode='subpixels', subpixels=10).to_image(data.shape) region = self.reg.copy(angle=0 * u.deg) region.visual = {'color': 'red'} if 'twit' in userargs: match = r'__init__.. got an unexpected keyword argument' with pytest.raises(TypeError, match=match): selector = region.as_mpl_selector( ax, callback=update_mask, **userargs) else: selector = region.as_mpl_selector( ax, callback=update_mask, **userargs) assert region._mpl_selector.artists[0].get_edgecolor() == ( 1, 0, 0, 1) if 'props' in userargs: assert region._mpl_selector.artists[0].get_facecolor() == ( 0, 0, 1, 1) assert region._mpl_selector.artists[0].get_linewidth() == 2 else: assert region._mpl_selector.artists[0].get_facecolor() == ( 0, 0, 0, 0) assert region._mpl_selector.artists[0].get_linewidth() == 1 for key, val in userargs.items(): assert getattr(region._mpl_selector, key) == val assert getattr(selector, key) == val plt.close() class TestEllipseSkyRegion(BaseTestSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = EllipseSkyRegion(center=SkyCoord(3, 4, unit='deg'), width=4 * u.deg, height=3 * u.deg, angle=5 * u.deg, meta=meta, visual=visual) expected_repr = (', width=4.0 deg, height=3.0 deg,' ' angle=5.0 deg)>') expected_str = ('Region: EllipseSkyRegion\ncenter: \nwidth: 4.0 deg\n' 'height: 3.0 deg\nangle: 5.0 deg') def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center.ra.deg, 3) assert_allclose(reg.width.to_value('deg'), 4) assert_allclose(reg.height.to_value('deg'), 3) assert_allclose(reg.angle.to_value('deg'), 5) assert reg.meta == self.meta assert reg.visual == self.visual def test_dimension_center(self): center = SkyCoord([1, 2] * u.deg, [3, 4] * u.deg) width = 2 * u.arcsec height = 3 * u.arcsec match = "'center' must be a scalar SkyCoord" with pytest.raises(ValueError, match=match): EllipseSkyRegion(center, width, height) def test_contains(self, wcs): position = SkyCoord([1, 3] * u.deg, [2, 4] * u.deg) # 1,2 is outside, 3,4 is the center and is inside assert all(self.reg.contains(position, wcs) == np.array([False, True], dtype='bool')) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.width = 3 * u.deg assert reg != self.reg class TestEllipseCovers: """ Test EllipsePixelRegion contains and covers boundary semantics using explicit expected values. """ @staticmethod def setup_ellipse(): """ Create an axis-aligned ellipse centered at (5, 5). """ # width=6 (semi-major=3 along x), height=4 (semi-minor=2 along y) return EllipsePixelRegion(PixCoord(5, 5), width=6, height=4, angle=0 * u.deg) @pytest.mark.parametrize(('method', 'expected'), [('contains', [True, False, False, False, False]), ('covers', [True, True, False, True, True])]) def test_array(self, method, expected): """ Test contains/covers boundary semantics for an array mixing interior, boundary, and exterior points. """ reg = self.setup_ellipse() # Center (in), right boundary, beyond right (out), top boundary, # left boundary x = np.array([5, 8, 9, 5, 2]) y = np.array([5, 5, 5, 7, 5]) result = getattr(reg, method)(PixCoord(x, y)) assert_equal(result, np.array(expected)) @pytest.mark.parametrize('method', ['contains', 'covers']) def test_rotated_ellipse(self, method): """ Test boundary semantics for an ellipse rotated by 90 degrees, which swaps the effect of the semi-major and semi-minor axes. """ # After 90 deg rotation, the semi-major axis (3) is vertical and # the semi-minor axis (2) is horizontal, so the boundary points # are (5, 8), (5, 2) (top/bottom) and (7, 5), (3, 5) (left/right). reg = EllipsePixelRegion(PixCoord(5, 5), width=6, height=4, angle=90 * u.deg) boundary = [(5, 8), (5, 2), (7, 5), (3, 5)] # Boundary points are excluded by contains but included by covers. on_boundary = (method == 'covers') for x, y in boundary: assert bool(getattr(reg, method)(PixCoord(x, y))) is on_boundary # Interior points are always inside; exterior points never are. assert getattr(reg, method)(PixCoord(5, 5)) # center assert getattr(reg, method)(PixCoord(5, 7)) # inside vertically assert not getattr(reg, method)(PixCoord(5, 9)) # beyond top assert not getattr(reg, method)(PixCoord(8, 5)) # beyond right @pytest.mark.skipif(not HAS_SHAPELY, reason='shapely is required') class TestEllipseShapelyComparison: """ Test that EllipsePixelRegion contains and covers match Shapely's contains and covers functions (DE-9IM semantics). """ # Test points grouped by location relative to the ellipse. # Shapely approximates the ellipse with a scaled polygonal # ``buffer``, whose vertices land on the semi-major/semi-minor axes. # The boundary points below are therefore restricted to those axes # so that they lie on both the true ellipse and Shapely's polygonal # approximation; off-axis boundary points would fall slightly inside # the inscribed polygon and disagree. boundary_points = [(8, 5), (2, 5), (5, 7), (5, 3)] interior_points = [(5, 5), (6, 5), (5, 6), (4, 4), (7, 5)] exterior_points = [(9, 5), (1, 5), (5, 8), (5, 2), (0, 0), (8, 6)] all_points = boundary_points + interior_points + exterior_points @staticmethod def setup_ellipse(): """ Create an axis-aligned ellipse centered at (5, 5). """ return EllipsePixelRegion(PixCoord(5, 5), width=6, height=4, angle=0 * u.deg) @staticmethod def shapely_ellipse(): """ Create an equivalent Shapely ellipse by scaling a unit circle. """ from shapely import affinity from shapely.geometry import Point as ShapelyPoint # width=6 -> semi-major a=3, height=4 -> semi-minor b=2 circle = ShapelyPoint(5, 5).buffer(1) return affinity.scale(circle, xfact=3, yfact=2) @pytest.mark.parametrize('method', ['contains', 'covers']) @pytest.mark.parametrize('point', all_points) def test_matches_shapely(self, method, point): """ Test that contains/covers match Shapely for points on the boundary, interior, and exterior of the ellipse. """ from shapely import Point, contains, covers shp_func = {'contains': contains, 'covers': covers}[method] x, y = point reg_result = bool( getattr(self.setup_ellipse(), method)(PixCoord(x, y))) shp_result = bool(shp_func(self.shapely_ellipse(), Point(x, y))) assert reg_result == shp_result astropy-regions-f4139aa/regions/shapes/tests/test_line.py000066400000000000000000000124541521703212700237320ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import numpy as np import pytest from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.tests.helper import assert_quantity_allclose from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from numpy.testing import assert_allclose, assert_equal from regions._utils.optional_deps import HAS_MATPLOTLIB from regions.core import PixCoord, RegionMeta, RegionVisual from regions.shapes.line import LinePixelRegion, LineSkyRegion from regions.shapes.tests.test_common import (BaseTestPixelRegion, BaseTestSkyRegion) from regions.tests.helpers import make_simple_wcs @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): filename = get_pkg_data_filename('data/example_header.fits') header = fits.getheader(filename) return WCS(header) class TestLinePixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = LinePixelRegion(PixCoord(3, 4), PixCoord(4, 4), meta=meta, visual=visual) sample_box = [-2, 8, -1, 9] inside = [] outside = [(3.1, 4.2), (5, 4)] expected_area = 0 expected_repr = ('') expected_str = ('Region: LinePixelRegion\nstart: PixCoord(x=3, y=4)\n' 'end: PixCoord(x=4, y=4)') def test_copy(self): reg = self.reg.copy() assert reg.start.xy == (3, 4) assert reg.end.xy == (4, 4) assert reg.meta == self.meta assert reg.visual == self.visual def test_pix_sky_roundtrip(self): wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) reg_new = self.reg.to_sky(wcs).to_pixel(wcs) assert_allclose(reg_new.start.x, self.reg.start.x) assert_allclose(reg_new.start.y, self.reg.start.y) assert_allclose(reg_new.end.x, self.reg.end.x) assert_allclose(reg_new.end.y, self.reg.end.y) assert reg_new.meta == self.reg.meta assert reg_new.visual == self.reg.visual # test that converted region meta and visual are copies and not views reg_new.meta['text'] = 'new' reg_new.visual['color'] = 'green' assert reg_new.meta['text'] != self.reg.meta['text'] assert reg_new.visual['color'] != self.reg.visual['color'] @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_as_artist(self): line2d = self.reg.as_artist() assert 'Line2D' in str(line2d) def test_rotate(self): reg = self.reg.rotate(PixCoord(2, 3), 90 * u.deg) assert_allclose(reg.start.xy, (1, 4)) assert_allclose(reg.end.xy, (1, 5)) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.start = PixCoord(1, 2) assert reg != self.reg class TestLineSkyRegion(BaseTestSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) start = SkyCoord(3 * u.deg, 4 * u.deg, frame='galactic') end = SkyCoord(3 * u.deg, 5 * u.deg, frame='galactic') reg = LineSkyRegion(start, end, meta=meta, visual=visual) expected_repr = (', end=)>') expected_str = ('Region: LineSkyRegion\nstart: \nend: ') def test_copy(self): reg = self.reg.copy() assert_allclose(reg.start.b.deg, 4) assert_allclose(reg.end.b.deg, 5) assert reg.meta == self.meta assert reg.visual == self.visual def test_transformation(self, wcs): pixline = self.reg.to_pixel(wcs) assert_allclose(pixline.start.x, -50.5) assert_allclose(pixline.start.y, 299.5) assert_allclose(pixline.end.x, -50.5) assert_allclose(pixline.end.y, 349.5) skyline = pixline.to_sky(wcs) assert_quantity_allclose(skyline.start.data.lon, self.reg.start.data.lon) assert_quantity_allclose(skyline.start.data.lat, self.reg.start.data.lat) assert_quantity_allclose(skyline.end.data.lon, self.reg.end.data.lon) assert_quantity_allclose(skyline.end.data.lat, self.reg.end.data.lat) def test_contains(self, wcs): position = SkyCoord([1, 2] * u.deg, [3, 4] * u.deg) # Lines do not contain things result = self.reg.contains(position, wcs) assert isinstance(result, np.ndarray) assert result.shape == (2,) assert_equal(result, [False, False]) scalar_position = SkyCoord(1 * u.deg, 3 * u.deg) result_scalar = self.reg.contains(scalar_position, wcs) assert result_scalar is False def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.start = SkyCoord(1 * u.deg, 2 * u.deg, frame='galactic') assert reg != self.reg reg.start = SkyCoord(3 * u.deg, 4 * u.deg, frame='icrs') assert reg != self.reg astropy-regions-f4139aa/regions/shapes/tests/test_lune.py000066400000000000000000000154561521703212700237530ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import pytest from astropy.coordinates import Latitude, Longitude, SkyCoord from astropy.io import fits from astropy.tests.helper import assert_quantity_allclose from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from numpy.testing import assert_allclose from regions.core import RegionMeta, RegionVisual from regions.shapes.circle import CircleSphericalSkyRegion from regions.shapes.lune import LuneSphericalSkyRegion from regions.shapes.polygon import (PolygonPixelRegion, PolygonSkyRegion, PolygonSphericalSkyRegion) from regions.shapes.tests.test_common import BaseTestSphericalSkyRegion @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): filename = get_pkg_data_filename('data/example_header.fits') header = fits.getheader(filename) return WCS(header) class TestLuneSphericalSkyRegion(BaseTestSphericalSkyRegion): inside = [(90 * u.deg, 0 * u.deg)] outside = [(75 * u.deg, 0 * u.deg)] meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = LuneSphericalSkyRegion(SkyCoord(3 * u.deg, 0 * u.deg), SkyCoord(178 * u.deg, 0 * u.deg), meta=meta, visual=visual) # Verify changing order of GC center does not change the contains results: reg_inv = LuneSphericalSkyRegion(SkyCoord(178 * u.deg, 0 * u.deg), SkyCoord(3 * u.deg, 0 * u.deg), meta=meta, visual=visual) expected_repr = (', center_gc2=)>') expected_str = ('Region: LuneSphericalSkyRegion\n' 'center_gc1: \n' 'center_gc2: ') def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center_gc1.ra.deg, 3) assert_allclose(reg.center_gc2.ra.deg, 178) assert reg.meta == self.meta assert reg.visual == self.visual def test_transformation(self, wcs): # Test error for no boundary distortions: match = 'Invalid parameter: `boundary_distortions=False`!' with pytest.raises(ValueError, match=match): self.reg.to_pixel(wcs) match = 'Invalid parameter: `boundary_distortions=False`!' with pytest.raises(ValueError, match=match): self.reg.to_sky(wcs) # Test for transformations with `boundary_distortions=True` polypix = self.reg.to_pixel(wcs, boundary_distortions=True, n_vertices=4) assert isinstance(polypix, PolygonPixelRegion) assert len(polypix.vertices) == 4 assert_allclose(polypix.vertices.x, [7797.817216, 2952.903737, 7679.463723, -6047.096263]) assert_allclose(polypix.vertices.y, [-560.164308, -1256.912621, -338.786552, 1455.912621]) polysky = self.reg.to_sky(wcs, boundary_distortions=True, n_vertices=4) assert isinstance(polysky, PolygonSkyRegion) assert len(polysky.vertices) == 4 assert_allclose(polysky.vertices.l.deg, [206.033656, 302.931925, 208.400726, 122.931925]) assert_allclose(polysky.vertices.b.deg, [-13.193286, -27.128252, -8.765731, 27.128252]) def test_frame_transformation(self): reg2 = self.reg.transform_to('galactic') reg2_cent = reg2.centroid transf_reg_cent = self.reg.centroid.transform_to('galactic') assert isinstance(reg2, LuneSphericalSkyRegion) assert transf_reg_cent.frame.name == reg2_cent.frame.name assert_quantity_allclose(reg2_cent.l, transf_reg_cent.l) assert_quantity_allclose(reg2_cent.b, transf_reg_cent.b) assert_allclose(reg2.center_gc1.l.deg, self.reg.center_gc1.transform_to('galactic').l.deg) assert reg2.frame.name == 'galactic' def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.center_gc1 = SkyCoord(4 * u.deg, 0 * u.deg) assert reg != self.reg def test_bounding_circle(self): skycoord = SkyCoord(90.5 * u.deg, 0 * u.deg) reg = CircleSphericalSkyRegion(skycoord, 90 * u.deg) assert self.reg.bounding_circle == reg def test_covers(self): skycoord = SkyCoord([90, 75] * u.deg, [0, 0] * u.deg) actual = self.reg.covers(skycoord) assert actual[0] assert not actual[1] assert self.reg.covers(SkyCoord(90 * u.deg, 0 * u.deg)) def test_bounding_lonlat(self): bounding_lonlat = self.reg.bounding_lonlat bounding_lonlat_inv = self.reg_inv.bounding_lonlat assert_quantity_allclose(bounding_lonlat[0], bounding_lonlat_inv[0]) assert_quantity_allclose(bounding_lonlat[1], bounding_lonlat_inv[1]) assert_quantity_allclose(bounding_lonlat[0], Longitude([88. * u.deg, 93. * u.deg])) assert_quantity_allclose(bounding_lonlat[1], Latitude([-90 * u.deg, 90 * u.deg])) reg2 = LuneSphericalSkyRegion(SkyCoord(3 * u.deg, 20 * u.deg), SkyCoord(178 * u.deg, 20 * u.deg), meta=self.meta, visual=self.visual) bounding_lonlat2 = reg2.bounding_lonlat assert bounding_lonlat2[0] is None reg3 = LuneSphericalSkyRegion(SkyCoord(3 * u.deg, -20 * u.deg), SkyCoord(178 * u.deg, -20 * u.deg), meta=self.meta, visual=self.visual) bounding_lonlat3 = reg3.bounding_lonlat assert bounding_lonlat3[0] is None def test_discretize_boundary(self): polylune = self.reg.discretize_boundary(n_vertices=100) assert isinstance(polylune, PolygonSphericalSkyRegion) assert len(polylune.vertices) == 100 assert polylune.contains(self.reg.centroid) polylune_inv = self.reg_inv.discretize_boundary(n_vertices=100) assert polylune_inv.contains(self.reg.centroid) match = 'must be greater than' with pytest.raises(ValueError, match=match): self.reg.discretize_boundary(n_vertices=1) astropy-regions-f4139aa/regions/shapes/tests/test_masks.py000066400000000000000000000036601521703212700241200ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This file sets up detailed tests for computing masks with reference images. """ import astropy.units as u import pytest from regions.core import PixCoord, RegionMask from regions.shapes.circle import CirclePixelRegion from regions.shapes.ellipse import EllipsePixelRegion from regions.shapes.polygon import PolygonPixelRegion from regions.shapes.rectangle import RectanglePixelRegion REGIONS = [CirclePixelRegion(PixCoord(3.981987, 4.131378), radius=3.3411), EllipsePixelRegion(PixCoord(5.981987, 4.131378), width=10.4466, height=6.6822, angle=12 * u.deg), RectanglePixelRegion(PixCoord(5.981987, 4.131378), width=7.2233, height=4.3411, angle=12 * u.deg), PolygonPixelRegion(PixCoord([-2.334, 3.631, 1.122, -1.341], [-3.121, -2.118, 2.987, 1.759]))] MODES = [{'mode': 'center'}, {'mode': 'exact'}, {'mode': 'subpixels', 'subpixels': 1}, {'mode': 'subpixels', 'subpixels': 5}, {'mode': 'subpixels', 'subpixels': 18}] def label(value): if isinstance(value, CirclePixelRegion): return 'circ' elif isinstance(value, EllipsePixelRegion): return 'elli' elif isinstance(value, RectanglePixelRegion): return 'rect' elif isinstance(value, PolygonPixelRegion): return 'poly' else: return '-'.join(f'{key}_{value}' for key, value in sorted(value.items())) @pytest.mark.array_compare(file_format='text', write_kwargs={'fmt': '%12.8e'}) @pytest.mark.parametrize('region', REGIONS, ids=label) @pytest.mark.parametrize('mode', MODES, ids=label) def test_to_mask(region, mode): try: mask = region.to_mask(**mode) except NotImplementedError: pytest.xfail() assert isinstance(mask, RegionMask) return mask.data.astype(float) astropy-regions-f4139aa/regions/shapes/tests/test_pixel_sky_conversions.py000066400000000000000000000636001521703212700274410ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for region pixel-to-sky and sky-to-pixel coordinate conversions. This module covers: * roundtrip conversions for all pixel and sky region classes through simple, rotated, distorted (SIP), sheared, flipped-parity, and generalized (`gwcs.wcs.WCS`) WCS objects, exercising the SVD/Jacobian shape conversion (the SIP and gWCS cases in particular force the distortion (Jacobian) code path of the internal ``_wcs_helpers``) * the rotation-angle convention for directed (elliptical and rectangular) regions Automatic dispatch uses the Jacobian method for distorted WCS (e.g., SIP) and the offset-based method for non-distorted WCS. The regions rotation-angle convention measures the angle of the region's width axis from the longitude (RA) axis (sky regions) or the positive ``x`` axis (pixel regions), counterclockwise. This differs from the photutils convention (position angle from North), so for an axis-aligned, isotropic WCS the regions sky angle and pixel angle are numerically *equal* after conversion (no 90 deg offset). """ import astropy.units as u import numpy as np import pytest from astropy.io.fits import Header from astropy.tests.helper import assert_quantity_allclose from astropy.wcs import WCS from numpy.testing import assert_allclose from regions._utils.optional_deps import HAS_GWCS from regions.core import PixCoord from regions.core.metadata import RegionVisual from regions.shapes.annulus import (CircleAnnulusPixelRegion, CircleAnnulusSkyRegion, EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion, RectangleAnnulusPixelRegion, RectangleAnnulusSkyRegion) from regions.shapes.circle import CirclePixelRegion, CircleSkyRegion from regions.shapes.ellipse import EllipsePixelRegion, EllipseSkyRegion from regions.shapes.rectangle import RectanglePixelRegion, RectangleSkyRegion from regions.shapes.text import TextPixelRegion, TextSkyRegion from regions.tests.helpers import WCS_CENTER as CENTER from regions.tests.helpers import make_gwcs # Module constants PIX_CENTER = (10, 10) # Rotation angles (deg) used to pin the sky/pixel angle convention. ANGLES_DEG = [0, 30, 60, 135, 200, 315] # Rotation angles (deg) used for the sheared-WCS and degenerate-shape # roundtrip tests. SHEAR_ANGLES_DEG = [0.0, 30.0, 75.0] # Each case bundles a matching (sky, pixel) region class pair with their # construction kwargs. ``size_attrs`` lists the shape attributes # compared in roundtrip assertions and ``has_angle`` flags whether the # region carries an ``angle`` rotation. _REGION_CASES = [ {'id': 'ellipse', 'sky_cls': EllipseSkyRegion, 'sky_kw': {'width': 200 * u.arcsec, 'height': 100 * u.arcsec, 'angle': 30 * u.deg}, 'pix_cls': EllipsePixelRegion, 'pix_kw': {'width': 5, 'height': 3, 'angle': 45 * u.deg}, 'size_attrs': ('width', 'height'), 'has_angle': True}, {'id': 'rectangle', 'sky_cls': RectangleSkyRegion, 'sky_kw': {'width': 200 * u.arcsec, 'height': 100 * u.arcsec, 'angle': 30 * u.deg}, 'pix_cls': RectanglePixelRegion, 'pix_kw': {'width': 5, 'height': 3, 'angle': 45 * u.deg}, 'size_attrs': ('width', 'height'), 'has_angle': True}, {'id': 'ellipse_annulus', 'sky_cls': EllipseAnnulusSkyRegion, 'sky_kw': {'inner_width': 100 * u.arcsec, 'outer_width': 200 * u.arcsec, 'inner_height': 50 * u.arcsec, 'outer_height': 100 * u.arcsec, 'angle': 30 * u.deg}, 'pix_cls': EllipseAnnulusPixelRegion, 'pix_kw': {'inner_width': 3, 'outer_width': 6, 'inner_height': 2, 'outer_height': 4, 'angle': 45 * u.deg}, 'size_attrs': ('inner_width', 'outer_width', 'inner_height', 'outer_height'), 'has_angle': True}, {'id': 'rectangle_annulus', 'sky_cls': RectangleAnnulusSkyRegion, 'sky_kw': {'inner_width': 100 * u.arcsec, 'outer_width': 200 * u.arcsec, 'inner_height': 50 * u.arcsec, 'outer_height': 100 * u.arcsec, 'angle': 30 * u.deg}, 'pix_cls': RectangleAnnulusPixelRegion, 'pix_kw': {'inner_width': 3, 'outer_width': 6, 'inner_height': 2, 'outer_height': 4, 'angle': 45 * u.deg}, 'size_attrs': ('inner_width', 'outer_width', 'inner_height', 'outer_height'), 'has_angle': True}, {'id': 'circle', 'sky_cls': CircleSkyRegion, 'sky_kw': {'radius': 100 * u.arcsec}, 'pix_cls': CirclePixelRegion, 'pix_kw': {'radius': 3}, 'size_attrs': ('radius',), 'has_angle': False}, {'id': 'circle_annulus', 'sky_cls': CircleAnnulusSkyRegion, 'sky_kw': {'inner_radius': 50 * u.arcsec, 'outer_radius': 100 * u.arcsec}, 'pix_cls': CircleAnnulusPixelRegion, 'pix_kw': {'inner_radius': 2, 'outer_radius': 4}, 'size_attrs': ('inner_radius', 'outer_radius'), 'has_angle': False}, ] # All region cases and the directed (rotatable) subset, wrapped as # pytest params with readable ids. REGION_CASES = [pytest.param(case, id=case['id']) for case in _REGION_CASES] DIRECTED_CASES = [pytest.param(case, id=case['id']) for case in _REGION_CASES if case['has_angle']] def _angle_diff_deg(actual, desired): """ Signed angular difference in degrees, wrapped to (-180, 180]. """ diff = (actual.to_value(u.deg) - desired.to_value(u.deg) + 180) % 360 return diff - 180 def _build_pair(case, angle): """ Build a matching (sky_region, pixel_region) pair from a case, overriding the rotation ``angle`` of both regions. """ sky = case['sky_cls'](CENTER, **{**case['sky_kw'], 'angle': angle}) pix = case['pix_cls'](PixCoord(*PIX_CENTER), **{**case['pix_kw'], 'angle': angle}) return sky, pix def _make_sip_wcs(ra_deg=100.0, dec_deg=30.0): """ Build a small TAN-SIP WCS centered at the given (RA, Dec). The SIP terms are tiny but nonzero, which forces the Jacobian (distortion) code path to be exercised by ``to_pixel``/``to_sky``. """ header = Header() header['NAXIS'] = 2 header['NAXIS1'] = 21 header['NAXIS2'] = 21 header['CRPIX1'] = 10.5 header['CRPIX2'] = 10.5 header['CRVAL1'] = ra_deg header['CRVAL2'] = dec_deg header['CTYPE1'] = 'RA---TAN-SIP' header['CTYPE2'] = 'DEC--TAN-SIP' cdelt = 0.1 / 3600.0 header['CD1_1'] = -cdelt header['CD1_2'] = 0.0 header['CD2_1'] = 0.0 header['CD2_2'] = cdelt header['A_ORDER'] = 2 header['A_2_0'] = 1e-7 header['A_0_2'] = 1e-7 header['B_ORDER'] = 2 header['B_2_0'] = 1e-7 header['B_0_2'] = 1e-7 return WCS(header) @pytest.fixture def simple_wcs(): """ A simple axis-aligned TAN WCS (RA increases to the left, equal pixel scales, North = +y). """ wcs = WCS(naxis=2) wcs.wcs.crpix = [10.5, 10.5] wcs.wcs.crval = [CENTER.ra.deg, CENTER.dec.deg] wcs.wcs.cdelt = [-0.1 / 3600, 0.1 / 3600] wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] return wcs @pytest.fixture def rotated_wcs(): """ A 25 deg rotated TAN WCS. """ rotation_deg = 25.0 cdelt = 0.1 / 3600 rad = np.radians(rotation_deg) cos_a = np.cos(rad) sin_a = np.sin(rad) wcs = WCS(naxis=2) wcs.wcs.crpix = [10.5, 10.5] wcs.wcs.crval = [CENTER.ra.deg, CENTER.dec.deg] wcs.wcs.cd = [[-cdelt * cos_a, cdelt * sin_a], [cdelt * sin_a, cdelt * cos_a]] wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] return wcs @pytest.fixture def sheared_wcs(): """ A sheared TAN WCS where the pixel x and y axes are 80 deg apart on the sky (10 deg of shear from a perpendicular axis-aligned grid). """ cdelt = 0.1 / 3600 a = np.radians(80.0) wcs = WCS(naxis=2) wcs.wcs.crpix = [10.5, 10.5] wcs.wcs.crval = [CENTER.ra.deg, CENTER.dec.deg] wcs.wcs.cd = [[-cdelt, cdelt * np.cos(a)], [0.0, cdelt * np.sin(a)]] wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] return wcs @pytest.fixture def sip_wcs(): """ A TAN-SIP WCS with small distortion terms centered at CENTER. """ return _make_sip_wcs() @pytest.fixture def flipped_wcs(): """ A flipped-parity TAN WCS (North down, East left). Both CDELT values are negative, so the pixel scale matrix has a positive determinant (parity = +1), opposite the standard astronomical convention. North (increasing Dec) points along -y and East (increasing RA) points along -x. """ wcs = WCS(naxis=2) wcs.wcs.crpix = [10.5, 10.5] wcs.wcs.crval = [CENTER.ra.deg, CENTER.dec.deg] wcs.wcs.cdelt = [-0.1 / 3600, -0.1 / 3600] wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] return wcs class TestSkyToPixel: """ Converting a sky region to a pixel region must return the matching pixel class with positive shape parameters. """ @pytest.mark.parametrize('case', REGION_CASES) def test_simple_wcs(self, simple_wcs, case): pix = case['sky_cls'](CENTER, **case['sky_kw']).to_pixel(simple_wcs) assert isinstance(pix, case['pix_cls']) for attr in case['size_attrs']: assert getattr(pix, attr) > 0 @pytest.mark.parametrize('case', REGION_CASES) def test_rotated_wcs(self, rotated_wcs, case): pix = case['sky_cls'](CENTER, **case['sky_kw']).to_pixel(rotated_wcs) assert isinstance(pix, case['pix_cls']) for attr in case['size_attrs']: assert getattr(pix, attr) > 0 @pytest.mark.parametrize('case', REGION_CASES) def test_sip_wcs(self, sip_wcs, case): """ The Jacobian (distortion) path is used automatically for a SIP WCS. """ pix = case['sky_cls'](CENTER, **case['sky_kw']).to_pixel(sip_wcs) assert isinstance(pix, case['pix_cls']) for attr in case['size_attrs']: assert getattr(pix, attr) > 0 class TestPixelToSky: """ Converting a pixel region to a sky region must return the matching sky class with positive angular shape parameters. """ @pytest.mark.parametrize('case', REGION_CASES) def test_simple_wcs(self, simple_wcs, case): sky = case['pix_cls'](PixCoord(*PIX_CENTER), **case['pix_kw']).to_sky(simple_wcs) assert isinstance(sky, case['sky_cls']) for attr in case['size_attrs']: assert getattr(sky, attr) > 0 * u.arcsec @pytest.mark.parametrize('case', REGION_CASES) def test_sip_wcs(self, sip_wcs, case): sky = case['pix_cls'](PixCoord(*PIX_CENTER), **case['pix_kw']).to_sky(sip_wcs) assert isinstance(sky, case['sky_cls']) for attr in case['size_attrs']: assert getattr(sky, attr) > 0 * u.arcsec class TestRoundtripSkyPixelSky: """ A sky -> pixel -> sky roundtrip must recover the original shape parameters and rotation angle. """ @pytest.mark.parametrize('case', REGION_CASES) def test_simple_wcs(self, simple_wcs, case): sky = case['sky_cls'](CENTER, **case['sky_kw']) sky_rt = sky.to_pixel(simple_wcs).to_sky(simple_wcs) assert isinstance(sky_rt, case['sky_cls']) for attr in case['size_attrs']: assert_quantity_allclose(getattr(sky_rt, attr), getattr(sky, attr)) if case['has_angle']: assert_quantity_allclose(sky_rt.angle, sky.angle, atol=1e-9 * u.deg) @pytest.mark.parametrize('case', REGION_CASES) def test_rotated_wcs(self, rotated_wcs, case): sky = case['sky_cls'](CENTER, **case['sky_kw']) sky_rt = sky.to_pixel(rotated_wcs).to_sky(rotated_wcs) for attr in case['size_attrs']: assert_quantity_allclose(getattr(sky_rt, attr), getattr(sky, attr)) if case['has_angle']: assert_quantity_allclose(sky_rt.angle, sky.angle, atol=1e-9 * u.deg) @pytest.mark.parametrize('case', REGION_CASES) def test_sip_wcs(self, sip_wcs, case): """ Roundtrip through a distorted (SIP) WCS, exercising the Jacobian code path. """ sky = case['sky_cls'](CENTER, **case['sky_kw']) sky_rt = sky.to_pixel(sip_wcs).to_sky(sip_wcs) for attr in case['size_attrs']: assert_quantity_allclose(getattr(sky_rt, attr), getattr(sky, attr), rtol=1e-3) if case['has_angle']: assert_quantity_allclose(sky_rt.angle, sky.angle, atol=1e-3 * u.deg) class TestRoundtripPixelSkyPixel: """ A pixel -> sky -> pixel roundtrip must recover the original center, shape parameters, and rotation angle. """ @pytest.mark.parametrize('case', REGION_CASES) def test_simple_wcs(self, simple_wcs, case): pix = case['pix_cls'](PixCoord(*PIX_CENTER), **case['pix_kw']) pix_rt = pix.to_sky(simple_wcs).to_pixel(simple_wcs) assert_allclose(pix_rt.center.x, pix.center.x) assert_allclose(pix_rt.center.y, pix.center.y) for attr in case['size_attrs']: assert_allclose(getattr(pix_rt, attr), getattr(pix, attr)) if case['has_angle']: assert_quantity_allclose(pix_rt.angle, pix.angle, atol=1e-9 * u.deg) @pytest.mark.parametrize('case', REGION_CASES) def test_rotated_wcs(self, rotated_wcs, case): pix = case['pix_cls'](PixCoord(*PIX_CENTER), **case['pix_kw']) pix_rt = pix.to_sky(rotated_wcs).to_pixel(rotated_wcs) assert_allclose(pix_rt.center.x, pix.center.x) assert_allclose(pix_rt.center.y, pix.center.y) for attr in case['size_attrs']: assert_allclose(getattr(pix_rt, attr), getattr(pix, attr)) if case['has_angle']: assert_quantity_allclose(pix_rt.angle, pix.angle, atol=1e-9 * u.deg) @pytest.mark.parametrize('case', REGION_CASES) def test_sip_wcs(self, sip_wcs, case): pix = case['pix_cls'](PixCoord(*PIX_CENTER), **case['pix_kw']) pix_rt = pix.to_sky(sip_wcs).to_pixel(sip_wcs) assert_allclose(pix_rt.center.x, pix.center.x, atol=1e-6) assert_allclose(pix_rt.center.y, pix.center.y, atol=1e-6) for attr in case['size_attrs']: assert_allclose(getattr(pix_rt, attr), getattr(pix, attr), rtol=1e-3) if case['has_angle']: assert_quantity_allclose(pix_rt.angle, pix.angle, atol=1e-3 * u.deg) @pytest.mark.skipif(not HAS_GWCS, reason='gwcs is required') class TestGWCSRoundtrip: """ Roundtrip conversions through a generalized WCS (`gwcs.wcs.WCS`). A gWCS has no ``has_distortion`` attribute, so the Jacobian path is always used. """ @pytest.mark.parametrize('case', REGION_CASES) def test_sky_pixel_sky(self, case): wcs = make_gwcs() sky = case['sky_cls'](CENTER, **case['sky_kw']) sky_rt = sky.to_pixel(wcs).to_sky(wcs) assert isinstance(sky_rt, case['sky_cls']) for attr in case['size_attrs']: assert_quantity_allclose(getattr(sky_rt, attr), getattr(sky, attr), rtol=1e-5) if case['has_angle']: assert_quantity_allclose(sky_rt.angle, sky.angle, atol=1e-4 * u.deg) @pytest.mark.parametrize('case', REGION_CASES) def test_pixel_sky_pixel(self, case): wcs = make_gwcs() pix = case['pix_cls'](PixCoord(*PIX_CENTER), **case['pix_kw']) pix_rt = pix.to_sky(wcs).to_pixel(wcs) assert_allclose(pix_rt.center.x, pix.center.x, rtol=1e-5) assert_allclose(pix_rt.center.y, pix.center.y, rtol=1e-5) for attr in case['size_attrs']: assert_allclose(getattr(pix_rt, attr), getattr(pix, attr), rtol=1e-5) if case['has_angle']: assert_quantity_allclose(pix_rt.angle, pix.angle, atol=1e-4 * u.deg) class TestSkyPixelAngleConvention: """ Verify the sky/pixel angle convention on an axis-aligned WCS. The regions convention measures the rotation angle of the region's width axis from the longitude (RA) axis (sky regions) or the positive ``x`` axis (pixel regions), counterclockwise. Internally the WCS helpers use the position-angle (from North) convention; the regions shape methods apply a 90 deg offset to bridge the two. For a standard axis-aligned WCS (RA increasing to the left, equal isotropic pixel scale, North = +y), the regions sky angle and pixel angle must be numerically equal after conversion. These tests pin that convention down so that any future refactor of the internal ``_wcs_helpers`` cannot silently re-introduce a symmetric 90 deg offset bug, which is invisible to roundtrip-only tests because the offset cancels in sky -> pixel -> sky (and vice versa). """ @pytest.mark.parametrize('case', DIRECTED_CASES) @pytest.mark.parametrize('angle_deg', ANGLES_DEG) def test_sky_to_pixel_angle(self, simple_wcs, case, angle_deg): """ For an axis-aligned WCS, ``sky.to_pixel`` produces a pixel region with the same numerical rotation angle as the sky region. """ sky, _ = _build_pair(case, angle_deg * u.deg) pix = sky.to_pixel(simple_wcs) assert abs(_angle_diff_deg(pix.angle, angle_deg * u.deg)) < 1e-3 @pytest.mark.parametrize('case', DIRECTED_CASES) @pytest.mark.parametrize('angle_deg', ANGLES_DEG) def test_pixel_to_sky_angle(self, simple_wcs, case, angle_deg): """ For an axis-aligned WCS, ``pixel.to_sky`` produces a sky region with the same numerical rotation angle as the pixel region. """ _, pix = _build_pair(case, angle_deg * u.deg) sky = pix.to_sky(simple_wcs) assert abs(_angle_diff_deg(sky.angle, angle_deg * u.deg)) < 1e-3 def test_text_region_rotation_sky_to_pixel(self, simple_wcs): """ ``TextSkyRegion.to_pixel`` must preserve the visual rotation angle (in degrees) for an axis-aligned WCS. """ sky = TextSkyRegion(CENTER, 'foo', visual=RegionVisual(rotation=30.0)) pix = sky.to_pixel(simple_wcs) diff = ((pix.visual['rotation'] - 30.0 + 180) % 360) - 180 assert abs(diff) < 1e-3 def test_text_region_rotation_pixel_to_sky(self, simple_wcs): """ ``TextPixelRegion.to_sky`` must preserve the visual rotation angle (in degrees) for an axis-aligned WCS. """ pix = TextPixelRegion(PixCoord(9.5, 9.5), 'foo', visual=RegionVisual(rotation=30.0)) sky = pix.to_sky(simple_wcs) diff = ((sky.visual['rotation'] - 30.0 + 180) % 360) - 180 assert abs(diff) < 1e-3 class TestShearedWCSRoundtrip: """ Verify that directed regions round-trip exactly through a sheared WCS, where the pixel x and y axes are not perpendicular on the sky. """ @pytest.mark.parametrize('case', DIRECTED_CASES) @pytest.mark.parametrize('angle_deg', SHEAR_ANGLES_DEG) def test_sky_pixel_sky(self, sheared_wcs, case, angle_deg): sky, _ = _build_pair(case, angle_deg * u.deg) sky_rt = sky.to_pixel(sheared_wcs).to_sky(sheared_wcs) assert abs(_angle_diff_deg(sky_rt.angle, angle_deg * u.deg)) < 1e-6 for attr in case['size_attrs']: assert_quantity_allclose(getattr(sky_rt, attr), getattr(sky, attr), rtol=1e-6) @pytest.mark.parametrize('case', DIRECTED_CASES) @pytest.mark.parametrize('angle_deg', SHEAR_ANGLES_DEG) def test_pixel_sky_pixel(self, sheared_wcs, case, angle_deg): _, pix = _build_pair(case, angle_deg * u.deg) pix_rt = pix.to_sky(sheared_wcs).to_pixel(sheared_wcs) assert abs(_angle_diff_deg(pix_rt.angle, angle_deg * u.deg)) < 1e-6 for attr in case['size_attrs']: assert_allclose(getattr(pix_rt, attr), getattr(pix, attr), rtol=1e-6) @pytest.mark.parametrize('angle_deg', SHEAR_ANGLES_DEG) def test_text_sky_pixel_sky_rotation(self, sheared_wcs, angle_deg): """ Text region rotation must round-trip through a sheared WCS. The SVD path preserves the input theta direction when the input ellipse is circular (no width/height), via the degenerate singular-value fallback in the shape helper. """ sky = TextSkyRegion(CENTER, 'foo', visual=RegionVisual(rotation=angle_deg)) rt = sky.to_pixel(sheared_wcs).to_sky(sheared_wcs) diff = ((rt.visual['rotation'] - angle_deg + 180) % 360) - 180 assert abs(diff) < 1e-6 @pytest.mark.parametrize('angle_deg', SHEAR_ANGLES_DEG) def test_text_pixel_sky_pixel_rotation(self, sheared_wcs, angle_deg): pix = TextPixelRegion(PixCoord(*PIX_CENTER), 'foo', visual=RegionVisual(rotation=angle_deg)) rt = pix.to_sky(sheared_wcs).to_pixel(sheared_wcs) diff = ((rt.visual['rotation'] - angle_deg + 180) % 360) - 180 assert abs(diff) < 1e-6 class TestCircularInputAnglePreserved: """ Verify that the SVD path preserves the input rotation angle when the input rectangular/elliptical region is shape-degenerate (width == height): the converted angle must round-trip exactly. For a circular shape the SVD principal axis is otherwise arbitrary, so the helper falls back to the mapped width semi-axis direction to preserve orientation. """ @pytest.mark.parametrize('angle_deg', SHEAR_ANGLES_DEG) @pytest.mark.parametrize( 'sky_cls', [RectangleSkyRegion, EllipseSkyRegion]) def test_square_sky_roundtrip(self, rotated_wcs, sky_cls, angle_deg): sky = sky_cls(CENTER, width=200 * u.arcsec, height=200 * u.arcsec, angle=angle_deg * u.deg) sky_rt = sky.to_pixel(rotated_wcs).to_sky(rotated_wcs) assert abs(_angle_diff_deg(sky_rt.angle, angle_deg * u.deg)) < 1e-6 @pytest.mark.parametrize('angle_deg', SHEAR_ANGLES_DEG) @pytest.mark.parametrize( 'pix_cls', [RectanglePixelRegion, EllipsePixelRegion]) def test_square_pixel_roundtrip(self, rotated_wcs, pix_cls, angle_deg): pix = pix_cls(PixCoord(*PIX_CENTER), width=4, height=4, angle=angle_deg * u.deg) pix_rt = pix.to_sky(rotated_wcs).to_pixel(rotated_wcs) assert abs(_angle_diff_deg(pix_rt.angle, angle_deg * u.deg)) < 1e-6 class TestFlippedParityWCS: """ Regression tests for a flipped-parity WCS (North down, East left; positive-determinant pixel scale matrix). Such a WCS previously produced regions that were mirrored about the x-axis. For the flipped axis-aligned WCS, the regions sky and pixel rotation angles are related by a mirror, ``pixel.angle == 360 deg - sky.angle`` (mod 360), rather than the identity relation that holds for a standard-parity axis-aligned WCS. (Regions measures the angle from the longitude/x axis, so there is no 90 deg offset as in the photutils position-angle convention.) """ def test_flipped_wcs_has_positive_parity(self, flipped_wcs): """ The flipped WCS fixture must have a positive-determinant pixel scale matrix (parity = +1). """ assert np.linalg.det(flipped_wcs.pixel_scale_matrix) > 0 def test_north_is_minus_y(self, flipped_wcs): """ A sky region whose width axis points North (regions sky angle = 90 deg, i.e. along the Dec axis) maps to a pixel region pointing along -y (down), i.e., pixel angle = 270 deg. """ sky = EllipseSkyRegion(CENTER, width=200 * u.arcsec, height=100 * u.arcsec, angle=90 * u.deg) pix = sky.to_pixel(flipped_wcs) assert abs(_angle_diff_deg(pix.angle, 270 * u.deg)) < 2e-5 def test_east_is_minus_x(self, flipped_wcs): """ A sky region whose width axis points East (regions sky angle = 180 deg) maps to a pixel region pointing along -x (left), i.e., pixel angle = 180 deg. """ sky = EllipseSkyRegion(CENTER, width=200 * u.arcsec, height=100 * u.arcsec, angle=180 * u.deg) pix = sky.to_pixel(flipped_wcs) assert abs(_angle_diff_deg(pix.angle, 180 * u.deg)) < 2e-5 @pytest.mark.parametrize('case', DIRECTED_CASES) @pytest.mark.parametrize('angle_deg', ANGLES_DEG) def test_sky_to_pixel_angle(self, flipped_wcs, case, angle_deg): """ Verify ``sky.to_pixel`` returns ``360 deg - angle`` (mod 360) for the flipped-parity WCS (not the unmirrored value). """ sky, _ = _build_pair(case, angle_deg * u.deg) pix = sky.to_pixel(flipped_wcs) assert abs(_angle_diff_deg(pix.angle, -angle_deg * u.deg)) < 2e-5 @pytest.mark.parametrize('case', DIRECTED_CASES) @pytest.mark.parametrize('angle_deg', ANGLES_DEG) def test_pixel_to_sky_angle(self, flipped_wcs, case, angle_deg): """ Verify ``pixel.to_sky`` returns ``360 deg - angle`` (mod 360) for the flipped-parity WCS (the inverse of the above relation). """ _, pix = _build_pair(case, angle_deg * u.deg) sky = pix.to_sky(flipped_wcs) assert abs(_angle_diff_deg(sky.angle, -angle_deg * u.deg)) < 2e-5 @pytest.mark.parametrize('case', DIRECTED_CASES) @pytest.mark.parametrize('angle_deg', ANGLES_DEG) def test_sky_pixel_sky_roundtrip_angle(self, flipped_wcs, case, angle_deg): """ Verify ``sky -> pixel -> sky`` preserves the original angle for the flipped-parity WCS. """ sky, _ = _build_pair(case, angle_deg * u.deg) sky_rt = sky.to_pixel(flipped_wcs).to_sky(flipped_wcs) assert abs(_angle_diff_deg(sky_rt.angle, angle_deg * u.deg)) < 2e-5 astropy-regions-f4139aa/regions/shapes/tests/test_point.py000066400000000000000000000077121521703212700241350ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import numpy as np import pytest from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from numpy.testing import assert_allclose, assert_equal from regions._utils.optional_deps import HAS_MATPLOTLIB from regions.core import PixCoord, RegionMeta, RegionVisual from regions.shapes.point import PointPixelRegion, PointSkyRegion from regions.shapes.tests.test_common import (BaseTestPixelRegion, BaseTestSkyRegion) from regions.tests.helpers import make_simple_wcs @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): filename = get_pkg_data_filename('data/example_header.fits') header = fits.getheader(filename) return WCS(header) class TestPointPixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = PointPixelRegion(PixCoord(3, 4), meta=meta, visual=visual) sample_box = [-2, 8, -1, 9] inside = [] outside = [(3.1, 4.2), (5, 4)] expected_area = 0 expected_repr = '' expected_str = 'Region: PointPixelRegion\ncenter: PixCoord(x=3, y=4)' def test_copy(self): reg = self.reg.copy() assert reg.center.xy == (3, 4) assert reg.meta == self.meta assert reg.visual == self.visual def test_pix_sky_roundtrip(self): wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) reg_new = self.reg.to_sky(wcs).to_pixel(wcs) assert_allclose(reg_new.center.x, self.reg.center.x) assert_allclose(reg_new.center.y, self.reg.center.y) assert reg_new.meta == self.reg.meta assert reg_new.visual == self.reg.visual # test that converted region meta and visual are copies and not views reg_new.meta['text'] = 'new' reg_new.visual['color'] = 'green' assert reg_new.meta['text'] != self.reg.meta['text'] assert reg_new.visual['color'] != self.reg.visual['color'] @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_as_artist(self): artist = self.reg.as_artist() assert artist.get_data() == ([3], [4]) artist = self.reg.as_artist(origin=(1, 1)) assert artist.get_data() == ([2], [3]) def test_rotate(self): reg = self.reg.rotate(PixCoord(2, 3), 90 * u.deg) assert_allclose(reg.center.xy, (1, 4)) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.center = PixCoord(1, 2) assert reg != self.reg class TestPointSkyRegion(BaseTestSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = PointSkyRegion(SkyCoord(3, 4, unit='deg'), meta=meta, visual=visual) expected_repr = (')>') expected_str = ('Region: PointSkyRegion\ncenter: ') def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center.ra.deg, 3) assert reg.meta == self.meta assert reg.visual == self.visual def test_contains(self, wcs): position = SkyCoord([1, 2] * u.deg, [3, 4] * u.deg) # Points do not contain things result = self.reg.contains(position, wcs) assert isinstance(result, np.ndarray) assert result.shape == (2,) assert_equal(result, [False, False]) scalar_position = SkyCoord(1 * u.deg, 3 * u.deg) result_scalar = self.reg.contains(scalar_position, wcs) assert result_scalar is False def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.center = SkyCoord(1, 2, unit='deg') assert reg != self.reg astropy-regions-f4139aa/regions/shapes/tests/test_polygon.py000066400000000000000000000524071521703212700244740ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst from copy import copy import astropy.units as u import numpy as np import pytest from astropy.coordinates import Latitude, Longitude, SkyCoord from astropy.tests.helper import assert_quantity_allclose from numpy.testing import assert_allclose, assert_equal from regions._utils.examples import _make_example_dataset from regions._utils.optional_deps import HAS_MATPLOTLIB, HAS_SHAPELY from regions.core import PixCoord, RegionBoundingBox, RegionMeta, RegionVisual from regions.shapes.circle import CircleSphericalSkyRegion from regions.shapes.polygon import (PolygonPixelRegion, PolygonSkyRegion, PolygonSphericalSkyRegion, RegularPolygonPixelRegion) from regions.shapes.tests.test_common import (BaseTestPixelRegion, BaseTestSkyRegion, BaseTestSphericalSkyRegion) from regions.tests.helpers import assert_skycoord_allclose, make_simple_wcs @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): config = dict(crpix=(18, 9), cdelt=(-10, 10), shape=(18, 36)) dataset = _make_example_dataset(config=config) return dataset.wcs class TestPolygonPixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = PolygonPixelRegion(PixCoord([1, 3, 1], [1, 1, 4]), meta=meta, visual=visual) sample_box = [0, 4, 0, 5] inside = [(2, 2)] outside = [(3, 2), (3, 3)] expected_area = 3 expected_repr = ('') expected_str = ('Region: PolygonPixelRegion\nvertices: ' 'PixCoord(x=[1 3 1], y=[1 1 4])') # We will be using this polygon for basic tests: # # (3,0) * # |\ # | \ # | \ # (0,0) *---* (2, 0) # def test_copy(self): reg = self.reg.copy() assert_allclose(reg.vertices.x, [1, 3, 1]) assert_allclose(reg.vertices.y, [1, 1, 4]) assert reg.meta == self.meta assert reg.visual == self.visual def test_pix_sky_roundtrip(self): wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) reg_new = self.reg.to_sky(wcs).to_pixel(wcs) assert_allclose(reg_new.vertices.x, self.reg.vertices.x) assert_allclose(reg_new.vertices.y, self.reg.vertices.y) assert reg_new.meta == self.reg.meta assert reg_new.visual == self.reg.visual # test that converted region meta and visual are copies and not views reg_new.meta['text'] = 'new' reg_new.visual['color'] = 'green' assert reg_new.meta['text'] != self.reg.meta['text'] assert reg_new.visual['color'] != self.reg.visual['color'] def test_to_spherical_sky(self, wcs): polysphsky = self.reg.to_spherical_sky( wcs, boundary_distortions=False) assert isinstance(polysphsky, PolygonSphericalSkyRegion) with pytest.raises(NotImplementedError): _ = self.reg.to_spherical_sky(wcs, boundary_distortions=True) def test_to_spherical_sky_no_wcs(self): match = 'missing 1 required positional argument' with pytest.raises(TypeError, match=match): self.reg.to_spherical_sky() def test_bounding_box(self): bbox = self.reg.bounding_box assert bbox == RegionBoundingBox(ixmin=1, ixmax=4, iymin=1, iymax=5) def test_to_mask(self): # The true area of this polygon is 3 # We only do very low-precision asserts below, # because results can be unstable with points # on the edge of the polygon. # Basically we check that mask.data is filled # with something useful at all. # Bounding box and output shape is independent of subpixels, # so we only assert on it once here, not in the other cases below mask = self.reg.to_mask(mode='center', subpixels=1) assert 1 <= np.sum(mask.data) <= 6 assert mask.bbox == RegionBoundingBox(ixmin=1, ixmax=4, iymin=1, iymax=5) assert mask.data.shape == (4, 3) # Test more cases for to_mask # This example is with the default: subpixels=5 mask = self.reg.to_mask(mode='subpixels') assert 2 <= np.sum(mask.data) <= 6 mask = self.reg.to_mask(mode='subpixels', subpixels=8) assert 2 <= np.sum(mask.data) <= 6 mask = self.reg.to_mask(mode='subpixels', subpixels=9) assert 2 <= np.sum(mask.data) <= 6 mask = self.reg.to_mask(mode='subpixels', subpixels=10) assert 2 <= np.sum(mask.data) <= 6 mask = self.reg.to_mask(mode='exact') assert_allclose(np.sum(mask.data), 3) @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_as_artist(self): patch = self.reg.as_artist() expected = [[1, 1], [3, 1], [1, 4], [1, 1]] assert_allclose(patch.xy, expected) def test_rotate(self): reg = self.reg.rotate(PixCoord(3, 1), -90 * u.deg) assert_allclose(reg.vertices.x, [3, 3, 6]) assert_allclose(reg.vertices.y, [3, 1, 3]) def test_origin(self): verts = PixCoord([1, 3, 1], [1, 1, 4]) reg1 = PolygonPixelRegion(verts) origin = PixCoord(1, 1) relverts = verts - origin reg2 = PolygonPixelRegion(relverts, origin=origin) assert_equal(reg1.vertices, reg2.vertices) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.vertices = PixCoord([1, 3, 1], [1, 1, 6]) assert reg != self.reg @staticmethod def rect_from_two_points(p1, p2): """ Build a |PixCoord| of the four axis-aligned rectangle corners defined by two opposite points. """ x1, y1 = p1 x2, y2 = p2 minx = min(x1, x2) maxx = max(x1, x2) miny = min(y1, y2) maxy = max(y1, y2) corners = np.array([ [minx, miny], [minx, maxy], [maxx, maxy], [maxx, miny], ]) pc = PixCoord(corners[:, 0], corners[:, 1]) return pc def test_pix_covers_boundary(self): """ Regression test that points coinciding with polygon vertices and edges are covered (boundary included), while ``contains`` excludes the boundary points. """ verts = np.array([(7, 1), (11, 1), (11, 7), (9, 7), (9, 5), (2, 5), (2, 3), (7, 3)]) reg = PolygonPixelRegion(PixCoord(verts[:, 0], verts[:, 1])) rect = self.rect_from_two_points((9, 5), (2, 3)) # Corners (2, 3), (2, 5), (9, 5) coincide with polygon vertices # (on the boundary); (9, 3) is strictly interior. assert np.all(reg.covers(rect)) assert_equal(reg.contains(rect), np.array([False, False, False, True])) class TestPolygonSkyRegion(BaseTestSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = PolygonSkyRegion(SkyCoord([3, 4, 3] * u.deg, [3, 4, 4] * u.deg), meta=meta, visual=visual) expected_repr = (')>') expected_str = ('Region: PolygonSkyRegion\nvertices: ') def test_copy(self): reg = self.reg.copy() assert_allclose(reg.vertices.ra.deg, [3, 4, 3]) assert reg.meta == self.meta assert reg.visual == self.visual def test_transformation(self, wcs): pixpoly = self.reg.to_pixel(wcs) assert_allclose(pixpoly.vertices.x, [11.187992, 10.976332, 11.024032], atol=1e-5) assert_allclose(pixpoly.vertices.y, [1.999486, 2.039001, 2.077076], atol=1e-5) poly = pixpoly.to_sky(wcs) # TODO: we should probably assert something about frame # attributes, or generally some better way to check if two # SkyCoord are the same? For now, we use the following line to # transform back to ICRS (`poly` is in Galactic, same as WCS) poly = PolygonSkyRegion( vertices=poly.vertices.transform_to(self.reg.vertices)) assert_quantity_allclose(poly.vertices.data.lon, self.reg.vertices.data.lon, atol=1e-3 * u.deg) assert_quantity_allclose(poly.vertices.data.lat, self.reg.vertices.data.lat, atol=1e-3 * u.deg) polysphsky = self.reg.to_spherical_sky( wcs=wcs, boundary_distortions=False) assert isinstance(polysphsky, PolygonSphericalSkyRegion) with pytest.raises(NotImplementedError): _ = self.reg.to_spherical_sky(wcs=wcs, boundary_distortions=True) def test_to_spherical_sky_no_wcs(self): match = "'wcs' must be set if `boundary_distortions=True`" with pytest.raises(ValueError, match=match): self.reg.to_spherical_sky(boundary_distortions=True) def test_contains(self, wcs): position = SkyCoord([1, 3.25] * u.deg, [2, 3.75] * u.deg) # 1,2 is outside, 3.25,3.75 should be inside the triangle... assert all(self.reg.contains(position, wcs) == np.array([False, True], dtype='bool')) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.vertices = SkyCoord([3, 4, 3], [3, 4, 6], unit='deg') assert reg != self.reg class TestRegularPolygonPixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = RegularPolygonPixelRegion(PixCoord(50, 50), 8, 20, angle=25 * u.deg, meta=meta, visual=visual) inside = [(40, 40)] outside = [(20, 20), (80, 90)] expected_area = 1131.37085 expected_repr = ('') expected_str = ('Region: RegularPolygonPixelRegion\n' 'center: PixCoord(x=50, y=50)\n' 'nvertices: 8\n' 'radius: 20\nangle: 25.0 deg') def test_copy(self): reg = self.reg.copy() x_expected = copy(self.reg.vertices.x) y_expected = copy(self.reg.vertices.y) assert_allclose(reg.vertices.x, x_expected) assert_allclose(reg.vertices.y, y_expected) assert reg.meta == self.meta assert reg.visual == self.visual def test_bounding_box(self): bbox = self.reg.bounding_box assert bbox == RegionBoundingBox(ixmin=31, ixmax=70, iymin=31, iymax=70) def test_to_mask(self): # The true area of this polygon is 3 # We only do very low-precision asserts below, # because results can be unstable with points # on the edge of the polygon. # Basically we check that mask.data is filled # with something useful at all. # Bounding box and output shape is independent of subpixels, # so we only assert on it once here, not in the other cases below mask = self.reg.to_mask(mode='center', subpixels=1) assert 1130 <= np.sum(mask.data) <= 1135 assert mask.bbox == RegionBoundingBox(ixmin=31, ixmax=70, iymin=31, iymax=70) assert mask.data.shape == (39, 39) # Test more cases for to_mask # This example is with the default: subpixels=5 mask = self.reg.to_mask(mode='subpixels') assert 1130 <= np.sum(mask.data) <= 1135 mask = self.reg.to_mask(mode='subpixels', subpixels=8) assert 1130 <= np.sum(mask.data) <= 1135 mask = self.reg.to_mask(mode='subpixels', subpixels=9) assert 1130 <= np.sum(mask.data) <= 1135 mask = self.reg.to_mask(mode='subpixels', subpixels=10) assert 1130 <= np.sum(mask.data) <= 1135 mask = self.reg.to_mask(mode='exact') assert 1130 <= np.sum(mask.data) <= 1135 @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_as_artist(self): patch = self.reg.as_artist() expected = [[41.54763477, 68.12615574], [31.20614758, 56.84040287], [31.87384426, 41.54763477], [43.15959713, 31.20614758], [58.45236523, 31.87384426], [68.79385242, 43.15959713], [68.12615574, 58.45236523], [56.84040287, 68.79385242], [41.54763477, 68.12615574]] assert_allclose(patch.xy, expected) def test_rotate(self): reg = self.reg.rotate(self.reg.center, 20 * u.deg) assert reg.angle.value == 45.0 x_vert = [35.85786438, 30., 35.85786438, 50., 64.14213562, 70., 64.14213562, 50.] y_vert = [64.14213562, 50., 35.85786438, 30., 35.85786438, 50., 64.14213562, 70.] assert_allclose(reg.vertices.x, x_vert) assert_allclose(reg.vertices.y, y_vert) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.radius = 25. assert reg != self.reg def test_to_polygon(self): polyreg = self.reg.to_polygon() assert polyreg.vertices == self.reg.vertices assert polyreg.meta == self.meta assert polyreg.visual == self.visual assert polyreg.origin == PixCoord(0, 0) class TestPolygonSphericalSkyRegion(BaseTestSphericalSkyRegion): inside = [(3.1 * u.deg, 3.5 * u.deg)] outside = [(3 * u.deg, 0 * u.deg)] meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = PolygonSphericalSkyRegion(SkyCoord([3, 4, 3] * u.deg, [3, 4, 4] * u.deg), meta=meta, visual=visual) expected_repr = (')>') expected_str = ('Region: PolygonSphericalSkyRegion\n' 'vertices: ') def test_copy(self): reg = self.reg.copy() assert_allclose(reg.vertices.ra.deg, [3, 4, 3]) assert_allclose(reg.vertices.dec.deg, [3, 4, 4]) assert reg.meta == self.meta assert reg.visual == self.visual def test_transformation(self, wcs): polypix = self.reg.to_pixel(wcs) assert isinstance(polypix, PolygonPixelRegion) assert_allclose(polypix.vertices.x, [11.18799228, 10.97633218, 11.02403192]) assert_allclose(polypix.vertices.y, [1.99948607, 2.0390009, 2.07707564]) polysky = self.reg.to_sky(wcs=wcs) assert isinstance(polysky, PolygonSkyRegion) assert_allclose(polysky.vertices.ra.deg, [3, 4, 3]) assert_allclose(polysky.vertices.dec.deg, [3, 4, 4]) polysky2 = polysky.to_spherical_sky(wcs=wcs) assert_quantity_allclose(self.reg.vertices.ra.deg, polysky2.vertices.ra.deg) assert_quantity_allclose(self.reg.vertices.dec.deg, polysky2.vertices.dec.deg) polysky3 = self.reg.to_sky(wcs=wcs, boundary_distortions=True, n_vertices=10) assert isinstance(polysky3, PolygonSkyRegion) assert len(polysky3.vertices) == 10 polypix2 = self.reg.to_pixel(wcs, boundary_distortions=True, n_vertices=10) assert isinstance(polypix2, PolygonPixelRegion) assert len(polypix2.vertices) == 10 def test_transformation_no_wcs(self): match = "'wcs' must be set if `boundary_distortions=True`" with pytest.raises(ValueError, match=match): self.reg.to_sky(boundary_distortions=True) def test_frame_transformation(self): reg = self.reg.transform_to('galactic') assert_skycoord_allclose( reg.centroid_mindist, self.reg.centroid_mindist.transform_to('galactic')) assert_skycoord_allclose(reg.vertices, self.reg.vertices.transform_to('galactic')) assert isinstance(reg, PolygonSphericalSkyRegion) assert reg.frame.name == 'galactic' assert reg != self.reg def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.vertices = SkyCoord([3, 5, 3] * u.deg, [3, 4, 4] * u.deg) assert reg != self.reg def test_bounding_circle(self): skycoord = SkyCoord(3.3333295725289807 * u.deg, 3.6666666666666665 * u.deg, frame='icrs') reg = CircleSphericalSkyRegion(skycoord, 0.7451014340169484 * u.deg) bc = self.reg.bounding_circle assert bc == reg def test_bounding_lonlat(self): bounding_lonlat = self.reg.bounding_lonlat assert_quantity_allclose(bounding_lonlat[0], Longitude([3 * u.deg, 4 * u.deg])) # GC connecting top two vertices "bluges up" above # the latitude value = 4 deg for those two points assert_quantity_allclose(bounding_lonlat[1], Latitude([3 * u.deg, 4.00015182 * u.deg])) def test_discretize_boundary(self): poly = self.reg.discretize_boundary(n_vertices=100) assert isinstance(poly, PolygonSphericalSkyRegion) assert len(poly.vertices) == 100 match = 'must be greater than' with pytest.raises(ValueError, match=match): self.reg.discretize_boundary(n_vertices=2) def test_centroid(self): # Test default polygon has mindist centroid outside region, # so falls back to average lon/lat centroid definition: assert self.reg.centroid == self.reg.centroid_avg # Test mindist centroid: reg = PolygonSphericalSkyRegion(SkyCoord([3, 5, 3] * u.deg, [3, 4, 5] * u.deg)) assert reg.centroid == reg.centroid_mindist @pytest.mark.skipif(not HAS_SHAPELY, reason='shapely is required') class TestPolygonShapelyComparison: """ Test that PolygonPixelRegion contains and covers match Shapely's contains and covers functions (DE-9IM semantics). """ # L-shaped polygon vertices and test points grouped by location. vertex_points = [(7, 1), (11, 1), (11, 7), (9, 7), (9, 5), (2, 5), (2, 3), (7, 3)] edge_points = [(9, 1), (11, 4), (10, 7), (9, 6), (5, 5), (2, 4), (5, 3), (7, 2)] interior_points = [(8, 2), (10, 3), (9, 5.5), (5, 4), (3, 4), (10, 6)] exterior_points = [(1, 1), (12, 5), (0, 4), (5, 0), (10, 8), (3, 2), (5, 2), (6, 4)] all_points = (vertex_points + edge_points + interior_points + exterior_points) @classmethod def setup_polygon(cls): """ Create an L-shaped polygon for testing. """ verts = np.array(cls.vertex_points) return PolygonPixelRegion(PixCoord(verts[:, 0], verts[:, 1])) @classmethod def shapely_polygon(cls): """ Create an equivalent Shapely polygon. """ from shapely import Polygon return Polygon(cls.vertex_points) @pytest.mark.parametrize('method', ['contains', 'covers']) @pytest.mark.parametrize('point', all_points) def test_matches_shapely(self, method, point): """ Test that contains/covers match Shapely for points on the vertices, edges, interior, and exterior of the polygon. """ from shapely import Point, contains, covers shp_func = {'contains': contains, 'covers': covers}[method] x, y = point reg_result = bool( getattr(self.setup_polygon(), method)(PixCoord(x, y))) shp_result = bool(shp_func(self.shapely_polygon(), Point(x, y))) assert reg_result == shp_result @pytest.mark.parametrize('method', ['contains', 'covers']) def test_array_matches_shapely(self, method): """ Test that contains/covers match Shapely for an array mixing vertex, edge, interior, and exterior points. """ from shapely import Point, contains, covers shp_func = {'contains': contains, 'covers': covers}[method] points = [(7, 1), (9, 1), (8, 2), (1, 1), (11, 7), (5, 5), (5, 4), (3, 2)] x = np.array([p[0] for p in points], dtype=float) y = np.array([p[1] for p in points], dtype=float) reg_results = getattr(self.setup_polygon(), method)(PixCoord(x, y)) expected = np.array([bool(shp_func(self.shapely_polygon(), Point(px, py))) for px, py in points]) assert_equal(reg_results, expected) astropy-regions-f4139aa/regions/shapes/tests/test_range.py000066400000000000000000000540051521703212700240750ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import pytest from astropy.coordinates import Angle, Latitude, Longitude, SkyCoord from astropy.io import fits from astropy.tests.helper import assert_quantity_allclose from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from numpy.testing import assert_allclose, assert_equal from regions.core import RegionMeta, RegionVisual from regions.core.compound import CompoundSphericalSkyRegion from regions.shapes.circle import CircleSphericalSkyRegion from regions.shapes.lune import LuneSphericalSkyRegion from regions.shapes.polygon import (PolygonPixelRegion, PolygonSkyRegion, PolygonSphericalSkyRegion) from regions.shapes.range import RangeSphericalSkyRegion from regions.shapes.tests.test_common import BaseTestSphericalSkyRegion @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): filename = get_pkg_data_filename('data/example_header.fits') header = fits.getheader(filename) return WCS(header) class TestRangeSphericalSkyRegion(BaseTestSphericalSkyRegion): inside = [(5 * u.deg, 0 * u.deg)] outside = [(75 * u.deg, 10 * u.deg)] meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[-4, 4] * u.deg, frame='icrs', meta=meta, visual=visual) expected_repr = ('') expected_str = ('Region: RangeSphericalSkyRegion\n' 'frame: icrs\n' 'longitude_range: [ 0. 10.] deg\n' 'latitude_range: [-4. 4.] deg') def test_no_range_set(self): match = ('A range for at least one of longitude and latitude must ' 'be set') with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion(frame='icrs') def test_invalid_lon_range(self): # Test input types: for lon_range in [u.Quantity([0, 10], u.m / u.s), [0 * u.m / u.s, 30 * u.m / u.s]]: match = 'must have angular units' with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion( longitude_range=lon_range, frame='icrs') for lon_range in [u.Quantity([0], u.deg), Angle([2 * u.radian])]: match = 'must be length 2' with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion( longitude_range=lon_range, frame='icrs') for lon_range in [[0 * u.deg], [0, 10], 'string']: match = 'must be an angle of length 2' with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion( longitude_range=lon_range, frame='icrs') # Test valid boundary values: for lon_range in [[-30, 30] * u.deg, [20, 560] * u.deg]: match = (r'Longitude values must be within \[0, 360\] degrees' r' or equivalent!') with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion( longitude_range=lon_range, frame='icrs') def test_invalid_lat_range(self): # Test input types: for lat_range in [u.Quantity([0, 10], u.m / u.s), [0 * u.m / u.s, 30 * u.m / u.s]]: match = 'must have angular units' with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion( latitude_range=lat_range, frame='icrs') for lat_range in [u.Quantity([0], u.deg), Angle([2 * u.radian])]: match = 'must be length 2' with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion( latitude_range=lat_range, frame='icrs') for lat_range in [[0 * u.deg], [0, 10], 'string']: match = 'must be an angle of length 2' with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion( latitude_range=lat_range, frame='icrs') # Test valid boundary values: for lat_range in [[-120, -30] * u.deg, [20, 100] * u.deg]: match = (r'Latitude values must be within \[-90, 90\] degrees' r' or equivalent!') with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion( latitude_range=lat_range, frame='icrs') def test_invalid_lon_bounds_input(self): invalid_bounds = CircleSphericalSkyRegion( SkyCoord(5 * u.deg, 0 * u.deg), 0.2 * u.deg) match = 'Invalid direct longitude bounds input!' with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion(_longitude_bounds=invalid_bounds, frame='icrs') def test_invalid_lat_bounds_input(self): invalid_bounds = LuneSphericalSkyRegion( SkyCoord(3 * u.deg, 0 * u.deg), SkyCoord(178 * u.deg, 0 * u.deg)) match = 'Invalid direct latitude bounds input!' with pytest.raises(ValueError, match=match): RangeSphericalSkyRegion(_latitude_bounds=invalid_bounds, frame='icrs') def test_lon_only(self): reg = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, frame='icrs') assert reg._bound_nverts == 2 assert reg.latitude_bounds is None coos = self.inside + self.outside lon, lat = zip(*coos) skycoord = SkyCoord(list(lon), list(lat)) actual = reg.contains(skycoord) assert_equal(actual[:len(self.inside)], True) assert_equal(actual[len(self.inside):], False) assert reg.contains(skycoord[0]) reg_transf = reg.transform_to('galactic') skycoord_transf = skycoord.transform_to('galactic') actual = reg_transf.contains(skycoord_transf) assert_equal(actual[:len(self.inside)], True) assert_equal(actual[len(self.inside):], False) # Manual "no latitude constraints" reg2 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[-90, 90] * u.deg, frame='icrs') assert reg2._bound_nverts == 2 actual = reg2.contains(skycoord) assert_equal(actual[:len(self.inside)], True) assert_equal(actual[len(self.inside):], False) assert reg2.contains(skycoord[0]) def test_covers(self): reg = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[-4, 4] * u.deg, frame='icrs') coords = self.inside + self.outside lon, lat = zip(*coords) skycoord = SkyCoord(list(lon), list(lat)) actual = reg.covers(skycoord) assert_equal(actual[:len(self.inside)], True) assert_equal(actual[len(self.inside):], False) assert reg.covers(skycoord[0]) def test_lat_only(self): reg = RangeSphericalSkyRegion(latitude_range=[-4, 4] * u.deg, frame='icrs') assert reg._bound_nverts == 0 assert reg.longitude_bounds is None coos = self.inside + self.outside lon, lat = zip(*coos) skycoord = SkyCoord(list(lon), list(lat)) actual = reg.contains(skycoord) assert_equal(actual[:len(self.inside)], True) assert_equal(actual[len(self.inside):], False) assert reg.contains(skycoord[0]) reg_transf = reg.transform_to('galactic') skycoord_transf = skycoord.transform_to('galactic') actual = reg_transf.contains(skycoord_transf) assert_equal(actual[:len(self.inside)], True) assert_equal(actual[len(self.inside):], False) def test_lon_over_meridian(self): reg = RangeSphericalSkyRegion(longitude_range=[358, 2] * u.deg, latitude_range=[-4, 4] * u.deg, frame='icrs') assert_allclose(reg.centroid.ra.deg, 0) assert_allclose(reg.centroid.dec.deg, 0) def test_lat_over_poles(self): reg = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[80, -80] * u.deg, frame='icrs') assert isinstance(reg.latitude_bounds, CompoundSphericalSkyRegion) assert reg._bound_nverts == 6 reg2 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[90, -80] * u.deg, frame='icrs') assert isinstance(reg2.latitude_bounds, CircleSphericalSkyRegion) assert reg2.latitude_bounds.center.dec.deg == -90 reg3 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[80, -90] * u.deg, frame='icrs') assert isinstance(reg3.latitude_bounds, CircleSphericalSkyRegion) assert reg3.latitude_bounds.center.dec.deg == 90 def test_centroid(self): assert_allclose(self.reg.centroid.ra.deg, 5) assert_allclose(self.reg.centroid.dec.deg, 0) # Long and narrow range: reg = RangeSphericalSkyRegion(longitude_range=[0, 350] * u.deg, latitude_range=[45, 50] * u.deg, frame='icrs') assert reg.centroid == reg.centroid_avg reg2 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, frame='icrs') assert_allclose(reg2.centroid.ra.deg, 5) assert_allclose(reg2.centroid.dec.deg, 0) reg3 = RangeSphericalSkyRegion(latitude_range=[-4, 4] * u.deg, frame='icrs') assert_allclose(reg3.centroid.ra.deg, 0) assert_allclose(reg3.centroid.dec.deg, 90) def test_centroid_avg(self): assert_allclose(self.reg.centroid_avg.ra.deg, 5) assert_allclose(self.reg.centroid_avg.dec.deg, 0) # Very wide range: reg = RangeSphericalSkyRegion(longitude_range=[200, 100] * u.deg, latitude_range=[-50, 80] * u.deg, frame='icrs') assert reg.centroid_avg == SkyCoord(330 * u.deg, 15 * u.deg) reg2 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, frame='icrs') assert_allclose(reg2.centroid_avg.ra.deg, 5) assert_allclose(reg2.centroid_avg.dec.deg, 0) reg3 = RangeSphericalSkyRegion(latitude_range=[-4, 4] * u.deg, frame='icrs') assert_allclose(reg3.centroid_avg.ra.deg, 0) assert_allclose(reg3.centroid_avg.dec.deg, 90) def test_vertices(self): assert_allclose(self.reg.vertices.ra.deg, [0, 10, 10, 0]) assert_allclose(self.reg.vertices.dec.deg, [-4, -4, 4, 4]) def test_copy(self): reg = self.reg.copy() assert_allclose(reg.centroid.ra.deg, 5) assert_allclose(reg.centroid.dec.deg, 0) assert reg.meta == self.meta assert reg.visual == self.visual def test_transformation(self, wcs): # Test error for no boundary distortions: match = 'Invalid parameter: `boundary_distortions=False`!' with pytest.raises(ValueError, match=match): self.reg.to_pixel(wcs) match = 'Invalid parameter: `boundary_distortions=False`!' with pytest.raises(ValueError, match=match): self.reg.to_sky(wcs) # Test boundary distortion transformations: polypix2 = self.reg.to_pixel(wcs, boundary_distortions=True, n_vertices=8) assert isinstance(polypix2, PolygonPixelRegion) assert len(polypix2.vertices) == 8 assert_allclose(polypix2.vertices.x, [-4861.625867, -4717.364168, -4536.821565, -5079.772466, -5685.546225, -5734.531503, -5771.862922, -5302.114084]) assert_allclose(polypix2.vertices.y, [-2724.76778, -2909.927597, -3091.161999, -3184.698562, -3236.051353, -3037.141783, -2837.972245, -2798.470494]) polysky2 = self.reg.to_sky(wcs, boundary_distortions=True, n_vertices=8) assert isinstance(polysky2, PolygonSkyRegion) assert len(polysky2.vertices) == 8 # WCS is Galactic: assert_allclose(polysky2.vertices.l.deg, [99.222517, 96.337283, 92.726431, 103.585449, 115.700925, 116.68063, 117.427258, 108.032282]) assert_allclose(polysky2.vertices.b.deg, [-56.485356, -60.188552, -63.81324, -65.683971, -66.711027, -62.732836, -58.749445, -57.95941]) def test_transformation_over_poles(self, wcs): # Transformation for over-poles case: reg = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[80, -80] * u.deg, frame='icrs') with pytest.raises(NotImplementedError): _ = reg.to_pixel(wcs, boundary_distortions=True, n_vertices=10) def test_transformation_no_wcs(self): match = 'missing 1 required positional argument' with pytest.raises(TypeError, match=match): self.reg.to_sky() def test_frame_transformation(self): reg2 = self.reg.transform_to('galactic') reg2_cent = reg2.centroid transf_reg_cent = self.reg.centroid.transform_to('galactic') assert isinstance(reg2, RangeSphericalSkyRegion) assert not reg2._is_original_frame assert transf_reg_cent.frame.name == reg2_cent.frame.name assert_quantity_allclose(reg2_cent.l, transf_reg_cent.l) assert_quantity_allclose(reg2_cent.b, transf_reg_cent.b) assert reg2.frame.name == 'galactic' assert reg2 != self.reg reg3 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[80, 90] * u.deg, frame='icrs').transform_to('galactic') assert reg3 != reg2 # Formally won't round trip, because the original lon/lat range info # is not stored: reg = reg2.transform_to('icrs') assert reg != self.reg def test_copy_frame_transformation(self): reg2 = self.reg.transform_to('galactic') reg3 = reg2.copy() assert reg2 == reg3 def test_repr_str_frame_transformation(self): expected_repr_transf = ( ', ' 'center_gc2=)>,\n' 'latitude_bounds=, ' 'inner_radius=86.0 deg, outer_radius=94.0 deg)>\n' ')>') expected_str_transf = ( 'Region: RangeSphericalSkyRegion\n' 'frame: galactic\n' 'longitude_bounds: , ' 'center_gc2=)>\n' 'latitude_bounds: , ' 'inner_radius=86.0 deg, outer_radius=94.0 deg)>') reg2 = self.reg.transform_to('galactic') assert repr(reg2) == expected_repr_transf assert str(reg2) == expected_str_transf def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.longitude_range = [0, 4] * u.deg assert reg != self.reg reg2 = CircleSphericalSkyRegion(SkyCoord(5 * u.deg, 0 * u.deg), 6.3999492595405645 * u.deg) assert self.reg != reg2 def test_bounding_circle(self): skycoord = SkyCoord(5 * u.deg, 0 * u.deg) reg = CircleSphericalSkyRegion(skycoord, 6.3999492595405645 * u.deg) bc = self.reg.bounding_circle assert_quantity_allclose(bc.radius, reg.radius) assert_quantity_allclose(bc.center.ra, reg.center.ra) assert_quantity_allclose(bc.center.dec, reg.center.dec) reg2 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, frame='icrs') assert reg2.bounding_circle == reg2.longitude_bounds.bounding_circle reg3 = RangeSphericalSkyRegion(latitude_range=[-4, 4] * u.deg, frame='icrs') assert reg3.bounding_circle == reg3.latitude_bounds.bounding_circle def test_bounding_circle_over_poles(self): # Wrap over poles: reg = RangeSphericalSkyRegion(longitude_range=[0, 350] * u.deg, latitude_range=[80, -80] * u.deg, frame='icrs') with pytest.raises(NotImplementedError): _ = reg.bounding_circle def test_bounding_lonlat(self): bounding_lonlat = self.reg.bounding_lonlat assert_quantity_allclose(bounding_lonlat[0], Longitude([0. * u.deg, 10. * u.deg])) assert_quantity_allclose(bounding_lonlat[1], Latitude([-4 * u.deg, 4 * u.deg])) # Transformed: reg_tr = self.reg.transform_to('galactic') bll_transf = reg_tr.bounding_lonlat assert_quantity_allclose(bll_transf[0], Longitude([92.7264313, 117.42725845] * u.deg)) assert_quantity_allclose( bll_transf[1], Latitude([-66.71102705, -56.48535559] * u.deg)) # Touching poles: should return longitude bounds reg2 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[80, 90] * u.deg, frame='icrs') bounding_lonlat2 = reg2.bounding_lonlat assert bounding_lonlat2[0] is not None reg3 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[-90, -80] * u.deg, frame='icrs') bounding_lonlat3 = reg3.bounding_lonlat assert bounding_lonlat3[0] is not None # Only lon or lat ranges: reg4 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, frame='icrs') bll = reg4.bounding_lonlat bound_bll = reg4.longitude_bounds.bounding_lonlat assert_quantity_allclose(bll[0], bound_bll[0]) assert_quantity_allclose(bll[1], bound_bll[1]) reg5 = RangeSphericalSkyRegion(latitude_range=[-4, 4] * u.deg, frame='icrs') bll = reg5.bounding_lonlat bound_bll = reg5.latitude_bounds.bounding_lonlat assert bll[0] is None assert_quantity_allclose(bll[1], bound_bll[1]) def test_bounding_lonlat_over_poles(self): # Wrap over poles: reg = RangeSphericalSkyRegion(longitude_range=[0, 350] * u.deg, latitude_range=[80, -80] * u.deg, frame='icrs') with pytest.raises(NotImplementedError): _ = reg.bounding_lonlat def test_discretize_boundary(self): polyrange = self.reg.discretize_boundary(n_vertices=100) assert isinstance(polyrange, PolygonSphericalSkyRegion) assert len(polyrange.vertices) == 100 # Longitude only reg2 = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, frame='icrs') polyrange2 = reg2.discretize_boundary(n_vertices=100) assert polyrange2 == reg2.longitude_bounds.discretize_boundary( n_vertices=100) assert len(polyrange2.vertices) == 100 # Latitude only reg3 = RangeSphericalSkyRegion(latitude_range=[-4, 4] * u.deg, frame='icrs') polyrange3 = reg3.discretize_boundary(n_vertices=100) assert polyrange3 == reg3.latitude_bounds.discretize_boundary( n_vertices=100) assert len(polyrange3.region1.vertices) == 100 assert len(polyrange3.region2.vertices) == 100 match = 'must be greater than' with pytest.raises(ValueError, match=match): self.reg.discretize_boundary(n_vertices=3) def test_discretize_over_poles(self): # Wrap over poles: reg = RangeSphericalSkyRegion(longitude_range=[0, 350] * u.deg, latitude_range=[80, -80] * u.deg, frame='icrs') with pytest.raises(NotImplementedError): _ = reg.discretize_boundary() astropy-regions-f4139aa/regions/shapes/tests/test_rectangle.py000066400000000000000000000462161521703212700247520ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import numpy as np import pytest from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.tests.helper import assert_quantity_allclose from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from numpy.testing import assert_allclose, assert_equal from regions._utils.optional_deps import HAS_MATPLOTLIB, HAS_SHAPELY from regions.core import PixCoord, RegionMeta, RegionVisual from regions.shapes.rectangle import RectanglePixelRegion, RectangleSkyRegion from regions.shapes.tests.test_common import (BaseTestPixelRegion, BaseTestSkyRegion) from regions.tests.helpers import make_simple_wcs @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): filename = get_pkg_data_filename('data/example_header.fits') header = fits.getheader(filename) return WCS(header) def test_corners(): xc, yc = 2, 2 angle = 30 * u.deg width = 2 height = 1 reg = RectanglePixelRegion(PixCoord(xc, yc), width=width, height=height, angle=angle) y1 = yc + np.cos(angle) * (height / 2) + np.sin(angle) * (width / 2) x1 = xc + np.cos(angle) * (width / 2) - np.sin(angle) * (height / 2) assert (x1, y1) in reg.corners reg = RectanglePixelRegion(PixCoord(xc, yc), width=width, height=height, angle=90 * u.deg) # simple case: rotate by 90 np.testing.assert_allclose([(2.5, 1.), (2.5, 3.), (1.5, 3.), (1.5, 1.)], reg.corners) reg = RectanglePixelRegion(center=PixCoord(xc, yc), width=width, height=height, angle=0 * u.deg) # simpler case: rotate by 0 np.testing.assert_array_equal([(1, 1.5), (3, 1.5), (3, 2.5), (1, 2.5)], reg.corners) poly = reg.to_polygon() assert len(poly.vertices) == 4 class TestRectanglePixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = RectanglePixelRegion(center=PixCoord(3, 4), width=4, height=3, angle=5 * u.deg, meta=meta, visual=visual) sample_box = [-2, 8, -1, 9] inside = [(4.5, 4)] outside = [(5, 2.5)] expected_area = 12 expected_repr = ('') expected_str = ('Region: RectanglePixelRegion\ncenter: PixCoord(x=3, ' 'y=4)\nwidth: 4\nheight: 3\nangle: 5.0 deg') def test_copy(self): reg = self.reg.copy() assert reg.center.xy == (3, 4) assert reg.width == 4 assert reg.height == 3 assert_allclose(reg.angle.to_value('deg'), 5) assert reg.meta == self.meta assert reg.visual == self.visual def test_pix_sky_roundtrip(self): wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) reg_new = self.reg.to_sky(wcs).to_pixel(wcs) assert_allclose(reg_new.center.x, self.reg.center.x) assert_allclose(reg_new.center.y, self.reg.center.y) assert_allclose(reg_new.width, self.reg.width) assert_allclose(reg_new.height, self.reg.height) assert_quantity_allclose(reg_new.angle, self.reg.angle) assert reg_new.meta == self.reg.meta assert reg_new.visual == self.reg.visual # test that converted region meta and visual are copies and not views reg_new.meta['text'] = 'new' reg_new.visual['color'] = 'green' assert reg_new.meta['text'] != self.reg.meta['text'] assert reg_new.visual['color'] != self.reg.visual['color'] @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') def test_as_artist(self): patch = self.reg.as_artist() # Note: `reg.center` is the center, `patch.xy` is the lower-left # corner assert_allclose(patch.xy, (1.138344, 2.331396), atol=1e-3) assert_allclose(patch.get_width(), 4) assert_allclose(patch.get_height(), 3) assert_allclose(patch.angle, 5) def test_rotate(self): reg = self.reg.rotate(PixCoord(2, 3), 90 * u.deg) assert_allclose(reg.center.xy, (1, 4)) assert_allclose(reg.angle.to_value('deg'), 95) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.angle = 35 * u.deg assert reg != self.reg def test_to_mask_exact(self): """ Test that the 'exact' mode of to_mask recovers the exact area of the rectangle and that all mask weights are valid fractions. """ mask = self.reg.to_mask(mode='exact') assert_allclose(np.sum(mask.data), self.expected_area) # all mask weights are valid fractions assert mask.data.min() >= 0.0 assert mask.data.max() <= 1.0 + 1e-12 # The exact area is recovered independent of the rotation angle for angle in (0, 30, 45, 90): reg = self.reg.copy(angle=angle * u.deg) mask = reg.to_mask(mode='exact') assert_allclose(np.sum(mask.data), reg.area) @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') @pytest.mark.parametrize('sync', (False, True)) def test_as_mpl_selector(self, sync): import matplotlib.pyplot as plt rng = np.random.default_rng(0) data = rng.random((16, 16)) mask = np.zeros_like(data) fig, ax = plt.subplots() ax.imshow(data) def update_mask(reg): mask[:] = reg.to_mask( mode='subpixels', subpixels=10).to_image(data.shape) # For now this will only work with unrotated rectangles. Once # this works with rotated rectangles, the following exception # check can be removed as well as the ``angle=0 * u.deg`` in the # call to copy() below. match = 'Cannot create matplotlib selector for rotated rectangle' with pytest.raises(NotImplementedError, match=match): self.reg.as_mpl_selector(ax) region = self.reg.copy(angle=0 * u.deg) selector = region.as_mpl_selector(ax, callback=update_mask, sync=sync) # noqa: E501, F841 from matplotlib.backend_bases import MouseEvent canvas = ax.figure.canvas evt = MouseEvent('button_press_event', canvas, *ax.transData.transform((7.3, 4.4)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('motion_notify_event', canvas, *ax.transData.transform((9.3, 5.4)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('button_release_event', canvas, *ax.transData.transform((9.3, 5.4)), button=1) canvas.callbacks.process(evt.name, evt) ax.figure.canvas.draw() if sync: assert_allclose(region.center.x, 8.3) assert_allclose(region.center.y, 4.9) assert_allclose(region.width, 2) assert_allclose(region.height, 1) assert_quantity_allclose(region.angle, 0 * u.deg) assert_equal(mask, region.to_mask( mode='subpixels', subpixels=10).to_image(data.shape)) else: assert_allclose(region.center.x, 3) assert_allclose(region.center.y, 4) assert_allclose(region.width, 4) assert_allclose(region.height, 3) assert_quantity_allclose(region.angle, 0 * u.deg) assert_equal(mask, 0) match = 'Cannot attach more than one selector to a reg' with pytest.raises(AttributeError, match=match): region.as_mpl_selector(ax) plt.close() @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') @pytest.mark.parametrize('anywhere', (False, True)) def test_mpl_selector_drag(self, anywhere): """ Test dragging of entire region from central handle and anywhere. """ import matplotlib.pyplot as plt rng = np.random.default_rng(0) data = rng.random((16, 16)) mask = np.zeros_like(data) fig, ax = plt.subplots() ax.imshow(data) def update_mask(reg): mask[:] = reg.to_mask( mode='subpixels', subpixels=10).to_image(data.shape) region = self.reg.copy(angle=0 * u.deg) selector = region.as_mpl_selector(ax, callback=update_mask, drag_from_anywhere=anywhere) assert selector.drag_from_anywhere is anywhere assert region._mpl_selector.drag_from_anywhere is anywhere # click_and_drag(selector, start=(3, 4), end=(3.5, 4.5)) from matplotlib.backend_bases import MouseEvent canvas = ax.figure.canvas evt = MouseEvent('button_press_event', canvas, *ax.transData.transform((3, 4)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('motion_notify_event', canvas, *ax.transData.transform((3.5, 4.5)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('button_release_event', canvas, *ax.transData.transform((3.5, 4.5)), button=1) canvas.callbacks.process(evt.name, evt) ax.figure.canvas.draw() assert_allclose(region.center.x, 3.5) assert_allclose(region.center.y, 4.5) assert_allclose(region.width, 4) assert_allclose(region.height, 3) evt = MouseEvent('button_press_event', canvas, *ax.transData.transform((3.25, 4.25)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('motion_notify_event', canvas, *ax.transData.transform((4.25, 5.25)), button=1) canvas.callbacks.process(evt.name, evt) evt = MouseEvent('button_release_event', canvas, *ax.transData.transform((4.25, 5.25)), button=1) canvas.callbacks.process(evt.name, evt) ax.figure.canvas.draw() # For drag_from_anywhere=False this will have created a new 1x1 # rectangle. if anywhere: assert_allclose(region.center.x, 4.5) assert_allclose(region.center.y, 5.5) assert_allclose(region.width, 4) assert_allclose(region.height, 3) else: assert_allclose(region.center.x, 4.5) assert_allclose(region.center.y, 5.5) assert_equal(mask, region.to_mask( mode='subpixels', subpixels=10).to_image(data.shape)) plt.close() @pytest.mark.skipif(not HAS_MATPLOTLIB, reason='matplotlib is required') @pytest.mark.parametrize('userargs', ({'useblit': True}, {'grab_range': 20, 'minspanx': 5, 'minspany': 4}, {'props': {'facecolor': 'blue', 'linewidth': 2}}, {'twit': 'gumby'})) def test_mpl_selector_kwargs(self, userargs): """ Test that additional kwargs are passed to selector. """ import matplotlib.pyplot as plt rng = np.random.default_rng(0) data = rng.random((16, 16)) mask = np.zeros_like(data) fig, ax = plt.subplots() ax.imshow(data) def update_mask(reg): mask[:] = reg.to_mask( mode='subpixels', subpixels=10).to_image(data.shape) region = self.reg.copy(angle=0 * u.deg) if 'twit' in userargs: match = r'__init__.. got an unexpected keyword argument' with pytest.raises(TypeError, match=match): selector = region.as_mpl_selector( ax, callback=update_mask, **userargs) else: selector = region.as_mpl_selector( ax, callback=update_mask, **userargs) assert region._mpl_selector.artists[0].get_edgecolor() == ( 0, 0, 1, 1) if 'props' in userargs: assert region._mpl_selector.artists[0].get_facecolor() == ( 0, 0, 1, 1) assert region._mpl_selector.artists[0].get_linewidth() == 2 else: assert region._mpl_selector.artists[0].get_facecolor() == ( 0, 0, 0, 0) assert region._mpl_selector.artists[0].get_linewidth() == 1 for key, val in userargs.items(): assert getattr(region._mpl_selector, key) == val assert getattr(selector, key) == val plt.close() def test_rectangular_pixel_region_bbox(): # odd sizes width = 7 height = 3 a = RectanglePixelRegion(PixCoord(50, 50), width=width, height=height, angle=0. * u.deg) assert a.bounding_box.shape == (height, width) a = RectanglePixelRegion(PixCoord(50.5, 50.5), width=width, height=height, angle=0. * u.deg) assert a.bounding_box.shape == (height + 1, width + 1) a = RectanglePixelRegion(PixCoord(50, 50), width=width, height=height, angle=90. * u.deg) assert a.bounding_box.shape == (width, height) # even sizes width = 8 height = 4 a = RectanglePixelRegion(PixCoord(50, 50), width=width, height=height, angle=0. * u.deg) assert a.bounding_box.shape == (height + 1, width + 1) a = RectanglePixelRegion(PixCoord(50.5, 50.5), width=width, height=height, angle=0. * u.deg) assert a.bounding_box.shape == (height, width) a = RectanglePixelRegion(PixCoord(50.5, 50.5), width=width, height=height, angle=90. * u.deg) assert a.bounding_box.shape == (width, height) class TestRectangleSkyRegion(BaseTestSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = RectangleSkyRegion(center=SkyCoord(3, 4, unit='deg'), width=4 * u.deg, height=3 * u.deg, angle=5 * u.deg, meta=meta, visual=visual) expected_repr = (', width=4.0 deg, height=3.0 deg, ' 'angle=5.0 deg)>') expected_str = ('Region: RectangleSkyRegion\ncenter: \nwidth: ' '4.0 deg\nheight: 3.0 deg\nangle: 5.0 deg') def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center.ra.deg, 3) assert_allclose(reg.width.to_value('deg'), 4) assert_allclose(reg.height.to_value('deg'), 3) assert_allclose(reg.angle.to_value('deg'), 5) assert reg.meta == self.meta assert reg.visual == self.visual def test_contains(self, wcs): position = SkyCoord([1, 3] * u.deg, [2, 4] * u.deg) # 1,2 is outside, 3,4 is the center and is inside assert all(self.reg.contains(position, wcs) == np.array([False, True], dtype='bool')) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.angle = 10 * u.deg assert reg != self.reg class TestRectangleCovers: """ Test RectanglePixelRegion contains and covers boundary semantics using explicit expected values. """ @staticmethod def setup_rectangle(): """ Create an axis-aligned rectangle centered at (5, 5). """ # width=6, height=4, so corners at (2, 3), (8, 3), (8, 7), (2, 7) return RectanglePixelRegion(PixCoord(5, 5), width=6, height=4, angle=0 * u.deg) @pytest.mark.parametrize(('method', 'expected'), [('contains', [True, False, False, False, False]), ('covers', [True, True, False, True, True])]) def test_array(self, method, expected): """ Test contains/covers boundary semantics for an array mixing interior, boundary, and exterior points. """ reg = self.setup_rectangle() # Center (in), vertex, beyond right (out), bottom edge, right edge x = np.array([5, 2, 9, 5, 8]) y = np.array([5, 3, 5, 3, 5]) result = getattr(reg, method)(PixCoord(x, y)) assert_equal(result, np.array(expected)) @pytest.mark.parametrize('method', ['contains', 'covers']) def test_rotated_rectangle(self, method): """ Test boundary semantics for a rectangle rotated by 45 degrees. Exact-boundary comparisons are sensitive to floating-point rounding, so points are nudged just inside and just outside the corner along the center-corner direction. """ reg = RectanglePixelRegion(PixCoord(5, 5), width=4, height=2, angle=45 * u.deg) # Center is always inside; a far point is always outside. assert getattr(reg, method)(PixCoord(5, 5)) assert not getattr(reg, method)(PixCoord(10, 10)) cos45 = np.cos(np.radians(45)) sin45 = np.sin(np.radians(45)) hw, hh = 2, 1 # half width, half height cx = 5 + cos45 * hw - sin45 * hh cy = 5 + sin45 * hw + cos45 * hh inside = PixCoord(5 + 0.99 * (cx - 5), 5 + 0.99 * (cy - 5)) outside = PixCoord(5 + 1.01 * (cx - 5), 5 + 1.01 * (cy - 5)) assert getattr(reg, method)(inside) assert not getattr(reg, method)(outside) @pytest.mark.skipif(not HAS_SHAPELY, reason='shapely is required') class TestRectangleShapelyComparison: """ Test that RectanglePixelRegion contains and covers match Shapely's contains and covers functions (DE-9IM semantics). """ # Test points grouped by location relative to the rectangle vertices = [(2, 3), (8, 3), (8, 7), (2, 7)] edge_points = [(5, 3), (8, 5), (5, 7), (2, 5)] interior_points = [(5, 5), (3, 4), (7, 6), (4, 5)] exterior_points = [(0, 0), (1, 5), (9, 5), (5, 2), (5, 8)] all_points = vertices + edge_points + interior_points + exterior_points @staticmethod def setup_rectangle(): """ Create an axis-aligned rectangle centered at (5, 5). """ return RectanglePixelRegion(PixCoord(5, 5), width=6, height=4, angle=0 * u.deg) @staticmethod def shapely_rectangle(): """ Create an equivalent Shapely rectangle polygon. """ from shapely import Polygon return Polygon([(2, 3), (8, 3), (8, 7), (2, 7)]) @pytest.mark.parametrize('method', ['contains', 'covers']) @pytest.mark.parametrize('point', all_points) def test_matches_shapely(self, method, point): """ Test that contains/covers match Shapely for points on the vertices, edges, interior, and exterior of the rectangle. """ from shapely import Point, contains, covers shp_func = {'contains': contains, 'covers': covers}[method] x, y = point reg = self.setup_rectangle() reg_result = bool(getattr(reg, method)(PixCoord(x, y))) shp_result = bool(shp_func(self.shapely_rectangle(), Point(x, y))) assert reg_result == shp_result astropy-regions-f4139aa/regions/shapes/tests/test_text.py000066400000000000000000000072101521703212700237610ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import numpy as np import pytest from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from numpy.testing import assert_allclose from regions.core import PixCoord, RegionMeta, RegionVisual from regions.shapes.tests.test_common import (BaseTestPixelRegion, BaseTestSkyRegion) from regions.shapes.text import TextPixelRegion, TextSkyRegion from regions.tests.helpers import make_simple_wcs @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): filename = get_pkg_data_filename('data/example_header.fits') header = fits.getheader(filename) return WCS(header) class TestTextPixelRegion(BaseTestPixelRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = TextPixelRegion(PixCoord(3, 4), 'Sample Text', meta=meta, visual=visual) sample_box = [-2, 8, -1, 9] inside = [] outside = [(3.1, 4.2), (5, 4)] expected_area = 0 expected_repr = ('") expected_str = ('Region: TextPixelRegion\ncenter: PixCoord(x=3, y=4)\n' "text: 'Sample Text'") def test_copy(self): reg = self.reg.copy() assert reg.center.xy == (3, 4) assert reg.text == 'Sample Text' assert reg.meta == self.meta assert reg.visual == self.visual def test_pix_sky_roundtrip(self): wcs = make_simple_wcs(SkyCoord(2 * u.deg, 3 * u.deg), 0.1 * u.deg, 20) reg_new = self.reg.to_sky(wcs).to_pixel(wcs) assert_allclose(reg_new.center.x, self.reg.center.x) assert_allclose(reg_new.center.y, self.reg.center.y) assert reg_new.meta == self.reg.meta assert reg_new.visual == self.reg.visual # test that converted region meta and visual are copies and not views reg_new.meta['text'] = 'new' reg_new.visual['color'] = 'green' assert reg_new.meta['text'] != self.reg.meta['text'] assert reg_new.visual['color'] != self.reg.visual['color'] def test_rotate(self): reg = self.reg.rotate(PixCoord(2, 3), 90 * u.deg) assert_allclose(reg.center.xy, (1, 4)) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.text = 'Hello World!' assert reg != self.reg class TestTextSkyRegion(BaseTestSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = TextSkyRegion(SkyCoord(3, 4, unit='deg'), 'Sample Text', meta=meta, visual=visual) expected_repr = (', text='Sample Text')>") expected_str = ('Region: TextSkyRegion\ncenter: \ntext: 'Sample Text'") def test_copy(self): reg = self.reg.copy() assert_allclose(reg.center.ra.deg, 3) assert reg.text == 'Sample Text' assert reg.meta == self.meta assert reg.visual == self.visual def test_contains(self, wcs): position = SkyCoord([1, 2] * u.deg, [3, 4] * u.deg) # Nothing is in a text region assert all(self.reg.contains(position, wcs) == np.array([False, False], dtype='bool')) def test_eq(self): reg = self.reg.copy() assert reg == self.reg reg.text = 'Hello World!' assert reg != self.reg astropy-regions-f4139aa/regions/shapes/tests/test_to_polygon.py000066400000000000000000000514101521703212700251670ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u from astropy.coordinates import SkyCoord from numpy.testing import assert_allclose from regions.core import PixCoord, RegionMeta, RegionVisual from regions.core.compound import (CompoundPixelRegion, CompoundSkyRegion, CompoundSphericalSkyRegion) from regions.shapes.annulus import (CircleAnnulusPixelRegion, CircleAnnulusSkyRegion, CircleAnnulusSphericalSkyRegion, EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion, RectangleAnnulusPixelRegion, RectangleAnnulusSkyRegion) from regions.shapes.circle import (CirclePixelRegion, CircleSkyRegion, CircleSphericalSkyRegion) from regions.shapes.ellipse import EllipsePixelRegion, EllipseSkyRegion from regions.shapes.lune import LuneSphericalSkyRegion from regions.shapes.polygon import (PolygonPixelRegion, PolygonSkyRegion, PolygonSphericalSkyRegion, RegularPolygonPixelRegion) from regions.shapes.range import RangeSphericalSkyRegion from regions.shapes.rectangle import RectanglePixelRegion, RectangleSkyRegion from regions.tests.helpers import make_simple_wcs class TestCirclePixelRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = CirclePixelRegion(PixCoord(3, 4), radius=2, meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, PolygonPixelRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.vertices.x) == 100 def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(n_vertices=50) assert len(poly.vertices.x) == 50 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual # meta and visual should be copies poly.meta['text'] = 'changed' poly.visual['color'] = 'red' assert poly.meta['text'] != self.meta['text'] assert poly.visual['color'] != self.visual['color'] def test_to_polygon_vertices(self): poly = self.reg.to_polygon(n_vertices=4) # theta = [0, pi/2, pi, 3*pi/2] assert_allclose(poly.vertices.x, [5, 3, 1, 3], atol=1e-10) assert_allclose(poly.vertices.y, [4, 6, 4, 2], atol=1e-10) def test_to_polygon_area(self): poly = self.reg.to_polygon(n_vertices=1000) assert_allclose(poly.area, self.reg.area, rtol=1e-4) def test_to_polygon_contains(self): poly = self.reg.to_polygon() assert poly.contains(PixCoord(3, 4)) assert not poly.contains(PixCoord(10, 10)) class TestCircleSkyRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) skycoord = SkyCoord(3 * u.deg, 4 * u.deg) reg = CircleSkyRegion(skycoord, 2 * u.arcsec, meta=meta, visual=visual) wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) def test_to_polygon_type(self): poly = self.reg.to_polygon(self.wcs) assert isinstance(poly, PolygonSkyRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon(self.wcs) assert len(poly.vertices.ra) == 100 def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(self.wcs, n_vertices=50) assert len(poly.vertices.ra) == 50 class TestCircleSphericalSkyRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = CircleSphericalSkyRegion( SkyCoord(3 * u.deg, 4 * u.deg), 2 * u.arcsec, meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, PolygonSphericalSkyRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.vertices.ra) == 100 def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(n_vertices=50) assert len(poly.vertices.ra) == 50 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual def test_to_polygon_contains(self): poly = self.reg.to_polygon() # Center should be inside assert poly.contains(SkyCoord(3 * u.deg, 4 * u.deg)) # Point far away should be outside assert not poly.contains(SkyCoord(180 * u.deg, 0 * u.deg)) class TestEllipsePixelRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = EllipsePixelRegion(PixCoord(3, 4), width=6, height=4, angle=0 * u.deg, meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, PolygonPixelRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.vertices.x) == 100 def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(n_vertices=50) assert len(poly.vertices.x) == 50 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual poly.meta['text'] = 'changed' poly.visual['color'] = 'red' assert poly.meta['text'] != self.meta['text'] assert poly.visual['color'] != self.visual['color'] def test_to_polygon_vertices_no_rotation(self): poly = self.reg.to_polygon(n_vertices=4) # Ellipse: width=6, height=4, center=(3,4), angle=0 # theta = [0, pi/2, pi, 3*pi/2] # x = 0.5*6*cos(theta) + 3 = [6, 3, 0, 3] # y = 0.5*4*sin(theta) + 4 = [4, 6, 4, 2] assert_allclose(poly.vertices.x, [6, 3, 0, 3], atol=1e-10) assert_allclose(poly.vertices.y, [4, 6, 4, 2], atol=1e-10) def test_to_polygon_vertices_rotated(self): reg = EllipsePixelRegion(PixCoord(0, 0), width=4, height=2, angle=90 * u.deg) poly = reg.to_polygon(n_vertices=4) # After 90 deg rotation, width along y and height along x assert_allclose(poly.vertices.x, [0, -1, 0, 1], atol=1e-10) assert_allclose(poly.vertices.y, [2, 0, -2, 0], atol=1e-10) def test_to_polygon_area(self): poly = self.reg.to_polygon(n_vertices=1000) assert_allclose(poly.area, self.reg.area, rtol=1e-4) def test_to_polygon_contains(self): poly = self.reg.to_polygon() assert poly.contains(PixCoord(3, 4)) assert not poly.contains(PixCoord(20, 20)) class TestEllipseSkyRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) skycoord = SkyCoord(3 * u.deg, 4 * u.deg) reg = EllipseSkyRegion(skycoord, 20 * u.arcsec, 10 * u.arcsec, meta=meta, visual=visual) wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) def test_to_polygon_type(self): poly = self.reg.to_polygon(self.wcs) assert isinstance(poly, PolygonSkyRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon(self.wcs) assert len(poly.vertices.ra) == 100 def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(self.wcs, n_vertices=50) assert len(poly.vertices.ra) == 50 class TestLuneSphericalSkyRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = LuneSphericalSkyRegion(SkyCoord(3 * u.deg, 0 * u.deg), SkyCoord(178 * u.deg, 0 * u.deg), meta=meta, visual=visual) # Verify changing order of GC center does not change the contains results: reg_inv = LuneSphericalSkyRegion(SkyCoord(178 * u.deg, 0 * u.deg), SkyCoord(3 * u.deg, 0 * u.deg), meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, PolygonSphericalSkyRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.vertices.ra) == 100 def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(n_vertices=50) assert len(poly.vertices.ra) == 50 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual def test_to_polygon_contains(self): poly = self.reg.to_polygon() # Inside point assert poly.contains(SkyCoord(90 * u.deg, 0 * u.deg)) # Point far away should be outside assert not poly.contains(SkyCoord(75 * u.deg, 0 * u.deg)) polylune_inv = self.reg_inv.discretize_boundary(n_vertices=100) # Inside point assert polylune_inv.contains(SkyCoord(90 * u.deg, 0 * u.deg)) # Point far away should be outside assert not polylune_inv.contains(SkyCoord(75 * u.deg, 0 * u.deg)) class TestRectanglePixelRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = RectanglePixelRegion(PixCoord(3, 4), width=4, height=2, angle=0 * u.deg, meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, PolygonPixelRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.vertices.x) == 4 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual poly.meta['text'] = 'changed' poly.visual['color'] = 'red' assert poly.meta['text'] != self.meta['text'] assert poly.visual['color'] != self.visual['color'] def test_to_polygon_vertices_no_rotation(self): poly = self.reg.to_polygon() # Rectangle with center=(3,4), width=4, height=2, angle=0 # Corners: (-2,-1), (2,-1), (2,1), (-2,1) + center assert_allclose(sorted(poly.vertices.x), [1, 1, 5, 5]) assert_allclose(sorted(poly.vertices.y), [3, 3, 5, 5]) def test_to_polygon_area(self): poly = self.reg.to_polygon() assert_allclose(poly.area, self.reg.area) def test_to_polygon_contains(self): poly = self.reg.to_polygon() assert poly.contains(PixCoord(3, 4)) assert not poly.contains(PixCoord(20, 20)) class TestRectangleSkyRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) skycoord = SkyCoord(3 * u.deg, 4 * u.deg) reg = RectangleSkyRegion(skycoord, 20 * u.arcsec, 10 * u.arcsec, meta=meta, visual=visual) wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) def test_to_polygon_type(self): poly = self.reg.to_polygon(self.wcs) assert isinstance(poly, PolygonSkyRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon(self.wcs) assert len(poly.vertices.ra) == 4 class TestRegularPolygonPixelRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = RegularPolygonPixelRegion(PixCoord(50, 50), nvertices=8, radius=20, angle=25 * u.deg, meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, PolygonPixelRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.vertices.x) == 8 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual def test_to_polygon_vertices(self): poly = self.reg.to_polygon() assert poly.vertices == self.reg.vertices assert poly.origin == PixCoord(0, 0) class TestRangeSphericalSkyRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = RangeSphericalSkyRegion(longitude_range=[0, 10] * u.deg, latitude_range=[-4, 4] * u.deg, frame='icrs', meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, PolygonSphericalSkyRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.vertices.ra) == 100 def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(n_vertices=50) assert len(poly.vertices.ra) == 50 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual def test_to_polygon_contains(self): poly = self.reg.to_polygon() # Center should be inside assert poly.contains(SkyCoord(5 * u.deg, 0 * u.deg)) # Point far away should be outside assert not poly.contains(SkyCoord(75 * u.deg, 10 * u.deg)) class TestCircleAnnulusPixelRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = CircleAnnulusPixelRegion(PixCoord(3, 4), inner_radius=2, outer_radius=5, meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, CompoundPixelRegion) assert isinstance(poly.region1, PolygonPixelRegion) assert isinstance(poly.region2, PolygonPixelRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.region1.vertices.x) == 100 assert len(poly.region2.vertices.x) == 100 def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(n_vertices=50) assert len(poly.region1.vertices.x) == 50 assert len(poly.region2.vertices.x) == 50 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual def test_to_polygon_contains(self): poly = self.reg.to_polygon() # Center should be outside (it's an annulus) assert not poly.contains(PixCoord(3, 4)) # Point at radius 3.5 should be inside assert poly.contains(PixCoord(6.5, 4)) # Point far away should be outside assert not poly.contains(PixCoord(20, 20)) class TestCircleAnnulusSkyRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) skycoord = SkyCoord(3 * u.deg, 4 * u.deg) reg = CircleAnnulusSkyRegion(skycoord, 20 * u.arcsec, 50 * u.arcsec, meta=meta, visual=visual) wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) def test_to_polygon_type(self): poly = self.reg.to_polygon(self.wcs) assert isinstance(poly, CompoundSkyRegion) def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(self.wcs, n_vertices=50) assert isinstance(poly, CompoundSkyRegion) class TestCircleAnnulusSphericalSkyRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = CircleAnnulusSphericalSkyRegion(SkyCoord(3 * u.deg, 4 * u.deg), 20 * u.arcsec, 30 * u.arcsec, meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, CompoundSphericalSkyRegion) assert isinstance(poly.region1, PolygonSphericalSkyRegion) assert isinstance(poly.region2, PolygonSphericalSkyRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.region1.vertices.ra) == 100 assert len(poly.region2.vertices.ra) == 100 def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(n_vertices=50) assert len(poly.region1.vertices.ra) == 50 assert len(poly.region2.vertices.ra) == 50 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual def test_to_polygon_contains(self): poly = self.reg.to_polygon() # Center should be outside (it's an annulus) assert not poly.contains(SkyCoord(3 * u.deg, 4 * u.deg)) # Point at radius 25*u.arcsec should be inside assert poly.contains(SkyCoord(3 * u.deg + 25 * u.arcsec, 4 * u.deg)) # Point far away should be outside assert not poly.contains(SkyCoord(180 * u.deg, 4 * u.deg)) class TestEllipseAnnulusPixelRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = EllipseAnnulusPixelRegion(PixCoord(3, 4), inner_width=2, outer_width=6, inner_height=4, outer_height=8, meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, CompoundPixelRegion) assert isinstance(poly.region1, PolygonPixelRegion) assert isinstance(poly.region2, PolygonPixelRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.region1.vertices.x) == 100 assert len(poly.region2.vertices.x) == 100 def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(n_vertices=50) assert len(poly.region1.vertices.x) == 50 assert len(poly.region2.vertices.x) == 50 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual def test_to_polygon_contains(self): poly = self.reg.to_polygon() # Center should be outside (it's an annulus) assert not poly.contains(PixCoord(3, 4)) # Point far away should be outside assert not poly.contains(PixCoord(20, 20)) class TestEllipseAnnulusSkyRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) skycoord = SkyCoord(3 * u.deg, 4 * u.deg) reg = EllipseAnnulusSkyRegion(skycoord, 20 * u.arcsec, 50 * u.arcsec, 50 * u.arcsec, 80 * u.arcsec, meta=meta, visual=visual) wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) def test_to_polygon_type(self): poly = self.reg.to_polygon(self.wcs) assert isinstance(poly, CompoundSkyRegion) def test_to_polygon_n_vertices(self): poly = self.reg.to_polygon(self.wcs, n_vertices=50) assert isinstance(poly, CompoundSkyRegion) class TestRectangleAnnulusPixelRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = RectangleAnnulusPixelRegion(PixCoord(3, 4), inner_width=2, outer_width=6, inner_height=4, outer_height=8, meta=meta, visual=visual) def test_to_polygon_type(self): poly = self.reg.to_polygon() assert isinstance(poly, CompoundPixelRegion) assert isinstance(poly.region1, PolygonPixelRegion) assert isinstance(poly.region2, PolygonPixelRegion) def test_to_polygon_nvertices(self): poly = self.reg.to_polygon() assert len(poly.region1.vertices.x) == 4 assert len(poly.region2.vertices.x) == 4 def test_to_polygon_meta(self): poly = self.reg.to_polygon() assert poly.meta == self.meta assert poly.visual == self.visual def test_to_polygon_contains(self): poly = self.reg.to_polygon() # Center should be outside (it's an annulus) assert not poly.contains(PixCoord(3, 4)) # Point far away should be outside assert not poly.contains(PixCoord(20, 20)) class TestRectangleAnnulusSkyRegionToPolygon: meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) skycoord = SkyCoord(3 * u.deg, 4 * u.deg) reg = RectangleAnnulusSkyRegion(skycoord, 20 * u.arcsec, 50 * u.arcsec, 50 * u.arcsec, 80 * u.arcsec, meta=meta, visual=visual) wcs = make_simple_wcs(skycoord, 5 * u.arcsec, 20) def test_to_polygon_type(self): poly = self.reg.to_polygon(self.wcs) assert isinstance(poly, CompoundSkyRegion) astropy-regions-f4139aa/regions/shapes/tests/test_whole_sky.py000066400000000000000000000051061521703212700250030ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import numpy as np import pytest from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from regions.core import RegionMeta, RegionVisual from regions.shapes.tests.test_common import BaseTestSphericalSkyRegion from regions.shapes.whole_sky import WholeSphericalSkyRegion @pytest.fixture(scope='session', name='wcs') def wcs_fixture(): filename = get_pkg_data_filename('data/example_header.fits') header = fits.getheader(filename) return WCS(header) class TestWholeSphericalSkyRegion(BaseTestSphericalSkyRegion): meta = RegionMeta({'text': 'test'}) visual = RegionVisual({'color': 'blue'}) reg = WholeSphericalSkyRegion(meta=meta, visual=visual) inside = [(3 * u.deg, 4 * u.deg), (1 * u.deg, 3 * u.deg)] outside = [] expected_repr = ('') expected_str = ('Region: WholeSphericalSkyRegion') def test_del(self): # test that Region shape attributes (descriptors) cannot be # deleted) if len(self.reg._params) > 0: with pytest.raises(AttributeError): delattr(self.reg, self.reg._params[0]) def test_copy(self): reg = self.reg.copy() assert isinstance(reg, WholeSphericalSkyRegion) assert reg.meta == self.meta assert reg.visual == self.visual def test_transformation(self, wcs): with pytest.raises(ValueError): _ = self.reg.to_sky(wcs=wcs) with pytest.raises(ValueError): _ = self.reg.to_pixel(wcs) def test_frame_transformation(self): reg2 = self.reg.transform_to('galactic') assert isinstance(reg2, WholeSphericalSkyRegion) def test_discretize_boundary(self): with pytest.raises(ValueError): _ = self.reg.discretize_boundary(n_vertices=100) def test_eq(self): reg = self.reg.copy() assert reg == self.reg def test_bounding_circle(self): bounding_circle = self.reg.bounding_circle assert bounding_circle is None def test_bounding_lonlat(self): bounding_lonlat = self.reg.bounding_lonlat assert bounding_lonlat is None def test_covers(self): """ Test that whole sky covers points. """ coord = SkyCoord(1 * u.deg, 2 * u.deg) assert self.reg.covers(coord) is True coords = SkyCoord([1, 2] * u.deg, [3, 4] * u.deg) assert np.all(self.reg.covers(coords) == [True, True]) astropy-regions-f4139aa/regions/shapes/text.py000066400000000000000000000143061521703212700215640ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines text regions in both pixel and sky coordinates. """ import math import astropy.units as u from regions._utils.wcs_helpers import (pixel_shape_to_sky_svd, sky_shape_to_pixel_svd) from regions.core.attributes import (RegionMetaDescr, RegionVisualDescr, ScalarPixCoord, ScalarSkyCoord) from regions.core.pixcoord import PixCoord from regions.shapes.point import PointPixelRegion, PointSkyRegion __all__ = ['TextSkyRegion', 'TextPixelRegion'] class TextPixelRegion(PointPixelRegion): """ A text string in pixel coordinates. Parameters ---------- center : `~regions.PixCoord` The leftmost point of the text string before rotation. text : str The text string. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from regions import PixCoord, RegionVisual, TextPixelRegion fig, ax = plt.subplots() center = PixCoord(x=15, y=10) visual = RegionVisual({'textangle': 30}) reg = TextPixelRegion(center=center, text='Hello World!', visual=visual) reg.plot(ax=ax) ax.set_xlim(10, 30) ax.set_ylim(2.5, 20) ax.set_aspect('equal') """ _params = ('center', 'text') _mpl_artist = 'Text' center = ScalarPixCoord('The leftmost pixel position (before rotation) ' 'as a |PixCoord|.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, text, meta=None, visual=None): super().__init__(center, meta, visual) self.text = text def to_sky(self, wcs): rotation_rad = math.radians(self.visual.get('rotation', 0.0)) # Extract the sky rotation angle of the text via a unit-size SVD # ellipse so the conversion correctly handles WCS shear. The # photutils SVD helpers measure the sky rotation as a position # angle (PA) from North; regions measures it from the RA axis. # Convert between them with a 90 deg offset. center, _, _, sky_angle = pixel_shape_to_sky_svd( (self.center.x, self.center.y), wcs, 1.0, 1.0, rotation_rad) sky_angle = (sky_angle + 90 * u.deg).wrap_at(360 * u.deg) # Rotation value is relative to the coordinate system axes; # convert from counterclockwise angle from the positive x axis # to angle relative to WCS longitude axis. visual = self.visual if 'rotation' in self.visual: visual = visual.copy() visual['rotation'] = sky_angle.to_value(u.deg) return TextSkyRegion(center, self.text, meta=self.meta.copy(), visual=visual.copy()) def to_spherical_sky(self, wcs, *, boundary_distortions=False, n_vertices=None): raise NotImplementedError def as_artist(self, origin=(0, 0), **kwargs): """ Return a matplotlib Text object for this region (`matplotlib.text.Text`). Parameters ---------- origin : array_like, optional The ``(x, y)`` pixel position of the origin of the displayed image. **kwargs : dict Any keyword arguments accepted by `~matplotlib.text.Text`. These keywords will override any visual meta attributes of this region. Returns ------- artist : `~matplotlib.text.Text` A matplotlib Text object. """ from matplotlib.text import Text mpl_kwargs = self.visual.define_mpl_kwargs(self._mpl_artist) mpl_kwargs.update(kwargs) return Text(self.center.x - origin[0], self.center.y - origin[1], self.text, **mpl_kwargs) class TextSkyRegion(PointSkyRegion): """ A text string in sky coordinates. Parameters ---------- center : `~astropy.coordinates.SkyCoord` The leftmost position of the text string before rotation. text : str The text string. meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ _params = ('center', 'text') center = ScalarSkyCoord('The leftmost position (before rotation) as a ' '|SkyCoord|.') meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') def __init__(self, center, text, meta=None, visual=None): super().__init__(center, meta, visual) self.text = text def to_pixel(self, wcs): rotation_rad = math.radians(self.visual.get('rotation', 0.0)) # Extract the pixel rotation angle of the text via a unit-size # SVD ellipse so the conversion correctly handles WCS shear. # Convert regions sky angle (from RA axis) to photutils PA (from # North) by subtracting 90 deg. center, _, _, pixel_angle = sky_shape_to_pixel_svd( self.center, wcs, 1.0, 1.0, rotation_rad - math.pi / 2) # Rotation value is relative to the WCS longitude axis; # convert to counterclockwise angle from the positive x axis. visual = self.visual if 'rotation' in self.visual: visual = visual.copy() visual['rotation'] = pixel_angle.to_value(u.deg) return TextPixelRegion(PixCoord(*center), self.text, meta=self.meta.copy(), visual=visual.copy()) def to_spherical_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): raise NotImplementedError astropy-regions-f4139aa/regions/shapes/whole_sky.py000066400000000000000000000035561521703212700226110ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines a whole-sky spherical sky region, for use with compound region logic and over-the-pole logic. """ import numpy as np from regions.core.core import SphericalSkyRegion from regions.core.metadata import RegionMeta, RegionVisual class WholeSphericalSkyRegion(SphericalSkyRegion): """ Spherical region representing the whole sky. Implemented to handle compound region logic, particularly with ranges and over-the-pole pole logic. Parameters ---------- meta : `~regions.RegionMeta` or `dict`, optional A dictionary that stores the meta attributes of the region. visual : `~regions.RegionVisual` or `dict`, optional A dictionary that stores the visual meta attributes of the region. """ def __init__(self, meta=None, visual=None): self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() def _containment(self, coord): if coord.isscalar: return True else: return np.ones(coord.shape, dtype=bool) def contains(self, coord): return self._containment(coord) def covers(self, coord): return self._containment(coord) @property def bounding_circle(self): # Not defined return None @property def bounding_lonlat(self): # Not defined return None def transform_to(self, frame, merge_attributes=True): return self.__class__() def discretize_boundary(self, *, n_vertices=100): raise ValueError('Not defined.') def to_sky(self, *, wcs=None, boundary_distortions=False, n_vertices=None): raise ValueError('Not defined.') def to_pixel(self, wcs, *, boundary_distortions=False, n_vertices=None): raise ValueError('Not defined.') astropy-regions-f4139aa/regions/tests/000077500000000000000000000000001521703212700201015ustar00rootroot00000000000000astropy-regions-f4139aa/regions/tests/__init__.py000066400000000000000000000000001521703212700222000ustar00rootroot00000000000000astropy-regions-f4139aa/regions/tests/helpers.py000066400000000000000000000132451521703212700221220ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import astropy.units as u import numpy as np from astropy.coordinates import SkyCoord from astropy.modeling import models from astropy.tests.helper import assert_quantity_allclose from astropy.wcs import WCS from numpy.testing import assert_allclose from regions.core import PixCoord, Region WCS_CENTER = SkyCoord(100 * u.deg, 30 * u.deg) WCS_CDELT_ARCSEC = 0.1 def make_simple_wcs(skycoord, resolution, size, *, rotation_deg=0): """ Create a simple TAN WCS object. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The sky coordinate for the center of the WCS. resolution : `~astropy.units.Quantity` The pixel scale as an angular Quantity. size : int The size of the WCS in pixels (assumed square). rotation_deg : float, optional Counter-clockwise rotation angle in degrees. Default is 0 (axis-aligned). Returns ------- wcs : `~astropy.wcs.WCS` A WCS object with the specified parameters. """ crpix = (size + 1) / 2 cdelt = resolution.to_value(u.deg) skycoord_icrs = skycoord.transform_to('icrs') ra = skycoord_icrs.ra.degree dec = skycoord_icrs.dec.degree wcs = WCS(naxis=2) wcs.wcs.crpix = [crpix, crpix] wcs.wcs.crval = [ra, dec] wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] if rotation_deg == 0: wcs.wcs.cdelt = np.array([-cdelt, cdelt]) else: theta = np.radians(rotation_deg) cos_t, sin_t = np.cos(theta), np.sin(theta) wcs.wcs.cd = [[-cdelt * cos_t, cdelt * sin_t], [cdelt * sin_t, cdelt * cos_t]] return wcs def make_gwcs(): """ Create a simple GWCS object with a TAN projection. Returns ------- wcs : `~gwcs.WCS` A GWCS object with a TAN projection centred at RA=100, Dec=30. """ from gwcs import WCS as GWCS from gwcs import coordinate_frames as cf cdelt_deg = WCS_CDELT_ARCSEC / 3600.0 shift_x = models.Shift(-10.5) shift_y = models.Shift(-10.5) scale = models.Scale(cdelt_deg) & models.Scale(cdelt_deg) rotation = models.AffineTransformation2D( matrix=np.array([[-1, 0], [0, 1]]), translation=[0, 0], ) tan = models.Pix2Sky_TAN() celestial_rotation = models.RotateNative2Celestial(100.0, 30.0, 180.0) transform = ((shift_x & shift_y) | scale | rotation | tan | celestial_rotation) detector = cf.Frame2D(name='detector', unit=(u.pix, u.pix)) sky = cf.CelestialFrame(name='sky', axes_order=(0, 1), reference_frame='icrs', axes_names=('lon', 'lat')) return GWCS(forward_transform=transform, input_frame=detector, output_frame=sky) def assert_skycoord_allclose(skycoord1, skycoord2, **kwargs): """ Test that two SkyCoord objects are nearly equal. Parameters ---------- skycoord1 : `~astropy.coordinates.SkyCoord` First SkyCoord object to compare. skycoord2 : `~astropy.coordinates.SkyCoord` Second SkyCoord object to compare. **kwargs : dict Additional keyword arguments passed to `astropy.tests.assert_quantity_allclose`. """ for attr in (skycoord1._extra_frameattr_names | skycoord2._extra_frameattr_names): if not skycoord1.frame._frameattr_equiv(getattr(skycoord1, attr), getattr(skycoord2, attr)): raise ValueError('Extra frame attributes are not equivalent') assert skycoord1.is_equivalent_frame(skycoord2) for attr in (set(skycoord1._sky_coord_frame._data.components) | set(skycoord2._sky_coord_frame._data.components)): sdata1 = skycoord1._sky_coord_frame._data sdata2 = skycoord2._sky_coord_frame._data assert_quantity_allclose(getattr(sdata1, attr), getattr(sdata2, attr), **kwargs) def assert_region_allclose(region1, region2, **kwargs): """ Test that two Region objects have parameters which are nearly equal. Meta and Visual properties are matched identically. Parameters ---------- region1 : `~regions.core.Region` First Region object to compare. region2 : `~regions.core.Region` Second Region object to compare. **kwargs : dict Additional keyword arguments passed to `astropy.tests.assert_quantity_allclose` or similar functions for comparing specific parameter types. """ if not (isinstance(region1, Region) and isinstance(region2, Region)): raise TypeError('Both inputs must be Region instances') # check that both have identical parameters region1_params = list(region1._params) region2_params = list(region2._params) if region1_params != region2_params: raise ValueError('Inputs do not have the same parameters') assert region1.meta == region2.meta assert region1.visual == region2.visual # now check the parameter values # Note that Quantity comparisons allow for different units # if they directly convertible (e.g., 1. * u.deg == 60. * u.arcmin) for param in region1_params: value1 = getattr(region1, param) value2 = getattr(region2, param) if isinstance(value1, SkyCoord): assert_skycoord_allclose(value1, value2, **kwargs) elif isinstance(value1, PixCoord): assert_allclose(value1.xy, value2.xy) elif isinstance(value1, u.Quantity): assert_quantity_allclose(value1, value2, **kwargs) elif isinstance(value1, str): assert value1 == value2 else: assert_allclose(value1, value2) astropy-regions-f4139aa/scripts/000077500000000000000000000000001521703212700167605ustar00rootroot00000000000000astropy-regions-f4139aa/scripts/pin_minimum_requirements.py000066400000000000000000000060501521703212700244570ustar00rootroot00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Generate a pip requirements file pinning build and runtime dependencies to their minimum supported versions, as declared in pyproject.toml. By default, reads [build-system].requires and [project].dependencies and writes _oldest_deps.txt. With --all, also includes the "all" extra and writes _oldest_deps-all.txt. """ import argparse import sys import tomllib from pathlib import Path from packaging.requirements import Requirement from packaging.version import Version script_name = Path(__file__).name out_path = Path(__file__).parent pyproject = out_path.parent / 'pyproject.toml' data = tomllib.loads(pyproject.read_text()) def pin_minimum(req_str_sections): """ Return a list of pinned requirement strings with the minimum versions for each package. Parameters ---------- req_str_sections : list of list of str A list of sections, where each section is a list of requirement strings (e.g. from [build-system].requires or [project].dependencies). Returns ------- result : list of str A list of requirement strings with minimum versions pinned (e.g. 'package==1.2.3'). """ mins = {} for section in req_str_sections: for req_str in section: req = Requirement(req_str) name = req.name.lower() for spec in req.specifier: if spec.operator == '>=': ver = Version(spec.version) if name not in mins or ver > mins[name][1]: mins[name] = (req.name, ver) break return [f'{name}=={ver}' for name, ver in mins.values()] parser = argparse.ArgumentParser( description=('Generate oldest-dependency constraint files from ' 'pyproject.toml.'), epilog=f'Example: python {script_name} --all', ) parser.add_argument( '--all', action='store_true', dest='include_all', help='also include the "all" extras in install constraints', ) parser.add_argument( '--path', type=Path, default=out_path, metavar='DIR', help=('directory where the output file will be written ' '(default: %(default)s)'), ) args = parser.parse_args() # Include project name in basename basename = data['project']['name'].replace('-', '_') + '_oldest_deps' sections = [data['build-system']['requires'], data['project']['dependencies']] if args.include_all: sections.append(data['project']['optional-dependencies']['all']) filename = f'{basename}-all.txt' else: filename = f'{basename}.txt' pins = pin_minimum(sections) output = args.path / filename header = ('# This file was generated by pin_minimum_requirements.py.\n' '# It pins the minimum supported versions of dependencies as ' 'declared in\n# pyproject.toml, including "build-system" ' 'dependencies (e.g., Cython).\n') output.write_text(header + '\n'.join(pins) + '\n') sys.stdout.write(f'Wrote {output}:\n') for p in pins: sys.stdout.write(f' {p}\n') astropy-regions-f4139aa/tox.ini000066400000000000000000000122101521703212700166000ustar00rootroot00000000000000[tox] envlist = py311-test-oldestdeps py{311,312,313,314}-test{,-alldeps,-devdeps,-predeps,-devinfra}{,-cov} oldest_cython build_docs linkcheck codestyle pep517 bandit isolated_build = true indexserver = NRAO = https://casa-pip.nrao.edu/repository/pypi-group/simple [testenv] # Suppress display of matplotlib plots generated during docs build setenv = MPLBACKEND=agg devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/scientific-python-nightly-wheels/simple https://pypi.anaconda.org/liberfa/simple https://pypi.anaconda.org/astropy/simple # Pass through the following environment variables which may be needed # for the CI passenv = HOME,WINDIR,LC_ALL,LC_CTYPE,CC,CI # Run the tests in a temporary directory to make sure that we don't # import this package from the source tree changedir = .tmp/{envname} # tox environments are constructed with so-called 'factors' (or terms) # separated by hyphens, e.g., test-devdeps-cov. Lines below starting # with factor: will only take effect if that factor is included in the # environment name. To see a list of example environments that can be # run, along with a description, run: # # tox -l -v # description = Run tests alldeps: with all optional dependencies predeps: with the latest pre-release versions of key dependencies devdeps: with the latest developer versions of key dependencies devinfra: like devdeps but also dev versions of infrastructure packages oldestdeps: with the oldest supported versions of key dependencies cov: and test coverage casa: with casatools and casatasks # The following provides some specific pinnings for key packages deps = cov: pytest-cov oldestdeps: packaging devdeps: numpy>=0.0.dev0 devdeps: matplotlib>=0.0.dev0 devdeps: pyerfa>=0.0.dev0 devdeps: astropy>=0.0.dev0 # Latest developer version of infrastructure packages devinfra: git+https://github.com/pytest-dev/pytest.git devinfra: git+https://github.com/astropy/extension-helpers.git devinfra: git+https://github.com/astropy/pytest-doctestplus.git devinfra: git+https://github.com/astropy/pytest-remotedata.git devinfra: git+https://github.com/astropy/pytest-astropy-header.git devinfra: git+https://github.com/astropy/pytest-arraydiff.git devinfra: git+https://github.com/astropy/pytest-filter-subpackage.git devinfra: git+https://github.com/astropy/pytest-astropy.git casa: :NRAO:casatools casa: :NRAO:casatasks # The following indicates which [project.optional-dependencies] from # pyproject.toml will be installed extras = test: test alldeps: all build_docs: docs install_command = !devdeps: python -I -m pip install devdeps: python -I -m pip install -v --pre # oldestdeps includes oldest build dependencies (e.g., Cython). commands_pre = oldestdeps: python -c "import sys; assert (3,11) <= sys.version_info < (3,13), 'Requires Python 3.11 or 3.12'" oldestdeps-!alldeps: python {toxinidir}/scripts/pin_minimum_requirements.py --path {envdir} oldestdeps-!alldeps: python -m pip install -r {envdir}/regions_oldest_deps.txt # Older pyparsing is needed to avoid pyparsing deprecation warnings # caused by matplotlib. oldestdeps-alldeps: python -m pip install "pyparsing==3.2.5" oldestdeps-alldeps: python {toxinidir}/scripts/pin_minimum_requirements.py --path {envdir} --all oldestdeps-alldeps: python -m pip install -r {envdir}/regions_oldest_deps-all.txt commands = pip freeze --all pytest --pyargs regions {toxinidir}/docs \ cov: --cov regions --cov-config={toxinidir}/pyproject.toml --cov-report xml:{toxinidir}/coverage.xml --cov-report term-missing \ {posargs} pip_pre = predeps: true !predeps: false [testenv:build_docs] changedir = docs description = Generate the HTML documentation using sphinx-build extras = docs commands = pip freeze --all sphinx-build -W -b html . _build/html [testenv:linkcheck] changedir = docs description = Check the links in the HTML docs extras = docs commands = pip freeze --all sphinx-build -W -b linkcheck . _build/html [testenv:codestyle] skip_install = true changedir = . description = Check code style with flake8 deps = flake8 commands = flake8 regions --count --max-line-length=79 [testenv:pep517] skip_install = true changedir = . description = Build source distribution and check with twine deps = build twine commands = python -m build --sdist . twine check dist/* --strict [testenv:bandit] skip_install = true changedir = . description = Security check with bandit deps = bandit commands = bandit -r regions -c pyproject.toml [testenv:oldest_cython] skip_install = true changedir = . description = Check Cython compilation with the oldest supported Cython version deps = packaging commands_pre = python -c "import sys; assert (3,11) <= sys.version_info < (3,13), 'Requires Python 3.11 or 3.12'" python {toxinidir}/scripts/pin_minimum_requirements.py --path {envdir} python -m pip install -r {envdir}/regions_oldest_deps.txt commands = pip freeze --all python -c "from Cython.Build import cythonize; cythonize('regions/**/*.pyx', force=True, language_level='3')"