# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Elliptical and elliptical-annulus apertures in both pixel and sky
coordinates.
"""
import math
import astropy.units as u
import numpy as np
from astropy.coordinates import Angle
from astropy.utils import lazyproperty
from photutils.aperture._batch_photometry import (SHAPE_ELLIPSE,
SHAPE_ELLIPTICAL_ANNULUS)
from photutils.aperture.attributes import (PixelPositions, PositiveScalar,
PositiveScalarAngle, ScalarAngle,
ScalarAngleOrValue,
SkyCoordPositions)
from photutils.aperture.core import (PixelAperture, SkyAperture,
_update_method_subpixels_docstring)
from photutils.aperture.mask import ApertureMask
from photutils.aperture.polygon import PolygonAperture, SkyPolygonAperture
from photutils.geometry import elliptical_overlap_grid
from photutils.utils._deprecation import (deprecated,
deprecated_positional_kwargs)
from photutils.utils._wcs_helpers import (pixel_shape_to_sky_svd,
sky_shape_to_pixel_svd)
__all__ = [
'EllipticalAnnulus',
'EllipticalAperture',
'EllipticalMaskMixin',
'SkyEllipticalAnnulus',
'SkyEllipticalAperture',
]
def _elliptical_polygon_offsets(a, b, theta, n_vertices, *, swap_axes=False):
"""
Compute the vertex offsets that approximate an ellipse with
semimajor axis ``a``, semiminor axis ``b``, and position angle
``theta`` (in radians) using ``n_vertices`` equally spaced vertices.
Parameters
----------
a : float
The semimajor axis of the ellipse.
b : float
The semiminor axis of the ellipse.
theta : float
The rotation angle of the ellipse in radians. With
``swap_axes=False`` (the pixel convention), it is measured
counterclockwise from the first (``x``) coordinate axis. With
``swap_axes=True`` (the sky convention), it is the position
angle measured counterclockwise from North (+latitude), where
PA=0 places the semimajor axis along North.
n_vertices : int
The number of vertices used to approximate the ellipse. Must be
at least 3.
swap_axes : bool, optional
If `True`, the unrotated semimajor axis is placed along the
second coordinate axis instead of the first. This matches the
sky-aperture convention where, at ``theta=0``, the semimajor axis
is along +latitude (North) and the semiminor axis is along
+longitude.
Returns
-------
offsets : 2D `~numpy.ndarray`
The vertex offsets that approximate the ellipse. The shape is
(n_vertices, 2) where the columns correspond to the x and y
coordinate offsets, respectively.
"""
n_vertices = int(n_vertices)
if n_vertices < 3:
msg = f'n_vertices must be at least 3, got {n_vertices}'
raise ValueError(msg)
angles = np.linspace(0.0, 2.0 * np.pi, n_vertices, endpoint=False)
x = a * np.cos(angles)
y = b * np.sin(angles)
if swap_axes:
x, y = y, x
cos_t = np.cos(theta)
sin_t = np.sin(theta)
return np.column_stack([x * cos_t - y * sin_t,
x * sin_t + y * cos_t])
[docs]
@deprecated('3.0', until='4.0')
class EllipticalMaskMixin: # pragma: no cover
"""
Mixin class to create masks for elliptical and elliptical-annulus
aperture objects.
.. deprecated:: 3.0
"""
[docs]
@_update_method_subpixels_docstring
def to_mask(self, method='exact', subpixels=5):
"""
Return a mask for the aperture.
Parameters
----------
<method_subpixels_descriptions>
Returns
-------
mask : `~photutils.aperture.ApertureMask` or list of \
`~photutils.aperture.ApertureMask`
A mask for the aperture. If the aperture is scalar then
a single `~photutils.aperture.ApertureMask` is returned,
otherwise a list of `~photutils.aperture.ApertureMask` is
returned.
"""
use_exact, subpixels = self._translate_mask_method(method, subpixels)
if hasattr(self, 'a'):
a = self.a
b = self.b
elif hasattr(self, 'a_in'): # annulus
a = self.a_out
b = self.b_out
else:
msg = 'Cannot determine the aperture shape'
raise ValueError(msg)
masks = []
for bbox, edges in zip(self._bbox, self._centered_edges, strict=True):
ny, nx = bbox.shape
mask = elliptical_overlap_grid(edges[0], edges[1], edges[2],
edges[3], nx, ny, a, b,
self._theta_rad, use_exact,
subpixels)
# Subtract the inner ellipse for an annulus
if hasattr(self, 'a_in'):
mask -= elliptical_overlap_grid(edges[0], edges[1], edges[2],
edges[3], nx, ny, self.a_in,
self.b_in, self._theta_rad,
use_exact, subpixels)
masks.append(ApertureMask(mask, bbox))
if self.isscalar:
return masks[0]
return masks
@staticmethod
def _calc_extents(semimajor_axis, semiminor_axis, theta_rad):
"""
Calculate half of the bounding box extents of an ellipse.
"""
return _calc_ellipse_extents(semimajor_axis, semiminor_axis, theta_rad)
def _calc_ellipse_extents(semimajor_axis, semiminor_axis, theta_rad):
"""
Calculate half of the bounding box extents of an ellipse.
"""
cos_theta = np.cos(theta_rad)
sin_theta = np.sin(theta_rad)
semimajor_x = semimajor_axis * cos_theta
semimajor_y = semimajor_axis * sin_theta
semiminor_x = semiminor_axis * -sin_theta
semiminor_y = semiminor_axis * cos_theta
x_extent = np.sqrt(semimajor_x**2 + semiminor_x**2)
y_extent = np.sqrt(semimajor_y**2 + semiminor_y**2)
return x_extent, y_extent
[docs]
class EllipticalAperture(PixelAperture):
"""
An elliptical aperture defined in pixel coordinates.
The aperture has a single fixed size/shape, but it can have multiple
positions (see the ``positions`` input).
Parameters
----------
positions : array_like
The pixel coordinates of the aperture center(s) in one of the
following formats:
* single ``(x, y)`` pair as a tuple, list, or `~numpy.ndarray`
* tuple, list, or `~numpy.ndarray` of ``(x, y)`` pairs
a : float
The semimajor axis of the ellipse in pixels.
b : float
The semiminor axis of the ellipse in pixels.
theta : float or `~astropy.units.Quantity`, optional
The rotation angle as an angular quantity
(`~astropy.units.Quantity` or `~astropy.coordinates.Angle`) or
value in radians (as a float) from the positive ``x`` axis. The
rotation angle increases counterclockwise.
Raises
------
ValueError : `ValueError`
If either axis (``a`` or ``b``) is negative.
Examples
--------
>>> from astropy.coordinates import Angle
>>> from photutils.aperture import EllipticalAperture
>>> theta = Angle(80, 'deg')
>>> aper = EllipticalAperture([10.0, 20.0], 5.0, 3.0)
>>> aper = EllipticalAperture((10.0, 20.0), 5.0, 3.0, theta=theta)
>>> pos1 = (10.0, 20.0) # (x, y)
>>> pos2 = (30.0, 40.0)
>>> pos3 = (50.0, 60.0)
>>> aper = EllipticalAperture([pos1, pos2, pos3], 5.0, 3.0)
>>> aper = EllipticalAperture((pos1, pos2, pos3), 5.0, 3.0, theta=theta)
"""
_params = ('positions', 'a', 'b', 'theta')
positions = PixelPositions('The center pixel position(s).')
a = PositiveScalar('The semimajor axis in pixels.')
b = PositiveScalar('The semiminor axis in pixels.')
theta = ScalarAngleOrValue('The counterclockwise rotation angle as an '
'angular Quantity or value in radians from '
'the positive x axis.')
@deprecated_positional_kwargs(since='3.0', until='4.0')
def __init__(self, positions, a, b, theta=0.0):
self.positions = positions
self.a = a
self.b = b
self.theta = theta
self._theta_rad = self.theta.to_value(u.radian)
@lazyproperty
def _xy_extents(self):
"""
The half of the bounding box extents of the ellipse in the x and
y directions.
"""
return _calc_ellipse_extents(self.a, self.b, self._theta_rad)
def _batch_shape_params(self):
return SHAPE_ELLIPSE, (self.a, self.b, self._theta_rad)
@lazyproperty
def area(self):
"""
The exact geometric area of the aperture shape.
"""
return math.pi * self.a * self.b
def _to_patch(self, *, origin=(0, 0), **kwargs):
"""
Return a `~matplotlib.patches.Patch` for the aperture.
Parameters
----------
origin : array_like, optional
The ``(x, y)`` position of the origin of the displayed
image.
**kwargs : dict, optional
Any keyword arguments accepted by
`matplotlib.patches.Patch`.
Returns
-------
patch : `~matplotlib.patches.Patch` or list of \
`~matplotlib.patches.Patch`
A patch for the aperture. If the aperture is scalar then a
single `~matplotlib.patches.Patch` is returned, otherwise a
list of `~matplotlib.patches.Patch` is returned.
"""
import matplotlib.patches as mpatches
xy_positions, patch_kwargs = self._define_patch_params(origin=origin,
**kwargs)
angle = self.theta.to_value(u.deg)
patches = [mpatches.Ellipse(xy_position, 2.0 * self.a, 2.0 * self.b,
angle=angle, **patch_kwargs)
for xy_position in xy_positions]
if self.isscalar:
return patches[0]
return patches
def _compute_overlap(self, edges, nx, ny, use_exact, subpixels):
"""
Compute the overlap of the aperture on the pixel grid.
Parameters
----------
edges : list of 4 1D `~numpy.ndarray`
The edges of the pixel grid in the form of
``[x_edges, y_edges, x_centers, y_centers]``.
nx, ny : int
The number of pixels in the x and y directions.
use_exact : bool
Whether to use the exact method for calculating the overlap.
subpixels : int
The number of subpixels to use in each dimension for the
subpixel method.
Returns
-------
overlap : 2D `~numpy.ndarray`
The overlap of the aperture on the pixel grid. The values
will be between 0 and 1, where 0 means no overlap and 1
means full overlap.
"""
return elliptical_overlap_grid(edges[0], edges[1], edges[2],
edges[3], nx, ny, self.a, self.b,
self._theta_rad, use_exact, subpixels)
[docs]
def to_sky(self, wcs):
"""
Convert the aperture to a `SkyEllipticalAperture` object defined
in celestial coordinates.
Parameters
----------
wcs : WCS object
A world coordinate system (WCS) transformation that
supports the `astropy shared interface for WCS
<https://docs.astropy.org/en/stable/wcs/wcsapi.html>`_
(e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`).
Returns
-------
aperture : `SkyEllipticalAperture` object
A `SkyEllipticalAperture` object.
Notes
-----
The aperture shape parameters are converted using the local WCS
properties (pixel scale, rotation angle) evaluated at the first
aperture position. Because aperture objects require scalar shape
parameters, only a single reference position is used for the
conversion. For apertures with multiple positions used with a
WCS that has spatially-varying distortions, this may produce
inaccurate results for positions far from the first position.
"""
xpos, ypos = np.transpose(self.positions)
positions = wcs.pixel_to_world(xpos, ypos)
first_pos = np.atleast_2d(self.positions)[0]
pixcoord = (float(first_pos[0]), float(first_pos[1]))
_, sky_width, sky_height, sky_angle = pixel_shape_to_sky_svd(
pixcoord, wcs, 2 * self.a, 2 * self.b, self._theta_rad)
a = Angle(sky_width / 2, 'arcsec')
b = Angle(sky_height / 2, 'arcsec')
return SkyEllipticalAperture(positions=positions, a=a, b=b,
theta=sky_angle)
[docs]
def to_polygon(self, *, n_vertices=100):
"""
Return a `~photutils.aperture.PolygonAperture` that approximates
this elliptical aperture.
Parameters
----------
n_vertices : int, optional
The number of polygon vertices used to approximate the
ellipse. Must be at least 3. Default is 100.
Returns
-------
aperture : `~photutils.aperture.PolygonAperture`
A polygon aperture that approximates the ellipse.
"""
offsets = _elliptical_polygon_offsets(
self.a, self.b, self.theta.to_value(u.radian), n_vertices)
return PolygonAperture._from_convex_offsets(self.positions, offsets)
[docs]
class EllipticalAnnulus(PixelAperture):
r"""
An elliptical annulus aperture defined in pixel coordinates.
The aperture has a single fixed size/shape, but it can have multiple
positions (see the ``positions`` input).
Parameters
----------
positions : array_like
The pixel coordinates of the aperture center(s) in one of the
following formats:
* single ``(x, y)`` pair as a tuple, list, or `~numpy.ndarray`
* tuple, list, or `~numpy.ndarray` of ``(x, y)`` pairs
a_in : float
The inner semimajor axis of the elliptical annulus in pixels.
a_out : float
The outer semimajor axis of the elliptical annulus in pixels.
b_out : float
The outer semiminor axis of the elliptical annulus in pixels.
b_in : `None` or float, optional
The inner semiminor axis of the elliptical annulus in pixels.
If `None`, then the inner semiminor axis is calculated as:
.. math::
b_{in} = b_{out} \left(\frac{a_{in}}{a_{out}}\right)
theta : float or `~astropy.units.Quantity`, optional
The rotation angle as an angular quantity
(`~astropy.units.Quantity` or `~astropy.coordinates.Angle`) or
value in radians (as a float) from the positive ``x`` axis. The
rotation angle increases counterclockwise.
Raises
------
ValueError : `ValueError`
If inner semimajor axis (``a_in``) is greater than outer semimajor
axis (``a_out``).
ValueError : `ValueError`
If either the inner semimajor axis (``a_in``) or the outer semiminor
axis (``b_out``) is negative.
Examples
--------
>>> from astropy.coordinates import Angle
>>> from photutils.aperture import EllipticalAnnulus
>>> theta = Angle(80, 'deg')
>>> aper = EllipticalAnnulus([10.0, 20.0], 3.0, 8.0, 5.0)
>>> aper = EllipticalAnnulus((10.0, 20.0), 3.0, 8.0, 5.0, theta=theta)
>>> pos1 = (10.0, 20.0) # (x, y)
>>> pos2 = (30.0, 40.0)
>>> pos3 = (50.0, 60.0)
>>> aper = EllipticalAnnulus([pos1, pos2, pos3], 3.0, 8.0, 5.0)
>>> aper = EllipticalAnnulus((pos1, pos2, pos3), 3.0, 8.0, 5.0,
... theta=theta)
"""
_params = ('positions', 'a_in', 'a_out', 'b_in', 'b_out', 'theta')
positions = PixelPositions('The center pixel position(s).')
a_in = PositiveScalar('The inner semimajor axis in pixels.')
a_out = PositiveScalar('The outer semimajor axis in pixels.')
b_in = PositiveScalar('The inner semiminor axis in pixels.')
b_out = PositiveScalar('The outer semiminor axis in pixels.')
theta = ScalarAngleOrValue('The counterclockwise rotation angle as an '
'angular Quantity or value in radians from '
'the positive x axis.')
@deprecated_positional_kwargs(since='3.0', until='4.0')
def __init__(self, positions, a_in, a_out, b_out, b_in=None, theta=0.0):
if not a_out > a_in:
msg = "'a_out' must be greater than 'a_in'"
raise ValueError(msg)
self.positions = positions
self.a_in = a_in
self.a_out = a_out
self.b_out = b_out
if b_in is None:
b_in = self.b_out * self.a_in / self.a_out
elif not b_out > b_in:
msg = "'b_out' must be greater than 'b_in'"
raise ValueError(msg)
self.b_in = b_in
self.theta = theta
self._theta_rad = self.theta.to_value(u.radian)
@lazyproperty
def _xy_extents(self):
"""
The half of the bounding box extents of the outer ellipse in the
x and y directions.
"""
return _calc_ellipse_extents(self.a_out, self.b_out, self._theta_rad)
def _batch_shape_params(self):
return SHAPE_ELLIPTICAL_ANNULUS, (self.a_in, self.b_in, self.a_out,
self.b_out, self._theta_rad)
@lazyproperty
def area(self):
"""
The exact geometric area of the aperture shape.
"""
return math.pi * (self.a_out * self.b_out - self.a_in * self.b_in)
def _to_patch(self, *, origin=(0, 0), **kwargs):
"""
Return a `~matplotlib.patches.Patch` for the aperture.
Parameters
----------
origin : array_like, optional
The ``(x, y)`` position of the origin of the displayed
image.
**kwargs : dict, optional
Any keyword arguments accepted by
`matplotlib.patches.Patch`.
Returns
-------
patch : `~matplotlib.patches.Patch` or list of \
`~matplotlib.patches.Patch`
A patch for the aperture. If the aperture is scalar then a
single `~matplotlib.patches.Patch` is returned, otherwise a
list of `~matplotlib.patches.Patch` is returned.
"""
import matplotlib.patches as mpatches
xy_positions, patch_kwargs = self._define_patch_params(origin=origin,
**kwargs)
patches = []
angle = self.theta.to_value(u.deg)
for xy_position in xy_positions:
patch_inner = mpatches.Ellipse(xy_position, 2.0 * self.a_in,
2.0 * self.b_in, angle=angle)
patch_outer = mpatches.Ellipse(xy_position, 2.0 * self.a_out,
2.0 * self.b_out, angle=angle)
path = self._make_annulus_path(patch_inner, patch_outer)
patches.append(mpatches.PathPatch(path, **patch_kwargs))
if self.isscalar:
return patches[0]
return patches
def _compute_overlap(self, edges, nx, ny, use_exact, subpixels):
"""
Compute the overlap of the aperture on the pixel grid.
Parameters
----------
edges : list of 4 1D `~numpy.ndarray`
The edges of the pixel grid in the form of
``[x_edges, y_edges, x_centers, y_centers]``.
nx, ny : int
The number of pixels in the x and y directions.
use_exact : bool
Whether to use the exact method for calculating the overlap.
subpixels : int
The number of subpixels to use in each dimension for the
subpixel method.
Returns
-------
overlap : 2D `~numpy.ndarray`
The overlap of the aperture on the pixel grid. The values
will be between 0 and 1, where 0 means no overlap and 1
means full overlap.
"""
overlap = elliptical_overlap_grid(edges[0], edges[1], edges[2],
edges[3], nx, ny, self.a_out,
self.b_out, self._theta_rad,
use_exact, subpixels)
overlap -= elliptical_overlap_grid(edges[0], edges[1], edges[2],
edges[3], nx, ny, self.a_in,
self.b_in, self._theta_rad,
use_exact, subpixels)
return overlap
[docs]
def to_sky(self, wcs):
"""
Convert the aperture to a `SkyEllipticalAnnulus` object defined
in celestial coordinates.
Parameters
----------
wcs : WCS object
A world coordinate system (WCS) transformation that
supports the `astropy shared interface for WCS
<https://docs.astropy.org/en/stable/wcs/wcsapi.html>`_
(e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`).
Returns
-------
aperture : `SkyEllipticalAnnulus` object
A `SkyEllipticalAnnulus` object.
Notes
-----
The aperture shape parameters are converted using the local WCS
properties (pixel scale, rotation angle) evaluated at the first
aperture position. Because aperture objects require scalar shape
parameters, only a single reference position is used for the
conversion. For apertures with multiple positions used with a
WCS that has spatially-varying distortions, this may produce
inaccurate results for positions far from the first position.
"""
xpos, ypos = np.transpose(self.positions)
positions = wcs.pixel_to_world(xpos, ypos)
first_pos = np.atleast_2d(self.positions)[0]
pixcoord = (float(first_pos[0]), float(first_pos[1]))
_, sky_w_out, sky_h_out, sky_angle = pixel_shape_to_sky_svd(
pixcoord, wcs, 2 * self.a_out, 2 * self.b_out, self._theta_rad)
_, sky_w_in, sky_h_in, _ = pixel_shape_to_sky_svd(
pixcoord, wcs, 2 * self.a_in, 2 * self.b_in, self._theta_rad)
a_out = Angle(sky_w_out / 2, 'arcsec')
b_out = Angle(sky_h_out / 2, 'arcsec')
a_in = Angle(sky_w_in / 2, 'arcsec')
b_in = Angle(sky_h_in / 2, 'arcsec')
return SkyEllipticalAnnulus(positions=positions, a_in=a_in,
a_out=a_out, b_out=b_out,
b_in=b_in, theta=sky_angle)
[docs]
class SkyEllipticalAperture(SkyAperture):
"""
An elliptical aperture defined in sky coordinates.
The aperture has a single fixed size/shape, but it can have multiple
positions (see the ``positions`` input).
Parameters
----------
positions : `~astropy.coordinates.SkyCoord`
The celestial coordinates of the aperture center(s). This can be
either scalar coordinates or an array of coordinates.
a : scalar `~astropy.units.Quantity`
The semimajor axis of the ellipse in angular units.
b : scalar `~astropy.units.Quantity`
The semiminor axis of the ellipse in angular units.
theta : scalar `~astropy.units.Quantity`, optional
The position angle (in angular units) of the ellipse semimajor
axis. For a right-handed world coordinate system, the position
angle increases counterclockwise from North (PA=0).
Examples
--------
>>> from astropy.coordinates import SkyCoord
>>> import astropy.units as u
>>> from photutils.aperture import SkyEllipticalAperture
>>> positions = SkyCoord(ra=[10.0, 20.0], dec=[30.0, 40.0], unit='deg')
>>> aper = SkyEllipticalAperture(positions, 1.0*u.arcsec, 0.5*u.arcsec)
"""
_params = ('positions', 'a', 'b', 'theta')
positions = SkyCoordPositions('The center position(s) in sky coordinates.')
a = PositiveScalarAngle('The semimajor axis in angular units.')
b = PositiveScalarAngle('The semiminor axis in angular units.')
theta = ScalarAngle('The position angle in angular units of the ellipse '
'semimajor axis.')
@deprecated_positional_kwargs(since='3.0', until='4.0')
def __init__(self, positions, a, b, theta=0.0 * u.deg):
self.positions = positions
self.a = a
self.b = b
self.theta = theta
self._theta_rad = self.theta.to_value(u.radian)
[docs]
def to_pixel(self, wcs):
"""
Convert the aperture to an `EllipticalAperture` object defined
in pixel coordinates.
Parameters
----------
wcs : WCS object
A world coordinate system (WCS) transformation that
supports the `astropy shared interface for WCS
<https://docs.astropy.org/en/stable/wcs/wcsapi.html>`_
(e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`).
Returns
-------
aperture : `EllipticalAperture` object
An `EllipticalAperture` object.
Notes
-----
The aperture shape parameters are converted using the local WCS
properties (pixel scale, rotation angle) evaluated at the first
aperture position. Because aperture objects require scalar shape
parameters, only a single reference position is used for the
conversion. For apertures with multiple positions used with a
WCS that has spatially-varying distortions, this may produce
inaccurate results for positions far from the first position.
"""
xpos, ypos = wcs.world_to_pixel(self.positions)
positions = np.transpose((xpos, ypos))
skypos = self.positions if self.isscalar else self.positions[0]
_, pix_width, pix_height, pix_angle = sky_shape_to_pixel_svd(
skypos, wcs,
2 * self.a.to_value(u.arcsec),
2 * self.b.to_value(u.arcsec),
self._theta_rad)
a = pix_width / 2
b = pix_height / 2
return EllipticalAperture(positions=positions, a=a, b=b,
theta=pix_angle)
[docs]
def to_polygon(self, *, n_vertices=100):
"""
Return a `~photutils.aperture.SkyPolygonAperture` that
approximates this elliptical aperture.
Parameters
----------
n_vertices : int, optional
The number of polygon vertices used to approximate the
ellipse. Must be at least 3. Default is 100.
Returns
-------
aperture : `~photutils.aperture.SkyPolygonAperture`
A sky polygon aperture that approximates the ellipse.
"""
unit = self.a.unit
offsets = _elliptical_polygon_offsets(
self.a.to_value(unit), self.b.to_value(unit),
self.theta.to_value(u.radian), n_vertices, swap_axes=True) * unit
return SkyPolygonAperture._from_convex_offsets(self.positions, offsets)
[docs]
class SkyEllipticalAnnulus(SkyAperture):
r"""
An elliptical annulus aperture defined in sky coordinates.
The aperture has a single fixed size/shape, but it can have multiple
positions (see the ``positions`` input).
Parameters
----------
positions : `~astropy.coordinates.SkyCoord`
The celestial coordinates of the aperture center(s). This can be
either scalar coordinates or an array of coordinates.
a_in : scalar `~astropy.units.Quantity`
The inner semimajor axis in angular units.
a_out : scalar `~astropy.units.Quantity`
The outer semimajor axis in angular units.
b_out : scalar `~astropy.units.Quantity`
The outer semiminor axis in angular units.
b_in : `None` or scalar `~astropy.units.Quantity`
The inner semiminor axis in angular units. If `None`, then the
inner semiminor axis is calculated as:
.. math::
b_{in} = b_{out} \left(\frac{a_{in}}{a_{out}}\right)
theta : scalar `~astropy.units.Quantity`, optional
The position angle (in angular units) of the ellipse semimajor
axis. For a right-handed world coordinate system, the position
angle increases counterclockwise from North (PA=0).
Examples
--------
>>> from astropy.coordinates import SkyCoord
>>> import astropy.units as u
>>> from photutils.aperture import SkyEllipticalAnnulus
>>> positions = SkyCoord(ra=[10.0, 20.0], dec=[30.0, 40.0], unit='deg')
>>> aper = SkyEllipticalAnnulus(positions, 0.5*u.arcsec, 2.0*u.arcsec,
... 1.0*u.arcsec)
"""
_params = ('positions', 'a_in', 'a_out', 'b_in', 'b_out', 'theta')
positions = SkyCoordPositions('The center position(s) in sky coordinates.')
a_in = PositiveScalarAngle('The inner semimajor axis in angular units.')
a_out = PositiveScalarAngle('The outer semimajor axis in angular units.')
b_in = PositiveScalarAngle('The inner semiminor axis in angular units.')
b_out = PositiveScalarAngle('The outer semiminor axis in angular units.')
theta = ScalarAngle('The position angle in angular units of the ellipse '
'semimajor axis.')
@deprecated_positional_kwargs(since='3.0', until='4.0')
def __init__(self, positions, a_in, a_out, b_out, b_in=None,
theta=0.0 * u.deg):
if not a_out > a_in:
msg = "'a_out' must be greater than 'a_in'"
raise ValueError(msg)
self.positions = positions
self.a_in = a_in
self.a_out = a_out
self.b_out = b_out
if b_in is None:
b_in = self.b_out * self.a_in / self.a_out
elif not b_out > b_in:
msg = "'b_out' must be greater than 'b_in'"
raise ValueError(msg)
self.b_in = b_in
self.theta = theta
self._theta_rad = self.theta.to_value(u.radian)
[docs]
def to_pixel(self, wcs):
"""
Convert the aperture to an `EllipticalAnnulus` object defined in
pixel coordinates.
Parameters
----------
wcs : WCS object
A world coordinate system (WCS) transformation that
supports the `astropy shared interface for WCS
<https://docs.astropy.org/en/stable/wcs/wcsapi.html>`_
(e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`).
Returns
-------
aperture : `EllipticalAnnulus` object
An `EllipticalAnnulus` object.
Notes
-----
The aperture shape parameters are converted using the local WCS
properties (pixel scale, rotation angle) evaluated at the first
aperture position. Because aperture objects require scalar shape
parameters, only a single reference position is used for the
conversion. For apertures with multiple positions used with a
WCS that has spatially-varying distortions, this may produce
inaccurate results for positions far from the first position.
"""
xpos, ypos = wcs.world_to_pixel(self.positions)
positions = np.transpose((xpos, ypos))
skypos = self.positions if self.isscalar else self.positions[0]
_, pix_w_out, pix_h_out, pix_angle = sky_shape_to_pixel_svd(
skypos, wcs,
2 * self.a_out.to_value(u.arcsec),
2 * self.b_out.to_value(u.arcsec),
self._theta_rad)
_, pix_w_in, pix_h_in, _ = sky_shape_to_pixel_svd(
skypos, wcs,
2 * self.a_in.to_value(u.arcsec),
2 * self.b_in.to_value(u.arcsec),
self._theta_rad)
a_out = pix_w_out / 2
b_out = pix_h_out / 2
a_in = pix_w_in / 2
b_in = pix_h_in / 2
return EllipticalAnnulus(positions=positions, a_in=a_in,
a_out=a_out, b_out=b_out,
b_in=b_in, theta=pix_angle)