Source code for photutils.aperture.rectangle

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Rectangular and rectangular-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_RECTANGLE,
                                                  SHAPE_RECTANGULAR_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 rectangular_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__ = [
    'RectangularAnnulus',
    'RectangularAperture',
    'RectangularMaskMixin',
    'SkyRectangularAnnulus',
    'SkyRectangularAperture',
]


def _rectangular_polygon_offsets(half_x, half_y, theta):
    """
    Compute the four corner offsets of a rectangle with half-extents
    ``half_x`` and ``half_y`` along the unrotated first and second
    coordinate axes, rotated by ``theta`` (in radians).

    The corners are ordered counter-clockwise in the unrotated frame.

    Parameters
    ----------
    half_x : float
        Half of the rectangle width along the unrotated first coordinate
        axis.

    half_y : float
        Half of the rectangle height along the unrotated second
        coordinate axis.

    theta : float
        The rotation angle in radians from the unrotated first
        coordinate axis.

    Returns
    -------
    offsets : 2D `~numpy.ndarray`
        The four corner offsets of the rectangle, shape (4, 2).
    """
    corners = np.array([[-half_x, -half_y], [half_x, -half_y],
                        [half_x, half_y], [-half_x, half_y]])
    cos_t = np.cos(theta)
    sin_t = np.sin(theta)
    rot_mat = np.array([[cos_t, -sin_t], [sin_t, cos_t]])
    return corners @ rot_mat.T


