Source code for regions.core.pixcoord

# Licensed under a 3-clause BSD style license - see LICENSE.rst
import copy

import numpy as np
from astropy.coordinates import SkyCoord

__all__ = ['PixCoord']


# Define global variables for the default 'origin' and 'mode' used for
# WCS transformations.
_DEFAULT_WCS_ORIGIN = 0
_DEFAULT_WCS_MODE = 'all'


[docs] class PixCoord: """ A class for pixel coordinates. This class can represent a scalar or an array of pixel coordinates. `~regions.PixCoord` objects can be added or subtracted to each other. They can also be compared for equality. The data members are either numbers or `~numpy.ndarray` (not `~astropy.units.Quantity` objects with unit "pixel"). Given a `astropy.wcs.WCS` object, it can be transformed to and from a `~astropy.coordinates.SkyCoord` object. Parameters ---------- x : float or array-like Pixel coordinate x value. y : float or array-like Pixel coordinate y value. Examples -------- Usage examples are provided in the :ref:`getting_started-coord` section of the documentation. """ def __init__(self, x, y): x, y = np.broadcast_arrays(x, y) if x.shape == (): self.x, self.y = x.item(), y.item() else: self.x, self.y = x, y
[docs] def copy(self): return self.__class__(copy.deepcopy(self.x), copy.deepcopy(self.y))
@staticmethod def _validate(obj, name, expected='any'): """ Validate that a given object is a valid `PixCoord`. Parameters ---------- obj : `PixCoord` The object to check. name : str The parameter name used for error messages. expected : {'any', 'scalar', 'array'} What kind of PixCoord to check for. Returns ------- obj : `PixCoord` The input object, if valid. """ if not isinstance(obj, PixCoord): raise TypeError(f'{name!r} must be a PixCoord') if expected == 'any': pass elif expected == 'scalar': if not obj.isscalar: raise ValueError(f'{name!r} must be a scalar PixCoord') elif expected == 'array': if obj.isscalar: raise ValueError(f'{name!r} must be a PixCoord array') else: raise ValueError(f'Invalid value for "expected": {expected!r}') return obj @property def isscalar(self): """ Whether the instance is scalar (e.g., a single (x, y) coordinate). """ return np.isscalar(self.x) def __repr__(self): return f'{self.__class__.__name__}(x={self.x}, y={self.y})' def __len__(self): if self.isscalar: raise TypeError(f'Scalar {self.__class__.__name__!r} object has ' 'no len()') return len(self.x) def __iter__(self): for (x, y) in zip(self.x, self.y, strict=True): yield PixCoord(x=x, y=y) def __getitem__(self, key): if self.isscalar: raise IndexError(f'Scalar {self.__class__.__name__!r} cannot be ' 'indexed or sliced.') # Let Numpy do the slicing x = self.x[key] y = self.y[key] return PixCoord(x=x, y=y) def __add__(self, other): if not isinstance(other, self.__class__): raise TypeError('Can add only to another PixCoord') return self.__class__(self.x + other.x, self.y + other.y) def __sub__(self, other): if not isinstance(other, self.__class__): raise TypeError('Can subtract only from another PixCoord') return self.__class__(self.x - other.x, self.y - other.y) def __eq__(self, other): """ Check whether ``other`` is `PixCoord` object and whether their abscissa and ordinate values are equal using `np.testing.assert_allclose` with its default tolerance values. """ if isinstance(other, self.__class__): return np.allclose([self.x, self.y], [other.x, other.y]) return False
[docs] def to_sky(self, wcs, origin=_DEFAULT_WCS_ORIGIN, mode=_DEFAULT_WCS_MODE): """ Convert to a `~astropy.coordinates.SkyCoord`. Parameters ---------- wcs : `~astropy.wcs.WCS` The WCS to use to convert pixels to world coordinates. origin : int, optional Whether to return 0 or 1-based pixel coordinates. mode : {'all', 'wcs'}, optional Whether to do the transformation including distortions (``'all'``) or only including only the core WCS transformation (``'wcs'``). Returns ------- coord : `~astropy.coordinates.SkyCoord` A new object with sky coordinates corresponding to the pixel coordinates. """ return SkyCoord.from_pixel(xp=self.x, yp=self.y, wcs=wcs, origin=origin, mode=mode)
[docs] @classmethod def from_sky(cls, skycoord, wcs, origin=_DEFAULT_WCS_ORIGIN, mode=_DEFAULT_WCS_MODE): """ Create `PixCoord` from a `~astropy.coordinates.SkyCoord`. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The sky coordinate. wcs : `~astropy.wcs.WCS` The WCS to use to convert pixels to world coordinates. origin : int, optional Whether to return 0 or 1-based pixel coordinates. mode : {'all', 'wcs'}, optional Whether to do the transformation including distortions (``'all'``) or only including only the core WCS transformation (``'wcs'``). Returns ------- coord : `PixCoord` A new `PixCoord` object at the position of the input sky coordinates. """ x, y = skycoord.to_pixel(wcs=wcs, origin=origin, mode=mode) return cls(x=x, y=y)
[docs] def separation(self, other): r""" Calculate the separation to another pixel coordinate. This is the two-dimensional Cartesian separation :math:`d` where .. math:: d = \sqrt{(x_1 - x_2) ^ 2 + (y_1 - y_2) ^ 2} Parameters ---------- other : `PixCoord` The other pixel coordinate. Returns ------- separation : `numpy.array` The separation in pixels. """ dx = other.x - self.x dy = other.y - self.y return np.hypot(dx, dy)
@property def xy(self): """ A 2-tuple ``(x, y)`` for this coordinate. """ return self.x, self.y
[docs] def rotate(self, center, angle): """ Rotate the pixel coordinate. Positive ``angle`` corresponds to counter-clockwise rotation. Parameters ---------- center : `PixCoord` The rotation center point. angle : `~astropy.coordinates.Angle` The rotation angle. Returns ------- coord : `PixCoord` The rotated coordinates (which is an independent copy). """ dx = self.x - center.x dy = self.y - center.y vec = np.array([dx, dy]) cosa, sina = np.cos(angle), np.sin(angle) rotation_matrix = np.array([[cosa, -sina], [sina, cosa]]) vec = np.matmul(rotation_matrix, vec) return self.__class__(center.x + vec[0], center.y + vec[1])