././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9311333 radio_beam-0.3.8/0000755000175100001770000000000014704473431013220 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9231331 radio_beam-0.3.8/.github/0000755000175100001770000000000014704473431014560 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/.github/dependabot.yml0000644000175100001770000000114514704473421017410 0ustar00runnerdocker# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "github-actions" # See documentation for possible values directory: ".github/workflows" # Location of package manifests schedule: interval: "weekly" groups: actions: patterns: - "*" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9231331 radio_beam-0.3.8/.github/workflows/0000755000175100001770000000000014704473431016615 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/.github/workflows/main.yml0000644000175100001770000000522514704473421020267 0ustar00runnerdockername: Run tests on: [push, pull_request] jobs: tests: name: ${{ matrix.name}} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest python-version: 3.11 name: Python 3.11 with minimal dependencies toxenv: py311-test - os: ubuntu-latest python-version: '3.10' name: Python 3.10 with minimal dependencies toxenv: py310-test - os: ubuntu-latest python-version: 3.9 name: Python 3.9 with minimal dependencies toxenv: py39-test - os: ubuntu-latest python-version: 3.8 name: Python 3.8 with minimal dependencies toxenv: py38-test - os: ubuntu-latest python-version: 3.7 name: Python 3.7 with minimal dependencies toxenv: py37-test - os: ubuntu-latest python-version: 3.9 name: Python 3.9 with all dependencies (except CASA) toxenv: py39-test-all # - os: ubuntu-18.04 # python-version: 3.6 # name: Python 3.6 with minimal dependencies and CASA # toxenv: py36-test-casa # - os: ubuntu-18.04 # python-version: 3.6 # name: Python 3.6, CASA, and dev versions of key dependencies # toxenv: py36-test-casa-dev - os: ubuntu-latest python-version: 3.11 name: Python 3.11, all dependencies, and dev versions of key dependencies toxenv: py311-test-dev - os: macos-latest python-version: 3.11 name: Python 3.11 with all dependencies, and dev versions of key dependencies (no CASA) on MacOS X toxenv: py311-test-all-dev - os: windows-latest python-version: 3.11 name: Python 3.11, all dependencies, and dev versions of key dependencies (no CASA) on Windows toxenv: py311-test-all-dev - os: ubuntu-latest python-version: 3.11 name: Documentation toxenv: build_docs steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install testing dependencies run: python -m pip install tox codecov - name: Run tests with ${{ matrix.name }} run: tox -v -e ${{ matrix.toxenv }} - name: Upload coverage to codecov if: ${{ contains(matrix.toxenv,'-cov') }} uses: codecov/codecov-action@v4 with: file: ./coverage.xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/.github/workflows/publish.yml0000644000175100001770000000172714704473421021014 0ustar00runnerdockername: Build and upload to PyPI on: [push, pull_request] jobs: build_sdist_and_wheel: name: Build source distribution runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 name: Install Python with: python-version: '3.9' - name: Install build run: python -m pip install build - name: Build sdist run: python -m build --sdist --wheel --outdir dist/ . - uses: actions/upload-artifact@v4 with: path: dist/* upload_pypi: name: Upload to PyPI needs: [build_sdist_and_wheel] runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') steps: - uses: actions/download-artifact@v4 with: name: artifact path: dist - uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.pypi_password }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/.gitignore0000644000175100001770000000117614704473421015214 0ustar00runnerdocker# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ bin/ build/ develop-eggs/ dist/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # Installer logs pip-log.txt pip-delete-this-directory.txt pip-wheel-metadata/ # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Rope .ropeproject # Django stuff: *.log *.pot # Sphinx documentation docs/_build/ docs/api *.fits # Other generated stuff */version.py */cython_version.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/.readthedocs.yml0000644000175100001770000000030114704473421016277 0ustar00runnerdockerversion: 2 build: os: "ubuntu-20.04" tools: python: "3.12" # Install regular dependencies. python: install: - method: pip path: . extra_requirements: - docs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/CHANGES.rst0000644000175100001770000000347614704473421015033 0ustar00runnerdocker0.3.8 (unreleased) ------------------ 0.3.7 (2023-12-07) ------------------ - Update rtd build file #121 - bugfix: brightness_temperature call spec #119 - pdates to testing and such #117 0.3.5/0.3.6 (2023-10-17) ------------------ - Add more documentation about common beam convolution by @keflavich in #113 - remove unused imports that are apparently deprecated anyway by @keflavich in #115 - Clean up _astropy_init.py by @pllim in #116 0.3.4 (2022-09-23) ------------------ - Simplified two-beam common beam determination (https://github.com/radio-astro-tools/radio-beam/pull/102) - Allow non-arcsecond units in beam tables (https://github.com/radio-astro-tools/radio-beam/pull/98) - Support for beam initialization from area (https://github.com/radio-astro-tools/radio-beam/pull/94) - Drop support for python3.6 0.3.3 (2021-03-19) ------------------ - Optimized the deconvolution operation to avoid extra unit conversions and creation of new `Beam` objects. (https://github.com/radio-astro-tools/radio-beam/pull/87) 0.3.2 (2019-08-27) ------------------ 0.3.1 (2019-02-20) ------------------ - Set mult/div for convolution/deconvolution in `Beam` and `Beams`. The `==` and `!=` operators also work with `Beams` now. (https://github.com/radio-astro-tools/radio-beam/pull/75) - Added common beam operations to `Beams`. (https://github.com/radio-astro-tools/radio-beam/pull/67) - Fix PA usage for plotting and kernel routines. (https://github.com/radio-astro-tools/radio-beam/pull/65) 0.2 (2017-10-25) ---------------- - Changed repo name to `radio-beam` from `radio_beam`. (https://github.com/radio-astro-tools/radio-beam/pull/59) - Enhancement: Added support for multiple beams through the `Beams` class. (https://github.com/radio-astro-tools/radio-beam/pull/51) 0.1 (2017-09-08) ---------------- First release ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/LICENSE.rst0000644000175100001770000000273314704473421015040 0ustar00runnerdockerCopyright (c) 2016, radio-astro-tools 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/MANIFEST.in0000644000175100001770000000047414704473421014762 0ustar00runnerdockerinclude README.md include CHANGES.rst include LICENSE.rst include pyproject.toml include setup.cfg recursive-include *.pyx *.c *.pxd recursive-include docs * recursive-include licenses * recursive-include cextern * recursive-include scripts * prune build prune docs/_build prune docs/api global-exclude *.pyc *.o ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9311333 radio_beam-0.3.8/PKG-INFO0000644000175100001770000000272614704473431014324 0ustar00runnerdockerMetadata-Version: 2.1 Name: radio-beam Version: 0.3.8 Summary: Operations for radio astronomy beams with astropy Home-page: http://radio_beam.readthedocs.org Author: Adam Leroy, Adam Ginsburg, Erik Rosolowsky, Tom Robitaille, and Eric Koch Author-email: adam.g.ginsburg@gmail.com, koch.eric.w@gmail.com License: BSD License-File: LICENSE.rst Requires-Dist: astropy Requires-Dist: numpy>=1.8.0 Requires-Dist: scipy Provides-Extra: test Requires-Dist: pytest-astropy; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: matplotlib; extra == "test" Provides-Extra: docs Requires-Dist: sphinx-astropy; extra == "docs" Requires-Dist: matplotlib; extra == "docs" Provides-Extra: all Requires-Dist: scipy; extra == "all" Requires-Dist: matplotlib; extra == "all" Radio Beam: Tools for Beam IO and Manipulation ============================================== Radio Beam is a simple toolkit for reading beam information from FITS headers and manipulating beams. Some example applications include: * Convolution and deconvolution * Unit conversion (Jy to/from K) * Handle sets of beams for spectral cubes with varying resolution between channels * Find the smallest common beam from a set of beams * Add the beam shape to a matplotlib plot See the [documentation](https://radio-beam.readthedocs.io/en/latest/) for more information. [![Build Status](https://travis-ci.org/radio-astro-tools/radio_beam.svg?branch=master)](https://travis-ci.org/radio-astro-tools/radio_beam) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/README.md0000644000175100001770000000131314704473421014474 0ustar00runnerdockerRadio Beam: Tools for Beam IO and Manipulation ============================================== Radio Beam is a simple toolkit for reading beam information from FITS headers and manipulating beams. Some example applications include: * Convolution and deconvolution * Unit conversion (Jy to/from K) * Handle sets of beams for spectral cubes with varying resolution between channels * Find the smallest common beam from a set of beams * Add the beam shape to a matplotlib plot See the [documentation](https://radio-beam.readthedocs.io/en/latest/) for more information. [![Build Status](https://travis-ci.org/radio-astro-tools/radio_beam.svg?branch=master)](https://travis-ci.org/radio-astro-tools/radio_beam) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9231331 radio_beam-0.3.8/docs/0000755000175100001770000000000014704473431014150 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/Makefile0000644000175100001770000001116414704473421015612 0ustar00runnerdocker# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest #This is needed with git because git doesn't create a dir if it's empty $(shell [ -d "_static" ] || mkdir -p _static) help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR) -rm -rf api html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Astropy.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Astropy.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Astropy" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Astropy" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9271333 radio_beam-0.3.8/docs/_static/0000755000175100001770000000000014704473431015576 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/_static/radiosnakes_nostruts2.svg0000644000175100001770000004634214704473421022675 0ustar00runnerdocker image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/_static/spectralcube.css0000644000175100001770000000035114704473421020762 0ustar00runnerdocker@import url("bootstrap-astropy.css"); div.topbar a.brand { background: transparent url("radiosnakes_nostruts2.svg") no-repeat 10px 4px; background-image: url("radiosnakes_nostruts2.svg"), none; background-size: 32px 32px; } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9191332 radio_beam-0.3.8/docs/_templates/0000755000175100001770000000000014704473431016305 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9271333 radio_beam-0.3.8/docs/_templates/autosummary/0000755000175100001770000000000014704473431020673 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/_templates/autosummary/base.rst0000644000175100001770000000037214704473421022340 0ustar00runnerdocker{% extends "autosummary_core/base.rst" %} {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #}././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/_templates/autosummary/class.rst0000644000175100001770000000037314704473421022534 0ustar00runnerdocker{% extends "autosummary_core/class.rst" %} {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #}././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/_templates/autosummary/module.rst0000644000175100001770000000037414704473421022715 0ustar00runnerdocker{% extends "autosummary_core/module.rst" %} {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #}././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/api.rst0000644000175100001770000000045014704473421015451 0ustar00runnerdockerAPI Documentation ================= .. automodapi:: radio_beam :no-inheritance-diagram: :inherited-members: .. automodapi:: radio_beam.commonbeam :no-inheritance-diagram: :no-inherited-members: .. automodapi:: radio_beam.utils :no-inheritance-diagram: :no-inherited-members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/commonbeam.rst0000644000175100001770000001736414704473421017031 0ustar00runnerdocker.. _com_beam: Finding the smallest common beam ================================ radio-beam implements an exact solution for sets of 2 beams and an approximate method---`the Khachiyan algorithm `_---for larger sets. The former case is straightforward to compute as the beams can be transformed into a space where the larger beam is circular to find the overlap area (`~radio_beam.commonbeam.common_2beams`). Our implementation borrows from the implementation in `CASA `_ (see `here `__). Note that CASA uses this method for sets of beams larger than 2 by iterating through the beams and comparing each beam to the largest beam from the previous iterations. However, this approach is not guaranteed to find the minimum enclosing beam. For sets of more than two beams, finding the smallest common beam is a convex optimization problem equivalent to finding the minimum enclosed ellipse for a set of ellipses centered on the origin (`Boyd & Vandenberghe `_, see `example `_ in Sec 8.4.1). To avoid having radio-beam depend on convex optimization libraries, we implement the Khachiyan algorithm as an approximate method for finding the minimum ellipse. This algorithm finds the minimum ellipse that encloses the convex hull of a set of points (`Khachiyan & Todd 1993 `_, `Todd & Yildirim 2005 `_). By sampling a points on the boundaries of the beams in the set, we create a set of points whose convex hull is used to find the common beam. Since the minimum ellipse method is approximate, some solutions for the common beam will be slightly underestimated and the solution cannot be deconvolved from the whole set of beams. To overcome this issue, a small `epsilon` correction factor is added to the ellipse edges to encourage a valid common beam solution. Since `epsilon` is added to all sides, this correction will at most increase the common beam area by :math:`(1+\epsilon)^2`. The default values of `epsilon` is :math:`5\times10^{-4}`, so this will have a very small effect on the size of the common beam. The implementation in radio-beam is adapted from `a generalized python implementation `_ and `the original matlab version `_ written by Nima Moshtagh (see accompanying paper `here `__). Convolution to a common resolution ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The typical use case for calculating a common beam is to convolve one or more data sets to a common resolution. Using radio-beam with spectral-cube, you would first compute the common beam, then convolve all data sets to that beam. For a varying-resolution spectral cube, one in which there are different beams for each channel, the process looks like:: >>> cube = SpectralCube.read('VaryingResolutionCube.image') # doctest: +SKIP >>> common_beam = cube.beams.common_beam() # doctest: +SKIP >>> cb_cube = cube.convolve_to(common_beam) # doctest: +SKIP If you have two different data sets, you would follow a similar process:: >>> cube1 = SpectralCube.read('cube1.image') # doctest: +SKIP >>> cube2 = SpectralCube.read('cube2.image') # doctest: +SKIP >>> common_beam = cube1.beam.commonbeam_with(cube2.beam) # doctest: +SKIP >>> cb_cube1 = cube1.convolve_to(common_beam) # doctest: +SKIP >>> cb_cube2 = cube2.convolve_to(common_beam) # doctest: +SKIP Note that this process is equivalent to calculating the common beam, deconvolving the original data's beam, and convolving with the resulting kernel.:: >>> cube1 = SpectralCube.read('cube1.image') # doctest: +SKIP >>> cube2 = SpectralCube.read('cube2.image') # doctest: +SKIP >>> common_beam = cube1.beam.commonbeam_with(cube2.beam) # doctest: +SKIP >>> kernel1 = common_beam.deconvolve(cube1.beam) # doctest: +SKIP >>> kernel2 = common_beam.deconvolve(cube2.beam) # doctest: +SKIP >>> cb_cube1 = cube1.spatial_smooth(kernel1) # doctest: +SKIP >>> cb_cube2 = cube2.spatial_smooth(kernel2) # doctest: +SKIP See also :doc:`spectral-cube:smoothing`. Could not find common beam to deconvolve all beams ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You may encounter the error "Could not find common beam to deconvolve all beams." This occurs because the Khachiyan algorithm has converged to within the allowed tolerance with a solution marginally *smaller* than the enclosing ellipse. To mitigate this issue, the default settings now enable the keyword `auto_increase_epsilon=True`, which allows for small increases in `epsilon` until the common beam solution can be deconvolved by all beams in the set. The solution for the common beam will be iterated until this point, or until (1) `max_iter` is reached (default is 10) or (2) `max_epsilon` is reached (default is 1e-3). These values appear to work well with different ALMA and VLA data, but may need to be changed for specific data cubes. **If you notice these default parameters do not work for your data, please raise an issue** `here `_. In the case this issue persists, there are a few ways it can be fixed: 1. **Changing the tolerance.** - The default tolerance for convergence of the Khachiyan algorithm (`~radio_beam.commonbeam.getMinVolEllipse`) is `tolerance=1e-5`. This tolerance can be changed in `~radio_beam.Beams.common_beam` by specifying a new tolerance. Convergence may be met by either increasing or decreasing the tolerance; it depends on having the algorithm not step within the minimum enclosing ellipse, leading to the error. Note that decreasing the tolerance by an order of magnitude will require an order of magnitude more iterations for the algorithm to converge. It will typically be faster to change `epsilon` (see below). 2. **Changing epsilon** - A second parameter `epsilon` controls the points sampled at the edges of the beams in the set (`~radio_beam.commonbeam.ellipse_edges`), which are used in the Khachiyan algorithm. `epsilon` is the fraction beyond the true edge of the ellipse that points will be sampled at. For example, the default value of `epsilon=1e-3` will sample points 0.1% larger than the edge of the ellipse. Increasing `epsilon` ensures that a valid common beam can be found, avoiding the tolerance issue, but will result in overestimating the common beam area. For most radio data sets, where the beam is oversampled by :math:`\sim 3--5` pixels, moderate increases in `epsilon` will increase the common beam area far less than a pixel area, making the overestimation negligible. 3. **Changing the `auto_increase_epsilon` keywords** - To avoid the manual guess-and-check, the `auto_increase_epsilon` can be made more lenient to encourage a valid solution. This can be achieved by (i) increasing the intial values of `epsilon` (equivalent to #2), (ii) decreasing the number of iterations (forces larger incremental steps in `epsilon`, or (iii) increasing `max_epsilon`. (i) and (ii) will both reduce the number of iterations making it quicker to test different keyword values. (iii) allows for the common beam solution to be moderately larger. As noted above, increasing `epsilon` allows for the common beam area to be overestimated *up to* :math:`(1+\epsilon)^2`. We recommend testing different values of tolerance to find convergence, and if the error persists, to then slowly increase epsilon until a valid common beam is found. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/conf.py0000644000175100001770000001643614704473421015460 0ustar00runnerdocker# -*- coding: utf-8 -*- # Licensed under a 3-clause BSD style license - see LICENSE.rst # # Astropy 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. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('..')) # IMPORTANT: the above commented section was generated by sphinx-quickstart, but # is *NOT* appropriate for astropy or Astropy affiliated packages. It is left # commented out with this explanation to make it clear why this should not be # done. If the sys.path entry above is added, when the astropy.sphinx.conf # import occurs, it will import the *source* version of astropy instead of the # version installed (if invoked as "make html" or directly with sphinx), or the # version in the build directory (if "python setup.py build_sphinx" is used). # Thus, any C-extensions that are needed to build the documentation will *not* # be accessible, and the documentation will not build correctly. import os import sys import datetime from importlib import import_module try: from sphinx_astropy.conf.v1 import * # noqa except ImportError: print('ERROR: the documentation requires the sphinx-astropy package to be installed') sys.exit(1) # Get configuration information from setup.cfg from configparser import ConfigParser conf = ConfigParser() conf.read([os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')]) setup_cfg = dict(conf.items('metadata')) # -- General configuration ---------------------------------------------------- # By default, highlight as Python 3. highlight_language = 'python3' # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.2' # To perform a Sphinx version check that needs to be more specific than # major.minor, call `check_sphinx_version("x.y.z")` here. # check_sphinx_version("1.2.1") # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns.append('_templates') # This is added to the end of RST files - a good place to put substitutions to # be used globally. rst_epilog += """ """ intersphinx_mapping['spectral-cube'] = ('https://spectral-cube.readthedocs.io/en/latest/', None) # noqa: F405 # -- Project information ------------------------------------------------------ # This does not *have* to match the package name, but typically does project = setup_cfg['name'] author = setup_cfg['author'] copyright = '{0}, {1}'.format( datetime.datetime.now().year, setup_cfg['author']) # 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. from importlib.metadata import distribution as get_distribution version = release = get_distribution(setup_cfg['name']).version # -- Options for HTML output -------------------------------------------------- # A NOTE ON HTML THEMES # The global astropy configuration uses a custom theme, 'bootstrap-astropy', # which is installed along with astropy. A different theme can be used or # the options for this theme can be modified by overriding some of the # variables set in the global configuration. The variables set in the # global configuration are listed below, commented out. # Add any paths that contain custom themes here, relative to this directory. # To use a different custom theme, add the directory containing the theme. #html_theme_path = [] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. To override the custom theme, set this to the # name of a builtin theme or the name of a custom theme in html_theme_path. #html_theme = None html_theme_options = { 'logotext1': 'radio-beam', # white, semi-bold 'logotext2': '', # orange, light 'logotext3': ':docs' # white, light } # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = '' # Static files to copy after template files html_static_path = ['_static'] html_style = 'spectralcube.css' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = '' # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = '{0} v{1}'.format(project, release) # Output file base name for HTML help builder. htmlhelp_basename = project + 'doc' # -- 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 + u' Documentation', author, 'manual')] # -- 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 + u' Documentation', [author], 1)] # -- Options for the edit_on_github extension --------------------------------- if eval(setup_cfg.get('edit_on_github')): extensions += ['sphinx_astropy.ext.edit_on_github'] versionmod = __import__(setup_cfg['package_name'] + '.version') edit_on_github_project = setup_cfg['github_project'] if versionmod.version.release: edit_on_github_branch = "v" + versionmod.version.version else: edit_on_github_branch = "master" edit_on_github_source_root = "" edit_on_github_doc_root = "docs" # -- Resolving issue number to links in changelog ----------------------------- github_issues_url = 'https://github.com/{0}/issues/'.format(setup_cfg['github_project']) # -- Turn on nitpicky mode for sphinx (to warn about references not found) ---- # # nitpicky = True # nitpick_ignore = [] # # 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: # # for line in open('nitpick-exceptions'): # if line.strip() == "" or line.startswith("#"): # continue # dtype, target = line.split(None, 1) # target = target.strip() # nitpick_ignore.append((dtype, str(target))) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/convolution_kernels.rst0000644000175100001770000000521014704473421021001 0ustar00runnerdocker.. _convkernels: Making convolution kernels ========================== `~radio_beam.Beam` can produce two types of kernels: a Gaussian (`~radio_beam.Beam.as_kernel`) and a top-hat (`~radio_beam.Beam.as_tophat_kernel`). As an example, consider the elliptical beam:: >>> import astropy.units as u >>> from radio_beam import Beam >>> my_beam = Beam(3*u.arcsec, 1.5*u.arcsec, 60*u.deg) Gaussian ^^^^^^^^ `~radio_beam.Beam.as_kernel` will return an elliptical Gaussian kernel given the angular size of a pixel:: >>> pix_scale = 0.5 * u.arcsec >>> gauss_kern = my_beam.as_kernel(pix_scale) `gauss_kern` will be a `~radio_beam.beam.EllipticalGaussian2DKernel` object and has the same methods, attributes and keyword arguments as `Kernel2D `__ in astropy's convolution package. These keyword arguments can be passed to `~radio_beam.Beam.as_kernel`. See the `astropy documentation `_ for more information on convolution kernels. Top-Hat ^^^^^^^ `~radio_beam.Beam.as_tophat_kernel` returns an elliptical top-hat kernel scales to have the same area as a Gaussian kernel within the FWHM. Similar to the Gaussian kernel, only the pixel scale needs to be given:: >>> tophat_kern = my_beam.as_tophat_kernel(pix_scale) `tophat_kern` is a `~radio_beam.beam.EllipticalTophat2DKernel` object, also derived from `Kernel2D `__ in astropy's convolution package. Keyword arguments can be passed to `~radio_beam.Beam.as_tophat_kernel`. The values in the kernel are normalized to unity, and it is suitable for convolution. However, the top-hat kernel is also useful for masking purposes, in which case a boolean version of the kernel is useful. To make a boolean version, we need to access the array in the kernel object and look for non-zero values:: >>> tophat_kern_bool = tophat_kern.array > 0 `tophat_kern_bool` is suitable for use with morphological operations, such as those in `scipy.ndimage `_. Convolution kernels from multiple beams ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From a `~radio_beam.Beams` object, a convolution kernel can be made for each beam in the set by slicing:: >>> from radio_beam import Beams >>> from astropy.io import fits >>> bin_hdu = fits.open('file.fits')[1] # doctest: +SKIP >>> beams = Beams.from_fits_bintable(bin_hdu) # doctest: +SKIP >>> beams[0].as_kernel(pix_scale) # doctest: +SKIP ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/index.rst0000644000175100001770000001403414704473421016012 0ustar00runnerdockerRadio Beam ========== radio-beam provides a tools for manipulating and utilizing two-dimensional Gaussian beams within the `astropy `__ framework. It is primarily built for handling radio astronomy data and is integrated into the `spectral-cube _` package, amongst others. radio-beam also handles operations on sets of beams, for example from a spectral cube with varying resolution in spectral channels. Of note are the algorithms for identifying the smallest common beam in a set (i.e., the minimum enclosing ellipse area). Getting started ^^^^^^^^^^^^^^^ .. toctree:: :maxdepth: 2 install.rst commonbeam.rst plotting_beams.rst convolution_kernels.rst api.rst Basic Examples ^^^^^^^^^^^^^^ Handling a single beam ---------------------- `~radio_beam.Beam` handles operations on individual beams. Read a beam from a FITS header:: >>> from radio_beam import Beam >>> from astropy.io import fits >>> header = fits.getheader('file.fits') # doctest: +SKIP >>> my_beam = Beam.from_fits_header(header) # doctest: +SKIP >>> print(my_beam) # doctest: +SKIP Beam: BMAJ=0.038652855902928 arcsec BMIN=0.032841067761183604 arcsec BPA=32.29655838013 deg To add the beam parameters to a FITS header:: >>> header.update(my_beam.to_header_keywords()) # doctest: +SKIP This will return new or add values for the `BMAJ`, `BMIN`, and `BPA` keywords. Create a new circular beam:: >>> from astropy import units as u >>> my_beam = Beam(0.5*u.arcsec) >>> my_beam Beam: BMAJ=0.5 arcsec BMIN=0.5 arcsec BPA=0.0 deg `~radio_beam.Beam` assumes a circular beam when a minor full-width-half-max (FWHM) is not given. To create an elliptical beam, the minor FWHM and position angle need to be given:: >>> my_beam_ellip = Beam(major=0.5*u.arcsec, minor=0.25*u.arcsec, pa=30*u.deg) >>> my_beam_ellip Beam: BMAJ=0.5 arcsec BMIN=0.25 arcsec BPA=30.0 deg The beam area in steradians is:: >>> my_beam_ellip.sr # doctest: +FLOAT_CMP Or projected into physical units given a distance:: >>> my_beam_ellip.beam_projected_area(840*u.kpc).to(u.pc**2) # doctest: +FLOAT_CMP A common unit conversion in radio astronomy is Jy/beam to K, which depends on the beam area. A `~radio_beam.Beam` object can be used for this unit conversion with `~astropy.units`:: >>> (1*u.Jy).to(u.K, u.brightness_temperature(25*u.GHz, my_beam)) # doctest: +FLOAT_CMP Or alternatively with:: >>> (1*u.Jy).to(u.K, my_beam.jtok_equiv(25*u.GHz)) # doctest: +FLOAT_CMP To get the value of 1 Jy in K for a given beam:: >>> my_beam.jtok(25*u.GHz) # doctest: +FLOAT_CMP Two beams can be convolved:: >>> my_asymmetric_beam = Beam(0.75*u.arcsec, 0.25*u.arcsec, 0*u.deg) >>> my_other_asymmetric_beam = Beam(0.75*u.arcsec, 0.25*u.arcsec, 90*u.deg) >>> my_asymmetric_beam.convolve(my_other_asymmetric_beam) # doctest: +FLOAT_CMP Beam: BMAJ=0.790569415042 arcsec BMIN=0.790569415042 arcsec BPA=45.0 deg And also deconvolved:: >>> my_big_beam = Beam(1.0*u.arcsec, 1.0*u.arcsec, 0*u.deg) >>> my_little_beam = Beam(0.5*u.arcsec, 0.5*u.arcsec, 0*u.deg) >>> my_big_beam.deconvolve(my_little_beam) # doctest: +FLOAT_CMP Beam: BMAJ=0.866025403784 arcsec BMIN=0.866025403784 arcsec BPA=0.0 deg An error is returned if the beam area is too small to deconvolve from the other:: >>> my_little_beam.deconvolve(my_big_beam) # doctest: +SKIP To find the smallest common beam between any two beams:: >>> my_asymmetric_beam.commonbeam_with(my_other_asymmetric_beam) # doctest: +FLOAT_CMP Beam: BMAJ=0.75 arcsec BMIN=0.75 arcsec BPA=90.0 deg Handling a sets of beams ------------------------ `~radio_beam.Beams` handles operations on sets of beams. To read a table of beams from a FITS table:: >>> from radio_beam import Beams >>> from astropy.io import fits >>> bin_hdu = fits.open('file.fits')[1] # doctest: +SKIP >>> beams = Beams.from_fits_bintable(bin_hdu) # doctest: +SKIP In the above example, the second FITS extension contains the beam tables, while the first would have the spectral cube data. To read a table of beams from a CASA image (must be run inside a CASA environment!):: >>> beams = Beams.from_casa_image('file.image') # doctest: +SKIP Create a table of beams:: >>> my_beams = Beams([1.5, 1.3] * u.arcsec, [1., 1.2] * u.arcsec, [0, 50] * u.deg) `~radio_beam.Beams` acts like a numpy array and can be sliced in the same way:: >>> my_beams[0] Beam: BMAJ=1.5 arcsec BMIN=1.0 arcsec BPA=0.0 deg >>> my_beams[1] Beam: BMAJ=1.3 arcsec BMIN=1.2 arcsec BPA=50.0 deg Find the largest beam in the set:: >>> my_beams.largest_beam() Beam: BMAJ=1.3 arcsec BMIN=1.2 arcsec BPA=50.0 deg Find the smallest common beam for the set (see :ref:`here ` for more on common beams):: >>> my_beams.common_beam() # doctest: +FLOAT_CMP Beam: BMAJ=1.50671729431 arcsec BMIN=1.25695643792 arcsec BPA=6.69089813778 deg Return the smallest and largest beams in a set (by beam area):: >>> smallest_beam, largest_beam = my_beams.extrema_beams() >>> smallest_beam Beam: BMAJ=1.5 arcsec BMIN=1.0 arcsec BPA=0.0 deg >>> largest_beam Beam: BMAJ=1.3 arcsec BMIN=1.2 arcsec BPA=50.0 deg Optionally mask out a beam (to exclude from the calculation):: >>> import numpy as np >>> beam_mask = np.array([True, False]) >>> smallest_beam, largest_beam = my_beams.extrema_beams(includemask=beam_mask) >>> smallest_beam Beam: BMAJ=1.5 arcsec BMIN=1.0 arcsec BPA=0.0 deg >>> largest_beam Beam: BMAJ=1.5 arcsec BMIN=1.0 arcsec BPA=0.0 deg This masking can be applied to most operations, including `~radio_beam.Beams.common_beam` to exclude large outliers in the set. One useful example is if a channel is blanked in a spectral-cube, and the beam is a `NaN`. To make a mask to select only finite beams:: >>> my_beams.isfinite array([ True, True]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/install.rst0000644000175100001770000000334014704473421016347 0ustar00runnerdockerInstalling ``radio-beam`` ============================ Requirements ------------ This package has the following dependencies: * `Python `_ 3.6 or later * `Numpy `_ 1.8 or later * `Astropy `__ 1.0 or later Installation ------------ To install the latest stable release, you can type:: pip install radio-beam or you can download the latest tar file from `PyPI `_ and install it using:: pip install -e . Developer version ----------------- If you want to install the latest developer version of the radio-beam code, you can do so from the git repository:: git clone https://github.com/radio-astro-tools/radio-beam.git cd radio-beam pip install -e . You may need to add the ``--user`` option to the last line `if you do not have root access `_. You can also install the latest developer version in a single line with pip:: pip install git+https://github.com/radio-astro-tools/radio-beam.git Installing into CASA -------------------- Installing packages in CASA is fairly straightforward. The process is described `here `_. In short, you can do the following: First, we need to make sure `pip `__ is installed. Start up CASA as normal, and type:: CASA <1>: from setuptools.command import easy_install CASA <2>: easy_install.main(['--user', 'pip']) Now, quit CASA and re-open it, then type the following to install ``radio-beam``:: CASA <1>: import pip CASA <2>: pip.main(['install', 'radio-beam', '--user']) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/make.bat0000644000175100001770000001064114704473421015556 0ustar00runnerdocker@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Astropy.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Astropy.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/nitpick-exceptions0000644000175100001770000000005614704473421017713 0ustar00runnerdockerpy:obj radio_beam,commonbeam.transform_ellipse././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/docs/plotting_beams.rst0000644000175100001770000000141514704473421017711 0ustar00runnerdocker.. _plotting: Add a beam to a matplotlib plot =============================== To show the beam on an image in matplotlib, use `~radio_beam.Beam.ellipse_to_plot`:: >>> from radio_beam import Beam >>> import astropy.units as u >>> import matplotlib.pyplot as plt >>> my_beam = Beam(5*u.arcsec, 3*u.arcsec, 30*u.deg) >>> ycen_pix, xcen_pix = 15, 15 >>> pixscale = 1 * u.arcsec >>> ellipse_artist = my_beam.ellipse_to_plot(xcen_pix, ycen_pix, pixscale) >>> ax = plt.imshow(image) # doctest: +SKIP >>> _ = ax.add_artist(ellipse_artist) # doctest: +SKIP The three inputs you need for adding to an arbitrary image are the x and y coordinates to center the beam at in the image, and the pixel scale of the image as defined in the WCS information. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/pyproject.toml0000644000175100001770000000020414704473421016127 0ustar00runnerdocker[build-system] requires = ["setuptools", "setuptools_scm", "wheel"] build-backend = 'setuptools.build_meta' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9271333 radio_beam-0.3.8/radio_beam/0000755000175100001770000000000014704473431015302 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/__init__.py0000644000175100001770000000052514704473421017414 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst from ._astropy_init import __version__, test from .beam import (Beam, EllipticalGaussian2DKernel, EllipticalTophat2DKernel) from .multiple_beams import Beams __all__ = ['Beam', 'EllipticalTophat2DKernel', 'EllipticalGaussian2DKernel', 'Beams'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/_astropy_init.py0000644000175100001770000000057214704473421020542 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst import os from astropy.tests.runner import TestRunner __all__ = ['__version__', 'test'] try: from .version import version as __version__ except ImportError: __version__ = '' # Create the test function for self test test = TestRunner.make_test_runner_in(os.path.dirname(__file__)) test.__test__ = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/beam.py0000644000175100001770000006635214704473421016573 0ustar00runnerdockerfrom astropy import units as u from astropy.io import fits from astropy import constants from astropy import wcs import numpy as np import warnings # Imports for the custom kernels from astropy.modeling.models import Ellipse2D, Gaussian2D from astropy.convolution import Kernel2D from astropy.convolution.kernels import _round_up_to_odd_integer from .utils import deconvolve_optimized, convolve, RadioBeamDeprecationWarning # Conversion between a twod Gaussian FWHM**2 and effective area FWHM_TO_AREA = 2*np.pi/(8*np.log(2)) SIGMA_TO_FWHM = np.sqrt(8*np.log(2)) class NoBeamException(Exception): pass def _to_area(major,minor): return (major * minor * FWHM_TO_AREA).to(u.sr) unit_format = {u.deg: r'\\circ', u.arcsec: "''", u.arcmin: "'"} def _with_default_unit(type_str, value, unit): if not hasattr(value, 'unit'): return value * unit if value.unit.is_equivalent(unit): return value else: raise u.UnitsError(f"{value.unit} for {type_str} is not equivalent to {equiv_unit}") class Beam(u.Quantity): """ An object to handle single radio beams. """ def __new__(cls, major=None, minor=None, pa=None, area=None, default_unit=u.arcsec, meta=None): """ Create a new Gaussian beam Parameters ---------- major : :class:`~astropy.units.Quantity` with angular equivalency The FWHM major axis minor : :class:`~astropy.units.Quantity` with angular equivalency The FWHM minor axis pa : :class:`~astropy.units.Quantity` with angular equivalency The beam position angle area : :class:`~astropy.units.Quantity` with steradian equivalency The area of the beam. This is an alternative to specifying the major/minor/PA, and will create those values assuming a circular Gaussian beam. default_unit : :class:`~astropy.units.Unit` The unit to impose on major, minor if they are specified as floats meta : dict, optional A dictionary of metadata to store with the beam. """ # improve to some kwargs magic later # error checking # ... given an area make a round beam assuming it is Gaussian if area is not None: if major is not None: raise ValueError("Can only specify one of {major,minor,pa} " "and {area}") if not area.unit.is_equivalent(u.sr): raise ValueError("Area unit should be equivalent to steradian.") rad = np.sqrt(area/(2*np.pi)) major = rad * SIGMA_TO_FWHM minor = rad * SIGMA_TO_FWHM pa = 0.0 * u.deg # give specified values priority if major is not None: major = _with_default_unit("major", major, default_unit) if pa is not None: pa = _with_default_unit("pa", pa, u.deg) else: pa = 0 * u.deg # some sensible defaults if minor is None: minor = major else: minor = _with_default_unit("minor", minor, default_unit) if minor > major: raise ValueError("Minor axis greater than major axis.") self = super(Beam, cls).__new__(cls, _to_area(major,minor).value, u.sr) self._major = major self._minor = minor self._pa = pa self.default_unit = default_unit if meta is None: self.meta = {} elif isinstance(meta, dict): self.meta = meta else: raise TypeError("metadata must be a dictionary") return self @classmethod def from_fits_bintable(cls, bintable, tolerance=0.01, unit=u.arcsec): """ Instantiate a single beam from a bintable from a CASA-produced image HDU. The beams in the BinTableHDU will be averaged to form a single beam. Parameters ---------- bintable : fits.BinTableHDU The table data containing the beam information tolerance : float The fractional tolerance on the beam size to include when averaging to a single beam Returns ------- beam : Beam A new beam object that is the average of the table beams """ from astropy.stats import circmean bmaj = bintable.data['BMAJ'] bmin = bintable.data['BMIN'] bpa = bintable.data['BPA'] if np.any(np.isnan(bmaj) | np.isnan(bmin) | np.isnan(bpa)): raise ValueError("NaN beam encountered.") for par in (bmin,bmaj): par_mean = par.mean() if (par.max() > par_mean*(1+tolerance)) or (par.min() restoration: 1.34841 by 0.830715 (arcsec) # at pa 82.8827 (deg) casaline = None for line in hdr['HISTORY']: if ('restoration' in line) and ('arcsec' in line): casaline = line #assert precedence for CASA style over AIPS # this is a dubious choice if casaline is not None: bmaj = float(casaline.split()[2]) * u.arcsec bmin = float(casaline.split()[4]) * u.arcsec bpa = float(casaline.split()[8]) * u.deg return cls(major=bmaj, minor=bmin, pa=bpa) elif aipsline is not None: bmaj = float(aipsline.split()[3]) * u.deg bmin = float(aipsline.split()[5]) * u.deg bpa = float(aipsline.split()[7]) * u.deg return cls(major=bmaj, minor=bmin, pa=bpa) else: return None @classmethod def from_casa_image(cls, imagename): ''' Instantiate beam from a CASA image. ** Must be run in a CASA environment! ** Parameters ---------- imagename : str Name of CASA image. ''' try: import casac except ImportError: raise ImportError("Could not import CASA (casac) and therefore" " cannot read CASA .image files") ia.open(imagename) beam_props = ia.restoringbeam() ia.close() beam_keys = ["major", "minor", "positionangle"] if not all([True for key in beam_keys if key in beam_props]): raise ValueError("The image does not contain complete beam " "information. Check the output of " "ia.restoringbeam().") major = beam_props["major"]["value"] * \ u.Unit(beam_props["major"]["unit"]) minor = beam_props["minor"]["value"] * \ u.Unit(beam_props["minor"]["unit"]) pa = beam_props["positionangle"]["value"] * \ u.Unit(beam_props["positionangle"]["unit"]) return cls(major=major, minor=minor, pa=pa) def attach_to_header(self, header, copy=True): ''' Attach the beam information to the provided header. Parameters ---------- header : astropy.io.fits.header.Header Header to add/update beam info. copy : bool, optional Returns a copy of the inputted header with the beam information. Returns ------- copy_header : astropy.io.fits.header.Header Copy of the input header with the updated beam info when `copy=True`. ''' if copy: header = header.copy() header.update(self.to_header_keywords()) return header def __repr__(self): return "Beam: BMAJ={0} BMIN={1} BPA={2}".format(self.major.to(self.default_unit),self.minor.to(self.default_unit),self.pa.to(u.deg)) def __repr_html__(self): return "Beam: BMAJ={0} BMIN={1} BPA={2}".format(self.major.to(self.default_unit),self.minor.to(self.default_unit),self.pa.to(u.deg)) def _repr_latex_(self): return "Beam: BMAJ=${0}^{{{fmt}}}$ BMIN=${1}^{{{fmt}}}$ BPA=${2}^\\circ$".format(self.major.to(self.default_unit).value, self.minor.to(self.default_unit).value, self.pa.to(u.deg).value, fmt = unit_format[self.default_unit]) def __str__(self): return self.__repr__() def convolve(self, other): """ Convolve one beam with another. Parameters ---------- other : `Beam` The beam to convolve with Returns ------- new_beam : `Beam` The convolved Beam """ new_major, new_minor, new_pa = convolve(self, other) return Beam(major=new_major, minor=new_minor, pa=new_pa) def __mul__(self, other): return self.convolve(other) # Does division do the same? Or what? Doesn't have to be defined. def __sub__(self, other): warnings.warn("Subtraction-as-deconvolution is deprecated. " "Use division instead.", RadioBeamDeprecationWarning) return self.deconvolve(other) def __truediv__(self, other): return self.deconvolve(other) def deconvolve(self, other, failure_returns_pointlike=False): """ Deconvolve a beam from another Parameters ---------- other : `Beam` The beam to deconvolve from this beam failure_returns_pointlike : bool Option to return a pointlike beam (i.e., one with major=minor=0) if the second beam is larger than the first. Otherwise, a ValueError will be raised Returns ------- new_beam : `Beam` The convolved Beam Raises ------ failure : ValueError If the second beam is larger than the first, the default behavior is to raise an exception. This can be overridden with failure_returns_pointlike """ new_major, new_minor, new_pa = deconvolve_optimized(self.to_header_keywords(), other.to_header_keywords(), failure_returns_pointlike=failure_returns_pointlike) # Keep the units from before new_major = (new_major * u.deg).to(self.major.unit) new_minor = (new_minor * u.deg).to(self.minor.unit) new_pa = (new_pa * u.rad).to(self.pa.unit) return Beam(major=new_major, minor=new_minor, pa=new_pa) def __eq__(self, other): # Catch floating point issues atol_deg = 1e-10 * u.deg this_pa = self.pa.to(u.deg) % (180.0 * u.deg) other_pa = other.pa.to(u.deg) % (180.0 * u.deg) if self.iscircular(): equal_pa = True else: equal_pa = True if np.abs(this_pa - other_pa) < atol_deg else False equal_maj = np.abs(self.major - other.major) < atol_deg equal_min = np.abs(self.minor - other.minor) < atol_deg if equal_maj and equal_min and equal_pa: return True else: return False def __ne__(self, other): return not self.__eq__(other) # Is it astropy convention to access properties through methods? @property def sr(self): return _to_area(self.major,self.minor) @property def major(self): """ Beam FWHM Major Axis """ return self._major @property def minor(self): """ Beam FWHM Minor Axis """ return self._minor @property def pa(self): return self._pa @property def isfinite(self): return ((self.major > 0) & (self.minor > 0) & np.isfinite(self.major) & np.isfinite(self.minor) & np.isfinite(self.pa)) def iscircular(self, rtol=1e-6): frac_diff = (self.major - self.minor).to(u.deg) / self.major.to(u.deg) return frac_diff <= rtol def beam_projected_area(self, distance): """ Return the beam area in pc^2 (or equivalent) given a distance """ return self.sr*(distance**2)/u.sr def jtok_equiv(self, freq): ''' Return conversion function between Jy/beam to K at the specified frequency. The function can be used with the usual astropy.units conversion: >>> beam = Beam.from_fits_header("header.fits") # doctest: +SKIP >>> (1.0*u.Jy).to(u.K, beam.jtok_equiv(1.4*u.GHz)) # doctest: +SKIP Parameters ---------- freq : astropy.units.quantity.Quantity Frequency to calculate conversion. Returns ------- u.brightness_temperature ''' if not isinstance(freq, u.quantity.Quantity): raise TypeError("freq must be a Quantity object. " "Try 'freq*u.Hz' or another equivalent unit.") try: return u.brightness_temperature(beam_area=self.sr, frequency=freq) except TypeError: # old astropy used ordered arguments return u.brightness_temperature(self.sr, freq) def jtok(self, freq, value=1.0*u.Jy): """ Return the conversion for the given value between Jy/beam to K at the specified frequency. Unlike :meth:`jtok_equiv`, the output is the numerical value that converts the units, without any attached unit. Parameters ---------- freq : astropy.units.quantity.Quantity Frequency to calculate conversion. value : astropy.units.quantity.Quantity Value (in Jy or an equivalent unit) to convert to K. Returns ------- value : float Value converted to K. """ return value.to(u.K, self.jtok_equiv(freq)) @property def beamarea_equiv(self): return u.beam_angular_area(self.sr) def commonbeam_with(self, other_beam): ''' Solve for the common beam with a given `~radio_beam.Beam`. This common beam operation is only valid for a set of 2 beams. For the general case, define a set of beams using `~radio_beam.Beams`. Parameters ---------- other_beam : radio_beam.Beam The beam to find the common beam with. ''' from .commonbeam import find_commonbeam_between return find_commonbeam_between(self, other_beam) def ellipse_to_plot(self, xcen, ycen, pixscale, **kwargs): """ Return a matplotlib ellipse for plotting Parameters ---------- xcen : int Center pixel in the x-direction. ycen : int Center pixel in the y-direction. pixscale : `~astropy.units.Quantity` Conversion from degrees to pixels. Returns ------- ~matplotlib.patches.Ellipse Ellipse patch object centered on the given pixel coordinates. """ from matplotlib.patches import Ellipse return Ellipse((xcen, ycen), width=(self.major.to(u.deg) / pixscale).to(u.dimensionless_unscaled).value, height=(self.minor.to(u.deg) / pixscale).to(u.dimensionless_unscaled).value, # PA is 90 deg offset from x-y axes by convention # (it is angle from NCP) angle=(self.pa+90*u.deg).to(u.deg).value, **kwargs) def as_kernel(self, pixscale, **kwargs): """ Returns an elliptical Gaussian kernel of the beam. .. warning:: This method is not aware of any misalignment between pixel and world coordinates. Parameters ---------- pixscale : `~astropy.units.Quantity` Conversion from angular to pixel size. kwargs : passed to EllipticalGaussian2DKernel """ # do something here involving matrices # need to rotate the kernel into the wcs pixel space, kinda... # at the least, need to rescale the kernel axes into pixels stddev_maj = (self.major.to(u.deg)/(pixscale.to(u.deg) * SIGMA_TO_FWHM)).decompose() stddev_min = (self.minor.to(u.deg)/(pixscale.to(u.deg) * SIGMA_TO_FWHM)).decompose() # position angle is defined as CCW from north # "angle" is conventionally defined as CCW from "west". # Therefore, add 90 degrees angle = (90*u.deg+self.pa).to(u.radian).value, return EllipticalGaussian2DKernel(stddev_maj.value, stddev_min.value, angle, **kwargs) def as_tophat_kernel(self, pixscale, **kwargs): ''' Returns an elliptical Tophat kernel of the beam. The area has been scaled to match the 2D Gaussian area: .. math:: \\begin{array}{ll} A_{\\mathrm{Gauss}} = 2\\pi\\sigma_{\\mathrm{Gauss}}^{2} A_{\\mathrm{Tophat}} = \\pi\\sigma_{\\mathrm{Tophat}}^{2} \\sigma_{\\mathrm{Tophat}} = \\sqrt{2}\\sigma_{\\mathrm{Gauss}} \\end{array} .. warning:: This method is not aware of any misalignment between pixel and world coordinates. Parameters ---------- pixscale : float deg -> pixels **kwargs : passed to EllipticalTophat2DKernel ''' # Based on Gaussian to Tophat area conversion # A_gaussian = 2 * pi * sigma^2 / (sqrt(8*log(2))^2 # A_tophat = pi * r^2 # pi r^2 = 2 * pi * sigma^2 / (sqrt(8*log(2))^2 # r = sqrt(2)/sqrt(8*log(2)) * sigma gauss_to_top = np.sqrt(2) maj_eff = gauss_to_top * self.major.to(u.deg) / \ (pixscale * SIGMA_TO_FWHM) min_eff = gauss_to_top * self.minor.to(u.deg) / \ (pixscale * SIGMA_TO_FWHM) # position angle is defined as CCW from north # "angle" is conventionally defined as CCW from "west". # Therefore, add 90 degrees angle = (90*u.deg+self.pa).to(u.radian).value, return EllipticalTophat2DKernel(maj_eff.value, min_eff.value, angle, **kwargs) def to_header_keywords(self): return {'BMAJ': self.major.to(u.deg).value, 'BMIN': self.minor.to(u.deg).value, 'BPA': self.pa.to(u.deg).value, } # Beam.__doc__ = Beam.__doc__ + Beam.__new__.__doc__ def mywcs_to_platescale(mywcs): pix_area = wcs.utils.proj_plane_pixel_area(mywcs) return pix_area**0.5 class EllipticalGaussian2DKernel(Kernel2D): """ 2D Elliptical Gaussian filter kernel. The Gaussian filter is a filter with great smoothing properties. It is isotropic and does not produce artifacts. Parameters ---------- stddev_maj : float Standard deviation of the Gaussian kernel in direction 1 stddev_min : float Standard deviation of the Gaussian kernel in direction 1 position_angle : float Position angle of the elliptical gaussian x_size : odd int, optional Size in x direction of the kernel array. Default = support_scaling * stddev. y_size : odd int, optional Size in y direction of the kernel array. Default = support_scaling * stddev. support_scaling : int The amount to scale the stddev to determine the size of the kernel mode : str, optional One of the following discretization modes: * 'center' (default) Discretize model by taking the value at the center of the bin. * 'linear_interp' Discretize model by performing a bilinear interpolation between the values at the corners of the bin. * 'oversample' Discretize model by taking the average on an oversampled grid. * 'integrate' Discretize model by integrating the model over the bin. factor : number, optional Factor of oversampling. Default factor = 10. See Also -------- Box2DKernel, Tophat2DKernel, MexicanHat2DKernel, Ring2DKernel, TrapezoidDisk2DKernel, AiryDisk2DKernel, Gaussian2DKernel, EllipticalTophat2DKernel Examples -------- Kernel response: .. plot:: :include-source: import matplotlib.pyplot as plt from radio_beam import EllipticalGaussian2DKernel gaussian_2D_kernel = EllipticalGaussian2DKernel(10, 5, np.pi/4) plt.imshow(gaussian_2D_kernel, interpolation='none', origin='lower') plt.xlabel('x [pixels]') plt.ylabel('y [pixels]') plt.colorbar() plt.show() """ _separable = True _is_bool = False def __init__(self, stddev_maj, stddev_min, position_angle, support_scaling=8, **kwargs): self._model = Gaussian2D(1. / (2 * np.pi * stddev_maj * stddev_min), 0, 0, x_stddev=stddev_maj, y_stddev=stddev_min, theta=position_angle) try: from astropy.modeling.utils import ellipse_extent except ImportError: raise NotImplementedError("EllipticalGaussian2DKernel requires" " astropy 1.1b1 or greater.") max_extent = \ np.max(ellipse_extent(stddev_maj, stddev_min, position_angle)) self._default_size = \ _round_up_to_odd_integer(support_scaling * 2 * max_extent) super(EllipticalGaussian2DKernel, self).__init__(**kwargs) self._truncation = np.abs(1. - 1 / self._array.sum()) class EllipticalTophat2DKernel(Kernel2D): """ 2D Elliptical Tophat filter kernel. The Tophat filter can produce artifacts when applied repeatedly on the same data. Parameters ---------- stddev_maj : float Standard deviation of the Gaussian kernel in direction 1 stddev_min : float Standard deviation of the Gaussian kernel in direction 1 position_angle : float Position angle of the elliptical gaussian x_size : odd int, optional Size in x direction of the kernel array. Default = support_scaling * stddev. y_size : odd int, optional Size in y direction of the kernel array. Default = support_scaling * stddev. support_scaling : int The amount to scale the stddev to determine the size of the kernel mode : str, optional One of the following discretization modes: * 'center' (default) Discretize model by taking the value at the center of the bin. * 'linear_interp' Discretize model by performing a bilinear interpolation between the values at the corners of the bin. * 'oversample' Discretize model by taking the average on an oversampled grid. * 'integrate' Discretize model by integrating the model over the bin. factor : number, optional Factor of oversampling. Default factor = 10. See Also -------- Box2DKernel, Tophat2DKernel, MexicanHat2DKernel, Ring2DKernel, TrapezoidDisk2DKernel, AiryDisk2DKernel, Gaussian2DKernel, EllipticalGaussian2DKernel Examples -------- Kernel response: .. plot:: :include-source: import matplotlib.pyplot as plt from radio_beam import EllipticalTophat2DKernel tophat_2D_kernel = EllipticalTophat2DKernel(10, 5, np.pi/4) plt.imshow(tophat_2D_kernel, interpolation='none', origin='lower') plt.xlabel('x [pixels]') plt.ylabel('y [pixels]') plt.colorbar() plt.show() """ _is_bool = True def __init__(self, stddev_maj, stddev_min, position_angle, support_scaling=1, **kwargs): self._model = Ellipse2D(1. / (np.pi * stddev_maj * stddev_min), 0, 0, stddev_maj, stddev_min, position_angle) try: from astropy.modeling.utils import ellipse_extent except ImportError: raise NotImplementedError("EllipticalTophat2DKernel requires" " astropy 1.1b1 or greater.") max_extent = \ np.max(ellipse_extent(stddev_maj, stddev_min, position_angle)) self._default_size = \ _round_up_to_odd_integer(support_scaling * 2 * max_extent) super(EllipticalTophat2DKernel, self).__init__(**kwargs) self._truncation = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/commonbeam.py0000644000175100001770000005154014704473421017775 0ustar00runnerdocker import numpy as np import astropy.units as u try: from scipy import optimize as opt from scipy.spatial import ConvexHull HAS_SCIPY = True except ImportError: HAS_SCIPY = False from .beam import Beam from .utils import BeamError, transform_ellipse, deconvolve_optimized __all__ = ['commonbeam', 'common_2beams', 'getMinVolEllipse', 'common_manybeams_mve', 'find_commonbeam_between'] def commonbeam(beams, method='pts', **method_kwargs): ''' Use analytic method if there are only two beams. Otherwise use constrained optimization to find the common beam. ''' if beams.size == 1: return beams[0] elif fits_in_largest(beams): return beams.largest_beam() else: if beams.size == 2: try: return common_2beams(beams) # Sometimes this method can fail. Use the many beam solution in # this case except (ValueError, BeamError): pass if method == 'pts': return common_manybeams_mve(beams, **method_kwargs) elif method == 'opt': return common_manybeams_opt(beams, **method_kwargs) else: raise ValueError("method must be 'pts' or 'opt'.") def common_2beams(beams, check_deconvolution=True): ''' Find a common beam from a `Beams` object with 2 beams. This function is based on the CASA implementation `ia.commonbeam`. Note that the solution is only valid for 2 beams. Parameters ---------- beams : `~radio_beam.Beams` Beams object with 2 beams. Returns ------- common_beam : `~radio_beam.Beam` The smallest common beam in the set of beams. ''' if beams.size != 2: raise BeamError("This method is only valid for two beams.") if (~beams.isfinite).all(): raise BeamError("All beams in the object are invalid.") return find_commonbeam_between(beams[0], beams[1], check_deconvolution=check_deconvolution) def find_commonbeam_between(beam1, beam2, check_deconvolution=True): ''' Find the common beam between 2 `~radio_beam.Beam` objects. This function is based on the CASA implementation `ia.commonbeam` that the solution is valid when comparing 2 beams. Parameters ---------- beam1 : `~radio_beam.Beam` Beam object. beam2 : `~radio_beam.Beam` Beam object. check_deconvolution : bool, optional Check that the common beam solution can be deconvolved from both input beams. Returns ------- common_beam : `~radio_beam.Beam` The smallest common beam in the set of beams. ''' # This code is based on the implementation in CASA: # https://open-bitbucket.nrao.edu/projects/CASA/repos/casa/browse/code/imageanalysis/ImageAnalysis/CasaImageBeamSet.cc if not beam1.isfinite or not beam2.isfinite: raise BeamError("At least one beam is invalid.") # If equal, the common beam is itself. if beam1 == beam2: return beam1 if beam1 > beam2: large_beam = beam1 small_beam = beam2 else: large_beam = beam2 small_beam = beam1 large_major = large_beam.major.to(u.arcsec) large_minor = large_beam.minor.to(u.arcsec) small_major = small_beam.major.to(u.arcsec) small_minor = small_beam.minor.to(u.arcsec) deconv_beam = large_beam.deconvolve(small_beam, failure_returns_pointlike=True) # Larger beam can be deconvolved. It is already the smallest common beam if deconv_beam.isfinite: return large_beam # If the smaller beam is a circle, the minor axis is the circle radius if small_beam.iscircular(): common_beam = Beam(large_beam.major, small_beam.major, large_beam.pa) return common_beam # Wrap angle about 0 to pi. pa_diff = ((small_beam.pa.to(u.rad).value - large_beam.pa.to(u.rad).value + np.pi / 2. + np.pi) % np.pi - np.pi / 2.) * u.rad # If the difference is pi / 2, the larger major is set to the # new major and the minor is the other major. if np.isclose(np.abs(pa_diff).value, np.pi / 2.): larger_major = large_beam.major >= small_beam.major major = large_major if larger_major else small_major minor = small_major if larger_major else small_major pa = large_beam.pa if larger_major else small_beam.pa conv_beam = Beam(major=major, minor=minor, pa=pa) return conv_beam else: # Transform to coordinates where large_beam is circular major_comb = np.sqrt(large_major * small_major) p = major_comb / large_major q = major_comb / large_minor # Transform beam into the same coordinates, and rotate so its # major axis is along the x axis. trans_major_sc, trans_minor_sc, trans_pa_sc = \ transform_ellipse(small_major, small_minor, pa_diff, p, q) # The transformed minor axis is major_comb, as defined in CASA trans_minor_sc = major_comb # Return beam to the original coordinates, still rotated with # the major along the x axis trans_major_unsc, trans_minor_unsc, trans_pa_unsc = \ transform_ellipse(trans_major_sc, trans_minor_sc, trans_pa_sc, 1 / p, 1 / q) # Lastly, rotate the PA to the enclosing ellipse trans_major = trans_major_unsc.to(u.arcsec) trans_minor = trans_minor_unsc.to(u.arcsec) trans_pa = trans_pa_unsc + large_beam.pa # The minor axis becomes an issue when checking against the smaller # beam from deconvolution. Adding a tiny fraction makes the deconvolved # beam JUST larger than zero (~1e-7). epsilon = 100 * np.finfo(trans_major.dtype).eps * trans_major.unit trans_beam = Beam(major=trans_major + epsilon, minor=trans_minor + epsilon, pa=trans_pa) if check_deconvolution: # Ensure this beam can now be deconvolved deconv_large_beam = \ trans_beam.deconvolve(large_beam, failure_returns_pointlike=True) deconv_prob_beam = \ trans_beam.deconvolve(small_beam, failure_returns_pointlike=True) if not deconv_large_beam.isfinite or not deconv_prob_beam.isfinite: raise BeamError("Failed to find common beam that both beams can " "be deconvolved by.") # Taken from CASA implementation, but by adding epsilon, this shouldn't # be needed # Scale the enclosing ellipse by a small factor until it does. # can_deconv = False # num = 0 # while not can_deconv: # deconv_large_beam = \ # trans_beam.deconvolve(large_beam, # failure_returns_pointlike=True) # deconv_prob_beam = \ # trans_beam.deconvolve(small_beam, # failure_returns_pointlike=True) # if (not deconv_large_beam.isfinite or not deconv_prob_beam.isfinite): # scale_factor = 1.001 # trans_beam = Beam(major=trans_major * scale_factor, # minor=trans_minor * scale_factor, # pa=trans_pa) # else: # can_deconv = True # if num == 10: # break # num += 1 common_beam = trans_beam return common_beam def boundingcircle(bmaj, bmin, bpa): thisone = np.argmax(bmaj) # PA really shouldn't matter here. But the minimization performed better # in some cases with a non-zero PA. Presumably this is b/c the PA of the # common beam is affected more by the beam with the largest major axis. return bmaj[thisone], bmaj[thisone], bpa[thisone] def PtoA(bmaj, bmin, bpa): ''' Express the ellipse parameters into `center-form `_. ''' A = np.zeros((2, 2)) A[0, 0] = np.cos(bpa)**2 / bmaj**2 + np.sin(bpa)**2 / bmin**2 A[1, 0] = np.cos(bpa) * np.sin(bpa) * (1 / bmaj**2 - 1 / bmin**2) A[0, 1] = A[1, 0] A[1, 1] = np.sin(bpa)**2 / bmaj**2 + np.cos(bpa)**2 / bmin**2 return A def BinsideA(B, A): try: np.linalg.cholesky(B - A) return True except np.linalg.LinAlgError: return False def myobjective_regularized(p, bmajvec, bminvec, bpavec): # Force bmaj > bmin if p[0] < p[1]: return 1e30 # We can safely assume the common major axis is at most the # largest major axis in the set if (p[0] <= bmajvec).any(): return 1e30 A = PtoA(*p) test = np.zeros_like(bmajvec) for idx, (bmx, bmn, bp) in enumerate(zip(bmajvec, bminvec, bpavec)): test[idx] = BinsideA(PtoA(bmx, bmn, bp), A) obj = 1 / np.linalg.det(A) if np.all(test): return obj else: return obj * 1e30 def common_manybeams_opt(beams, p0=None, opt_method='Nelder-Mead', optdict={'maxiter': 5000, 'ftol': 1e-14, 'maxfev': 5000}, verbose=False, brute=False, brute_steps=40): ''' Optimize the common beam solution by maximizing the determinant of the common beam. ..note:: This method is experimental and requires further testing. Parameters ---------- beams : `~radio_beam.Beams` Beams object. p0 : tuple, optional Initial guess parameters (`major, minor, pa`). opt_method : str, optional Optimization method to use. See `~scipy.optimize.minimize`. The default of Nelder-Mead is the only method we have had some success with. optdict : dict, optional Dictionary parameters passed to `~scipy.optimize.minimize`. verbose : bool, optional Print the full output from `~scipy.optimize.minimize`. brute : bool, optional Use `~scipy.optimize.brute` to find the optimal solution. brute_steps : int, optional Number of positions to sample in each parameter (3). Returns ------- com_beam : `~radio_beam.Beam` Common beam. ''' raise NotImplementedError("This method is not fully tested. Remove this " "line for testing purposes.") if not HAS_SCIPY: raise ImportError("common_manybeams_opt requires scipy.optimize.") bmaj = beams.major.value bmin = beams.minor.value bpa = beams.pa.to(u.rad).value if p0 is None: p0 = boundingcircle(bmaj, bmin, bpa) # It seems to help to make the initial guess slightly larger p0 = (1.1 * p0[0], 1.1 * p0[1], p0[2]) if brute: maj_range = [beams.major.max(), 1.5 * beams.major.max()] maj_step = (maj_range[1] - maj_range[0]) / brute_steps min_range = [beams.minor.min(), 1.5 * beams.major.max()] min_step = (min_range[1] - min_range[0]) / brute_steps rranges = (slice(maj_range[0], maj_range[1], maj_step), slice(min_range[0], min_range[1], min_step), slice(0, 179.9, 180. / brute_steps)) result = opt.brute(myobjective_regularized, rranges, args=(bmaj, bmin, bpa), full_output=True, finish=opt.fmin) params = result[0] else: result = opt.minimize(myobjective_regularized, p0, method=opt_method, args=(bmaj, bmin, bpa), options=optdict, tol=1e-14) params = result.x if verbose: print(result.viewitems()) if not result.success: raise Warning("Optimization failed") com_beam = Beam(params[0] * beams.major.unit, params[1] * beams.major.unit, (params[2] % np.pi) * u.rad) # Test if it deconvolves all if not fits_in_largest(beams, com_beam): raise BeamError("Could not find common beam to deconvolve all beams.") return com_beam def fits_in_largest(beams, large_beam=None): ''' Test if all beams can be deconvolved by the largest beam ''' if large_beam is None: large_beam = beams.largest_beam() large_hdr_keywords = large_beam.to_header_keywords() majors = beams.major.to(u.deg).value minors = beams.minor.to(u.deg).value pas = beams.pa.to(u.deg).value # Catch differences below << 1 microarsec = 2.8e10 # This is the same limit used for checking equal beams in Beam.__eq__ atol_limit = 1e-12 for major, minor, pa in zip(majors, minors, pas): equal = abs(large_hdr_keywords['BMAJ'] - major) < atol_limit equal = equal and (abs(large_hdr_keywords['BMIN'] - minor) < atol_limit) # Check if the beam is circular # This checks for fractional changes below 1e-6 between the major and minor. # Same limit used in Beam.__eq__ iscircular = (major - minor) / minor < 1e-6 # position angle only matters if the beam is asymmetric if not iscircular: equal = equal and (abs(((large_hdr_keywords['BPA'] % np.pi) - (pa % np.pi))) < atol_limit) if equal: continue out = deconvolve_optimized(large_hdr_keywords, {'BMAJ': major, 'BMIN': minor, 'BPA': pa}, failure_returns_pointlike=True) if np.any([ax == 0. for ax in out[:2]]): return False return True def getMinVolEllipse(P, tolerance=1e-5, maxiter=1e5): """ Use the Khachiyan Algorithm to compute that minimum volume ellipsoid. For the purposes of finding a common beam, there is an added check that requires the center to be within the tolerance range. Adapted code from: https://github.com/minillinim/ellipsoid/blob/master/ellipsoid.py That implementation relies on the original work by Nima Moshtagh: http://www.mathworks.com/matlabcentral/fileexchange/9542 and an alternate python version from: http://cctbx.sourceforge.net/current/python/scitbx.math.minimum_covering_ellipsoid.html Parameters ---------- P : `~numpy.ndarray` Points to compute solution. tolerance : float, optional Allowed error range in the Khachiyan Algorithm. Decreasing the tolerance by an order of magnitude requires an order of magnitude more iterations to converge. maxiter : int, optional Maximum iterations. Returns ------- center : `~numpy.ndarray` Center point of the ellipse. Is required to be smaller than the tolerance. radii : `~numpy.ndarray` Radii of the ellipse. rotation : `~numpy.ndarray` Rotation matrix of the ellipse. """ N, d = P.shape d = float(d) # Q will be our working array Q = np.vstack([np.copy(P.T), np.ones(N)]) QT = Q.T # initializations err = 1.0 u = np.ones(N) / N # Khachiyan Algorithm i = 0 while err > tolerance: V = np.dot(Q, np.dot(np.diag(u), QT)) # M the diagonal vector of an NxN matrix M = np.diag(np.dot(QT, np.dot(np.linalg.inv(V), Q))) j = np.argmax(M) maximum = M[j] step_size = (maximum - d - 1.0) / ((d + 1.0) * (maximum - 1.0)) new_u = (1.0 - step_size) * u err = np.linalg.norm(new_u - u) if err <= tolerance: break new_u[j] += step_size u = new_u i += 1 if i == maxiter: raise ValueError("Reached maximum iterations without converging." " Try increasing the tolerance.") # center of the ellipse center = np.atleast_2d(np.dot(P.T, u)) # For our purposes, the centre should always be very small center_square = np.outer(center, center) if not (center_square < tolerance**2).any(): raise ValueError("The solved centre ({0}) is larger than the tolerance" " ({1}). Check the input data.".format(center, tolerance)) # the A matrix for the ellipse A = np.linalg.inv(np.dot(P.T, np.dot(np.diag(u), P)) - center_square) / d # ellip_vals = np.dot(P - center, np.dot(A, (P - center).T)) # assert (ellip_vals <= 1. + tolerance).all() # Get the values we'd like to return U, s, rotation = np.linalg.svd(A) radii = 1.0 / np.sqrt(s) radii *= 1. + tolerance return center, radii, rotation def ellipse_edges(beam, npts=300, epsilon=1e-3): ''' Return the edge points of the beam. Parameters ---------- beam : `~radio_beam.Beam` Beam object. npts : int, optional Number of samples. epsilon : float Increase the radii of the ellipse by 1 + epsilon. This is to ensure that `getMinVolEllipse` returns a marginally deconvolvable beam to within the error tolerance. Returns ------- pts : `~numpy.ndarray` The x, y coordinates of the ellipse edge. ''' bpa = beam.pa.to(u.rad).value major = beam.major.to(u.deg).value * (1. + epsilon) minor = beam.minor.to(u.deg).value * (1. + epsilon) phi = np.linspace(0, 2 * np.pi, npts) x = major * np.cos(phi) y = minor * np.sin(phi) xr = x * np.cos(bpa) - y * np.sin(bpa) yr = x * np.sin(bpa) + y * np.cos(bpa) pts = np.vstack([xr, yr]) return pts def common_manybeams_mve(beams, tolerance=1e-4, nsamps=200, epsilon=5e-4, auto_increase_epsilon=True, max_epsilon=1e-3, max_iter=10): ''' Calculate a common beam size using the Khachiyan Algorithm to find the minimum enclosing ellipse from all beam edges. Parameters ---------- beams : `~radio_beam.Beams` Beams object. tolerance : float, optional Allowed error range in the Khachiyan Algorithm. Decreasing the tolerance by an order of magnitude requires an order of magnitude more iterations to converge. nsamps : int, optional Number of edge points to sample from each beam. epsilon : float, optional Increase the radii of each beam by a factor of 1 + epsilon to ensure the common beam can marginally be deconvolved for all beams. Small deviations result from the finite sampling of points and the choice of the tolerance. auto_increase_epsilon : bool, optional Re-run the algorithm when the solution cannot quite be deconvolved from from all the beams. When `True`, epsilon is slightly increased with each iteration until the common beam can be deconvolved from all beams. Default is `True`. max_epsilon : float, optional Maximum epsilon value that is acceptable. Reached with `max_iter`. Default is `1e-3`. max_iter : int, optional Maximum number of times to increase epsilon to try finding a valid common beam solution. Returns ------- com_beam : `~radio_beam.Beam` The common beam for all beams in the set. ''' if not HAS_SCIPY: raise ImportError("common_manybeams_mve requires scipy.optimize.") step = 1 while True: pts = [] for beam in beams: pts.append(ellipse_edges(beam, nsamps, epsilon=epsilon)) all_pts = np.hstack(pts).T # Now find the outer edges of the convex hull. hull = ConvexHull(all_pts) edge_pts = all_pts[hull.vertices] center, radii, rotation = \ getMinVolEllipse(edge_pts, tolerance=tolerance) # The rotation matrix is coming out as: # ((sin theta, cos theta) # (cos theta, - sin theta)) pa = np.arctan2(- rotation[0, 0], rotation[1, 0]) * u.rad if pa.value == -np.pi or pa.value == np.pi: pa = 0.0 * u.rad com_beam = Beam(major=radii.max() * u.deg, minor=radii.min() * u.deg, pa=pa) # If common beam is just slightly smaller than one of the beams, # we increase epsilon to encourage a solution marginally larger # so all beams can be convolved. if auto_increase_epsilon: if not fits_in_largest(beams, com_beam): # Increase epsilon and run again epsilon += (step + 1) * (max_epsilon - epsilon) / max_iter step += 1 if step == max_iter + 1: raise BeamError("Could not increase epsilon to find" " common beam.") continue else: break else: break if not fits_in_largest(beams, com_beam): raise BeamError("Could not find common beam to deconvolve all beams.") return com_beam ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/conftest.py0000644000175100001770000000151514704473421017502 0ustar00runnerdocker# this contains imports plugins that configure py.test for astropy tests. # by importing them here in conftest.py they are discoverable by py.test # no matter how it is invoked within the source tree. import os from setuptools._distutils.version import LooseVersion # Import casatools and casatasks here if available as they can otherwise # cause a segfault if imported later on during tests. try: import casatools import casatasks except ImportError: pass from astropy.version import version as astropy_version if astropy_version < '3.0': from astropy.tests.pytest_plugins import * del pytest_report_header else: from pytest_astropy_header.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS def pytest_configure(config): config.option.astropy_header = True PYTEST_HEADER_MODULES['Astropy'] = 'astropy' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/multiple_beams.py0000644000175100001770000004010414704473421020654 0ustar00runnerdocker from astropy import units as u from astropy.io import fits from astropy import constants from astropy import wcs import numpy as np import warnings from .beam import Beam, _to_area, SIGMA_TO_FWHM, _with_default_unit from .commonbeam import commonbeam from .utils import InvalidBeamOperationError class Beams(u.Quantity): """ An object to handle a set of radio beams for a data cube. """ def __new__(cls, major=None, minor=None, pa=None, areas=None, default_unit=u.arcsec, meta=None, beams=None): """ Create a new set of Gaussian beams Parameters ---------- major : :class:`~astropy.units.Quantity` with angular equivalency The FWHM major axes minor : :class:`~astropy.units.Quantity` with angular equivalency The FWHM minor axes pa : :class:`~astropy.units.Quantity` with angular equivalency The beam position angles areas : :class:`~astropy.units.Quantity` with steradian equivalency The area of the beams. This is an alternative to specifying the major/minor/PA, and will create those values assuming a circular Gaussian beam. default_unit : :class:`~astropy.units.Unit` The unit to impose on major, minor if they are specified as floats meta : dict, optional A dictionary of metadata to include in the header. beams : List of :class:`~radio_beam.Beam` objects List of individual `Beam` objects. The resulting `Beams` object will have major and minor axes in degrees. """ # improve to some kwargs magic later # error checking if beams is not None: major = [beam.major.to(u.deg).value for beam in beams] * u.deg minor = [beam.minor.to(u.deg).value for beam in beams] * u.deg pa = [beam.pa.to(u.deg).value for beam in beams] * u.deg # ... given an area make a round beam assuming it is Gaussian if areas is not None: rad = np.sqrt(areas / (2 * np.pi)) * u.deg major = rad * SIGMA_TO_FWHM minor = rad * SIGMA_TO_FWHM pa = np.zeros_like(areas) * u.deg # give specified values priority if major is not None: major = _with_default_unit("major", major, default_unit) if pa is not None: if len(pa) != len(major): raise ValueError("Number of position angles must match number of major axis lengths") pa = _with_default_unit("pa", pa, u.deg) else: pa = np.zeros(major.shape) * u.deg # some sensible defaults if minor is None: minor = major elif len(minor) != len(major): raise ValueError("Minor and major axes must have same number of values") else: minor = _with_default_unit("minor", minor, default_unit) if np.any(minor > major): raise ValueError("Minor axis greater than major axis.") self = super(Beams, cls).__new__(cls, value=_to_area(major, minor).value, unit=u.sr) self.major = major self.minor = minor self.pa = pa self.default_unit = default_unit if meta is None: self.meta = [{}]*len(self) else: self.meta = meta return self @property def meta(self): return self._meta @meta.setter def meta(self, value): if len(value) == len(self): self._meta = value else: raise TypeError("metadata must be a list of dictionaries") def __len__(self): return len(self.major) @property def isfinite(self): return ((self.major > 0) & (self.minor > 0) & np.isfinite(self.major) & np.isfinite(self.minor) & np.isfinite(self.pa)) def __getslice__(self, start, stop, increment=None): return self.__getitem__(slice(start, stop, increment)) def __getitem__(self, view): if isinstance(view, (int, np.int64)): return Beam(major=self.major[view], minor=self.minor[view], pa=self.pa[view], meta=self.meta[view]) elif isinstance(view, slice): return Beams(major=self.major[view], minor=self.minor[view], pa=self.pa[view], meta=self.meta[view]) elif isinstance(view, np.ndarray): if view.dtype.name != 'bool': raise ValueError("If using an array to index beams, it must " "be a boolean array.") return Beams(major=self.major[view], minor=self.minor[view], pa=self.pa[view], meta=[x for ii,x in zip(view, self.meta) if ii]) else: raise ValueError("Invalid slice") def __array_finalize__(self, obj): # If our unit is not set and obj has a valid one, use it. if self._unit is None: unit = getattr(obj, '_unit', None) if unit is not None: self._set_unit(unit) if isinstance(obj, Beams): # Multiplication and division should change the area, # but not the PA or major/minor ratio self.major = obj.major self.minor = obj.minor self.pa = obj.pa self.meta = obj.meta # Copy info if the original had `info` defined. Because of the way the # DataInfo works, `'info' in obj.__dict__` is False until the # `info` attribute is accessed or set. Note that `obj` can be an # ndarray which doesn't have a `__dict__`. if 'info' in getattr(obj, '__dict__', ()): self.info = obj.info @property def sr(self): return _to_area(self.major, self.minor) @classmethod def from_fits_bintable(cls, bintable): """ Instantiate a Beams list from a bintable HDU from a CASA-produced image HDU. Parameters ---------- bintable : fits.BinTableHDU The table data containing the beam information Returns ------- beams : Beams A new Beams object """ header = bintable.header # Read the bmaj/bmin units from the header # (we still assume BPA is degrees because we've never seen an exceptional case) # this will crash if there is no appropriate header info maj_kw = [kw for kw, val in header.items() if val == 'BMAJ'][0] min_kw = [kw for kw, val in header.items() if val == 'BMIN'][0] maj_unit = header[maj_kw.replace('TTYPE', 'TUNIT')] min_unit = header[min_kw.replace('TTYPE', 'TUNIT')] # AIPS uses non-FITS-standard unit names; this catches the # only case we've seen so far if maj_unit == 'DEGREES': maj_unit = 'degree' if min_unit == 'DEGREES': min_unit = 'degree' maj_unit = u.Unit(maj_unit) min_unit = u.Unit(min_unit) major = u.Quantity(bintable.data['BMAJ'], maj_unit) minor = u.Quantity(bintable.data['BMIN'], min_unit) pa = u.Quantity(bintable.data['BPA'], u.deg) meta = [{key: row[key] for key in bintable.columns.names if key not in ('BMAJ', 'BPA', 'BMIN')} for row in bintable.data] return cls(major=major, minor=minor, pa=pa, meta=meta) @classmethod def from_casa_image(cls, imagename): ''' Instantiate beams from a CASA image. Cannot currently handle beams for different polarizations. ** Must be run in a CASA environment! ** Parameters ---------- imagename : str Name of CASA image. ''' try: from casatools import image as iatool ia = iatool() except ImportError: raise ImportError("Could not import CASA and therefore" " cannot read CASA .image files") ia.open(imagename) beam_props = ia.restoringbeam() ia.close() nchans = beam_props['nChannels'] # Assuming there is always a 0th channel... maj_unit = u.Unit(beam_props['beams']['*0']['*0']['major']['unit']) min_unit = u.Unit(beam_props['beams']['*0']['*0']['minor']['unit']) pa_unit = u.Unit(beam_props['beams']['*0']['*0']['positionangle']['unit']) major = np.empty((nchans)) * maj_unit minor = np.empty((nchans)) * min_unit pa = np.empty((nchans)) * pa_unit for chan in range(nchans): chan_name = '*{}'.format(chan) chanbeam_props = beam_props['beams'][chan_name]['*0'] # Can CASA have mixes of units between channels? Let's test just # in case assert maj_unit == u.Unit(chanbeam_props['major']['unit']) assert min_unit == u.Unit(chanbeam_props['minor']['unit']) assert pa_unit == u.Unit(chanbeam_props['positionangle']['unit']) major[chan] = chanbeam_props['major']['value'] * maj_unit minor[chan] = chanbeam_props['minor']['value'] * min_unit pa[chan] = chanbeam_props['positionangle']['value'] * pa_unit return cls(major=major, minor=minor, pa=pa) def average_beam(self, includemask=None, raise_for_nan=True): """ Average the beam major, minor, and PA attributes. This is usually a dumb thing to do! """ warnings.warn("Do not use the average beam for convolution! Use the" " smallest common beam from `Beams.common_beam()`.") from astropy.stats import circmean if includemask is None: includemask = self.isfinite else: includemask = np.logical_and(includemask, self.isfinite) new_beam = Beam(major=self.major[includemask].mean(), minor=self.minor[includemask].mean(), pa=circmean(self.pa[includemask], weights=(self.major / self.minor)[includemask])) if raise_for_nan and np.any(np.isnan(new_beam)): raise ValueError("NaNs after averaging. This is a bug.") return new_beam def largest_beam(self, includemask=None): """ Returns the largest beam (by area) in a list of beams. """ if includemask is None: includemask = self.isfinite else: includemask = np.logical_and(includemask, self.isfinite) largest_idx = (self.major * self.minor)[includemask].argmax() new_beam = Beam(major=self.major[includemask][largest_idx], minor=self.minor[includemask][largest_idx], pa=self.pa[includemask][largest_idx]) return new_beam def smallest_beam(self, includemask=None): """ Returns the smallest beam (by area) in a list of beams. """ if includemask is None: includemask = self.isfinite else: includemask = np.logical_and(includemask, self.isfinite) largest_idx = (self.major * self.minor)[includemask].argmin() new_beam = Beam(major=self.major[includemask][largest_idx], minor=self.minor[includemask][largest_idx], pa=self.pa[includemask][largest_idx]) return new_beam def extrema_beams(self, includemask=None): return [self.smallest_beam(includemask), self.largest_beam(includemask)] def common_beam(self, includemask=None, method='pts', **kwargs): ''' Return the smallest common beam size. For set of two beams, the solution is solved analytically. All larger sets solve for the minimum volume ellipse using the `Khachiyan Algorithm `_, where the convex hull of the set of ellipse edges is used to find the boundaries of the set. Since the minimum ellipse method is approximate, some solutions for the common beam will be slightly underestimated and the solution cannot be deconvolved from the whole set of beams. To overcome this issue, a small `epsilon` correction factor is added to the ellipse edges to encourage a valid common beam solution. Since `epsilon` is added to all sides, this correction will at most increase the common beam size by :math:`2\times(1+\epsilon)`. The default values of `epsilon` is :math:`5\times10^{-4}`, so this will have a very small effect on the size of the common beam. In some cases, `epsilon` must be increased to find a valid common beam solution. The algorithm does this by default (set by `auto_increase_epsilon=True`), and will incrementally increase `epsilon` until the common beam can be deconvolved from all beams, or until either(1) `max_iter` is reached (default is 10) or (2) `max_epsilon` is reached (default is 1e-3). In practice, we find these settings work well for different ALMA or VLA data, but these `kwargs` may need to be changed for discrepant cases. Parameters ---------- includemask : `~numpy.ndarray`, optional Boolean mask. method : {'pts'}, optional Many beam method. Only `pts` is currently available. kwargs : Passed to `~radio_beam.commonbeam`. ''' return commonbeam(self if includemask is None else self[includemask], method=method, **kwargs) def __iter__(self): for i in range(len(self)): yield self[i] def __mul__(self, other): # Other must be a single beam. Assume multiplying is convolving # as set of beams with a given beam if not isinstance(other, Beam): raise InvalidBeamOperationError("Multiplication is defined as a " "convolution of the set of beams " "with a given beam. Must be " "multiplied with a Beam object.") return Beams(beams=[beam * other for beam in self]) def __truediv__(self, other): # Other must be a single beam. Assume dividing is deconvolving # as set of beams with a given beam if not isinstance(other, Beam): raise InvalidBeamOperationError("Division is defined as a " "deconvolution of the set of beams" " with a given beam. Must be " "divided by a Beam object.") return Beams(beams=[beam / other for beam in self]) def __add__(self, other): raise InvalidBeamOperationError("Addition of a set of Beams " "is not defined.") def __sub__(self, other): raise InvalidBeamOperationError("Addition of a set of Beams " "is not defined.") def __eq__(self, other): # other should be a single beam, or a another Beams object if isinstance(other, Beam): return np.array([beam == other for beam in self]) elif isinstance(other, Beams): # These should have the same size. if not self.size == other.size: raise InvalidBeamOperationError("Beams objects must have the " "same shape to test " "equality.") return np.all([beam == other_beam for beam, other_beam in zip(self, other)]) else: raise InvalidBeamOperationError("Must test equality with a Beam" " or Beams object.") def __ne__(self, other): eq_out = self.__eq__(other) # If other is a Beam, will get array back if isinstance(eq_out, np.ndarray): return ~eq_out # If other is a Beams, will get boolean back else: return not eq_out ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9271333 radio_beam-0.3.8/radio_beam/tests/0000755000175100001770000000000014704473431016444 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/__init__.py0000644000175100001770000000017114704473421020553 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This packages contains affiliated package tests. """ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9311333 radio_beam-0.3.8/radio_beam/tests/data/0000755000175100001770000000000014704473431017355 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/data/NGC0925.bima.mmom0.fits.gz0000644000175100001770000000565314704473421023555 0ustar00runnerdocker‹7ºÌVNGC0925.bima.mmom0.fitsíks¢LÇ¿J¿KfÇKCƒ\ž‡­BC3ÞVIvò’(1Ì X€I|>ýv£à 庻µ›î©Š¨øóôésο˜tû£ž€šš`ê:¯®·ðAà‚Û®>~`83Û%}€vWužá‰˜gxž±3#0@°^šÉ”]¨?»p†1ÏY-^L¸¯ÙZ˜Žo¹ŽŸl_Oü8ë3óµa{2V#j´D"IÏißa1ÞÖ‰yˆ$FB<a1ž®õ´Ig8RÀÕ½ªwÆšöã*?e×:7ZOg6öÕÙ†6¶§Õ!—ŸÜãpÙòÓX5Ü_¶ïIí1ÕGGi!ïj¬ÖëõIwPÞìÖ¾ªüÇÆþk Uø­.þBÿ…¼+×ùÅöA¸ß_¾ ÿPì¿C,迈Ç78±¬}Ä!ïêIë ë½É¸¼ÿ¸*û‹ýW)û¯Jñ_È»šèÃÖ’Rþ»Qu­Ž‹4æ1’$Õ!ªCIg‘ŒÌˆ ”>Ö&úíXûǶ¿ Ï »í/S$ß´^oÒQ{ľæmøR©þ¶ûêyŒê‹Ô’$$0ˆp½BùÉí~w°Çã9N%Vâ¦ïq¢uîÕAÄc9I¬†//íÇAWí»zxn¶5µßøÑoN ûð Ç qjA‡¼Ä bö ÛZGíÜu ÄòåÆ÷VOæC â¥õª÷Ïî~& Äso¢k#P)Oë[×€¨¤^öH} í»z7mwjëR㡆{Ïû[$ßFjøÎ׸Ë·x1¼Ä‰Åâ×?•¤pÈ#ù+2‰gúf8 S¨»I¼wË/,Éáh‰‡cû, ì8Ö­œº$æâ]&Ãÿ¶?ᕎcÞpß)o8ìü#¼ãÝöÔ»ód9eøùä ¯ªxŽx‡ÌßáSÞ«mÌß [YåEáùx¹0 ”ºÿfà²|ÍçcÞçÄ»e~ êKçDœpŠ`/X/–mkðjÙ&8ç‘“ñÀs«9^Âò±­¹Ž >~ÔÀu¸ü¿¿oø`ãyº‹'Ï{ýÃúáz3ës†×-‘ûƒ„)y[ï“ÄS,aþö•O>ÌÔØªq5¾Öª 5±&Õ˜Ë1x+<'_½7ÁZiq­dÊé˜öé5 yÛñ;CσaØ6NáÎ ³ÀNÏæcÞvrœ œ„$Ahm6y‘…)Å/×6fû‰ÅˆÚ›„¾‹Bü|÷íÛ=yÖl{MÈCN„ѦÈå¯/Gýí/”.ôw»gžú'ü_׿׼¨3sfNv—|¨->Þ¤ŽF½g9~|Ú†+Ö­ìº$уžÃçlø‰2“çøFÄ«ìxÝ–w~¹Ã%7'@È"á;+Šù«uæNYyûá<Ï}P1· %Hùµ^šÞïšùx©gA/ñBI% ½ÝœÀ2“o>‰H¡=@ES\s󯤖ã@¤»?ð°ŒØH’o¥ÍP#^wð¤õ¸×äè|±vÈ uM¾Ïòß ï#Ö+°]#LlËÁKÿm$´á¸{GîФ€«íNÂ$Ãn´ÁMÁžÑFm´ÑFm´Ñ–ÔþžSCÀ{././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/data/NGC0925.bima.mmom0.image.tar.gz0000644000175100001770000001525614704473421024457 0ustar00runnerdocker‹‰#XìlçyÇ)K²~X’eËVÿ|㟒MQwG%l!Ó”-ÇY’2ê«q"OôÙÇ;îî([Y²E“-†YÐvù§éš,È’ k [»%ö£ÝÖÝ/tÉÖŠÙ€¶hÝë†yÏïøœýt'žŸOr¼‡ïû¼÷>ï÷}Þ÷Ž´-MŸNs A Í*5)T«é5.VUÜ:8 í39ÿÙ#À‡9ž‹q¼‹8>ùoa +Ò0-É`, Jy5¿µêïP¦WšU¯ZÒ¬z+aãó/ æÿv°öü;¯!E›Óo¶{‚£ÑÈŠóÏ ‚7ÿÁüÇ >À¸[9Е¸Ëç¿´P—YŠÓ«¬&›&Ì|o±1ë–ööäºn*–n,°9Ý`¦>g]• y¬*k²!Yr…AžT­ÕÚì}¿GDl„õ®ÿ¹w±W]ÿöºÇý_€XÿQ1ÂÓú¿¼L¹Ž¢%iɨ­¬¤µ;Õ¶±Éõ¾´¹v·[þ8mâÎ䆋Ÿ 4gµXÌNiùš=«öq¦¹Îöz´ë•Z]•sFE6äJVª{³ßæÖÿðÀœï 4ÛTõòïb™¶*(‡‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ˆ»›ÛöoÿÓôoÿ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ be~ð*éLg:Ó™Ît¦3élŸ?þÕÆƒ?ùà ï ‚ ‚ ‚ ‚ ‚x™šžÌ-6‚ ‚ b£lrÏ#®=â³ópŒ»ö'|åÏÃqصÿ*ÐüåF¶ý]8˜kw´Û\{'ØCÞõÁ>ãÚö¯QÚáÚŸ{»kìa×~É×öU°{\û`oqí¿û>×¾öA×Þ„å1xÙíÚ—^×þ¼œpíÇáå˜kÿ¼ì÷Ƹ Çþ#Ÿ½§ít;jeøÊŸnG­^iG­¾ÝŽZýg;jÕÝãÝÓZ•;P««¨Õ¨Õ}m_î@­¾ÖZýyjòVjÕÓ‰åÇ;Q«S¨Õt'jµÐ‰Z=Ó‰Z½Ò‰cÿ7Ÿ=°íèfÔJö•ÿÊfÔê+›Q«¿ÜŒZýûfÔê¿7ãxºP«ó]¨Õå.Ôêá.ÔêÉ.lû\jõÛ]¨Õt¡&ov¡Vÿã+ß×ZźQ«‰nÔJíF­~µµúJ7Žý ŸÝÖƒöhjõQ_ù£=¨Õ³=¨Õõ VßéA­¾×ƒãmëE­¦zQ«ŸíE­ô^Ôê±^lû…^ÔêK½¨ÕK½¨É뽨Õ÷}åƒ[P«ã[P«ÈÔêâÔêñ-¨Õ³[pìßôÙ×}öÞ>ÔêÁ>,7ûP«Ï÷¡V¿Ó‡Z}«µú—>ïõ>Ôê~Ô*ßZÍö£V×ú±í/÷£VOõ£V_ìGM^ëG­þÕWÞ>€Zí@­Ž  V…ÔjaµúüŽýUŸý–ÏÞºµúÐV,ŸÛŠZ}z+jõå­¨ÕŸlE­þn+Ž÷­­¨Õø j•D­fQ«+ƒØöQ«_D­žDM~wµú{_ùOQ«Ám¨Õîm¨Ufj¥nC­>½ Çþ²Ï~ÓgoÚŽZñÛ±üÂvÔê‘í¨Õç¶£V¿·µúúvï›ÛQ«ƒC¨Utµ:3„Z}|ÛþÜjõÐjõØjòC¨Õ7|åo¡Ví;P«þ¨U|juqjõÈûs>ûuŸýã¨ÕáX~n'jU߉Z=±µza'jõÕ8Þ×w¢V;‡Q«ÑaÔêþaÔê#ÃØ¶2ŒZÕ†Q«kèɯ £V_ó•ÿÓ0jõ“aÔ*pjuâÔªpjU¿ÇþŸýšÏÿ-°¹vï.›kØ…ý&À>îÚe°zºí˜_Ü…zþé.'<ÇþÇ]Ø×¦{±¯#`ïríÔ½¨Uì=®}ì¤7×`Ç]û;`§]›íF;æÚŸÜý¾à³¿ö¤kïÁò”Ï®ìAŸßô•ÿõŒÿúŒ¿o/Æ`oó·q:ûÏ^Œ_Ù‹ñi/ÆÿݽÿÖ}ÿä>ì·vƵ_ö•¿á³íI÷b;´c»?Æ6í+ŸÛ±=³cû‡ýÛžÛäŒí‘Øï³0¶·}åƒ íCŸ‡}å_;äiË0?ÿ‹á:ÝwæUô>ÌÉ<Ø×þ”Ïþ}°Ã®ý`ñâ9è‹ç jõ3ñú&Ø{]û3±¯âšúúAÜ+Þò]s.8ê]ÿê6{}ž8„ý¾t÷“ë‡p½l9Œñ>ŒûLü0æÕ™Ã¨êóyÔçóÙø/ýúaŒá/Àºö}å{`l>‚ñœ?‚ñè¾òÇ`¿OÁ=óÅ#¸þ1Ø ×þéÔm÷QŒaò¨/Ïb Ÿ=Š}=cxÍg¿qûúþQ\³â1ܯ.Ãýê±c¸_= ÷º?<†×ùŸÏõc¸FB#˜gF0Wë#8O`Ì/úüÿlóüG#˜“»FqŸOŒb¿EÝžEmŸÅ<cûÚƒ}<@AAAAAAqç`ÿYçFñϵéxÓ&‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ˆÛÅñ§™ó7ærR6®Èª¼À¦TUÑtÅdYÉXP%­Â& CZðüÎHK²|…¤Š¢³Ü¬)ó’¥ ¬"YR¾;]˜È&Yvjº”)Lf ¹l 62/¦¢kL ñƉ5i!‘Mç²Ù‰éSI–¹&—–\aº–d‰ÄÙ™i.–ä#INL†ù×òK뵚)3E«7,“Í骪_M¶|3õ†Q–SZµÌ%ÑWQ†U½šªJªÙ0æå…”øê%Í2SÇ}Š¥ÔäT”óU ¥’ªÒãB±pØ>…£±à‰ÖÉ.tß5ëFƒ•Š\†¾ëh†\7S|0â/+_’´ï+™WÌeÆaZz=u",æN3…ó™BòÜÔtÆ®`e=+äò¥cIºi–uÃUɲÕ@ôB6w*6aW­¢_5âxä?Ú¼„¦³Zý[Ä虉é›f#g$MS´*3kºn]ËnzrªTL1¬b>“N1~ lÄëòª¡X–¬¦óÅbzâ\êÉK¦9kO±vÙ‰Ú¼ªXåK£é‰ü9×+-©Ê¬!-ª…þÎLMÚ#g"âÜ×ó’2g«l>=óЇF?è)‰&ÃqJyJù»%åŤMŠ”ò”òwMÊG“ðÊ…)å)åĒb‚RžRþnIùXRH$¹¥<¥üÝ’òñ$Ï%Ã"¥<¥üÝ“ò"Ÿ)åWHy!B)¿ñ”?Oçò CC‘*ìt=­×’ÌËOž;%—Ç1ÏoQjr\.]âû™›'9èÀs[%3=—ælÚeIÅB½ayS*C,3ça­øÜw­ð`H7v¹¡%ânÝ*òàïz­ ë±(Ûü¡,ŽQUgÎOž›8½rÇB$Û7ÝVí¹é²¸kì±9UªÎKjªE&ìTe+5f^’ FÄQ¯**Í+òU¦Ï9íªvn‚hpmeVQkÍ)ªÌÞÙOÖ]b·2¡‹Y:+f2lÄÞÖšWƒzÉdU]¯´z,é°¡0­Q›• »WÈXݨ˜nˆr6³h™½‰h²ÚêÁ}Êå 4‘T6ôt.¡Yê‚çv–ô–x$NÄbѦ)Æ.Рàã‘°€o9ûÅS4¬âEOï*W•ŠÜ,^L ÄŒÄ9ÏŒG¸bb,åÖ“㹞tŽÝâtžÃ"¹R•SÂ{”ÓçŠË$ñ¬´¡†´½#sØI\Ç„Ä KaÖK›0—ˆÇWÊp!›nᘎ®']ï]º¸»ŸT«„¹÷j뻫ÓsL°„•Ò1x~…´iæJÓŒQ1ònw>Øî–˜Ë‡dox+î‹b„ó™3çOeà± Ÿ5æOÙäÚ3>26ѨŽqè½bâ‹IÎyÞpýVÍ|×ç©ßªð?s4*eøDrM©5jþC©^W•¦Ö%™Áš€”L»µ²5'`$RU^g>gY¹¢HÚú›Db§'òùsð,çÏ»ò qxPSÇ –?[ò*WT>àÃC“ç¶Úó¤ë²X;+ªv '„c'„x‘ÄÇ&åYøÐäù.?%aûo`pðyÞs[eJ<—%Rbñ)±ÂÉo§Ü¬(X ñèss)>$b™!Wa),Ð †•9À+= ·žŠ};mz3Åd'ÕrjDƒB4ÈK¼ ó ˜’8£^Ãær ®èÞ­à³pXLˆ…L±äÛu 2|œ_´é L›‚Æ=ßM8ŸB=·Uõ\–ŠÅKõùëYEI±•.·ÜOBS(‹†x.‘9ÁqìÜkBb”wÞHFÙ”ËAV—Ài,‚„wÊ+rÕe3;Q:SÄT“¬K撵˕-¸¹ž‹u&òûO€ãɰÒ„<¯Õò¬éá×ê­Á´ q„5óJ«´&™W<=jz¨j…¸P8 ‘®ÇJÃÿ¼Œ‰„ƒÐÊ^Èüh1›Ëf¦K¾Á c’sÛmÕ¬2T¶¹–ß*ƒmùÀ†£áÀ|ž×ô79UÂé˜S,˜Iç焇ä1MŸ‡P›ÞKâÌN\söŸØpB’ƒ)i:­dÓÁâø¬R“Æß•í¸uI7Æ­Ëãù™“ç¦Òãvåøôét3‘¡e³MÈŽÖS× u³`:WÊ$ÙŒ)·6Âyɸj1eŽ©ºä̤¢Áª©ܘ"…àIÙpV.TÛë%™\Z²ŒÓHMº¬©Ÿ…BYMÑðM]ò,g!º¶!×ôy95'©¦döw—–ÑË}°MñÐRW%CyÈÙdœtUÖ¤šœ:xpôq½ßG– ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ bc,ó#gŸÃ:®êUKšUåqç5T‘¬›îÃþ±ÔÑhÄ>ó1‘óŸm1Êø0Çs1N€—Ôr¼`Ü-çŠ4LK2 ȆR^Ío­ú;”W€@ S³³dÏ4›¶I8·ÁÑ›W%Esk:€’çÍ)Ù,7][l;§WYM6MûWXîµD8¶8- ÎoÑp/»Û¾t³dÉ•Ún¶Q§{¾·X–TÉHëj£¦Ù~4.<ÌqÓ¡£4•ÍÀy {ös~ GY×*&”ô-I«HF¥he%m¹‚î%ò}fùH/.i»ÝùÌôTÉnä^ /›™(Î2Kò_ù6¯¼™Ì2Ói¥ÝE›pg&ŸKŸ±/=SJ»ŸÓ;µ(ZöÏSniÑ/Lå S¥ n£µØâë¢çfçªÍg¯cW6S,NœÎÜTˆÛb÷¹\z¢4•›þÇØ“;y6“.]œ:õ>yãÆÿõí*mËõpzë³ÍWæÏS¹/7¹ûæÃ_¾H¯BmîKp´‹Y××5Gáè<©êå+Üg€¡<Ôõ“à;ûÓU?ج÷þïŽë¦Xãþωœ¸ôþÏǺÿ·”»$àè04{ªÛÜ «Ó=:à¸7°d‘ÛGûûô­`Åõß\ösÜÅR1û.âZëŸã"ÞúçÅXÖ¿(‚;­ÿÛ7ôí;â>E¼7¬±þmN×}¬¹þÃü’û¿†m€Öÿm ´P—YŠMÙsÞ[l̺ï{{ßïÀˆÛš÷ÿ[ÐÇZë?—Üÿ#1zþ¿=4Ÿ{Ûîs ¤¨r%-«ª÷‰Ü~¸µ?ÖöLåuS±Q—[ØÀ§_ïøg8zKxÍ[_z.]p´×¤º[ì\Åÿ=†ÿ²öäoÞÀ× Nãu»nÇÿבּþßÍ×þ-V_ÿ|,–Þÿ…0=ÿßšëÿ€½xýßÿ·òÑ÷ÿ«|ÿìý^`Ùo¿XnéÚ<º½ï™Àîó]isY·çv ù‘{å¥oÓÙÐËvõ¾¯ì®)fÙ}t]»y“ðëv·ÇÓcä÷¼½ì°ÛŸYöBCvŸ–¬B‰^—ýa볦l̈¯¬ Ê`Êë m ®+š¥hÕ²¬YÎeÖnÓ[QÜ_Í­O­«º¡V`·Ý·Ã±/°ÌVÚvp[ô;- ¹®JeÙnµs=­zêÊ5YÝP?N‹öÓeZúÙä76xþ¦ÏoxðëïÇ?øõ÷cÖaî I66|ᦆ/lxøëïçÿÚ»šØ6Š(iåd2;oæ½yoÞ¼™óÃGÓ1nÆÇ枟9pà0Ì…£S‡§¦Á©ÁŸLhÈd!̃=}¥J™Ãe%W¥ÉÁNÁÌ£¥¤Éæy„u®'‹{ó¬°!Ó´.KêtÇ9° äzHÿ^_xäÂ*ü í}(&qÜŸNÍòý¼¤z 'Jýcå²´bŸÒîµÙp(¥~>þ·S׿Ùò^<Úž„} \öV•ˆõáÜø£¥²vʲ{\én鉒T– |±6vŒa¹¼lŽ[Û˜´øÚYEÕ±µƒ%[5$U`„ÛQ½pVZTí-²V´¿qyÖËcÝ/Η4•Ÿ£½ª¤{ŠLe8´ÇpAî³S¨9Ãjžh¸‰æÿRZ9i6ŸÎ^ÆSúêû£«—^»Ùkeõ^ rO(õ‡`d¸ò܆¬ó_sfòó zllÑðm دÈj¾(Ù¢L`šé)KY÷ÃS:÷ªÅô•G{¦/¯?î0ž¨ÕÖDQ¯',¬5x÷âÚOË#©ë_œ=Î~=ÝæækY<Æ„¶f'ÙškR¡"–÷­¾!- ü>ÄÌ'ç‰Ì:ÌØ©¥l$Sóá”ÿ2'ù¦ZŽ×«¬ó¬ެåO›êC»štYýL~ðLj'¿ñôÚöž¹×j×Þ>ø^÷\Yy…¹†~“S†^m Rêœås9¤¢`¿–O)T߯ðËL¸CúŒ‰ÔbÉ›*¡†Ñö¾’;A/g‹è_µÖƒš¿.Îu‡ƒØáâ¢(Íì„«xXÙ (â¾·²NÝzøß¹ýƒ ÷¶CÛ4o›5Dnã+ÄDÖ„DÂ8)à‚9?…Cs3p}ª± žj¡K’ê‘¿ÄDºv MŽÍÎu^vYfû6 ã½BûMÙðv5Éuù¾C+CE*천©À¼˜A7„ ‚HTŸ¬¨ê¢lzQ71SNñ<Ëò|²AïÒ¢"/Tå„–µÌ"÷{DË@ÜŠk4bðÝŠ]UqõL>«/po…Uc](ñͨp¦WUñL°žLIbž9:˜:þÝç1–S/MY¿ý㛩›·N]d[¯¦þ>yþ¹¯µ­0 û¬Ñ؈?˜~Ìz½ò¹R9ìë3ÜuMÎë+—·FÖR·o¼o0ï1›Ï Ù ²ë‡í„ÎH‹á<£Z|õøñ°|Þw郺] BÖpûˆ°!dÕ~ÇâJÅywJÝ\÷¾¥€òvíÛxl¶î©,SöNè Œ¦Ý~zÛÞK÷¯_ƒ9Þ!ãœUæÝYhøèEfúŠ]åc1Õi!UøËÀ¸1½Ê²™£qº’Wà€˜Ò'ÂMg­˜Íü8âÊJ:è-2̤Y4PµƒytՀݻ§ÂÅ·oŠ<Þ¨®T%§áífE.S&§j’‹Åª o¶·ê±á]`œ¬»- RIq.¸Ø{Ûâ¬aƒ;ÌŸ±’gªxS{W‡Ù¯Ù}âÉ6‚ÏýVÒ>*h>ÿ#9¼;N÷?]Cü*GaþV×îFÔÕÿ‚´xz÷P[h€ŽŽ&ä¸ù߉á8ÜÿN&X,Ùê>¸ÇõßGþmÉòÏÿ‰{ä?’Iý': ”þ·˜äŸÿ3êÕÿáø(é7`çû1Dž™û"ʺ—€\ÿ[¢á›ÿ“öèrt”îÿwvþÏ“¬åüŸ_˜oþ jšÓÒØ%5i@|«”ÔY ô¿Å, ý'ã^ýÙ“ ÿÿÕXúßÿCÊ?ÿ'x5Îÿ¹£gàKñK/ _‚¿lãÃèUöMŸܨÑô†—u]Aò|êBP}Ÿ¦©17¨^Mß?¶^»N0v—„Ø«»¶±—”ýoñ ™øeÿGþ¡{ð‹ÿ;Ïÿ.þO @ @ @ @ „‰ÿßÜ&././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/data/generate_commonbeam_table.py0000644000175100001770000000501114704473421025061 0ustar00runnerdocker ''' UNUSED IN CURRENT TESTING SUITE! Run in a CASA environment to generate the smallest common beam for a range of parameters. ''' RUN = False if RUN: try: import casac except ImportError: raise ImportError("This script must be run in CASA") # Tested for CASA 4.7.2 import casadef if casadef.casa_version != "4.7.2": raise Exception("This script is only tested in CASA 4.7.2. " "Found version {}".format(casadef.casa_version)) import numpy as np # Generate fake cube w/ 2 spectral channels im1 = ia.newimagefromarray(pixels=ia.makearray(0, [4, 4, 1, 2])) # Give the first channel a circular beam im1.setrestoringbeam(major='3arcsec', minor='3arcsec', pa='0deg', channel=0) # Give the other channel an elongated beam, and rotate it majors = [] minors = [] pas = [] # Include the PA of the one beam so tests can be run later. orig_pas = np.arange(179) for pa in orig_pas: im1.setrestoringbeam(major='4arcsec', minor='2.5arcsec', pa='{}deg'.format(pa), channel=1) com_beam = im1.commonbeam() majors.append(com_beam['major']['value']) minors.append(com_beam['minor']['value']) pas.append(com_beam['pa']['value']) common_beams = np.array([orig_pas, majors, minors, pas]).T np.savetxt("commonbeam_CASA_comparison.csv", common_beams, delimiter=",") # im1 = ia.newimagefromarray(pixels=ia.makearray(0, [4, 4, 1, 4])) # for i, pa in enumerate([0, 20, 40, 60]): # im1.setrestoringbeam(channel=i, major='4arcsec', minor='3arcsec', # pa="{}deg".format(pa)) # im2 = ia.newimagefromarray(pixels=ia.makearray(0, [4, 4, 1, 4])) # for i, pa in enumerate([0, 60, 20, 40]): # im2.setrestoringbeam(channel=i, major='4arcsec', minor='3arcsec', # pa="{}deg".format(pa)) # im3 = ia.newimagefromarray(pixels=ia.makearray(0, [4, 4, 1, 4])) # for i, pa in enumerate([60, 40, 20, 0]): # im3.setrestoringbeam(channel=i, major='4arcsec', minor='3arcsec', # pa="{}deg".format(pa)) # im4 = ia.newimagefromarray(pixels=ia.makearray(0, [4, 4, 1, 4])) # for i, pa in enumerate([60, 0, 20, 40]): # im4.setrestoringbeam(channel=i, major='4arcsec', minor='3arcsec', # pa="{}deg".format(pa)) # print(im1.commonbeam()) # print(im2.commonbeam()) # print(im3.commonbeam()) # print(im4.commonbeam()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/data/header_aips.hdr0000644000175100001770000004200214704473421022315 0ustar00runnerdockerSIMPLE = T / BITPIX = -32 / NAXIS = 2 / NAXIS1 = 2707 / NAXIS2 = 2707 / EXTEND = T /Tables following main image BLOCKED = T /Tape may be blocked OBJECT = 'omega_ce' /Source name TELESCOP= 'ATCA ' / INSTRUME= ' ' / OBSERVER= '' / DATE-OBS= '2010-01-22' /Obs start date YYYY-MM-DD DATE-MAP= '2016-07-20' /Last processing date YYYY-MM-DD BSCALE = 1.00000000000E+00 /REAL = TAPE * BSCALE + BZERO BZERO = 0.00000000000E+00 / BUNIT = 'Jy/beam ' /Units of flux EQUINOX = 2.000000000E+03 /Epoch of RA DEC VELREF = 259 />256 RADIO, 1 LSR 2 HEL 3 OBS ALTRVAL = -6.85933178146E+07 /Altenate FREQ/VEL ref value ALTRPIX = 1.000000000E+00 /Altenate FREQ/VEL ref pixel OBSRA = 2.01691208348E+02 /Antenna pointing RA OBSDEC = -4.74768611168E+01 /Antenna pointing DEC DATAMAX = 3.216996155E+02 /Maximum pixel value DATAMIN = -6.924145508E+01 /Minimum pixel value CTYPE1 = 'RA---SIN' / CRVAL1 = 2.01691208348E+02 / CDELT1 = -9.166666860E-05 / CRPIX1 = 1.356000000E+03 / CROTA1 = 0.000000000E+00 / CTYPE2 = 'DEC--SIN' / CRVAL2 = -4.74768611168E+01 / CDELT2 = 9.166666860E-05 / CRPIX2 = 1.356000000E+03 / CROTA2 = 0.000000000E+00 / HISTORY / History of input map 1 HISTORY -------------------------------------------------------------------- HISTORY /Begin "HISTORY" information found in fits tape header by IMLOD HISTORY EXTEND = T / File may contain extensions HISTORY IRAF-TLM= '2015-07-27T09:00:19' / Time of last modification HISTORY BTYPE = 'Intensity' HISTORY RADESYS = 'FK5 ' HISTORY LONPOLE = 1.800000000000E+02 HISTORY LATPOLE = -4.747686111680E+01 HISTORY PC01_01 = 1.000000000000E+00 HISTORY PC02_01 = 0.000000000000E+00 HISTORY PC03_01 = 0.000000000000E+00 HISTORY PC04_01 = 0.000000000000E+00 HISTORY PC01_02 = 0.000000000000E+00 HISTORY PC02_02 = 1.000000000000E+00 HISTORY PC03_02 = 0.000000000000E+00 HISTORY PC04_02 = 0.000000000000E+00 HISTORY PC01_03 = 0.000000000000E+00 HISTORY PC02_03 = 0.000000000000E+00 HISTORY PC03_03 = 1.000000000000E+00 HISTORY PC04_03 = 0.000000000000E+00 HISTORY PC01_04 = 0.000000000000E+00 HISTORY PC02_04 = 0.000000000000E+00 HISTORY PC03_04 = 0.000000000000E+00 HISTORY PC04_04 = 1.000000000000E+00 HISTORY CUNIT1 = 'deg ' HISTORY CUNIT2 = 'deg ' HISTORY CUNIT3 = 'Hz ' HISTORY CUNIT4 = ' ' HISTORY PV2_1 = 0.000000000000E+00 HISTORY PV2_2 = 0.000000000000E+00 HISTORY RESTFRQ = 4.475999873410E+09 /Rest Frequency (Hz) HISTORY SPECSYS = 'TOPOCENT' /Spectral reference frame HISTORY casacore non-standard usage: 4 LSD, 5 GEO, 6 SOU, 7 GAL HISTORY TIMESYS = 'UTC ' HISTORY OBSGEO-X= -4.750915837000E+06 HISTORY OBSGEO-Y= 2.792906182000E+06 HISTORY OBSGEO-Z= -3.200483747000E+06 HISTORY WCSDIM = 4 HISTORY CD1_1 = -9.1666666666670E-5 HISTORY CD2_2 = 9.16666666666702E-5 HISTORY CD3_3 = 2049241561.325 HISTORY CD4_4 = 1. HISTORY LTV1 = -1205. HISTORY LTV2 = -1205. HISTORY LTM1_1 = 1. HISTORY LTM2_2 = 1. HISTORY LTM3_3 = 1. HISTORY LTM4_4 = 1. HISTORY WAXMAP01= '1 0 2 0 0 0 0 0 ' HISTORY WAT0_001= 'system=image' HISTORY WAT1_001= 'wtype=sin axtype=ra' HISTORY WAT2_001= 'wtype=sin axtype=dec' HISTORY WAT3_001= 'label=freq' HISTORY WAT4_001= 'label=stokes' HISTORY /END FITS tape header "HISTORY" information HISTORY -------------------------------------------------------------------- HISTORY IMLOD OUTNAME ='WCEN_LOW ' OUTCLASS ='ICLN ' HISTORY IMLOD OUTSEQ = 0 INTAPE = 1 OUTDISK= 1 HISTORY IMLOD INFILE = 'PWD:wcen/wcen_atca_5500_pbcor_c.fits ' HISTORY IMLOD RELEASE = '31DEC14' HISTORY / History of input map 2 HISTORY -------------------------------------------------------------------- HISTORY /Begin "HISTORY" information found in fits tape header by IMLOD HISTORY EXTEND = T /Tables following main image HISTORY BLOCKED = T /Tape may be blocked HISTORY / IEEE not-a-number used for blanked f.p. pixels HISTORY -------------------------------------------------------------------- HISTORY /Begin "HISTORY" information found in fits tape header by IMLOD HISTORY EXTEND = T / File may contain extensions HISTORY IRAF-TLM= '2015-07-27T09:11:10' / Time of last modification HISTORY BTYPE = 'Intensity' HISTORY RADESYS = 'FK5 ' HISTORY LONPOLE = 1.800000000000E+02 HISTORY LATPOLE = -4.747808335040E+01 HISTORY PC01_01 = 1.000000000000E+00 HISTORY PC02_01 = 0.000000000000E+00 HISTORY PC03_01 = 0.000000000000E+00 HISTORY PC04_01 = 0.000000000000E+00 HISTORY PC01_02 = 0.000000000000E+00 HISTORY PC02_02 = 1.000000000000E+00 HISTORY PC03_02 = 0.000000000000E+00 HISTORY PC04_02 = 0.000000000000E+00 HISTORY PC01_03 = 0.000000000000E+00 HISTORY PC02_03 = 0.000000000000E+00 HISTORY PC03_03 = 1.000000000000E+00 HISTORY PC04_03 = 0.000000000000E+00 HISTORY PC01_04 = 0.000000000000E+00 HISTORY PC02_04 = 0.000000000000E+00 HISTORY PC03_04 = 0.000000000000E+00 HISTORY PC04_04 = 1.000000000000E+00 HISTORY CUNIT1 = 'deg ' HISTORY CUNIT2 = 'deg ' HISTORY CUNIT3 = 'Hz ' HISTORY CUNIT4 = ' ' HISTORY PV2_1 = 0.000000000000E+00 HISTORY PV2_2 = 0.000000000000E+00 HISTORY RESTFRQ = 7.975999774420E+09 /Rest Frequency (Hz) HISTORY SPECSYS = 'TOPOCENT' /Spectral reference frame HISTORY casacore non-standard usage: 4 LSD, 5 GEO, 6 SOU, 7 GAL HISTORY TIMESYS = 'UTC ' HISTORY OBSGEO-X= -4.750915837000E+06 HISTORY OBSGEO-Y= 2.792906182000E+06 HISTORY OBSGEO-Z= -3.200483747000E+06 HISTORY WCSDIM = 4 HISTORY CD1_1 = -6.6666666666670E-5 HISTORY CD2_2 = 6.66666666666701E-5 HISTORY CD3_3 = 2049361403.91 HISTORY CD4_4 = 1. HISTORY LTV1 = -784. HISTORY LTV2 = -784. HISTORY LTM1_1 = 1. HISTORY LTM2_2 = 1. HISTORY LTM3_3 = 1. HISTORY LTM4_4 = 1. HISTORY WAXMAP01= '1 0 2 0 0 0 0 0 ' HISTORY WAT0_001= 'system=image' HISTORY WAT1_001= 'wtype=sin axtype=ra' HISTORY WAT2_001= 'wtype=sin axtype=dec' HISTORY WAT3_001= 'label=freq' HISTORY WAT4_001= 'label=stokes' HISTORY /END FITS tape header "HISTORY" information HISTORY -------------------------------------------------------------------- HISTORY IMLOD OUTNAME ='WCENUP ' OUTCLASS ='ICLN ' HISTORY IMLOD OUTSEQ = 0 INTAPE = 1 OUTDISK= 1 HISTORY IMLOD INFILE = 'PWD:wcen_atca_9000_pbcor_c.fits ' HISTORY IMLOD RELEASE = '31DEC14' HISTORY CONVL RELEASE ='31DEC14 ' /********* Start 16-JUL-2016 18:59:57 HISTORY CONVL INNAME='WCENUP ' INCLASS='ICLN ' HISTORY CONVL INSEQ= 1 INDISK= 1 HISTORY CONVL OUTNAME='WCEN_CONV ' OUTCLASS='CONVL ' HISTORY CONVL OUTSEQ= 1 OUTDISK= 1 HISTORY CONVL BLC= 1. 1. 1. 1. 1. 1. 1./BLC HISTORY CONVL TRC= 2275. 2275. 1. 1. 1. 1. 1./TRC HISTORY CONVL FACTOR= 2.23193E+00 / Units scaling factor HISTORY CONVL BMAJ= 2.9700 BMIN= 1.6500 BPA= -15.1/Output beam HISTORY CONVL / plane 1 conv with 2.17968 x 1.23466 at 168.0 HISTORY CONVL OPCODE=' ' /Operation requested HISTORY CONVL DOBLANK = 1 / Blanks restored after FFT HISTORY OHGEO RELEASE ='31DEC14 ' /********* Start 16-JUL-2016 19:00:16 HISTORY OHGEO INNAME ='WCEN_CONV' HISTORY OHGEO INCLASS ='CONVL' HISTORY OHGEO INSEQ = 1 HISTORY OHGEO INDISK = 1 HISTORY OHGEO IN2NAME ='WCENLOW' HISTORY OHGEO IN2CLASS ='ICLN' HISTORY OHGEO IN2SEQ = 1 HISTORY OHGEO IN2DISK = 1 HISTORY OHGEO REWEIGHT( 1) = 1.00000E+00 HISTORY OHGEO REWEIGHT( 2) = 3.33400E-01 HISTORY OHGEO APARM( 9) = 1.00000E+00 HISTORY FITTP DATAOUT = 'PWD:wcen_up_ohgeo.fits' / data written to disk file HISTORY /END FITS tape header "HISTORY" information HISTORY -------------------------------------------------------------------- HISTORY IMLOD OUTNAME ='WCEN_OHGEO ' OUTCLASS ='ICLN ' HISTORY IMLOD OUTSEQ = 0 INTAPE = 1 OUTDISK= 1 HISTORY IMLOD INFILE = 'PWD:wcen/wcen_up_ohgeo.fits ' HISTORY IMLOD RELEASE = '31DEC14' HISTORY / End of old histories HISTORY COMB RELEASE='31DEC14 ' HISTORY COMB INNAME='WCEN_LOW ' INCLASS='ICLN ' HISTORY COMB INSEQ= 1 INDISK= 1 HISTORY COMB IN2NAME='WCEN_OHGEO ' IN2CLASS='ICLN ' HISTORY COMB IN2SEQ= 1 IN2DISK= 1 HISTORY COMB OUTNAME='WCEN_ST ' OUTCLASS='MEAN ' HISTORY COMB OUTSEQ= 1 OUTDISK= 1 HISTORY COMB USERID= 5 HISTORY COMB CTYPE='MEAN' /Mean HISTORY COMB BLC= 1 1 1 1 1 1 1 / Bottom left corner HISTORY COMB TRC= 2707 2707 1 1 1 1 1 / Top right corner HISTORY COMB A(1)= 6.9856E-01 A(2)= 3.0144E-01 HISTORY COMB / Undefined pixels magic-value blanked HISTORY FITTP DATAOUT = 'PWD:wcen/wcen_stacked.fits' / data written to disk file ORIGIN = 'AIPSmacbook-pro 31DEC14' / DATE = '2016-07-20' / File written on Greenwich yyyy-mm-dd HISTORY AIPS IMNAME='WCEN_ST ' IMCLASS='MEAN ' IMSEQ= 1 / HISTORY AIPS USERNO= 5 / COMMENT FITS (Flexible Image Transport System) format is defined in 'Astronomy COMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H HISTORY AIPS CLEAN BMAJ= 8.2477E-04 BMIN= 4.5736E-04 BPA= -15.06 HISTORY AIPS CLEAN NITER= 0 PRODUCT=0 / DIRTY MAP END ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/data/header_jybeam.hdr0000644000175100001770000000670214704473421022637 0ustar00runnerdockerSIMPLE = T / Written by IDL: Fri Feb 20 13:46:36 2009 BITPIX = -32 / NAXIS = 4 / NAXIS1 = 1884 / NAXIS2 = 2606 / NAXIS3 = 200 // NAXIS4 = 1 / EXTEND = T / BSCALE = 1.00000000000E+00 / BZERO = 0.00000000000E+00 / BLANK = -1 / TELESCOP= 'VLA ' / CDELT1 = -5.55555561268E-04 / CRPIX1 = 1.37300000000E+03 / CRVAL1 = 2.31837500515E+01 / CTYPE1 = 'RA---SIN' / CDELT2 = 5.55555561268E-04 / CRPIX2 = 1.15200000000E+03 / CRVAL2 = 3.05765277962E+01 / CTYPE2 = 'DEC--SIN' / CDELT3 = 1.28821496879E+03 / CRPIX3 = 1.00000000000E+00 / CRVAL3 = -3.21214698632E+05 / CTYPE3 = 'VELO-HEL' / CDELT4 = 1.00000000000E+00 / CRPIX4 = 1.00000000000E+00 / CRVAL4 = 1.00000000000E+00 / CTYPE4 = 'STOKES ' / DATE-OBS= '1998-06-18T16:30:25.4' / RESTFREQ= 1.42040571841E+09 / CELLSCAL= 'CONSTANT' / BUNIT = 'JY/BEAM ' / EPOCH = 2.00000000000E+03 / OBJECT = 'M33 ' / OBSERVER= 'AT206 ' / VOBS = -2.57256763070E+01 / LTYPE = 'channel ' / LSTART = 2.15000000000E+02 / LWIDTH = 1.00000000000E+00 / LSTEP = 1.00000000000E+00 / BTYPE = 'intensity' / DATAMIN = -6.57081836835E-03 / DATAMAX = 1.52362231165E-02 / BMAJ = 0.0002777777777777778 BMIN = 0.0002777777777777778 BPA = 0.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/data/m33_beams_bintable.fits.gz0000644000175100001770000002460314704473421024300 0ustar00runnerdocker‹—õ%Yÿm33_beams_bintable.fitsÿÿìÏ1 1Fá«ü7ÐÒÆB1B@—…¤H;š,l‘Œ$±ØÛ»v bí|xðŒ¾ŽìÑ`±ÁÓÄ9TÆY[ƒR)yÊMGmGí:½ÝÚ£œi§J¨Ë#à‹áà´A§·]{éo!ƒ§OyŽ!•™Siö”³j8uöŽ !„Bˆ¿ñÿÿì˜=oƒ0†ÿÊmLUiÔ. uZ"0(q*ut¨•VŒ€(åß×`1ð¡ŠëЪ·Ò£{}wæµk;¹wú†ãRf;1:ŸÎGÊó ?ÆÈg©½é0oÊ?¯ßyΣR9^·ñå¼Åûg ì"o®–™ÇP6‰NðÌ~ž¥ü8½ÚñcUŠ2õËË8Ïêã™æÝ}—§(Åt=Âup lø¼@%äêü"ÈÔB&B­ä(õiŒgÂ"Hã d*à”Ës6Û¸Ä{Ü÷óV]½‘ŒÏIZüòùPç-jûêù ¶_·¢1“÷Bvƒõø©ùÓ“ÛöóúÙ¦0ØÏßž^ì`ì5$Í~`8¾½…¹õe›`çkžIô«y¼u™æñ<*D4—WëµZ½.ÅÑk!ëµõ.µÞнKd½š÷&N¼ZïmÃk'A¯æ™[@ÊoÕðÚ!¿^~èÿÿûµ?_ÿÿìœå›dÕõp) Ü AÂ08Á‚ÿ ÚÝAB€ ƒ5n,0Ó¸»»tª­º¤»º»ª«ñÁ!Ø».wå¡?¾óí®çœ{νu÷Ý{¯ªž9W}ôɜޣ®|ñ¦}‹ùö>ì2YD6»v‡QYv6ìŸÆdƒ°Óa×s—Ý»ö1‡¿’]ë…}Äáⲫ`gÊ–= ëð|KÊ…5Ãæs¸”ì~X­ã––Ý+‡-àð×3örì—‘];v#‡ËÎØËÞ°›9\Nv)lØ-./;¶ì6W ûƒlEÙßa¿—­$û+lcØí®,;¶>ìW‘uÃÖ†ÝÉ᪲ØêŽ[MV [Ñó­.+‡-#[Cvl ç®)ûl1Ùod{Íé=ò[ç®%Û ö¥×ñ[Ù°a·r¸¶lkØ»Î]G¶,ïëÊfÃFeëÉÖ%e¿“­KxÖ—­{C6K¶ ìUçn Ûöì1gÏXãiÙ†!ûðkØ#ÎÝHöìØ]n,ûv—ën"›‚Ýâ¸Mec°žo3Yv÷esY?lžl Ù°‹œû{Ù5°ë`ßr¸¥ìrX¿ßq¸•ìqØ)îokÙƒ°Üß6²»`ÇÀîåp[Ù-°ØÝþA¶Ö»Ãífì¥A¶½ì X5ì~w]+•í(;v¨sw’Ûß½ì,ûl/Ø=î";¶›sÿOÖÛQ¶«¬ ¶l7YðœnöâMóƒ5v—-#çÊá²-a0÷‹àPv l-؃þQ¶7lUØî)Û¶¼ãö’í[ö‡{˶…Å·l‹9½=?z¯ö•mûÆqûÉÖƒ}*Û?d|Èùˆß¾àû'Y‘qÄo_ð¾>À¹ËÂÆÝó²ÅaÙA²Å`C®q°çû/ì¯ãñÑóºã•=¯È“½ {ι‡ËÆaOÀæðÙ°s/â°D¶ö¼¬T¶vìËdïÁn‚½Èa¹ìEØõ°G8¬ñìöüö5‡•2žÝžȪd䔞 å°Zv'ìÏW#»v†¬VFNé9Ùë­“‘SzŽ—ÕË.ƒ-k]ë’5ÊÈ)=­ÞÓ&1ÓSç¸f1ÓSé=m‘ýv„¬UFÌôäÜ6Y+l_Y»¬6ǹ²í`A ~Åa§Œ˜éÙɘé’ÛÊ=wˈ™žMe=2b¦g¶Ïß‘²`ë¹—£dÄLÏšîåϲMœ<GËÖ‡­+;FÄÌ’~F‘ýõ|ÇÊV™ÓÛýë'ãÑý•ÏÁñ²%aŸ8÷Yö¾kü5dï +ÈN”} ó|“ñ¥+)»Uö6¬_v›ìß°7e·ËÈ)]¯ºÆ2ú”®¼÷wÊ=åÜ»d÷ÂqÜÝ2â£ë~Ù=2ú”®»d÷Êž‚5æKÞ'{6ßœw¿Œ>¥+¨1çðÙy°+ŒÕegÂþ—ß’Ñt]gñ°ŒÞ ëJÙ#2â£ë÷÷¨Œøè:Áó=&k†-{\FNéêò¾ Ùt¶¿9ô#Ù(lOßC˨s:©Eûÿ´HFžéœíù>q5`ëŸÊV„ñÄégÎý¶²ï«ÏgìeYÏ÷…ŒÞ¾s Ÿ«/e#°¨ì+Ùí°•ÌÝ_Ën Ï÷ûFF]×ñ‰ïöÿÊè?:~`\àš¾•Ñt|cñŒÞ ã3ïË÷²»a)÷üƒìVØ€qùãÏ{éx;ÑÿM—ÿ†ñ>Œèÿ¦©ë:^’éÿ¦©¹:ž ?߈þoz®sÉçýßô)Î¥ÞŠèÿ¦wnÀôÓ<ÏA-J½ÑÿM ësú¿évصáuDôÓ€]B‘S#ú¿éßÃ΃‘S#ú¿é’ŸoDÿ7}lnXÛFôÓûÁþæÐˆþoºä7Þ×ýßtìh™þošþ¨£Û¹ú¿ib¦£E¦ÿ›ÞV'ÓÿMÿV!ÓÿMS‡u!ÓÿMS‡u$ÓÿMS‡uõ$95¢ÿ›þ•syGDôEâ£ã`™þ¯ø,¨'ɳý_ñ+XPO’#ú¿â'°Í\CÿW¤Oéâ|Ñÿƒçyϧÿ+~[Ó{¥ÿ+Rëu¬,Óÿ°e]WÿW¤OéXÂóéÿŠ“°ŒüÑÿé·Ú¿‡‘#ú¿âìK×ÐÿïanÔ{ ÿ+Þæ\êóˆþ¯ØçÜ€éÿŠ `cÞý_ñj؈LÿW$§´½=@DÿWäyiÜ ïëˆþ¯HÿÑž&~ùÌ"ú¿"y«ý¯CÿW<öT˜"ú¿"½}û#2ý_‘Ú§ýeÎÇóÑÿW Ï×wLÿWä}ß~[ø.‰èÿмïÛoß§ý_ñpØÕa½ÑÿÉ3í—‡ïÓˆþ¯¸ìB™þ¯HÝÔ~ŽLÿW¤·o?]¦ÿ+¿í'ù~Ñÿ‰ßöã§ÿ+3í–éÿŠÄL{gXGôŵ`ÍžOÿW\V'ÓÿéíÛ˽6ý_qiØŽÓÿMQ¿´ïãýÓÿMQ·“Ëú‡éÿ¦xžÛç„ïñˆþoŠÞ¥}g÷§ÿ›z¶Lÿ7Åóܾ¥Lÿ7EÎkßÔgWÿ7ul}Þkä­ˆþoêØÚ2ýß1ÓĵdDÿ7õ ,ˆ7jɈþoê9ز2ýßÔã°%|þôSÂ"Ž;äç5Ú_B]ÑÿM½ ú7ê·ˆþoŠ8j[äuèÿ¦ˆ™¶weú¿©+`“0êäˆþoªö!×FÑÿMUæeú¿)â²­ßuËg¬Ô“Ô—ýßÔq°W½ýßy¦í™þoªö´÷@ÿ7Eh{D¦ÿ›"¦Ûî“éÿ¦Jaw¨±#ú¿©ƒaA=IÑÿM­ {˜gˆ˜Œèÿ¦¨UÚî…»ýßý~[пQÛGôSÄLÛE®«ÿ›"Ï´ëuèÿ¦6Íõ9ÐÿMQ›µâ8ýß1Óv‚ãôS¼7ÚŽqœþoŠ<ÓÖ%ÓÿM- kq/ú¿©(¬ÎÏRÿW Fj«áÚÈÑý_¡8ryDÿWøvkèÿ Ôfmû¹?ý_øh;"ü®2¢ÿ+$»¦ÿ+Ú¶w/ú¿Â;°­dú¿ý~Û¦2ý_!xv7fÔ$ý_áZØú2ý_á!Øfá÷’ý_:¬m¶LÿW ßo[Î5ôúý¶%eú¿Âu°¨ïý_á ç^Óÿ.tnp½ú¿BïœÞÖEžOÿW(ƒýhžÑÿ…õ$5DDÿW j%çõ‘Ÿ"ú¿ïöVúÁ>òXDÿWXô~äèˆþ¯@ÝúqÔú¿B]È~Ú‹þ¯P {ÉØ×ÿŽ€=ë8ý_<Óú¸ãzgìå!Ÿý_<ÓzLÿW(‡‘ßÐoFô…Ã`7ÈôúýÖ뜫ÿ+l»Òuõ…õ`—ÊôzñÖ d—θ/g{ú¿y¦õ4×Ðÿˆ™Ö¿É®˜±Æ±¾¯æÍXãH™þ/Ožií0>ôùE°f÷¢ÿË“gZkÜ‹þ/O¿ßZæ8ý_žš¿µ„Ï—:9¢ÿËó<·,Óÿå߆ííºú¿<1Óº»Lÿ— ¶³Lÿ—'V[ƒïÅé×#ú¿<}Eë k¹÷çýµ$Í)ú¿<õ_ËBX ¦ÿË÷À‚ïäÒ0ý_ž~¦%p(oÃôù9°çÝŸþ/¿3ìI™þ/O?ÓòˆLÿ—'Ï´Üçùôyj³–;dú¿<µYËMÎÕÿåégZæËôyj³–«dú¿ü °àw(ïÀôùÅËgÑÿåƒ]þ&!¢ÿ›üv†{ÑÿM~»Èû¬ÿ›$§´œë\ýßdÁ¹ÓÿMÒï·œì\ýßdÖêžõ“ô-Çó¼POGô“ôÝ-G›Ïõ“ÿ•øYêÿ&‰™–ƒeú¿Ég`û¹†þoò1Øž2ýß$µYË®2ýßämÎ î•þo²Ï¹ÓÿMÒk´l[ÓÿM΃íþþ ¢ÿ›¤ŸiÙ&ü]ADÿ7yl-çêÿ&φ­êç¦ÿ›<¶‚Lÿ7y liçêÿ&O€Å}?ëÿ&yž›”éÿ&‰ßæo|'êÿ&ëaŸ9Nÿ7I¿ßüCø»‡ˆþo’:¬ùëð7ýßä~Î Æéÿ&÷†¥­'õ“ôVÍCÖ»ú¿Iú™æwÜ‹þor[Øœ«ÿ›$Ï4¿ì8ýßäF°geú¿Ib¦ù ™þo’ÐüçÓÿMP#5ßÏsE_ÑÿMЋ7ßiŽ×ÿMšo ¿SŠèÿ&ã°ë|ëÿ&蛯t]ýßİKeú¿‰×`W >7ýßýQó™°`œþo";Í5ô䀿 Ïãó‰èÿ&†aÇÊô aGÁøÌ"ú¿‰§a¬ÁgÑÿM< «†ñYDôτ짹ú¿ b¦¹Ì5ôäžæCeú¿ òLó2ýßÄ-°½½ýßy¦yÇéÿ&è5šwö3ÒÿM\ÛÎçEÿ7A½Ö¼¥ãôÄLó&ŽÓÿMgšgÉôƒ­-ÓÿMš×ð|ú¿‰nØJ~nú¿‰]ÂóõчGôÏ×·½?ë X,¾;£ú¿‰Ãçô6}ÆjTÿ7qìË0Þ¢ú¿ b¦iQ¸nTÿ7Anlz/ü]_Tÿ7AÛÔŽ\kTÿ7A-Ú4º‘¨þobsØHxŸ£ú¿‰Ù°þðóˆêÿ&裚ÞrœþobMØ«ŽÓÿM¬{1¼WQýßįaO;Nÿ7ñ+Øc2ýß8}JÓ+Ü« aú¿ñ7`ÏÁfÃôãÄeÓ-Þýß8y¦iAø¹EõãS°kÜ‹þo<›'Óÿ ¾çýÕÿ ¾ç]]mÆ^ÎtÏú¿ñÁNu/ú¿ñça'ºýßø°c«ÿ'›z\Cÿ7N¿ßÔ.ÓÿóL61HžˆêÿÆ©}šJÚ+ªÿç=Þt,x®ôã—‡,¨_¢ú¿qz즽Â÷KTÿ7NÝD9?xÖô㧇l±5c/;z¾ fìe[™þo|uXŸ5{Tÿ—£l:ðÅ›n`ͨþ/GÍÐDØGMÕÿo jGÞ/Qýßøf°Ueú¿qzˆ&rÞüà™Ôÿï [&|7Eõãô.M‹ûìêÿƩÚ"îYÿ7N¬6~+ÓÿÿÄ ïĨþoœœÒø±Lÿ7¾>ì=Ù6?߃ŸæRûEõãÔ\‹B§ÕÿÓ»4Ž„õdTÿ7NïÒ8à½×ÿG`oÉô9rJcÐû‘£ú¿9¥ñE¯Cÿ—{–à>ÿ¦ÿËQ_5¾;¦ÿÁ‚ïÊÉ'Qý_ŽœÒx—kèÿrô.w„u{Tÿ—£o¤íã¹êÿrô.×z_ô9ê°ÆøéÿräžÆKdú¿õPcð}^3ú¿Ü¹°¹2ý_îØ©ÎÕÿåè÷Oô3×ÿ宇ýÅqú¿ÜU°™þ/w¬M¦ÿËQ‡56x>ý_î,XµLÿ—#§4š£ú¿1Óx¨çÓÿåÈ)ò} ÿË‘S÷ò^éÿr­°Ýü,õ9ê°Ædú¿}Tã¶>Cú¿½xã2ý_ŽœÒ¸‘{ÑÿåÖ€mÏçFüEõ¹a[Êô¹a«9Wÿ—Û¶‚Lÿ—#V—–éÿrÀâ^›þ/·ëœÞ†}ïêÿrËèçLÿ7öìS?sý_nØG®¡ÿË‘S¦½÷ú¿½KÄãôcßÀ2ŽÓÿ‘S†dú¿±a eú¿±"ìuϧÿ#§4¼â8ýߨsé#£ú¿±·KÕÿ ÿ’éÿÆè» ¿¿ŒêÿÆè!O¢ú¿±'a7™ËôcÄeÃõÖ ú¿±{aWËôcô. —™·ôc7À.tœþoìZØ9ŽÓÿýv†ùMÿ7F¬6œì^ôcÄjÃq2ýߨ°£œ«ÿ;Ö!ÓÿýÖd|èÿƈ™†Zï³þo¬V.Óÿ5“éÿÆèí0fôcôÓ »›õc³`;Ëôcÿ£Æœ¼ÿôcô) ;ðyŸ¢ú¿1zˆ†­}'êÿÆèí6ö¹×ÿm›%Óÿm [Û=ϱ—Õ}ÖôcÔ˜ Áß ÐgDÏš±Æ¯aôQýßy¦aq×Ðÿ- ‹ø>Ðÿ¦ÂóõñŽêÿFaäÐ>Þ¹Qýß(5R}·èµ¢ú¿Qj¤ú÷ÌGú¿Qú£ú¼kèÿFéSêG§ÿ¥ß¯ÿ_λxÆAÝI½dÆÁïÃè—¢ú¿Ñ7a¯:Wÿ7J}_ÿ${&6¢ú¿QÞÅõÀˆ¨þoôDØ}0â ªÿ}(d?å7ýß(9¥þ©ð;ë¨þoô¦ð|?1ýßèõÎ ˜þoôØUžOÿ7J=^Ä}iTÿ7JŸRï?ýßè9°³§ÿ¥«?ݺ]ÿ7JÞª?)ü›Ÿ¨þo”¼UÄ}ZTÿ7JMX”õ¤þo”Þ¾¾ÓóéÿFÉ)õÛ¤§êÿF‰Áú Þèñ¢ú¿Ñc`®¡ÿ=v„kèÿFéSêô|ú¿Qb¦~_×Õÿeß Ï×GŒGõÙIXð{Lb<ªÿ%§Ôoçùô£{Á¶tú¿QrJý&Þý_öK؆öú¿ì"Øz0êШþo”œR¿ºkèÿF‡­dléÿ²?–‘éÿFWvnPëÿF—…­(ÓÿeÉ)uß¿ú¿QòVÝçÞSý_öØG2ý_–X­{×û§ÿËÒÛ×Mz>ý_–üQ— {ö¨þ/Ë{¼Žúo~ðþÓÿeŸÙO׫ÿË’SêÞp ý_öaØ«2ý_–œR÷¢sõYrJÝÓ2ý_–œR÷¨÷êù{yÀ=ëÿ²ó`w9Nÿ—%§ÔÝâû@ÿ—í…õ9Nÿ—%§Ô]+ÓÿeÉ)uó\Cÿ—=v‰Lÿ—%ë·}Óÿeç® ~Tÿ—­…]þ ªÿË–;—ûÕÿeKaÇ ú¿ìÁ° Æ‡éÿ²ûÁÚ½§ú¿ìa¾ôYrO]Lÿ—]&<_õjTÿ—å9­k“éÿ²›Àþä^ôYrJÝÞ®«ÿËЃՕ:Wÿ— žÓCeú¿,9¥î^‡þ/»ì÷®¡ÿËÆaËô™ïa³dú¿ÌW°µýÜô™·`¿1¯êÿ2ÔHu«Àèû¢ú¿Lö¿ÔÿeÈ)u‹›+ô™Øb2ý_æÎ9½µß Ás ÿËÜûL¦ÿËÐÛ×~ä\ý_æ9Ø´Ïþ/“„1øLÿ—IÀ²°Ïaú¿Ì=°açêÿ2ÄLmÂg\ÿ—!fjß”éÿ2ÄLí«îEÿ—!fj_pœþ/CÌÔ>å8ý_†:¬ö1¨ÿË3µ÷;Nÿ—!fjïtœþ/CÌÔÞâžõ™£` §ÿËtÀ®õ|ú¿ y«vž÷@ÿ—9 v±Lÿ—!fjÏó:ô™C`gº†þ/CÌÔþÝ5ôj½Ú Ïcͨþ/Sû‹Ÿ‡þ/Co_Ûí\ý_†Þ¾¶M¦ÿËÐÛ×6Èô™ßÁªÝŸþ/C­W[â8ý_fØ!Þ+ý_†Þ¾vÿÅôzûÚ=Ãk‹éÿ2QØ®á1ý_šÞ¾vÇÅf¬±Mèúbñkl.Óÿ¥ß…m¾‹cú¿4yµv½0öcú¿t¶–Lÿ—¦w©]5ŒÕ˜þ/M=T»ñA¼ÄôéaK‚qú¿ô °ÕÂwbLÿ—&§Ô® Óÿ¥ŸÓ[ókèÿÒÀ> ß/1ý_ú<Ø¢ÐÿÅôiÞÏ5ï‡ù<¦ÿKÓÔ|~Óÿ¥ƒg2þ"¦ÿK5ÓîEÿ—&Ô,ôèÿÒÔ“5¯»?ý_šz²æÇéÿÒôö5Ï{ŸõécaO:Nÿ—ÞöZø½ULÿ—¦–¯!‡ömÓÿ¥k`O„y&¦ÿK—Á‡Óÿ¥éíkîuœþ/Mž©¹ÒçEÿ—Þv™Lÿ—&ÏÔ\à3©ÿK“gjΖéÿÒô5§Éôémaó9Õÿ¥·€ÆBLÿ—&ÏÔåºú¿ôÚ°çêÿÒ«ÁšBßÓÿ¥W„9wULÿ—^¹äí˜þ/½˜sÉÓ1ý_ŠÞ¾¦6üÍqLÿ—"ÏÔ¿“æ½Óÿ¥è÷köéÿRô.5;»?ý_êض<㼇cú¿Ô£°à;õ`Ïú¿uXÍ&^‡þ/µ6+¬-bú¿Ôë°udú¿Ô+°5œ«ÿK‘gjVòèÿROÀ–q®þ/EïR³„ãô©»a‹ù>Ðÿ¥nÓ[ý­Lÿ—êƒ}á\ý_ŠÞ¥úc×Ðÿ¥:ÃóõÓÿ¥š÷_™þ/ų[ýyø¾˜þ/E¨Nº®þ/EÌT'¼^ý_Š˜©~Óuõ©#a¯ùéÿRô.Õ/8Nÿ—j€=-Óÿ¥Êœ<ú¿Ô¡Î ˜þ/EïR}§÷@ÿ—Úv³Lÿ—"~«xú¿Ôΰk§ÿK‘gª¯p/ú¿ñQ}±ãô)òLuoاÄô)òLõ\×Ðÿ¥Ö‚ê\ý_jU؉2ý_ŠPý×=|Æ=áw@1ý_j=X[ø7î1ýßÈw°÷¢ÿ¡~©®õ}¥ÿyVþ–.¦ÿ™†â\ý_Š\V½¿ëêÿFÈeÕ{¹®þo„ëÿFÈoÕ«zÿô#Ás:›¹Ü“˜þo„ýß09¥2"{îç½T|þ¿1ýß0±Zñ¥Lÿ7L¬V,òÚôÃ{ÃÞ“éÿ†w‡dú¿áeaÁßçQÓÅôüÇ+>ôèÿ†é*Šáwà1ýßð†°·Ì—ú¿¡ïa¯Û—éÿ†¾„½b¡ÿ¦w©xÚëÕÿ o {Ô÷†þox6ì~ßaú¿ab¦âNëýßð°›}ýß0y«b¾Lÿ7ôìçêÿ†¨Ã*æyú¿¡ Ø%~nú¿¡ ¬×{¯ÿ„)Óÿ 3÷ÚôC<Ï•éÿ†è÷+Žq ýßP Öí3¤ÿ"oU´zú¿!j¤Šϧÿ¢«¨ò|ú¿¡a%ŽÓÿ QûTÐûõQÛÇôC=°™þoˆ>¥¢ÜgCÿ7DŸR|¿@¯Óÿ Í…í`>×ÿ ÛÚü«ÿ:¶™ï ýßÐѰ «ÿ¢gªX×w¢þo¨ öcZÿ7DüV¬â8ýß½KÅrŽÓÿ Ñ»T,å3¤ÿ¢·¯ˆÉôC{Îé-ÿÞ¹ú¿!j¤ò¯\Cÿ7DN)ÿ„{°$Lÿ7DÞ*ÿ@¦ÿÚ6åùôCôöå9™þoˆÞ¾<å^ôCÄGù Lÿ7´ìs·þo°£÷룷ŒéÿyvË_•éÿy®Ê_éÿ`OÉôƒÿý+ü›‹Ø73ÖxÖü«ÿƒ=î{Wÿ7˜…õ¹?ýß 9¥üŸ^‡þoðØ?¼?ÌX÷bçêÿ_†²¸þoP~–Lÿ7øìÔðŠëÿÉ)å'†Ÿ[\ÿ7xìXçêÿoÖqýß ñQ~røÛž¸þo÷xyðÛizиþo÷}yµsõƒÂJÃëëÿ‰òưV‰ëÿOv.1×ÿ Ò3•ï¾ÿâú¿AzûòÝÂØëÿéíËw”éÿÏ‚mÃ\ÆÄõƒ§Á6—éÿ+a³]Cÿ7xl½°ŠëÿéíË× ßCqýß ½}ùªaNŽëÿçÀ–눸þopØÒa__mÆq犯þóe?†ÿE\ÿ7Hß]öµçÓÿ ÒÛ—}Öüqýß uXÙ‡áß•ÇõƒÔaeE×Õÿ R‡•;Nÿ7¸,í8ýß }wÙ Lÿ7ÀsZ–û­¸þo€ç´lT¦ÿøörûqýßÀ{°ç¿‹ëÿ&aO¸†þo€˜){ÈkÓÿ /˨1ûø,âú¿E°Û½ú¿j²¼Wú¿b¿,ø?Rø,âú¿bµìJ™þoà Ø¥îYÿ7@o_v¾{Ñÿ Ü ;Ë=ëÿn‚ê8ýßÀõ°=Ÿþo€©ìXÇéÿ¨ÃÊŽtœþoàvX‡×¦ÿ o•5Éô§Àª½§ú¿úî²Rϧÿ §”ß¿Cqýß9¥l™þo€üV¶§Lÿ7@V¶[øwEqýßuXÙN¡ ëÿƒmë\ýßÀ°-Œ7ýßuXÙFŽÓÿ üÄà¡0ýßïû²u}çèÿ‚gwMÙ3ÖX1ü-S\ÿ7@Tök™þo`]د\Wÿ7°æœÞÒeú¿•a߸gýßÀr°Ï¼ú¿z—Òdú¿~ž«Ò÷½ý_?}w)5fß!0ý_ÿç°Œëêÿú©ÃJ‡\Wÿ×OVúŽãôý<Ï¥¯ËôýiØ+2ý_?uXéóîOÿ×ÿìI?7ý_?uXéÃÎÕÿõS‡•Þ+Óÿõ?»Ýóéÿú çN?®ÿë¿;œ|‡×ÿõŸ»6t¹qý_ÿ™°yáßcÇõý×Á.p]ý_?1SzŽ÷Eÿ×OÌ”žî¸ªkœãý¯ž±Æq2ý_?1Sz”sõýÄLi§×¦ÿë'fJ›eú¿þ.Xñ¦ÿë§+­tœþ¯Ÿš°ôÇéÿúéíK4.õýÔH¥ûú>ÐÿõS‡•ΑéÿúÿÛE¦ÿë_üF3xëÿï¿‘å¹ëÿúégJ·ÝW\ÿ×O?Sº‘Lÿ×OVºŽ¹Lÿ×O\–®inÔÿ%>€^”5ãú¿D6K¦ÿëâc ϧÿë'§”FÍ«ú¿½xÉwŽÓÿ%¾€}é{\ÿ—ø¶H¦ÿK%ïû.Öÿ%ˆ’‚÷Jÿ— O)“éÿÁ²0â/®ÿKœáùþ Óÿ%x&KÆCG×ÿ%^„½æºú¿½}É‹2ý_‚:¬ä™þ/AN)yL¦ÿKSJðèÿä”’»ÌÓú¿Ä|Ø­Þý_â*XŸçÓÿ%¨ÃJ®‘éÿÄGÉ<çêÿÔ>%7…¿i‹ëÿÔ%%óÃß¾Åõ â£d®çÓÿ%þ ;Å{ªÿKSJNð×ÿ%È)%Ç8Wÿ— >Jºeú¿D9¬Íóéÿä”’ßWú¿ÄŸ`UÆ–þ/Áû¾¤Äuõ ƒç*Èo×Âô ƒç*Èo×Àô‰­eÁú¿Äf°Ý]Cÿ·0ÛÆóéÿR«”lúظþ/±º,¸ý_bEØFîOÿ— .K~'Óÿ%ˆ™’µ\Wÿ·ðGØj~n×,öË¿_þýòï—¿üûåß/ÿ~ù÷Ë¿ÿïÿÿÿ­ bÀ{././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/data/m83.moment0.fits.gz0000644000175100001770000002044514704473421022654 0ustar00runnerdocker‹b¸ÌVm83.moment0.fitsí}SÛF»Æ¿Š†ÜöØF’å×g83HKŸRpÛôaFØÔØ’+Ézæô³Ÿ]É–lÙºï½o©1‡Æ“pàǵÚ]{iµº<}ÿá݉¦h[^Cm_ùÞLC-òµ·§ÃK-Œlolãm? ioN‡N?ðz‚gý¬íÈ֢癳’½Î?ž^j<]ð¼ùôÖ 4ÿnIv§Žº¾nåýztyøñär;ϼ³”7òý`ìzvähögg;N;ºÅ5¶è3 £)õ}p?;“U– Î8ÞÈÑf¾ëE9ÚÂ;vF©J%¾E´—_ß­¶?S·šf§otÄ«»­>’âm«‹¤ý Þjûk˜ýf¯Åæ½;?ûp.˜ölôô¸='¼3QàGG›øÞ½ÍÇq«9'Œ\{"x“e…¼;®ñVå­è[òħ]œ\ß^ü”ò £mv =~%ú~øóZ{çzŽ(_iwóÇ\ò9_ž%ïðøäò79¾ÔÞþ»½¥ýü1·#?:VZ`øFÎt ïä§ŸOÏÎ?n?3Õ÷\K¨ëùŸe1mülðºüpr´Ð7<ÿp~tr6̵ç´ï{¹p&[°xþæòû“óÆÇ¬ý™f[ïÍ^·eÈñþjz­/ÞjþmèRÛ³ö±±Röµ×‚÷[Z¿mËÒ%¯ß6ÍBÞoï?Y{±zF§g6õ^»Û/âý§÷þÇã†`fõÑîX³i´­žÕõ¯å7Éã¶$Ê~:µ£ÑƒëÝkLJÓ˜¼–_Šú0u£ÕÐ; £?Ô[Ë´{ÍžÕ]ÜÓËóF¯£kÔ±Ô—¢¥¶7rüÓâñåßÚ§©6ŒFHyýpz9<¿øM;:¼<Ô.‡‡CÑ¥¿¾‘¦¡OÖ'Œ†¡Mc`öfK;={{®]^ŸÔÜ©}ïƒÁhâØÞ7ß”âïâ½G÷ph†¥mYºÿŽì;î&ö}lc®®µÈ?y¢·¤o–ã=:<™ 41–7 1ò͡ӰnÌåwÿ÷ÑX¾-¿á±©™}ÝêiC1Kð¾.þšÑXýÙÕ~UWÞG7Q´½¹;¾¹¹9Ôuó&Õ÷±ÓÓïzöÍÇÎm»9 ÷X­…«oêÓ~+ôMmonOöø<{ù#?Þ0Èüçz³;wqúöÊ–7í@üÀ’g{Ï{¥xáìiµ~¿Lk¯ ïÎu&ãŒWº¼¶9žgWÆ›?¶wO«4ˆIÈ’XO¶=g’ŒùUð‘í­Ôoiž+ë#ªŽ—Låªã­ž3«àÝ9N:ÿ­¤~'îlêzSû³ä‰7JòäÇ‘?™O½XŸ0‡hÿÅôùó(tŰª:þ¼ÛóDƒ~¼—úÞÚ“Ð)}üþt?Ôªá =ú$µh4A”SïÒ¼ÅIIÔÇ­sÏ®o9k])2‰‘¯F‰>=--—gÇé­Ú? ÞÄr‚‰;u£jôÍg³^ÈãyrÀÏÆ9¼¢ý âüé­˜rJN¸Þ ™õ+䄇¹»“ú¬ÒÇO΂3^«4Oê»s£åñ›ˆ²—:~Rß OLøŸKñÄÀìÍ\g$ç½Z7kÎ\}òóeP)JÁò ì1Ûsè<¹ÞØ #;’-fÏó“CXÌk¼{r'€‹ãg(•â9ŸÅ`0–ï„Êçˆ'´…îŸK¿Ö*­Oú±ó¸l/JãÄ“ÕY%o¡OŒyn«ŒW ú*ã-"i`D{Iã³Ò<×Sïñö'†P}þñîÿi9ä‹Ã§tþÀx²NªäÙ?÷Æê~¨ƒŒWžcq™ÏG*¼¸ÌðD;ó#mÑßÔÚ È³?¯ðŒò<׳oÃTŸÒx…è[á5Œ²<1ß—–|Á+ß^$o—T»µCgäE•ñìäâÉ‚·gÏfKƒÀãÝp6YÌXUÇ{¬Ü ‹?ŸUTö£3³ƒrüº?ŽÓÈIµ¼OÌ-©y˜ úIz ò^@ ëÛ} êcä± o=UjÏ ‘Ç‚œ–?~òáN'‰¾Õá”Ù^ü§‰.OoåÛŸ<`Ë¢64?Ýœy÷¤±¥¯øtÄãŸ>˜í/|öFi{>Si„×îZÛ®WpÏGoçç#Lß®ÏGRß¶|—{>Ây´ö‡ðÈç#„G>!<òù+/õ|„ðÈç#ŒW8]àñÈç#„G>aí…z>Bx¹óQéú Ÿ?ùüµÚù£30º+Ÿì‰{Ø‘¼'1t¢dªôƹw=OÞÌŠÑ}=¦ã©6ß¼¿¼Œß–­uq’ø¶‘¼êñE79ìÈlXó䗆ܮ!ˆtñÑ™‰/SÞeü~Mÿ4ÝkÚôRþwúU˜üšƒZmµml{”·7hé@yãëç£HˬÄñ›©¾QÔ(~!¢P}¦ðy}q¯"×Nèé›åx»]?¤ o§ë‡p}¹õ*ú^I^þþÆnÝè×M£n¶Òò²Kå¥ÝO§PÞ¸;' áð|\¡ÖÓà<ÚzŸ„g<Úz$œG[ŸƒóhëUpÞ4Lj¸*}c_|"†ë„‡å‰8/£U£ïÞ=Dšè˜wUˬl<Í XÍ(|ku­¶:[É}ËÓcÐ §¾=¤ß˜ò¶þ€hg3?DO’+/~y—c ,¯.õÅS} Õë‹—ÒÌ–õ{U“øí .>¤úÄ×”ãT­>1ÞOíLß•^׌ºfÖµl¼¯kV]k×µN]ëÖµ^]ëÇ cñ·µøÛNþýoÊŸ_×Åï–K”›Ýßž§Y^9à×ãa?[ÿY¾Bxú„¹ãýR_ùñ%þ<[%Þ•n½h}Σ]ŸIxùùÂkö÷fks~ô¢ü}{`åõ•ò÷oçþÓ·kèËù{õ÷oçþ¾½9ß/åïÙß#<²¿GxdðÈþ;~TðÈþá‘ý=Â#û{„Gö÷ïÕù{…ò®ú{ãKû{…ñj§þ뿯Íßãým·þÑGö÷ØxOõ÷ìïÛ›ùÕ«ö÷A+??zQþ¾·y}¿”¿Gx;÷÷˜¾]û{D_Îß›¨¿Gx;÷÷B_~}D)ðÈþá‘ý=Â#û{„Gö÷Øñ£ú{„Gö÷ìïÙß#<²¿Gx¯Îß+”wÕß·¾´¿W¯vêï±þûÚü=Þßvëï}d÷TðÈþ¾?ÐÿQþ^¸Ð—ìïÛææõ…2þãíÚߣúvìï1}9ßúËXnÎãíÚßcú¨þ-/Ñߣýƒèï1ÕßK^•ù=Æ£ú{ŒGõ÷êï1Õßc<ª¿Çx¯Íß«”w—þÓ·kößWæïúÛNý=¦êïÑñžèï1Íßwº±¹^` _?›¸Ñ7ßjWqçuÇálŠòãÒrdíÏl%ýʸ֞ÜÑÃAma“±è9À°ÿ-ù7åþTÛŸzÑþï¶Žü}yòß'Žïí Â…6u½Û1›—ûï{­ÜîKÜ~þפ¼ô×Uß^¶¿¤<«fØcÿ : <Ú~Ò8¶Ÿ4Σí'óhûIã<Ú~Ò õAʇ^uûIãõýŸqmÿgEžòþÏÊú÷Æy´ýŸ^uû?ã<ÚþÏ8¶ÿ3ΣíÿŒóhû?ã<ÚþÏ ÜŸ´ÿ3Σíÿ¬¦O}ÿgœGÛÿYAiÿç„íŸHÛÿçÑöÆy´ýŸqmÿgœGÛÿçÑöVÖ§¸_³²¾Êx´ýŸ <¥ýŸUÛŸêþÏ1Ïʯ÷áïÿ¬ÆSßÿY§¾ÿ3Σíÿ¬ÎSÛÿçÑöVà‘öVÒGØÿYIaÿç„—_ÏÅßÿY§¾ÿ3ΣíÿŒóhû?ã<ÚþÏ8|ý«ÒþÏ8¶ÿ3Σíÿœð@¿KÏc-h?ýÇÂúvŸÇ‚úhÏ÷ÃyŒ<ä1òXÇÈcA^.ÕF«Õú«ÕkõÓúý×;âúhû3(Ô/=ßyŒ|ä1ò]ÇÈwáöBÏw‘þAÎw-ðù ô|ä1ò]ÇÈwA#ßyŒ|ÕGÌwa=ßEõó]ÇÈwq-ßUÑGÉwA#ß…û/=ßyŒ|×ó0z¾ òù.Ècä» ‘ï‚b¾ òù.¬žïÂ홞ï‚r¾‹ðÈù.Â#络ï"<ò~ ¼ŸVëyìñ¡ehÿÒŽ1ºiûû{ógD9ßÅÆj¾‹ðÈùngóù¥ò]„GÎw9ßExä|á‘ó]„GÎw9ßUÐGÊw15ßUÐGÊw9ßUáQò]5}êù.Â#ç»Xÿ¥æ»ÍýôK廜ï"j¾‹µgj¾‹ðÈù.Â#络ï*´?R«Ðþªä‘ó]5}•ñÈù®*O5ßUj„|á‘ó])ßíÀë×Èù.Â#绊<å|á‘ó]ŒGÍwq}´|×GËw9ßUà‘ò]„GÎw9ßExä|W¡ò]¬>èù.Ècä» “ïnì¿ëD¿?¯…»É;ë/ï%ìßÙ\o‘é#ï_ý~ë¾âÅ<^~1kݨ›õêï‹yk?Zí<gÿqPcÿqˆÇÙâqöãFÊ;qnŸã’êþÙ /²½±Œå}3&NÐøñûÆ~ö÷O_̵Â,•D3ôÉ\hù¹*ŽŸà¹SûÞ©Š7qÃ(ÖVÓ?î&óÏc9õžsæ…Y¿3÷óòÇ×Í8wK/Wßÿð'º-¯»|øÖÿüoÙòŠÉ¾3{p¦c7¨¤>’‡3ùÙåéÌ0B®¿AúÂQ`G£õöÒÛ¼,nÃÁ`ÌaúXË«õGX¦ý×;¸j׋ÇY^é×ÙÃr¦É{µ {ìÚ“GgâDÓhº|eüXË•ÿIy‹ïXÎD“xE>™gyF[|¾|´ÚÁög%}-ï²¼ùû·¾–÷u—7´¡+ÎÀƒØ·‰¿ºöV ˆƒ«Ó½ÙÊž¯Öêµ»­v·­›õŸÄÿèõŸã‘ÿ^kÿµßÐâŸÐ·ýïÏê3Võeë7¬¾ÙÓ…¼n{ÇúÌ}fæï»zÇêFߨõñk­êË®ïw»½^Ë2tëï8~†¾™¯½Þç''åÍϧ×Êë‹Þ/ëf¥¼ò­F6_­äéÉ…ú¶<ÏõU×ǖ羨úز~÷U×Ç–õŽ/©>ÌöÀÊ_?,qôV,ážo–ãAQZÞJVÿñôùÑݪ>9ãß+óU^<×[lã½æþ!Ë ž/wÝ?dþ'ÕVÞ×GËÜô¯¹>dyÁñ`×õÑ´ ú‘S£Êg _kÃøQèëÏ+Ü´§âÃMøàÑëÝiâüáßiÑóÌhCíGQ=!®/¿žÐwù<†¦úæ“Hs=ô±ÆM‰Z‘ª~ü6¯Ç/4ånX¾[ŠWx )-¯YÍ \}E!k/ÕÜ ÀÔ'>Fq#&êË7eT/SŸüb)¤o½ù⇓©/pîœÀY.ð¼ªË‘Û?Û R[ðÒq–ɓזW€×®Y0yölæx ߬°¾Dåø…³§©=KÊ+“9Œ·y½{…—®Ý—+éyw®BÇËú@׃XúÀ(Ìs×ÏAÞ ¸~ëÛýõsP_îúùFË2âøèÅë;A^þú¹Âõx˜G¿~·?úõsXýú¹äåýK™ëç qýä1®ŸÃ¼õëç+7çñxŒëÝz½ä1®wƒ¼õëÝWzÓ4:ýV¿®éâO:¾ˆÏ±¹ŠÊ‹S¿ôëç qý?~´ëç qýˆ×Ï1}Ôëç’—÷/E×Åt+þÂk†ieç7]®Â“ó¼N·×6{Zà?…Í Û_Þ¿”õ/y½¬ïeøB}L¿QÈËù •õ0ç7Š×‹òüF±>žß(ä1ýF!é7 òrüF1ç7 yL¿ñ8~£ÇôÅýwÃo˜f¿×ýÒ~X~£Êõz é7ª\¯ò˜~£Êõz’—¿¾úÂü†Yè¯x~£˜÷2ü ïEøb}<¿QÌËù SÍo<–ßÚËoúX~£˜ÇóÅ<žßËËðfþz^I¿QÌãù ÇðÅ<žß(æmúVÛè[_ØoõËòÅ<žß@ŽÙoóx~£˜Çó >†ß0óùÁKóÅ÷ƒñüFñý~/Ão¼äûA}L¿¡z?bKÑoTy?"Üþx~£ÊûAÓoTy?"V^Žßȯ·-ë7 yL¿ñ8~£Çô…¼-~£Û7¿ôõ ~y~£Çôðñ£ûBÓoò˜~ÒÇñ/<ß×ÿ½¶õ˜²¼/y}¬ÕÛœ¯¾êúèmúû—Tmks=×k®YÞ—Û?zòþA¸½¼ªúHÊ ÕG¼Ÿ¯8¯ÕHüfªoUR#ÛõÆæýûñ&HBäÚ”5}³o·û£'úòþ`MßN÷GÇõ妬:ìÿpm¿pœGÛÿYAßÚÛ¯çÑöãÆy´ýÂqíy• Ç´¿7ΣíŸó–› T¥oì‹OÄðšð°ýÍ^Þ߯ò2šº>ˆwo‹’ÞpUS¿?àÖöÆ3; o©ïG¦>z¨ÕµZám1)oñó³1¢ÞˆßþÔÚÝ®w—öøå]ŽYñíâ—ëñ?©>ô̪öâéK¦˜Ëú½ªÉ ‡í .>¤úÄ×UHäéKï5Hô!ûÁ+µç§Åð,yq©Çý$+oòµS²Ì<}3[ާK}åûoüy¶ßºxWî6X”ïâ<Ú~Œ=¹ÀÆól^³ß•å…üýÎýnk`åçç¥ü.ÂÛ¹ßÅôíÚï"úhÏÇyd¿‹ðÈ~ÓGõ»X{¦ú]„Gö»ìw±ãGõ»ìw[›ûG”ò»ìwÙï"¼WçwÊ›÷»FüÏò»ˆ¾û]DÙïâíy·~ÑGö»ìwÙï Þ?g¿–¤¼ù¼ó%ù]KßÜß§ŒßÅx»ö»¨¾û]L_Îïšuë/ôŠû/Æ£ú]ÉË÷ß2~ãQý.Æ£ú]ŒGõ»êwÑãGô»êw1Õïb<ªßÅxT¿‹ñvïwï*õ»*åÍû]3þçËø]•ñj—~í¿D¿«Ðžwêw1}T¿‹Ž§D¿‹ñ¨~·m ôüýÓPø\ÙËäžféŽÚ²ªåÇïûw§õa\kOîèá ¶0‹ÎX´G´KÁñgÚ]àOµý”çEû¿Û~8ò÷åiu?œ8¾·/ ?ÖÔõnÇl^î¿ïµn sº/‘ûÛU6¾,åßÑ^äñÛXoÿ²µõò[~=ƒ·óõò˜¾]¯—Gôùóh6•}²­V²?¢/÷°ã´;®ExÅ?çñŠÍ8'Æ“è!å­?>…Ã+6§<ž4ã·éÃC÷ô¦bù=Ê#Ý¿€Õõþ„G¾«Â‡ŸóxÅ“¯øaô<ù~ T_üðäÊÊûÉqfÉ£kó“ÕÞ¯Î8áur`¶6÷ƒ]ÏÿØ,oþø¥åuîDmÄ_Igt,¿”qXrIªO›þÌ "× Þ磭ëš÷œ|9“É烚Þ4í`:£ZüÎóÚ;aärƒlþßsðþíå"*lld… kçÅ!ßugœ(ÿØÙ|ë -Ÿòɇ¤ÄkY´xêuäÈ©ÆAÍG` [ÍŽ.þ«Õ£^¿Ùê ë%fE2~\TY–wæªîEB©ÕÄ·¿ œ??"?;ÞH~×–*)¬«èùBOŽ{ÿ%ïþ.kãýå@;ß‘éo&ߊ3Îd¢Ý:q{ïŒ7U¿Êé;³£¹h¬ !RR1/Ÿg/y#áë¼÷s(Óù$r““üÑ$-¯|Òâo¥”ŽR^}Gñï—“ëøÆ!£®eõ!š™&Ÿ ,Ôð!pÂ_@>Lvúcþ¦Ò¿K_Ü$å!;#ß{ž+Êôù%\±¾¾ìZ…ã銾ä¸ÄÆó#aäílü{Àg'¢ßS¡¾·n$oºu쩼+i¬­äróëÈâÓà@3š}«Ó¶´Ûgùôážnuµo’‘î[ÍŽÄ|Zkô:M½Ýêkߌûoôå×O.õ¹G™À‹Xƒ¬áÓx¼¿Íô=¹ÂuFNÒKq!øÇOžß ›ÓŽåÉóùË)¾7²×烋·ÊòVç[Ò \Õ>š­Ñb>œ–w1Á± ÓjßÈoX›×Ȭë£Ñ¹½[›Fw­žiŒoâÿX|{vþìºÔÃ%yåM>[”Z”÷}¯%Å%³3OÞŒùéÓ±× ƒ7vƒWOt$irâ'¨hÐó?”ßìyæ —/ûô|õö·8±Æ»7˨'7¯ŠÖ±É3å^Z+ÏÛ7Y™íöjæ”å‰y\dNLJÃÃÚµ¶?¥ÄF²á¤¼¸5E©¢p~{xñqxÓ^ ¦ÉY>¾ü IÓ¬®,Ž_~yçüq¡}mˆÉÞ-ÅÛÍõP‚¾õÈ);ìYžXv *¥/ÁÄ­z¯/JSI ô¿zºþ¯–ž­G×ÿj‰·’gI×­ìï·ëz·ÞÛx¿_7ÄÀx ¢Ïù<šÌ…©þ:Ô ÷ƒWçOy¼ÐŸ¸+‡ø¼x=ÿåäÝùÑéð·e/%9ˆq&(™žf†[u­—”l¼Êƒ/ÌüF.\¯L«e¥‘ Ùìhñ@±-LyE!ñUT¿­¢x޳öWMœXNßfœXȳâœ-qb–ïV'–Ò·%NÌê#‰ =Ÿ'´@±Œ¾-qb¦¯š8q‹>q¾³p{8N|ÈæÓ•Ä‰¥ôm‰W®§äòDËìm™'ê;a¶zV>PìYͶÞ.Ê×õmÉ7ÔâÄ}Õæ‰2ï”úº²ýI‰ùí‘^Ùùw£¼Eãß <ÿöÛæK?ÿ6º-ùÀzüœò ‹t¥Û³¹¹þŽÏ¿,}Àù÷màÆ¼¾&ŸÝmnn÷ðÊúïzy[›·‹í¿ÿ¯ûo¾~7òû÷_–>µþÛ´eþÇôߤ¼_Ï¿¯µÿŠúí_Þß}ÿU×§Ü»«È?ïhþËÑ÷%ç¿}_rþ{)&yRŸ!}†ð7ž—ºÛõ4,}”õ4-«'ê$žÿöZzWÌ…óó_³Ùë™Ý‚ùïª>c u7ëiŽ/µ“³cíÝù÷ÃÃ7ïNJÖǯ<¶ž<`ËsæÍh.­G½Ùjêšhž¢øí}ÃÜ׆!Ÿ5зdäRÒ×××××××××××××××××Wu¯ÿ$êdþ@s././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/data/ngc0925_na.fits.gz0000644000175100001770000001343714704473421022437 0ustar00runnerdocker‹eºÌVngc0925_na.fitsí sÚJ²Ç¿ÊÜݪk'1 Ñ‹Ç]Ÿ*ÂV¯ì$÷Ö­” ²­ H,$>Ÿ~{f$! (YœH‰ù—ÑôLOwÏ_òXë »*B×(☠ šÚÖ£½Z¬ÑÆFm2FënÍôÕ,êjj“¡öñ¯<}µÒ_ÐLßèhó²4¢)»£¯|ÔÆèžµ]<+d?ºdsaXkÓ¶ÖÑíëZªíƒ×;Ñ¡M à<èanO¿.Ô9Í÷jkBÚwÑ¿iÕy ¡ ß?WÆöv55¥/b/•5AíªãÖ`¼û®Bß ðA|‡ÖOFw=5/Þ 9VG÷êxÊQO嵕‰Z(ðp½.—¸ZIàvÄÊàaMFÜjC†Œ>ÁQêõJíö1^O!ýÇsœTâä_óñºúzƒ–+{j¬×¦õKmÞõ50/±ïûO•æÛ^e¸Þ;ËܬÉè{œo¿'¸^u8hÝ"o<óeÎ=Ôwœ€*êÒž>ÞHAmµË»W»#µs`~ð¨ò/ÉÀjkƒ+„Qw<‚7oÕ.ˆ%#~DéNF÷J—ñ¤²,ùÛ'¡Š2ßé³ÎHýgþw´2ÑW}¾ß„G‚Ó¾zYëpÔA-qøoi~7æ‘q½¤öœöáÝhÉ<^ˆ=´ó/§òˆ=rå=„œç‡HíAÜ4yët{ˆž=À•r5 >DžùûÔ<+8Y_sÓ2€ ¡Ú[ÝÚ`ôÁ¾\Òœæï%ü¦‚¡wð:Z·Û¨¶ÐŸ 4]SÍÈål×pY˜¯ÊHß ˆ®;j³KL)Äs ‘o”åop7é+<8¹M‰ö ™/ð­®2__tµ¾œCGÚ?6vc#Fikã?¯Ù«DG§õ'ÊP%FÁW¨ïe‘k6^Sé_t/®Ð?ïÜ0KW0ð•n‹ü“Û‰xrõmhàÅÑJܾ jÙÖfeÏI ó´ÒH;0$“òHRHRü2`åTixS{µ2æúÆ^¡…=££ZIà ó”!²—šÎ_£Û“Û7ÑzêHéß°Nã*Xlˆ¸Á7F ì=ÞpÞ‹á¡O.c\ã$™F ðŠE6ìU2Í—ˆ’Dhüµn'6ÇHu_Àì…Ÿ/éf¥ÏÑôY·,c]ßòZCeÔ»Äo…ºjâ! £¡CáåÚ˜¾IÑŒ'PDƒ@ƒü£Œ´ÉÑ7eÒ…K}ݲÃ<‘ñ8Â+Ïáë?e£õ³>³¿Aj­?¥áÉ>ÞxûÀêB (^õ3ÇÜ.ìü0 _àzçÆJ·¦‡s¤(^ñ¤2…µº°=Ì ´1,Jà»@OÁ«»×Ky“OÙy¨oÃtÓ§ææ…Íä)&°Rº­,§ê?Â{ÒáǧÛÕW# 2ì&ÆÜXOí%ñ,nA gxÜšTbÞÃ|z&Kå7só cnaZ0ÅÖ¤„¸1¿Â‡ql4ïóÚ¤°9µË•±^³+ôþ¤r@"´2fÛ鑚b7R»ª2& Ü…€!Ÿã„´«ˆËë îUR Œ^ε~²Õ|Ÿç_ε~ÚÕ<ÌË/ÞðxÑñ†˜¶}¥5¢Œ>ˆ×`=wÌARyëhL+•˜ DY°¾à®GñbíѺg˜Ä./Ò|Ò« ó¡~œ\_tn.àÛ{u4¾¦lèH÷EdTz×o îú§…•¾W»§Ž€î1ÀÜ>æ¬C<-Mw>ad|ß°ê?z4ç1S7‚—ݾ¼Å{-ömuýöåAû6‡Ðß™íË70ŽâUZúÜ|X‘pwM–ÚùvfÌñ­:Ô>H2ƃ»ñ÷œ$ÔÞ‰õš{âÅÕÞ(‰6Až—Ô0M`h.‚Þfn_sà†À¾÷ÆŠÚ Ü:QG*^§«Ü¸Dàu Ð{"K%CAêz|ómwwO‚q"Þk·4ÁÊ=Ó*£í×xd4ßã‘°7=OëóÌ!øOaw<Âî½d<×!pŒâx„˜Y{˜·³/£»E§úêØÙ²ã¿¯ßS†,#çüÞ` ³B@RªXÇtah¾ùò­Rà±èh±!)Q¼ñ‰ ¢ÆE•ñZ¥ÃÅ’JÂ#Ä»k*£‘ò %ðœIx#µ£ÐöÁôÉæÑ£±2Hº¡;ÛR)yŸBP;s6>uRr²Ihíƒñh¯Œ˜øÑ¼Ö3Iÿ”¯OàF©7¾‚/öò ÒÖ)9Wáƒ:cøÐ:{Áf˜§Â‚±²vY3ÀÉ&ûñ‰ütx~‡t!ÙŠ¾Ç .èBÚ=ȃÁò¬¾†sÒOd‹³Œ¾m¢xΑw´}?‡—æÈÿñþøó¤\$òQ¼"þ;—øï§æ¿ìñ“óßÔþ…î¦Å-`_Óò'ºÔ”Éñ"b,¯éðšÊX¥Ø¿ë¥ãUTˆö¶s¢·’ðØ.SñÊ/"t|ªª/Ïý‡—“}ó¾Þñ°«Mvõ+/ŸŠ%e8"þ”lÛàjƒç å ¶ñò›¿./¯ùËx!û²S‚öe'ÆÚ×ãåd_ÆË/~fÁ(aÅßÖÀ̶â÷SûYïQZBÓJ¯*Õu‘ýÇöÖhêvâ+Çy¾c¸2¾šöv=¡;ZOí~™Ý(q/ÆrÅ‚Ã4õù<CáÈM.â”0¯³ ³"~vÿ=W…šŸ×n¶ýÌþžßó÷ŒçþìØ˜ÚT±AÆöÝ=«J<›ë½ŠÙÙ+ô%ÿQ} '48|˜އZäÜ× /²ñ¢õ%ˆÿ=ô%ñjöxÞ+Ó—ðüNKÂô%¸Âñõ%u&–u%¸ÌÕ8hAºÐ—áú’B_’Œ—H_r4î/ô%QËù¹êK²Çç£/øY¬6¤$ú’}{œIý¹Ð—·o­Á%Ñ—œ­}ýý…ìúbßzC’¢x…¾¤Ð—ú’üB_’ö8¨/á } 9ÿ¼ô%¾½Ð—üÖú^Žâñß¹Ä?5ÿÍM_òÃòßB_Ãû9ú’SöSÎ[_2¹ïtoÒí7’ÝFH¶à¯$ïµñHrÔRÇAýBþþª ÿ#Üœì/j?@_® »ýÜú^D¯yáý æ¼ýY¬Ë|²ýçz©~ãúâVévàÝð2E‰-È£û ðÓ´@õ}OûmDÕäQI+ã_HÿnF¹× Ïɼò³›¹Ï•Pßá=:dM_̹æúâa¦y;½v~Œ¼“" µÏÑGxÂWmODÛ/H™ÏÝà;ÁêÏ£eg=;4ÌÓv¾VdŽ=‰'ÐæÙ@O+3Y¾êŒçLßÙ„º`o즪;Sc Ò¹éͪQ¼üÖóW§7ãQ¡7KÏ+ôf…Þì7Л!>ê+\Ä)9éÍDA¬ ~Ó‡ý3;5àŸc¼ß>//ÿìòøC äÓùgç÷Ï»zljÿìêëeÔÑ賩& ‚½¨ToÈA½¨Y/*1…¨S¼äùèB/z˜WèE ½h2^¡ÍW/ê.çg«Íoœ^”<õµÖ¸(^¬=Îd?ù×׋žð¼*j_IŽâzÂBOXè ó =aÚ#ä¯ =áyë w z¡'ü}ô„Áø@ õSœè~¡sÿ~}=á©zQ¡Å+ì{.öý©ùo~zÑ•ÿzÑÞOÒ‹ž°ŸòËêEë AÜkßÏÒ‹ dKlO/ŠålzÑj]%ÉÓ‹J–äsЋÊ>½¨\èEó^•^TÉ3£~¤^”ÌÔŸ¤ ?>Z/š}=uzQzÑô¼B/ZèE½hœ¾Ó{æe}>a‹Gô¢ayâo Ðï¥=ñþ…W¢ÇôxÂ^û$Q”êb5cû„_J/*4Äj¯Ð‹Òãìõ¢CÆ ÁKï•>±¬È,Kvº«‡yáúF“œò#ô¢ê„G%¢×°pi컋ÎdØP>Èœô¹Ç‰æjåïË£Mw@/нh3‡ö½½èiùQ¡ï,ô…¾3Ž—@ß)sG×x˜÷Êôqå´ƒ¼èýËèx-´ª×œ¸wOäþ¥·œŸÛþ寯O<Ù¾RïµØ÷×ߟŽÖŸ&µ¯ÔàjQ¼©?Å‚øNàëEúmenÀFÉx…þúÓBzçÿ}©ø,õ§MݚєŲ7ð±Z€Cü+E?Ì{]úSß‚~úS°Ç\·¾ÐíhðÈÄ&&Œ”ÕÒž'ÜZ ò =k¾zÖ„ñ}¤ZïµÄ“E¾kß$Ï?=[ûùBœ}¥"_(ò…"_(ò…ã¼"_(ò…"_(òçUqÿG¾÷œ¢8ïû?NÓ{ó{õìH½÷~þñëê½ETè½Óó ½w¡÷þõõÞ<®GÝÆEœæeÓ{ËÏÕë~ÞÖ{³Çïž±Þ[Dç¬÷îÃÕ~n¶ÚîõîÖ#vâüùÀgª§öxb¨}2ÅzM®Ö3¶O<3½÷Ý=„N{ñš]OKïïú^¼&“߇+ˆQlVú4yXæµ<½÷Ú}ܺU¢xzŸç6m|«u&ž¸=ðô^úB_Mׯô8o0j³z±/œª {5cuÞ¥=!R} tt(íò´žr3ÊæOk IÜ{^ã1Švþ*õMaõrÈqsn9óÀýy6Há–ð|÷T-Ÿ“ycµËRâ2Ž×‘+ΉG`»;4²\q×´”®ÖÌ­}¾|0cÃíN3p×ìîö{òàù÷ó²Ã×ëæ¿YºÞa¾×ëÖs.´lwíµow¿ ql'óTãjÌÏÆóúÀ»÷üÕéý׺š#Ï­‡e¥íõ_Î<'~v¤¾òy¤ŽS Pÿ©ÝîXû_õ’Æ¹×ÆH)½ÖAŸ¯ßÑÔnû”. òîî›;ýüx'Ÿ†žwFGo”IÄû”3¯£ÜÇù]o‡ò£¥DŸËwÏ™'äÌsæI9óäœyÕœyµœyõ|y˜Ë‡74ïÆïnDÎK8I†¸²¶ëe¬×h©¯Ix}mÂâ Ú°, ûñ$½PιÐÔ!¯Ý}ÌØq‘¼žÖ*“֭뱤ÛG?G½ÝŒK•äÑ'y¼´Ï‡ÛãUóáÑÕ÷í-[‘¼šãEtå|MÏëtÛ> åa‡‡³ðF ­\Òæy7Q§1H(¾W[>àé¼~sðQ;Í Õ%²ð>h}Úwn÷Á°¹r>CÖÙ缉3áJb™ëùÍG;3TÐÄÞ¥QÇ‘ Èùö{Üõ¶½á¥×¸:'¤½àPüGÚçðöÚXA-ËL¢Á5æ³c<ˆn{Š $õ:}¼£ÀxCCÿ‚VÆÚœmáº=à7ÓšÙßÖ^³«ôÿLZªúëC\½ù†ª1^ Þ΢ñ`½]ku9'¨·»¼¼öß/Xo÷5pWoï)ŸáÜx½™ËË©ÞÎxðÅIßpù*ñ—#¼Éˆðˆ·+_¹_à+—•7:šç ²c¹ßK8þ•ŒV:¡ÕՆǮw·ÊìAO î‡ö”1¬ù íáÛe4™÷CëmïHQâËU\¹ùÓ0–¨ƒþqÚä<˜tï?UšªÒ;Âc¤Ë²,ãj4ïÂcy®€­ƒÌ5z\Ù ú”͵óx£E¼ðvGà“–æwò µ±!ú ™SôUŸoÉã‚à ÀãMFJœÙ¿È 1|ÿåÅú—˜Ù»ÏËË¿0^ÿÂNŒÏ/'ÿÂxà^|Þ%î3}˜ Qóø’ËËë¶|C†K–â½÷±7è¡Ìã¯ÚàªQ¼ØñcÝ}^^ãñöôä&ÁñGNäâÇŸÇËiü1ÞÉË[ˆÇ–7gA ­rYxn8Éò"ˆšCA}Ý­ d[ó´A°¸-àžÃömÝù¤GyúÃúróý™S˜q4Èk‘z&‘øEGíJ·j÷"È@¼G} p òúÊGmLyˆöbøì´¼Öh¨}tybµÎÑ“óò:Âí»WºNû¤²ÌªÌ2RºöT˜hñþ f¶íóòòŒ—`=Jè<^Nú«a³5H¼?] ìOã†Ä5D>Š—Ÿ=\^ðv°ìö`¼$öà>ù±öðxþžl7@ÀWþOÎùK<î5ñ°WôÓþ !Þp¤*k,Âr¹V‡fÛÆÞ$ˆH”ÊGk;áöúü#«lðu/•ÖÜ\"{»¼×€ø&ïœæD«ÇyJô©×X©ä=×ùGú`Ày²öy‚[XBQpyoÑG‡™Ò;¯\–ªuïm2â‘öášà%"lçï?‡Äœy±Ä=ËȪþŠõ47.õÕtaZo€ùxysû×›ÿûöÈlÙù?Hi3û+ÜàÄ(^¼¿:î öyyé/¡¿úŸŸ{¼œÖÆË¯^âð"ë%'ð*Ý3 Ð^!¶B¤lë¹Ù-[¤«ŠäõY¢îìÔ8Yd¿¦‚,v;°“·“Û3Ÿ"Trƒ‘v£õi©hñnÎtÁݤ€1;Ro4RoQÛÿÅä ô öŸìme¢’¯À#ã½ÄUK˜Ö÷*¨cún_·-t³2 ë›9}F/p”‹Òl¿}îõ’ÆÁK­5þ´^hði=gX¹¿¶åïn¬Žúba! Þˆ¸²è£5èõÔþ¾£7û]væÆw“ÜáÁdf4'), ('BMIN', '>f4'), ('BPA', '>f4'), ('CHAN', '>i4'), ('POL', '>i4')]) beams['BMIN'] = [0.1,0.1001,0.09999,0.099999] # arcseconds beams['BMAJ'] = [0.2,0.2001,0.1999,0.19999] beams['BPA'] = [45.1,45.101,45.102,45.099] # degrees beams['CHAN'] = [0,0,0,0] beams['POL'] = [0,0,0,0] beams = fits.BinTableHDU(beams) beam = Beam.from_fits_bintable(beams) npt.assert_almost_equal(beam.minor.to(u.arcsec).value, 0.10002226, decimal=4) npt.assert_almost_equal(beam.major.to(u.arcsec).value, 0.19999751, decimal=4) npt.assert_almost_equal(beam.pa.to(u.deg).value, 45.10050065568665, decimal=4) @pytest.mark.skipif("not HAS_CASA") def test_from_casa_image(): # Extract from tar import tarfile fname_tar = data_path("NGC0925.bima.mmom0.image.tar.gz") tar = tarfile.open(fname_tar) tar.extractall(path=data_dir) tar.close() fname = data_path("NGC0925.bima.mmom0.image") bima_casa_beam = Beam.from_casa_image(fname) def test_attach_to_header(): fname = data_path("NGC0925.bima.mmom0.fits.gz") hdr = fits.getheader(fname) hdr_copy = hdr.copy() del hdr_copy["BMAJ"], hdr_copy["BMIN"], hdr_copy["BPA"] bima_beam = Beam.from_fits_header(fname) new_hdr = bima_beam.attach_to_header(hdr_copy) npt.assert_equal(new_hdr["BMAJ"], hdr["BMAJ"]) npt.assert_equal(new_hdr["BMIN"], hdr["BMIN"]) npt.assert_equal(new_hdr["BPA"], hdr["BPA"]) def test_beam_projected_area(): distance = 250 * u.pc major = 0.1 * u.rad beam = Beam(major, major, 30 * u.deg) beam_sr = (major**2 * 2 * np.pi / (8 * np.log(2))).to(u.sr) assert_quantity_allclose(beam_sr.value * distance ** 2, beam.beam_projected_area(distance)) def test_jtok(): major = 0.1 * u.rad beam = Beam(major, major, 30 * u.deg) freq = 1.42 * u.GHz conv_factor = u.brightness_temperature(beam_area=beam.sr, frequency=freq) assert_quantity_allclose((1 * u.Jy).to(u.K, equivalencies=conv_factor), beam.jtok(freq)) def test_jtok_equiv(): major = 0.1 * u.rad beam = Beam(major, major, 30 * u.deg) freq = 1.42 * u.GHz conv_factor = u.brightness_temperature(beam_area=beam.sr, frequency=freq) conv_beam_factor = beam.jtok_equiv(freq) assert_quantity_allclose((1 * u.Jy).to(u.K, equivalencies=conv_factor), (1 * u.Jy).to(u.K, equivalencies=conv_beam_factor)) assert_quantity_allclose((1 * u.K).to(u.Jy, equivalencies=conv_factor), (1 * u.K).to(u.Jy, equivalencies=conv_beam_factor)) def test_beamarea_equiv(): major = 0.1 * u.rad beam = Beam(major, major, 30 * u.deg) conv_factor = u.beam_angular_area(beam.sr) assert_quantity_allclose((1 * u.Jy / u.beam).to(u.Jy / u.sr, equivalencies=conv_factor), (1 * u.Jy / u.beam).to(u.Jy / u.sr, equivalencies=beam.beamarea_equiv)) assert_quantity_allclose((1 * u.Jy / u.sr).to(u.Jy / u.beam, equivalencies=conv_factor), (1 * u.Jy / u.sr).to(u.Jy / u.beam, equivalencies=beam.beamarea_equiv)) # Add a by-hand check value = (1 * u.Jy / u.sr).to(u.Jy / u.beam, equivalencies=conv_factor).value byhand_value = 1 * beam.sr.value npt.assert_allclose(value, byhand_value) def test_convolution(): # equations from: # https://github.com/pkgw/carma-miriad/blob/CVSHEAD/src/subs/gaupar.for # (github checkin of MIRIAD, code by Sault) major1 = 1 * u.deg minor1 = 0.5 * u.deg pa1 = 0.0 * u.deg beam1 = Beam(major1, minor1, pa1) major2 = 1 * u.deg minor2 = 0.75 * u.deg pa2 = 90.0 * u.deg beam2 = Beam(major2, minor2, pa2) alpha = (major1 * np.cos(pa1))**2 + (minor1 * np.sin(pa1))**2 + \ (major2 * np.cos(pa2))**2 + (minor2 * np.sin(pa2))**2 beta = (major1 * np.sin(pa1))**2 + (minor1 * np.cos(pa1))**2 + \ (major2 * np.sin(pa2))**2 + (minor2 * np.cos(pa2))**2 gamma = 2 * ((minor1**2 - major1**2) * np.sin(pa1) * np.cos(pa1) + (minor2**2 - major2**2) * np.sin(pa2) * np.cos(pa2)) s = alpha + beta t = np.sqrt((alpha - beta)**2 + gamma**2) conv_major = np.sqrt(0.5 * (s + t)) conv_minor = np.sqrt(0.5 * (s - t)) conv_pa = 0.5 * np.arctan2(- gamma, alpha - beta) conv_beam = beam1.convolve(beam2) assert_quantity_allclose(conv_major, conv_beam.major) assert_quantity_allclose(conv_minor, conv_beam.minor) assert_quantity_allclose(conv_pa, conv_beam.pa) def test_deconvolution(): # equations from: # https://github.com/pkgw/carma-miriad/blob/CVSHEAD/src/subs/gaupar.for # (github checkin of MIRIAD, code by Sault) major1 = 2.0 * u.deg minor1 = 1.0 * u.deg pa1 = 45.0 * u.deg beam1 = Beam(major1, minor1, pa1) major2 = 1 * u.deg minor2 = 0.5 * u.deg pa2 = 0.0 * u.deg beam2 = Beam(major2, minor2, pa2) alpha = (major1 * np.cos(pa1))**2 + (minor1 * np.sin(pa1))**2 - \ (major2 * np.cos(pa2))**2 - (minor2 * np.sin(pa2))**2 beta = (major1 * np.sin(pa1))**2 + (minor1 * np.cos(pa1))**2 - \ (major2 * np.sin(pa2))**2 - (minor2 * np.cos(pa2))**2 gamma = 2 * ((minor1**2 - major1**2) * np.sin(pa1) * np.cos(pa1) + (minor2**2 - major2**2) * np.sin(pa2) * np.cos(pa2)) s = alpha + beta t = np.sqrt((alpha - beta)**2 + gamma**2) deconv_major = np.sqrt(0.5 * (s + t)) deconv_minor = np.sqrt(0.5 * (s - t)) deconv_pa = 0.5 * np.arctan2(- gamma, alpha - beta) deconv_beam = beam1.deconvolve(beam2) assert_quantity_allclose(deconv_major, deconv_beam.major) assert_quantity_allclose(deconv_minor, deconv_beam.minor) assert_quantity_allclose(deconv_pa, deconv_beam.pa) def test_conv_deconv(): beam1 = Beam(10. * u.arcsec, 5. * u.arcsec, 30. * u.deg) beam2 = Beam(5. * u.arcsec, 3. * u.arcsec, 120. * u.deg) beam3 = beam1.convolve(beam2) assert beam2 == beam3.deconvolve(beam1) assert beam1 == beam3.deconvolve(beam2) assert beam1.convolve(beam2) == beam2.convolve(beam1) # Test multiplication and subtraction (i.e., convolution and deconvolution) # subtraction-as-deconvolution is deprecated. Check that one of the gives # the warning with warnings.catch_warnings(record=True) as w: assert beam2 == beam3 - beam1 assert len(w) == 1 assert w[0].category == RadioBeamDeprecationWarning assert str(w[0].message) == ("Subtraction-as-deconvolution is deprecated. " "Use division instead.") # Dividing should give the same thing assert beam2 == beam3 / beam1 assert beam1 == beam3 / beam2 assert beam3 == beam1 * beam2 @pytest.mark.parametrize(('major', 'minor', 'pa', 'return_pointlike'), [[maj, min, pa, ret] for maj, min, pa, ret in product([10], np.arange(1, 11), np.linspace(0, 180, 10), [True, False])]) def test_deconv_pointlike(major, minor, pa, return_pointlike): beam1 = Beam(major * u.arcsec, major * u.arcsec, pa * u.deg) if return_pointlike: point_beam = Beam(0 * u.deg, 0 * u.deg, 0 * u.deg) point_beam == beam1.deconvolve(beam1, failure_returns_pointlike=True) else: try: beam1.deconvolve(beam1, failure_returns_pointlike=False) except BeamError: pass def test_isfinite(): beam1 = Beam(10. * u.arcsec, 5. * u.arcsec, 30. * u.deg) assert beam1.isfinite # raises an exception because major < minor #beam2 = Beam(-10. * u.arcsec, 5. * u.arcsec, 30. * u.deg) #assert not beam2.isfinite beam3 = Beam(10. * u.arcsec, -5. * u.arcsec, 30. * u.deg) assert not beam3.isfinite @pytest.mark.parametrize(("major", "minor", "pa"), [(10, 10, 60), (10, 10, -120), (10, 10, -300), (10, 10, 240), (10, 10, 59), (10, 10, -121)]) def test_beam_equal(major, minor, pa): beam1 = Beam(10 * u.deg, 10 * u.deg, 60 * u.deg) beam2 = Beam(major * u.deg, minor * u.deg, pa * u.deg) assert beam1 == beam2 assert not beam1 != beam2 @pytest.mark.parametrize(("major", "minor", "pa"), [(10, 8, 60), (10, 8, -120), (10, 8, 240)]) def test_beam_equal_noncirc(major, minor, pa): ''' Beams with PA +/- 180 deg are equal ''' beam1 = Beam(10 * u.deg, 8 * u.deg, 60 * u.deg) beam2 = Beam(major * u.deg, minor * u.deg, pa * u.deg) assert beam1 == beam2 assert not beam1 != beam2 @pytest.mark.parametrize(("major", "minor", "pa"), [(10, 8, 60), (12, 10, 60), (12, 10, 59)]) def test_beam_not_equal(major, minor, pa): beam1 = Beam(10 * u.deg, 10 * u.deg, 60 * u.deg) beam2 = Beam(major * u.deg, minor * u.deg, pa * u.deg) assert beam1 != beam2 def test_from_aips_issue43(): """ regression test for issue 43 """ aips_fname = data_path("header_aips.hdr") aips_hdr = fits.Header.fromtextfile(aips_fname) aips_beam_hdr = Beam.from_fits_header(aips_hdr) npt.assert_almost_equal(aips_beam_hdr.pa.value, -15.06) def test_small_beam_convolution(): # regression test for #68 beam1 = Beam((0.1*u.arcsec).to(u.deg), (0.00001*u.arcsec).to(u.deg), 30*u.deg) beam2 = Beam((0.3*u.arcsec).to(u.deg), (0.00001*u.arcsec).to(u.deg), 120*u.deg) conv = beam1.convolve(beam2) np.testing.assert_almost_equal(conv.pa.to(u.deg).value, -60) def test_major_minor_swap(): with pytest.raises(ValueError) as exc: beam1 = Beam(minor=10. * u.arcsec, major=5. * u.arcsec, pa=30. * u.deg) assert "Minor axis greater than major axis." in exc.value.args[0] def test_commonbeam_builtin(): ''' Test the built in common beam for Beam with a 2nd beam. This test case should come out to a round common beam of 10 arcsec. ''' beam1 = Beam(10 * u.arcsec, 8 * u.arcsec, 60 * u.deg) beam2 = Beam(10 * u.arcsec, 8 * u.arcsec, 150 * u.deg) exp_combeam = Beam(10 * u.arcsec) com_beam = beam1.commonbeam_with(beam2) assert com_beam == exp_combeam # Order should not matter com_beam_rev = beam2.commonbeam_with(beam1) assert com_beam_rev == exp_combeam assert com_beam_rev == com_beam ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/test_beams.py0000644000175100001770000005501414704473421021150 0ustar00runnerdockerimport numpy as np import numpy.testing as npt from astropy import units as u from astropy.io import fits import warnings import pytest from ..multiple_beams import Beams from ..beam import Beam from ..commonbeam import common_2beams, common_manybeams_mve, find_commonbeam_between from ..utils import InvalidBeamOperationError, BeamError from .test_beam import data_path def symm_beams_for_tests(): majors = [1, 1, 1, 2, 3, 4] * u.arcsec minors = majors pas = [0] * 6 * u.deg return Beams(major=majors, minor=minors, pa=pas), majors, minors, pas def asymm_beams_for_tests(): majors = [1, 1, 1, 2, 3, 4] * u.arcsec minors = majors / 2. pas = [-36, 20, 80, 41, -82, 11] * u.deg return Beams(major=majors, minor=minors, pa=pas), majors, minors, pas def load_commonbeam_comparisons(): common_beams = np.loadtxt(data_path("commonbeam_CASA_comparison.csv"), delimiter=',') return common_beams def test_beam_areas(): beams, majors = symm_beams_for_tests()[:2] areas = 2 * np.pi / (8 * np.log(2)) * (majors.to(u.rad)**2).to(u.sr) assert np.all(areas.value == beams.sr.value) assert np.all(beams.value == beams.sr.value) def test_beams_from_fits_bintable(): fname = data_path("m33_beams_bintable.fits.gz") bintable = fits.open(fname)[1] beams = Beams.from_fits_bintable(bintable) # Check that the units are arcsec (defined by our test data) assert (beams.major.to(u.arcsec).value == bintable.data['BMAJ']).all() assert (beams.minor.to(u.arcsec).value == bintable.data['BMIN']).all() assert (beams.pa.value == bintable.data['BPA']).all() def test_beams_from_fits_bintable_nonarcsec(): fname = data_path("m33_beams_bintable.fits.gz") bintable = fits.open(fname)[1] # Set to deg for BMAJ and BMIN bintable.header['TUNIT1'] = 'deg' bintable.header['TUNIT2'] = 'deg' bintable.data['BMAJ'] /= 3600. bintable.data['BMIN'] /= 3600. beams = Beams.from_fits_bintable(bintable) assert (beams.major.to(u.deg).value == bintable.data['BMAJ']).all() assert (beams.minor.to(u.deg).value == bintable.data['BMIN']).all() assert (beams.pa.to(u.deg).value == bintable.data['BPA']).all() def test_beams_from_list_of_beam(): beams, majors = symm_beams_for_tests()[:2] new_beams = Beams(beams=[beam for beam in beams]) assert beams == new_beams abeams = asymm_beams_for_tests()[0] new_abeams = Beams(beams=[beam for beam in abeams]) assert abeams == new_abeams def test_beams_equality_beams(): beams, majors = symm_beams_for_tests()[:2] assert beams == beams assert not beams != beams abeams, amajors = asymm_beams_for_tests()[:2] assert not (beams == abeams) assert beams != abeams def test_beams_equality_beam(): # Test whether all are equal to a single beam beams = Beams([1.] * 5 * u.arcsec) beam = Beam(1 * u.arcsec) assert np.all(beams == beam) assert not np.any(beams != beam) @pytest.mark.xfail(raises=InvalidBeamOperationError, strict=True) def test_beams_equality_fail(): # Test whether all are equal to a single beam beams = Beams([1.] * 5 * u.arcsec) beams == 2 @pytest.mark.xfail(raises=InvalidBeamOperationError, strict=True) def test_beams_notequality_fail(): # Test whether all are equal to a single beam beams = Beams([1.] * 5 * u.arcsec) beams != 2 @pytest.mark.xfail(raises=InvalidBeamOperationError, strict=True) def test_beams_equality_fail_shape(): # Test whether all are equal to a single beam beams = Beams([1.] * 5 * u.arcsec) assert np.all(beams == beams[1:]) @pytest.mark.xfail(raises=InvalidBeamOperationError, strict=True) def test_beams_add_fail(): # Test whether all are equal to a single beam beams = Beams([1.] * 5 * u.arcsec) beams + 2 @pytest.mark.xfail(raises=InvalidBeamOperationError, strict=True) def test_beams_sub_fail(): # Test whether all are equal to a single beam beams = Beams([1.] * 5 * u.arcsec) beams - 2 @pytest.mark.xfail(raises=InvalidBeamOperationError, strict=True) def test_beams_mult_fail(): # Test whether all are equal to a single beam beams = Beams([1.] * 5 * u.arcsec) beams * 2 @pytest.mark.xfail(raises=InvalidBeamOperationError, strict=True) def test_beams_div_fail(): # Test whether all are equal to a single beam beams = Beams([1.] * 5 * u.arcsec) beams / 2 def test_beams_mult_convolution(): beams, majors = asymm_beams_for_tests()[:2] beam = Beam(1 * u.arcsec) conv_beams = beams * beam individ_conv_beams = [beam_i.convolve(beam) for beam_i in beams] new_beams = Beams(beams=individ_conv_beams) assert conv_beams == new_beams def test_beams_div_deconvolution(): beams, majors = asymm_beams_for_tests()[:2] beam = Beam(0.25 * u.arcsec) deconv_beams = beams / beam individ_deconv_beams = [beam_i.deconvolve(beam) for beam_i in beams] new_beams = Beams(beams=individ_deconv_beams) assert deconv_beams == new_beams def test_indexing(): beams, majors = symm_beams_for_tests()[:2] assert hasattr(beams[slice(0, 3)], 'major') assert np.all(beams[slice(0, 3)].major.value == majors[:3].value) assert np.all(beams[slice(0, 3)].minor.value == majors[:3].value) assert hasattr(beams[:3], 'major') assert np.all(beams[:3].major.value == majors[:3].value) assert np.all(beams[:3].minor.value == majors[:3].value) assert hasattr(beams[3], 'major') assert beams[3].major.value == 2 assert beams[3].minor.value == 2 assert isinstance(beams[4], Beam) # Also test int64 chan = np.int64(3) assert hasattr(beams[chan], 'major') assert beams[chan].major.value == 2 assert beams[chan].minor.value == 2 assert isinstance(beams[chan], Beam) mask = np.array([True, False, True, False, True, True], dtype='bool') assert hasattr(beams[mask], 'major') assert np.all(beams[mask].major.value == majors[mask].value) def test_average_beams(): beams, majors = symm_beams_for_tests()[:2] assert np.all(beams.average_beam().major.value == majors.mean().value) mask = np.array([True, False, True, False, True, True], dtype='bool') assert np.all(beams[mask].average_beam().major.value == majors[mask].mean().value) @pytest.mark.parametrize(("beams", "majors", "minors", "pas"), [symm_beams_for_tests(), asymm_beams_for_tests()]) def test_largest_beams(beams, majors, minors, pas): assert beams.largest_beam().major.value == majors.max().value assert beams.largest_beam().minor.value == minors.max().value # Slice the object mask = np.array([True, False, True, False, True, True], dtype='bool') assert beams[mask].largest_beam().major.value == majors[mask].max().value assert beams[mask].largest_beam().minor.value == minors[mask].max().value # Apply a mask only for the operation assert beams.largest_beam(mask).major.value == majors[mask].max().value assert beams.largest_beam(mask).minor.value == minors[mask].max().value @pytest.mark.parametrize(("beams", "majors", "minors", "pas"), [symm_beams_for_tests(), asymm_beams_for_tests()]) def test_smallest_beams(beams, majors, minors, pas): assert beams.smallest_beam().major.value == majors.min().value assert beams.smallest_beam().minor.value == minors.min().value # Slice the object mask = np.array([True, False, True, False, True, True], dtype='bool') assert beams[mask].smallest_beam().major.value == majors[mask].min().value assert beams[mask].smallest_beam().minor.value == minors[mask].min().value # Apply a mask only for the operation assert beams.smallest_beam(mask).major.value == majors[mask].min().value assert beams.smallest_beam(mask).minor.value == minors[mask].min().value @pytest.mark.parametrize(("beams", "majors", "minors", "pas"), [symm_beams_for_tests(), asymm_beams_for_tests()]) def test_extrema_beams(beams, majors, minors, pas): extrema = beams.extrema_beams() assert extrema[0].major.value == majors.min().value assert extrema[0].minor.value == minors.min().value assert extrema[1].major.value == majors.max().value assert extrema[1].minor.value == minors.max().value # Slice the object mask = np.array([True, False, True, False, True, True], dtype='bool') extrema = beams[mask].extrema_beams() assert extrema[0].major.value == majors[mask].min().value assert extrema[0].minor.value == minors[mask].min().value assert extrema[1].major.value == majors[mask].max().value assert extrema[1].minor.value == minors[mask].max().value # Apply a mask only for the operation extrema = beams.extrema_beams(mask) assert extrema[0].major.value == majors[mask].min().value assert extrema[0].minor.value == minors[mask].min().value assert extrema[1].major.value == majors[mask].max().value assert extrema[1].minor.value == minors[mask].max().value @pytest.mark.parametrize("majors", [[1, 1, 1, 2, np.nan, 4], [0, 1, 1, 2, 3, 4]]) def test_beams_with_invalid(majors): majors = np.asarray(majors) * u.arcsec beams = Beams(major=majors) # Average assert beams.average_beam().major.value == np.nanmean( majors[np.nonzero(majors)]).value # Largest assert beams.largest_beam().major.value == np.nanmax(majors).value # Smallest assert beams.smallest_beam().major.value == np.nanmin( majors[np.nonzero(majors)]).value # Extrema extrema = beams.extrema_beams() assert extrema[0].major.value == np.nanmin( majors[np.nonzero(majors)]).value assert extrema[1].major.value == np.nanmax(majors).value # Additional masking mask = np.array([True, False, True, False, True, True], dtype='bool') if np.isnan(majors).any(): bad_mask = np.isfinite(majors) else: bad_mask = majors.value != 0 combined_mask = np.logical_and(mask, bad_mask) # Average assert beams[mask].average_beam().major.value == np.nanmean( majors[combined_mask]).value # Largest assert beams[mask].largest_beam().major.value == np.nanmax( majors[combined_mask]).value # Smallest assert beams[mask].smallest_beam().major.value == np.nanmin( majors[combined_mask]).value # Extrema extrema = beams[mask].extrema_beams() assert extrema[0].major.value == np.nanmin(majors[combined_mask]).value assert extrema[1].major.value == np.nanmax(majors[combined_mask]).value def test_beams_iter(): beams, majors = symm_beams_for_tests()[:2] # Ensure iterating through yields the same as slicing for i, beam in enumerate(beams): assert beam == beams[i] # @pytest.mark.parametrize('comp_vals', # [vals for vals in load_commonbeam_comparisons()]) # def test_commonbeam_casa_compare(comp_vals): # # These are the common beam parameters assuming 2 beams: # # 1) 3"x3" # # 2) 4"x2.5", varying the PA in 1 deg increments from 0 to 179 deg # # See data/generate_commonbeam_table.py # pa, com_major, com_minor, com_pa = comp_vals # beams = Beams(major=[3, 4] * u.arcsec, minor=[3, 2.5] * u.arcsec, # pa=[0, pa] * u.deg) # common_beam = beams.common_beam() # # npt.assert_almost_equal(common_beam.major.value, com_major) # # npt.assert_almost_equal(common_beam.minor.value, com_minor) # npt.assert_almost_equal(common_beam.pa.to(u.deg).value, com_pa) # npt.assert_almost_equal(common_beam.pa.to(u.deg).value, pa) def test_common_beam_smallcircular(): ''' Simple solution if the smallest beam is circular with a radius larger: Major axis is from the largest beam, minor axis is the radius of the smaller, and the PA is from the largest beam. ''' for pa in [0., 18., 68., 122.]: beams = Beams(major=[3, 4] * u.arcsec, minor=[3, 2.5] * u.arcsec, pa=[0, pa] * u.deg) targ_beam = Beam(4 * u.arcsec, 3 * u.arcsec, pa * u.deg) assert targ_beam == beams.common_beam() def test_commonbeam_notlargest(): beams = Beams(major=[3, 4] * u.arcsec, minor=[3, 2.5] * u.arcsec) target_beam = Beam(major=4 * u.arcsec, minor=3 * u.arcsec) assert beams.common_beam() == target_beam def test_commonbeam_largest(): ''' commonbeam is the largest in this set. ''' beams, majors = symm_beams_for_tests()[:2] assert beams.common_beam() == beams.largest_beam() # With masking mask = np.array([True, False, True, True, True, False], dtype='bool') assert beams[mask].common_beam() == beams[mask].largest_beam() assert beams.common_beam(mask) == beams.largest_beam(mask) # Implements the same test suite used in CASA def casa_commonbeam_suite(): cases = [] # https://open-bitbucket.nrao.edu/projects/CASA/repos/casa/browse/code/imageanalysis/ImageAnalysis/test/tCasaImageBeamSet.cc # In some cases, I find smaller common beams than are listed in the CASA # tests. The values for the CASA tests are commented out. # 1 cases.append((Beams(major=[4] * 2 * u.arcsec, minor=[2] * 2 * u.arcsec, pa=[0, 60] * u.deg), Beam(major=4.4812 * u.arcsec, minor=3.2883 * u.arcsec, pa=30.0 * u.deg))) # Beam(major=4.4856 * u.arcsec, minor=3.2916 * u.arcsec, # pa=30.0 * u.deg))) # 2 cases.append((Beams(major=[4] * 2 * u.arcsec, minor=[2] * 2 * u.arcsec, pa=[20, 80] * u.deg), Beam(major=4.4812 * u.arcsec, minor=3.2883 * u.arcsec, pa=50.0 * u.deg))) # Beam(major=4.4856 * u.arcsec, minor=3.2916 * u.arcsec, # pa=50.0 * u.deg))) # 3 cases.append((Beams(major=[4] * 2 * u.arcsec, minor=[2] * 2 * u.arcsec, pa=[1, 89] * u.deg), Beam(major=4.042 * u.arcsec, minor=3.958 * u.arcsec, pa=45.0 * u.deg))) # 4 cases.append((Beams(major=[4] * 2 * u.arcsec, minor=[2] * 2 * u.arcsec, pa=[0, 90] * u.deg), Beam(major=4 * u.arcsec, minor=4 * u.arcsec, pa=0.0 * u.deg))) # 5 cases.append((Beams(major=[4, 1.5] * u.arcsec, minor=[2, 1] * u.arcsec, pa=[0, 90] * u.deg), Beam(major=4 * u.arcsec, minor=2 * u.arcsec, pa=0.0 * u.deg))) # 6 cases.append((Beams(major=[8, 4] * u.arcsec, minor=[1, 1] * u.arcsec, pa=[0, 20] * u.deg), Beam(major=8.3684 * u.arcsec, minor=1.6253 * u.arcsec, pa=2.7679 * u.deg))) # Beam(major=8.377 * u.arcsec, minor=1.628 * u.arcsec, # pa=2.7679 * u.deg))) # 7 cases.append((Beams(major=[4, 8] * u.arcsec, minor=[1, 1] * u.arcsec, pa=[0, 20] * u.deg), Beam(major=8.369 * u.arcsec, minor=1.626 * u.arcsec, pa=17.232 * u.deg))) # 10 cases.append((Beams(major=[4, 1] * u.arcsec, minor=[2, 1] * u.arcsec, pa=[0, 0] * u.deg), Beam(major=4 * u.arcsec, minor=2 * u.arcsec, pa=0.0 * u.deg))) return cases @pytest.mark.parametrize(("beams", "target_beam"), casa_commonbeam_suite()) def test_commonbeam_angleoffset(beams, target_beam): # https://open-bitbucket.nrao.edu/projects/CASA/repos/casa/browse/code/imageanalysis/ImageAnalysis/test/tCasaImageBeamSet.cc#447 common_beam = beams.common_beam() # Order shouldn't matter common_beam_rev = beams[::-1].common_beam() assert common_beam == common_beam_rev npt.assert_allclose(common_beam.major.value, target_beam.major.value, rtol=1e-3) npt.assert_allclose(common_beam.minor.value, target_beam.minor.value, rtol=1e-3) # Only check when beam is elliptical. Otherwise PA does not matter. if not common_beam.iscircular: npt.assert_allclose(common_beam.pa.to(u.deg).value, target_beam.pa.value, rtol=1e-3) @pytest.mark.parametrize(("beams", "target_beam"), casa_commonbeam_suite()) def test_find_commonbeam_between(beams, target_beam): # https://open-bitbucket.nrao.edu/projects/CASA/repos/casa/browse/code/imageanalysis/ImageAnalysis/test/tCasaImageBeamSet.cc#447 common_beam = find_commonbeam_between(beams[0], beams[1]) # Order shouldn't matter common_beam_rev = find_commonbeam_between(beams[1], beams[0]) assert common_beam == common_beam_rev npt.assert_allclose(common_beam.major.value, target_beam.major.value, rtol=1e-3) npt.assert_allclose(common_beam.minor.value, target_beam.minor.value, rtol=1e-3) # Only check when beam is elliptical. Otherwise PA does not matter. if not common_beam.iscircular(): npt.assert_allclose(common_beam.pa.to(u.deg).value, target_beam.pa.value, rtol=1e-3) def casa_commonbeam_suite_multiple(): cases = [] # 8 cases.append((Beams(major=[4] * 4 * u.arcsec, minor=[2] * 4 * u.arcsec, pa=[0, 60, 20, 40] * u.deg), Beam(major=4.48904471492 * u.arcsec, minor=3.28268221138 * u.arcsec, pa=30.0001561178 * u.deg))) # This is the beam size in the CASA tests. The MVE method finds a slightly # smaller beam area, so that's what is tested against above and below. # Beam(major=4.485 * u.arcsec, minor=3.291 * u.arcsec, # pa=30 * u.deg))) # 9 cases.append((Beams(major=[4] * 4 * u.arcsec, minor=[2] * 4 * u.arcsec, pa=[0, 20, 40, 60] * u.deg), Beam(major=4.48904471492 * u.arcsec, minor=3.28268221138 * u.arcsec, pa=30.0001561178 * u.deg))) return cases @pytest.mark.parametrize(("beams", "target_beam"), casa_commonbeam_suite_multiple()) def test_commonbeam_multiple(beams, target_beam): # https://open-bitbucket.nrao.edu/projects/CASA/repos/casa/browse/code/imageanalysis/ImageAnalysis/test/tCasaImageBeamSet.cc#447 common_beam = beams.common_beam(epsilon=1e-4) # The above should be using the MVE method common_beam_check = common_manybeams_mve(beams, epsilon=1e-4) assert common_beam == common_beam_check npt.assert_almost_equal(common_beam.major.to(u.arcsec).value, target_beam.major.value, decimal=6) npt.assert_almost_equal(common_beam.minor.to(u.arcsec).value, target_beam.minor.value, decimal=6) npt.assert_allclose(common_beam.pa.to(u.deg).value, target_beam.pa.value, rtol=1e-3) @pytest.mark.parametrize(("beams", "target_beam"), casa_commonbeam_suite()) def test_commonbeam_methods(beams, target_beam): epsilon = 5e-4 tolerance = 1e-4 two_beam_method = common_2beams(beams) many_beam_method = common_manybeams_mve(beams, epsilon=epsilon, tolerance=tolerance) # Good to ~5x the given epsilon npt.assert_allclose(two_beam_method.major.to(u.arcsec).value, many_beam_method.major.to(u.arcsec).value, rtol=3e-3) npt.assert_allclose(two_beam_method.minor.to(u.arcsec).value, many_beam_method.minor.to(u.arcsec).value, rtol=3e-3) # Only test if the beam is circ_check = not two_beam_method.iscircular(rtol=3e-3) or \ not many_beam_method.iscircular(rtol=3e-3) if circ_check: # The pa can be sensitive to small changes so give it a larger # acceptable tolerance range. npt.assert_allclose(two_beam_method.pa.to(u.deg).value, many_beam_method.pa.to(u.deg).value, rtol=5e-3) def test_catch_common_beam_opt(): ''' The optimization method is close to working, but requires more testing. Ensure it cannot be used. ''' beams = Beams(major=[4] * 4 * u.arcsec, minor=[2] * 4 * u.arcsec, pa=[0, 20, 40, 60] * u.deg) with pytest.raises(NotImplementedError): beams.common_beam(method='opt') def test_major_minor_swap(): with pytest.raises(ValueError) as exc: beams = Beams(minor=[10.,5.] * u.arcsec, major=[5., 5.] * u.arcsec, pa=[30., 60.] * u.deg) assert "Minor axis greater than major axis." in exc.value.args[0] def test_common_beam_mve_auto_increase_epsilon(): ''' Here's a case where the default MVE parameters fail. By slowly increasing the epsilon* value, we get a common beam the can be deconvolved correctly over the set. * epsilon is the small factor added to the ellipse perimeter radius: radius * (1 + epsilon). The solution is then marginally larger than the true optimal solution, but close enough for effectively all use cases. ''' major = [8.517199, 8.513563, 8.518497, 8.518434, 8.528561, 8.528236, 8.530046, 8.530528, 8.530696, 8.533117] * u.arcsec minor = [5.7432523, 5.7446027, 5.7407207, 5.740814, 5.7331843, 5.7356524, 5.7338963, 5.733251, 5.732933, 5.73209] * u.arcsec pa = [-32.942623, -32.931957, -33.07815, -33.07532, -33.187653, -33.175243, -33.167213, -33.167244, -33.170418, -33.180233] * u.deg beams = Beams(major=major, minor=minor, pa=pa) err_str = 'Could not find common beam to deconvolve all beams.' with pytest.raises(BeamError, match=err_str): com_beam = beams.common_beam(method='pts', epsilon=5e-4, auto_increase_epsilon=False) # Force running into the max iteration of epsilon increases. err_str = 'Could not increase epsilon to find common beam.' with pytest.raises(BeamError, match=err_str): com_beam = beams.common_beam(method='pts', epsilon=5e-4, max_iter=2, max_epsilon=6e-4, auto_increase_epsilon=True) # Should run when epsilon is allowed to increase a bit. com_beam = beams.common_beam(method='pts', epsilon=5e-4, auto_increase_epsilon=True, max_epsilon=1e-3) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/tests/test_kernels.py0000644000175100001770000000271714704473421021526 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst from .. import beam as radio_beam from astropy import units as u import numpy.testing as npt import numpy as np import pytest from packaging.version import Version as parse_version from astropy.version import version SIGMA_TO_FWHM = radio_beam.SIGMA_TO_FWHM min_astropy_version = parse_version("1.1") @pytest.mark.skipif(parse_version(version) < min_astropy_version, reason="Must have astropy version >1.1") def test_gauss_kernel(): fake_beam = radio_beam.Beam(10*u.deg) # Let pixscale be 0.1 deg/pix kernel = fake_beam.as_kernel(0.1*u.deg) direct_kernel = \ radio_beam.EllipticalGaussian2DKernel(100. / SIGMA_TO_FWHM, 100. / SIGMA_TO_FWHM, 0.0) npt.assert_allclose(kernel.array, direct_kernel.array) @pytest.mark.skipif(parse_version(version) < min_astropy_version, reason="Must have astropy version >1.1") def test_tophat_kernel(): fake_beam = radio_beam.Beam(10*u.deg) # Let pixscale be 0.1 deg/pix kernel = fake_beam.as_tophat_kernel(0.1*u.deg) direct_kernel = \ radio_beam.EllipticalTophat2DKernel(100. / (SIGMA_TO_FWHM/np.sqrt(2)), 100. / (SIGMA_TO_FWHM/np.sqrt(2)), 0.0) npt.assert_allclose(kernel.array, direct_kernel.array) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/radio_beam/utils.py0000644000175100001770000001776214704473421017030 0ustar00runnerdocker import math import numpy as np import astropy.units as u DEG2RAD = math.pi / 180. class BeamError(Exception): """docstring for BeamError""" pass class InvalidBeamOperationError(Exception): pass class RadioBeamDeprecationWarning(Warning): pass def deconvolve_optimized(beamprops1, beamprops2, failure_returns_pointlike=False): """ An optimized, non-Quantity version of beam deconvolution. Because no unit conversions are handled, the inputs MUST be in degrees for the major, minor, and position angle. Parameters ---------- beamprops1: dict Dictionary with keys 'BMAJ', 'BMIN', and 'BPA' for the beam to deconvolve from. Can be produced with `~radio_beam.Beam.to_fits_keywords`. beamprops2: dict Same as `beamprops1` for the second beam. failure_returns_pointlike : bool, optional Return a point beam (zero area) when deconvolution fails. If `False`, this will instead raise a `~radio_beam.utils.BeamError` when deconvolution fails. Returns ------- new_major : float Deconvolved major FWHM. new_minor : float Deconvolved minor FWHM. new_pa : float Deconvolved position angle. """ # blame: https://github.com/pkgw/carma-miriad/blob/CVSHEAD/src/subs/gaupar.for # (githup checkin of MIRIAD, code by Sault) maj1 = beamprops1['BMAJ'] min1 = beamprops1['BMIN'] pa1 = beamprops1['BPA'] * DEG2RAD maj2 = beamprops2['BMAJ'] min2 = beamprops2['BMIN'] pa2 = beamprops2['BPA'] * DEG2RAD alpha = ((maj1 * math.cos(pa1))**2 + (min1 * math.sin(pa1))**2 - (maj2 * math.cos(pa2))**2 - (min2 * math.sin(pa2))**2) beta = ((maj1 * math.sin(pa1))**2 + (min1 * math.cos(pa1))**2 - (maj2 * math.sin(pa2))**2 - (min2 * math.cos(pa2))**2) gamma = 2 * ((min1**2 - maj1**2) * math.sin(pa1) * math.cos(pa1) - (min2**2 - maj2**2) * math.sin(pa2) * math.cos(pa2)) s = alpha + beta t = math.sqrt((alpha - beta)**2 + gamma**2) # Deal with floating point issues # This matches the arcsec**2 check for deconvolve below # Difference is we keep things in deg^2 here atol_t = np.finfo(np.float64).eps / 3600.**2 # To deconvolve, the beam must satisfy: # alpha < 0 alpha_cond = alpha + np.finfo(np.float64).eps < 0 # beta < 0 beta_cond = beta + np.finfo(np.float64).eps < 0 # s < t st_cond = s < t + atol_t if alpha_cond or beta_cond or st_cond: if failure_returns_pointlike: return 0., 0., 0. else: raise BeamError("Beam could not be deconvolved") else: new_major = math.sqrt(0.5 * (s + t)) new_minor = math.sqrt(0.5 * (s - t)) # absolute tolerance needs to be <<1 microarcsec atol = 1e-7 / 3600. if (math.sqrt(abs(gamma) + abs(alpha - beta))) < atol: new_pa = 0.0 else: new_pa = 0.5 * math.atan2(-1. * gamma, alpha - beta) # In the limiting case, the axes can be zero to within precision # Add the precision level onto each axis so a deconvolvable beam # is always has beam.isfinite == True new_major += np.finfo(np.float64).eps new_minor += np.finfo(np.float64).eps return new_major, new_minor, new_pa def deconvolve(beam, other, failure_returns_pointlike=False): """ Deconvolve a beam from another Parameters ---------- beam : `Beam` The defined beam. other : `Beam` The beam to deconvolve from this beam failure_returns_pointlike : bool Option to return a pointlike beam (i.e., one with major=minor=0) if the second beam is larger than the first. Otherwise, a ValueError will be raised Returns ------- new_beam : `Beam` The convolved Beam Raises ------ failure : ValueError If the second beam is larger than the first, the default behavior is to raise an exception. This can be overridden with failure_returns_pointlike """ # The header keywords handle the conversions to degree for BMAJ, BMIN, BPA. beamprops1 = beam.to_header_keywords() beamprops2 = other.to_header_keywords() return deconvolve_optimized(beamprops1, beamprops2, failure_returns_pointlike=failure_returns_pointlike) def convolve(beam, other): """ Convolve one beam with another. Parameters ---------- other : `Beam` The beam to convolve with Returns ------- new_beam : `Beam` The convolved Beam """ # blame: https://github.com/pkgw/carma-miriad/blob/CVSHEAD/src/subs/gaupar.for # (github checkin of MIRIAD, code by Sault) alpha = ((beam.major * np.cos(beam.pa))**2 + (beam.minor * np.sin(beam.pa))**2 + (other.major * np.cos(other.pa))**2 + (other.minor * np.sin(other.pa))**2) beta = ((beam.major * np.sin(beam.pa))**2 + (beam.minor * np.cos(beam.pa))**2 + (other.major * np.sin(other.pa))**2 + (other.minor * np.cos(other.pa))**2) gamma = (2 * ((beam.minor**2 - beam.major**2) * np.sin(beam.pa) * np.cos(beam.pa) + (other.minor**2 - other.major**2) * np.sin(other.pa) * np.cos(other.pa))) s = alpha + beta t = np.sqrt((alpha - beta)**2 + gamma**2) new_major = np.sqrt(0.5 * (s + t)) new_minor = np.sqrt(0.5 * (s - t)) # absolute tolerance needs to be <<1 microarcsec if np.isclose(((abs(gamma) + abs(alpha - beta))**0.5).to(u.arcsec).value, 1e-7): new_pa = 0.0 * u.deg else: new_pa = 0.5 * np.arctan2(-1. * gamma, alpha - beta) return new_major, new_minor, new_pa def transform_ellipse(major, minor, pa, x_scale, y_scale): ''' Transform an ellipse by scaling in the x and y axes. Parameters ---------- major : `~astropy.units.Quantity` Major axis. minor : `~astropy.units.Quantity` Minor axis. pa : `~astropy.units.Quantity` PA of the major axis. x_scale : float x axis scaling factor. y_scale : float y axis scaling factor. Returns ------- trans_major : `~astropy.units.Quantity` Major axis in the transformed frame. trans_minor : `~astropy.units.Quantity` Minor axis in the transformed frame. trans_pa : `~astropy.units.Quantity` PA of the major axis in the transformed frame. ''' # This code is based on the implementation in CASA: # https://open-bitbucket.nrao.edu/projects/CASA/repos/casa/browse/code/imageanalysis/ImageAnalysis/CasaImageBeamSet.cc major = major.to(u.arcsec) minor = minor.to(u.arcsec) pa = pa.to(u.rad) cospa = np.cos(pa) sinpa = np.sin(pa) cos2pa = cospa**2 sin2pa = sinpa**2 major2 = major**2 minor2 = minor**2 a = (cos2pa / major2) + (sin2pa / minor2) b = -2 * cospa * sinpa * (major2**-1 - minor2**-1) c = (sin2pa / major2) + (cos2pa / minor2) x2_scale = x_scale**2 y2_scale = y_scale**2 r = a / x2_scale s = b**2 / (4 * x2_scale * y2_scale) t = c / y2_scale udiff = r - t u2 = udiff**2 f1 = u2 + 4 * s f2 = np.sqrt(f1) * np.abs(udiff) j1 = (f2 + f1) / f1 / 2 j2 = (f1 - f2) / f1 / 2 k1 = (j1 * r + j1 * t - t) / (2 * j1 - 1) k2 = (j2 * r + j2 * t - t) / (2 * j2 - 1) c1 = np.sqrt(k1)**-1 c2 = np.sqrt(k2)**-1 pa_sign = 1 if pa.value >= 0 else -1 if c1 == c2: # Transformed to a circle trans_major = 1 / c1 trans_minor = trans_major trans_pa = 0. * u.rad elif c1 > c2: # c1 and c2 are the major and minor axes; use j1 to get PA trans_major = c1 trans_minor = c2 trans_pa = pa_sign * np.arccos(np.sqrt(j1)) else: # Opposite case where the axes are switched; get PA from j2 trans_major = c2 trans_minor = c1 trans_pa = pa_sign * np.arccos(np.sqrt(j2)) return trans_major, trans_minor, trans_pa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263384.0 radio_beam-0.3.8/radio_beam/version.py0000644000175100001770000000063314704473430017342 0ustar00runnerdocker# file generated by setuptools_scm # don't change, don't track in version control TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple, Union VERSION_TUPLE = Tuple[Union[int, str], ...] else: VERSION_TUPLE = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE __version__ = version = '0.3.8' __version_tuple__ = version_tuple = (0, 3, 8) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9311333 radio_beam-0.3.8/radio_beam.egg-info/0000755000175100001770000000000014704473431016774 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263384.0 radio_beam-0.3.8/radio_beam.egg-info/PKG-INFO0000644000175100001770000000272614704473430020077 0ustar00runnerdockerMetadata-Version: 2.1 Name: radio-beam Version: 0.3.8 Summary: Operations for radio astronomy beams with astropy Home-page: http://radio_beam.readthedocs.org Author: Adam Leroy, Adam Ginsburg, Erik Rosolowsky, Tom Robitaille, and Eric Koch Author-email: adam.g.ginsburg@gmail.com, koch.eric.w@gmail.com License: BSD License-File: LICENSE.rst Requires-Dist: astropy Requires-Dist: numpy>=1.8.0 Requires-Dist: scipy Provides-Extra: test Requires-Dist: pytest-astropy; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: matplotlib; extra == "test" Provides-Extra: docs Requires-Dist: sphinx-astropy; extra == "docs" Requires-Dist: matplotlib; extra == "docs" Provides-Extra: all Requires-Dist: scipy; extra == "all" Requires-Dist: matplotlib; extra == "all" Radio Beam: Tools for Beam IO and Manipulation ============================================== Radio Beam is a simple toolkit for reading beam information from FITS headers and manipulating beams. Some example applications include: * Convolution and deconvolution * Unit conversion (Jy to/from K) * Handle sets of beams for spectral cubes with varying resolution between channels * Find the smallest common beam from a set of beams * Add the beam shape to a matplotlib plot See the [documentation](https://radio-beam.readthedocs.io/en/latest/) for more information. [![Build Status](https://travis-ci.org/radio-astro-tools/radio_beam.svg?branch=master)](https://travis-ci.org/radio-astro-tools/radio_beam) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263384.0 radio_beam-0.3.8/radio_beam.egg-info/SOURCES.txt0000644000175100001770000000271314704473430020662 0ustar00runnerdocker.gitignore .readthedocs.yml CHANGES.rst LICENSE.rst MANIFEST.in README.md pyproject.toml setup.cfg setup.py specs.txt tox.ini .github/dependabot.yml .github/workflows/main.yml .github/workflows/publish.yml docs/Makefile docs/api.rst docs/commonbeam.rst docs/conf.py docs/convolution_kernels.rst docs/index.rst docs/install.rst docs/make.bat docs/nitpick-exceptions docs/plotting_beams.rst docs/_static/radiosnakes_nostruts2.svg docs/_static/spectralcube.css docs/_templates/autosummary/base.rst docs/_templates/autosummary/class.rst docs/_templates/autosummary/module.rst radio_beam/__init__.py radio_beam/_astropy_init.py radio_beam/beam.py radio_beam/commonbeam.py radio_beam/conftest.py radio_beam/multiple_beams.py radio_beam/utils.py radio_beam/version.py radio_beam.egg-info/PKG-INFO radio_beam.egg-info/SOURCES.txt radio_beam.egg-info/dependency_links.txt radio_beam.egg-info/not-zip-safe radio_beam.egg-info/requires.txt radio_beam.egg-info/top_level.txt radio_beam/tests/__init__.py radio_beam/tests/setup_package.py radio_beam/tests/test_beam.py radio_beam/tests/test_beams.py radio_beam/tests/test_kernels.py radio_beam/tests/data/NGC0925.bima.mmom0.fits.gz radio_beam/tests/data/NGC0925.bima.mmom0.image.tar.gz radio_beam/tests/data/generate_commonbeam_table.py radio_beam/tests/data/header_aips.hdr radio_beam/tests/data/header_jybeam.hdr radio_beam/tests/data/m33_beams_bintable.fits.gz radio_beam/tests/data/m83.moment0.fits.gz radio_beam/tests/data/ngc0925_na.fits.gz././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263384.0 radio_beam-0.3.8/radio_beam.egg-info/dependency_links.txt0000644000175100001770000000000114704473430023041 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263384.0 radio_beam-0.3.8/radio_beam.egg-info/not-zip-safe0000644000175100001770000000000114704473430021221 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263384.0 radio_beam-0.3.8/radio_beam.egg-info/requires.txt0000644000175100001770000000020214704473430021365 0ustar00runnerdockerastropy numpy>=1.8.0 scipy [all] scipy matplotlib [docs] sphinx-astropy matplotlib [test] pytest-astropy pytest-cov matplotlib ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263384.0 radio_beam-0.3.8/radio_beam.egg-info/top_level.txt0000644000175100001770000000001314704473430021517 0ustar00runnerdockerradio_beam ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729263384.9311333 radio_beam-0.3.8/setup.cfg0000644000175100001770000000253014704473431015041 0ustar00runnerdocker[metadata] name = radio-beam description = Operations for radio astronomy beams with astropy long_description = file: README.md author = Adam Leroy, Adam Ginsburg, Erik Rosolowsky, Tom Robitaille, and Eric Koch author_email = adam.g.ginsburg@gmail.com, koch.eric.w@gmail.com license = BSD url = http://radio_beam.readthedocs.org edit_on_github = False github_project = radio-astro-tools/radio-beam [options] zip_safe = False packages = find: install_requires = astropy numpy>=1.8.0 scipy [options.extras_require] test = pytest-astropy pytest-cov matplotlib docs = sphinx-astropy matplotlib all = scipy matplotlib [options.package_data] radio_beam.tests = data/* [tool:pytest] testpaths = "radio_beam" "docs" astropy_header = true doctest_plus = enabled text_file_format = rst addopts = --doctest-rst [coverage:run] source = radio_beam omit = radio_beam/_astropy_init* radio_beam/conftest* radio_beam/cython_version* radio_beam/setup_package* radio_beam/*/setup_package* radio_beam/*/*/setup_package* radio_beam/tests/* radio_beam/*/tests/* radio_beam/*/*/tests/* radio_beam/version* [coverage:report] exclude_lines = pragma: no cover except ImportError raise AssertionError raise NotImplementedError def main\(.*\): pragma: py{ignore_python_version} def _ipython_key_completions_ [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/setup.py0000644000175100001770000000211314704473421014726 0ustar00runnerdocker#!/usr/bin/env python import os import sys from setuptools import setup TEST_HELP = """ Note: running tests is no longer done using 'python setup.py test'. Instead you will need to run: tox -e test If you don't already have tox installed, you can install it with: pip install tox If you only want to run part of the test suite, you can also use pytest directly with:: pip install -e . pytest For more information, see: http://docs.astropy.org/en/latest/development/testguide.html#running-tests """ if 'test' in sys.argv: print(TEST_HELP) sys.exit(1) DOCS_HELP = """ Note: building the documentation is no longer done using 'python setup.py build_docs'. Instead you will need to run: tox -e build_docs If you don't already have tox installed, you can install it with: pip install tox For more information, see: http://docs.astropy.org/en/latest/install.html#builddocs """ if 'build_docs' in sys.argv or 'build_sphinx' in sys.argv: print(DOCS_HELP) sys.exit(1) setup(use_scm_version={'write_to': os.path.join('radio_beam', 'version.py')}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/specs.txt0000644000175100001770000000076614704473421015106 0ustar00runnerdockerLingering wish list: - parameterized spectral behavior (1/nu, tabular?) - build array for a specific WCS (deal with rotated/weird images) - instantiate from CASA header - compare two beams (or an array of beams) and recommend a "common beam" to target for a matched resolution image. Analytic solution and then tolerance ~ Nyquist on top of the analytic solution. - expose the factor (area ratio) to normalize by in convolution in order to keep Jy/beam (i.e., to use the new beam as a unit) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729263377.0 radio_beam-0.3.8/tox.ini0000644000175100001770000000200714704473421014531 0ustar00runnerdocker[tox] envlist = py{37,38,39,310,311}-test{,-all,-dev} build_docs codestyle requires = setuptools >= 30.3.0 pip >= 19.3.1 isolated_build = true indexserver = NRAO = https://casa-pip.nrao.edu/repository/pypi-group/simple [testenv] passenv = HOME WINDIR DISPLAY LC_ALL LC_CTYPE ON_TRAVIS changedir = .tmp/{envname} description = run tests with pytest deps = dev: git+https://github.com/astropy/astropy#egg=astropy casa: :NRAO:casatools casa: :NRAO:casatasks extras = test all: all commands = pip freeze pytest --pyargs radio_beam {toxinidir}/docs --cov radio_beam --cov-config={toxinidir}/setup.cfg {posargs} coverage xml -o {toxinidir}/coverage.xml [testenv:build_docs] changedir = docs description = invoke sphinx-build to build the HTML docs extras = docs commands = sphinx-build -W -b html . _build/html {posargs} [testenv:codestyle] deps = flake8 skip_install = true commands = flake8 --max-line-length=100 radio_beam