[docs] @deprecated('3.0', until='4.0') class RectangularMaskMixin: # pragma: no cover """ Mixin class to create masks for rectangular or rectangular-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, 'w'): w = self.w h = self.h elif hasattr(self, 'w_out'): # annulus w = self.w_out h = self.h_out else: msg = 'Cannot determine the aperture radius' raise ValueError(msg) masks = [] for bbox, edges in zip(self._bbox, self._centered_edges, strict=True): ny, nx = bbox.shape mask = rectangular_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, w, h, self._theta_rad, use_exact, subpixels) # Subtract the inner rectangle for an annulus if hasattr(self, 'w_in'): mask -= rectangular_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, self.w_in, self.h_in, self._theta_rad, use_exact, subpixels) masks.append(ApertureMask(mask, bbox)) if self.isscalar: return masks[0] return masks
@staticmethod def _calc_extents(width, height, theta_rad): """ Calculate half of the bounding box extents of a rectangle. """ return _calc_rectangle_extents(width, height, theta_rad) @staticmethod def _lower_left_positions(positions, width, height, theta_rad): """ Calculate lower-left positions from the input center positions. Used for creating `~matplotlib.patches.Rectangle` patch for the aperture. """ return _calc_lower_left_positions(positions, width, height, theta_rad)
def _calc_rectangle_extents(width, height, theta_rad): """ Calculate half of the bounding box extents of a rectangle. """ half_width = width / 2.0 half_height = height / 2.0 sin_theta = math.sin(theta_rad) cos_theta = math.cos(theta_rad) x_extent1 = abs((half_width * cos_theta) - (half_height * sin_theta)) x_extent2 = abs((half_width * cos_theta) + (half_height * sin_theta)) y_extent1 = abs((half_width * sin_theta) + (half_height * cos_theta)) y_extent2 = abs((half_width * sin_theta) - (half_height * cos_theta)) x_extent = max(x_extent1, x_extent2) y_extent = max(y_extent1, y_extent2) return x_extent, y_extent def _calc_lower_left_positions(positions, width, height, theta_rad): """ Calculate lower-left positions from the input center positions. Used for creating `~matplotlib.patches.Rectangle` patch for the aperture. """ half_width = width / 2.0 half_height = height / 2.0 sin_theta = math.sin(theta_rad) cos_theta = math.cos(theta_rad) xshift = (half_height * sin_theta) - (half_width * cos_theta) yshift = -(half_height * cos_theta) - (half_width * sin_theta) return np.atleast_2d(positions) + np.array([xshift, yshift])
[docs] class RectangularAperture(PixelAperture): """ A rectangular 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 w : float The full width of the rectangle in pixels. For ``theta=0`` the width side is along the ``x`` axis. h : float The full height of the rectangle in pixels. For ``theta=0`` the height side is along the ``y`` axis. 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 width (``w``) or height (``h``) is negative. Examples -------- >>> from astropy.coordinates import Angle >>> from photutils.aperture import RectangularAperture >>> theta = Angle(80, 'deg') >>> aper = RectangularAperture([10.0, 20.0], 5.0, 3.0) >>> aper = RectangularAperture((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 = RectangularAperture([pos1, pos2, pos3], 5.0, 3.0) >>> aper = RectangularAperture((pos1, pos2, pos3), 5.0, 3.0, theta=theta) """ _params = ('positions', 'w', 'h', 'theta') positions = PixelPositions('The center pixel position(s).') w = PositiveScalar('The full width in pixels.') h = PositiveScalar('The full height in pixels.') theta = ScalarAngleOrValue('The counterclockwise rotation angle as an ' 'angular Quantity or a value in radians from ' 'the positive x axis.') @deprecated_positional_kwargs(since='3.0', until='4.0') def __init__(self, positions, w, h, theta=0.0): self.positions = positions self.w = w self.h = h self.theta = theta self._theta_rad = self.theta.to_value(u.radian) @lazyproperty def _xy_extents(self): """ The half-width and half-height of the bounding box of the rectangle. """ return _calc_rectangle_extents(self.w, self.h, self._theta_rad) def _batch_shape_params(self): return SHAPE_RECTANGLE, (self.w, self.h, self._theta_rad) @lazyproperty def area(self): """ The exact geometric area of the aperture shape. """ return self.w * self.h 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) xy_positions = _calc_lower_left_positions(xy_positions, self.w, self.h, self._theta_rad) angle = self.theta.to_value(u.deg) patches = [mpatches.Rectangle(xy_position, self.w, self.h, 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 rectangular_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, self.w, self.h, self._theta_rad, use_exact, subpixels)
[docs] def to_sky(self, wcs): """ Convert the aperture to a `SkyRectangularAperture` 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 : `SkyRectangularAperture` object A `SkyRectangularAperture` 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, sky_h, sky_angle = pixel_shape_to_sky_svd( pixcoord, wcs, self.w, self.h, self._theta_rad) width = Angle(sky_w, 'arcsec') height = Angle(sky_h, 'arcsec') return SkyRectangularAperture(positions=positions, w=width, h=height, theta=sky_angle)
[docs] def to_polygon(self): """ Return a `~photutils.aperture.PolygonAperture` with the four corners of this rectangle as its vertices. Returns ------- aperture : `~photutils.aperture.PolygonAperture` A polygon aperture exactly equivalent to the rectangle. """ offsets = _rectangular_polygon_offsets( 0.5 * self.w, 0.5 * self.h, self.theta.to_value(u.radian)) return PolygonAperture._from_convex_offsets(self.positions, offsets)
[docs] class RectangularAnnulus(PixelAperture): r""" A rectangular 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 w_in : float The inner full width of the rectangular annulus in pixels. For ``theta=0`` the width side is along the ``x`` axis. w_out : float The outer full width of the rectangular annulus in pixels. For ``theta=0`` the width side is along the ``x`` axis. h_out : float The outer full height of the rectangular annulus in pixels. h_in : `None` or float The inner full height of the rectangular annulus in pixels. If `None`, then the inner full height is calculated as: .. math:: h_{in} = h_{out} \left(\frac{w_{in}}{w_{out}}\right) For ``theta=0`` the height side is along the ``y`` axis. 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 width (``w_in``) is greater than outer width (``w_out``). ValueError : `ValueError` If either the inner width (``w_in``) or the outer height (``h_out``) is negative. Examples -------- >>> from astropy.coordinates import Angle >>> from photutils.aperture import RectangularAnnulus >>> theta = Angle(80, 'deg') >>> aper = RectangularAnnulus([10.0, 20.0], 3.0, 8.0, 5.0) >>> aper = RectangularAnnulus((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 = RectangularAnnulus([pos1, pos2, pos3], 3.0, 8.0, 5.0) >>> aper = RectangularAnnulus((pos1, pos2, pos3), 3.0, 8.0, 5.0, ... theta=theta) """ _params = ('positions', 'w_in', 'w_out', 'h_in', 'h_out', 'theta') positions = PixelPositions('The center pixel position(s).') w_in = PositiveScalar('The inner full width in pixels.') w_out = PositiveScalar('The outer full width in pixels.') h_in = PositiveScalar('The inner full height in pixels.') h_out = PositiveScalar('The outer full height in pixels.') theta = ScalarAngleOrValue('The counterclockwise rotation angle as an ' 'angular Quantity or a value in radians from ' 'the positive x axis.') @deprecated_positional_kwargs(since='3.0', until='4.0') def __init__(self, positions, w_in, w_out, h_out, h_in=None, theta=0.0): if not w_out > w_in: msg = "'w_out' must be greater than 'w_in'" raise ValueError(msg) self.positions = positions self.w_in = w_in self.w_out = w_out self.h_out = h_out if h_in is None: h_in = self.w_in * self.h_out / self.w_out elif not h_out > h_in: msg = "'h_out' must be greater than 'h_in'" raise ValueError(msg) self.h_in = h_in self.theta = theta self._theta_rad = self.theta.to_value(u.radian) @lazyproperty def _xy_extents(self): """ The half-width and half-height of the bounding box of the rectangle. """ return _calc_rectangle_extents(self.w_out, self.h_out, self._theta_rad) def _batch_shape_params(self): return SHAPE_RECTANGULAR_ANNULUS, (self.w_in, self.h_in, self.w_out, self.h_out, self._theta_rad) @lazyproperty def area(self): """ The exact geometric area of the aperture shape. """ return self.w_out * self.h_out - self.w_in * self.h_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) inner_xy_positions = _calc_lower_left_positions(xy_positions, self.w_in, self.h_in, self._theta_rad) outer_xy_positions = _calc_lower_left_positions(xy_positions, self.w_out, self.h_out, self._theta_rad) patches = [] angle = self.theta.to_value(u.deg) for xy_in, xy_out in zip(inner_xy_positions, outer_xy_positions, strict=True): patch_inner = mpatches.Rectangle(xy_in, self.w_in, self.h_in, angle=angle) patch_outer = mpatches.Rectangle(xy_out, self.w_out, self.h_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 = rectangular_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, self.w_out, self.h_out, self._theta_rad, use_exact, subpixels) overlap -= rectangular_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, self.w_in, self.h_in, self._theta_rad, use_exact, subpixels) return overlap
[docs] def to_sky(self, wcs): """ Convert the aperture to a `SkyRectangularAnnulus` 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 : `SkyRectangularAnnulus` object A `SkyRectangularAnnulus` 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, self.w_out, self.h_out, self._theta_rad) _, sky_w_in, sky_h_in, _ = pixel_shape_to_sky_svd( pixcoord, wcs, self.w_in, self.h_in, self._theta_rad) w_in = Angle(sky_w_in, 'arcsec') w_out = Angle(sky_w_out, 'arcsec') h_in = Angle(sky_h_in, 'arcsec') h_out = Angle(sky_h_out, 'arcsec') return SkyRectangularAnnulus(positions=positions, w_in=w_in, w_out=w_out, h_out=h_out, h_in=h_in, theta=sky_angle)
[docs] class SkyRectangularAperture(SkyAperture): """ A rectangular 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. w : scalar `~astropy.units.Quantity` The full width of the rectangle in angular units. For ``theta=0`` the width side is along the North-South axis. h : scalar `~astropy.units.Quantity` The full height of the rectangle in angular units. For ``theta=0`` the height side is along the East-West axis. theta : scalar `~astropy.units.Quantity`, optional The position angle (in angular units) of the rectangle "width" side. 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 SkyRectangularAperture >>> positions = SkyCoord(ra=[10.0, 20.0], dec=[30.0, 40.0], unit='deg') >>> aper = SkyRectangularAperture(positions, 1.0*u.arcsec, 0.5*u.arcsec) """ _params = ('positions', 'w', 'h', 'theta') positions = SkyCoordPositions('The center position(s) in sky coordinates.') w = PositiveScalarAngle('The full width in angular units.') h = PositiveScalarAngle('The full height in angular units.') theta = ScalarAngle('The position angle (in angular units) of the ' 'rectangle "width" side.') @deprecated_positional_kwargs(since='3.0', until='4.0') def __init__(self, positions, w, h, theta=0.0 * u.deg): self.positions = positions self.w = w self.h = h self.theta = theta self._theta_rad = self.theta.to_value(u.radian)
[docs] def to_pixel(self, wcs): """ Convert the aperture to a `RectangularAperture` 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 : `RectangularAperture` object A `RectangularAperture` 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, pix_h, pix_angle = sky_shape_to_pixel_svd( skypos, wcs, self.w.to_value(u.arcsec), self.h.to_value(u.arcsec), self._theta_rad) return RectangularAperture(positions=positions, w=pix_w, h=pix_h, theta=pix_angle)
[docs] def to_polygon(self): """ Return a `~photutils.aperture.SkyPolygonAperture` with the four corners of this rectangle as its vertices. Returns ------- aperture : `~photutils.aperture.SkyPolygonAperture` A sky polygon aperture exactly equivalent to the rectangle. """ unit = self.w.unit # At theta=0, the width (w) is along +lat (North) and the height # (h) is along +lon, so the unrotated first axis uses the # half-height and the second axis uses the half-width. offsets = _rectangular_polygon_offsets( 0.5 * self.h.to_value(unit), 0.5 * self.w.to_value(unit), self.theta.to_value(u.radian)) * unit return SkyPolygonAperture._from_convex_offsets(self.positions, offsets)
[docs] class SkyRectangularAnnulus(SkyAperture): r""" A rectangular 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. w_in : scalar `~astropy.units.Quantity` The inner full width of the rectangular annulus in angular units. For ``theta=0`` the width side is along the North-South axis. w_out : scalar `~astropy.units.Quantity` The outer full width of the rectangular annulus in angular units. For ``theta=0`` the width side is along the North-South axis. h_out : scalar `~astropy.units.Quantity` The outer full height of the rectangular annulus in angular units. h_in : `None` or scalar `~astropy.units.Quantity` The inner full height of the rectangular annulus in angular units. If `None`, then the inner full height is calculated as: .. math:: h_{in} = h_{out} \left(\frac{w_{in}}{w_{out}}\right) For ``theta=0`` the height side is along the East-West axis. theta : scalar `~astropy.units.Quantity`, optional The position angle (in angular units) of the rectangle "width" side. 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 SkyRectangularAnnulus >>> positions = SkyCoord(ra=[10.0, 20.0], dec=[30.0, 40.0], unit='deg') >>> aper = SkyRectangularAnnulus(positions, 3.0*u.arcsec, 8.0*u.arcsec, ... 5.0*u.arcsec) """ _params = ('positions', 'w_in', 'w_out', 'h_in', 'h_out', 'theta') positions = SkyCoordPositions('The center position(s) in sky coordinates.') w_in = PositiveScalarAngle('The inner full width in angular units.') w_out = PositiveScalarAngle('The outer full width in angular units.') h_in = PositiveScalarAngle('The inner full height in angular units.') h_out = PositiveScalarAngle('The outer full height in angular units.') theta = ScalarAngle('The position angle (in angular units) of the ' 'rectangle "width" side.') @deprecated_positional_kwargs(since='3.0', until='4.0') def __init__(self, positions, w_in, w_out, h_out, h_in=None, theta=0.0 * u.deg): if not w_out > w_in: msg = "'w_out' must be greater than 'w_in'" raise ValueError(msg) self.positions = positions self.w_in = w_in self.w_out = w_out self.h_out = h_out if h_in is None: h_in = self.w_in * self.h_out / self.w_out elif not h_out > h_in: msg = "'h_out' must be greater than 'h_in'" raise ValueError(msg) self.h_in = h_in self.theta = theta self._theta_rad = self.theta.to_value(u.radian)
[docs] def to_pixel(self, wcs): """ Convert the aperture to a `RectangularAnnulus` 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 : `RectangularAnnulus` object A `RectangularAnnulus` 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, self.w_out.to_value(u.arcsec), self.h_out.to_value(u.arcsec), self._theta_rad) _, pix_w_in, pix_h_in, _ = sky_shape_to_pixel_svd( skypos, wcs, self.w_in.to_value(u.arcsec), self.h_in.to_value(u.arcsec), self._theta_rad) return RectangularAnnulus(positions=positions, w_in=pix_w_in, w_out=pix_w_out, h_out=pix_h_out, h_in=pix_h_in, theta=pix_angle)