diff --git a/.travis.yml b/.travis.yml index 7ca71a3..d0cc2a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -96,8 +96,6 @@ matrix: # versions of Python, we can vary Python and Numpy versions at the same # time. - - os: linux - env: PYTHON_VERSION=3.5 NUMPY_VERSION=1.12 - os: linux env: PYTHON_VERSION=3.6 NUMPY_VERSION=1.13 - os: linux @@ -153,4 +151,4 @@ script: after_success: # If coveralls.io is set up for this package, uncomment the line below. # The coveragerc file may be customized as needed for your package. - # - if [[ $SETUP_CMD == *coverage* ]]; then coveralls --rcfile='packagename/tests/coveragerc'; fi + - if [[ $SETUP_CMD == *coverage* ]]; then coveralls --rcfile='radio_beam/tests/coveragerc'; fi diff --git a/CHANGES.rst b/CHANGES.rst index f2da6d9..b037ae9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,8 @@ 0.3 (unreleased) ---------------- + - 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. diff --git a/radio_beam/beam.py b/radio_beam/beam.py index 716b743..a62d9a7 100644 --- a/radio_beam/beam.py +++ b/radio_beam/beam.py @@ -12,7 +12,7 @@ from astropy.convolution import Kernel2D from astropy.convolution.kernels import _round_up_to_odd_integer -from .utils import deconvolve, convolve +from .utils import deconvolve, convolve, RadioBeamDeprecationWarning # Conversion between a twod Gaussian FWHM**2 and effective area FWHM_TO_AREA = 2*np.pi/(8*np.log(2)) @@ -346,6 +346,12 @@ def __mul__(self, 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): diff --git a/radio_beam/conftest.py b/radio_beam/conftest.py index 10cac21..bdaf64c 100644 --- a/radio_beam/conftest.py +++ b/radio_beam/conftest.py @@ -2,8 +2,19 @@ # by importing them here in conftest.py they are discoverable by py.test # no matter how it is invoked within the source tree. -from astropy.tests.pytest_plugins import * +from astropy.version import version as astropy_version +if astropy_version < '3.0': + # With older versions of Astropy, we actually need to import the pytest + # plugins themselves in order to make them discoverable by pytest. + from astropy.tests.pytest_plugins import * +else: + # As of Astropy 3.0, the pytest plugins provided by Astropy are + # automatically made available when Astropy is installed. This means it's + # not necessary to import them here, but we still need to import global + # variables that are used for configuration. + from astropy.tests.plugins.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS +from astropy.tests.helper import enable_deprecations_as_exceptions ## Uncomment the following line to treat all DeprecationWarnings as ## exceptions # enable_deprecations_as_exceptions() diff --git a/radio_beam/multiple_beams.py b/radio_beam/multiple_beams.py index dc7d2df..3e9dbed 100644 --- a/radio_beam/multiple_beams.py +++ b/radio_beam/multiple_beams.py @@ -8,6 +8,7 @@ from .beam import Beam, _to_area, SIGMA_TO_FWHM from .commonbeam import commonbeam +from .utils import InvalidBeamOperationError class Beams(u.Quantity): @@ -16,7 +17,8 @@ class Beams(u.Quantity): """ def __new__(cls, major=None, minor=None, pa=None, - areas=None, default_unit=u.arcsec, meta=None): + areas=None, default_unit=u.arcsec, meta=None, + beams=None): """ Create a new set of Gaussian beams @@ -34,12 +36,20 @@ def __new__(cls, major=None, minor=None, pa=None, Gaussian beam. default_unit : :class:`~astropy.units.Unit` The unit to impose on major, minor if they are specified as floats + 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.major.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 @@ -143,8 +153,10 @@ def __array_finalize__(self, obj): 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.minajor = obj.minor + self.minor = obj.minor self.pa = obj.pa self.meta = obj.meta @@ -325,3 +337,60 @@ def common_beam(self, includemask=None, method='pts', **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 diff --git a/radio_beam/tests/test_beam.py b/radio_beam/tests/test_beam.py index adfc5c8..c737bbd 100644 --- a/radio_beam/tests/test_beam.py +++ b/radio_beam/tests/test_beam.py @@ -6,6 +6,7 @@ from astropy.io import fits from astropy import units as u import os +import warnings import numpy as np import numpy.testing as npt from astropy.tests.helper import assert_quantity_allclose @@ -17,6 +18,9 @@ except ImportError: HAS_CASA = False +from ..utils import RadioBeamDeprecationWarning + + data_dir = os.path.join(os.path.dirname(__file__), 'data') @@ -244,8 +248,20 @@ def test_conv_deconv(): assert beam1.convolve(beam2) == beam2.convolve(beam1) # Test multiplication and subtraction (i.e., convolution and deconvolution) - assert beam2 == beam3 - beam1 - assert beam1 == beam3 - beam2 + # 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 diff --git a/radio_beam/tests/test_beams.py b/radio_beam/tests/test_beams.py index bf21d66..0b9c7dc 100644 --- a/radio_beam/tests/test_beams.py +++ b/radio_beam/tests/test_beams.py @@ -9,6 +9,7 @@ from ..multiple_beams import Beams from ..beam import Beam from ..commonbeam import common_2beams, common_manybeams_mve +from ..utils import InvalidBeamOperationError from .test_beam import data_path @@ -62,6 +63,132 @@ def test_beams_from_fits_bintable(): assert (beams.pa.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 + + +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] diff --git a/radio_beam/utils.py b/radio_beam/utils.py index 1ac419a..50acd5a 100644 --- a/radio_beam/utils.py +++ b/radio_beam/utils.py @@ -8,6 +8,14 @@ class BeamError(Exception): pass +class InvalidBeamOperationError(Exception): + pass + + +class RadioBeamDeprecationWarning(Warning): + pass + + def deconvolve(beam, other, failure_returns_pointlike=False): """ Deconvolve a beam from another diff --git a/setup.cfg b/setup.cfg index 74ebf1c..a7d27ec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ all_files = 1 upload-dir = docs/_build/html show-response = 1 -[pytest] +[tool:pytest] minversion = 2.2 norecursedirs = build docs/_build doctest_plus = enabled