Source code for cornish.region.circle


from __future__ import annotations # remove in Python 3.10
# Needed for forward references, see:
# https://stackoverflow.com/a/33533514/2712652

import logging
from typing import Union, Iterator

from math import radians as deg2rad
from math import degrees as rad2deg

import astropy
import astropy.units as u

import numpy as np
import starlink
import starlink.Ast as Ast

from .region import ASTRegion
from .polygon import ASTPolygon
from ..mapping.frame import ASTFrame
from ..mapping.frame.sky_frame import ASTICRSFrame

__all__ = ["ASTCircle"]

CENTER_EDGE = 0
CENTER_RADIUS = 1

logger = logging.getLogger("cornish")

[docs]class ASTCircle(ASTRegion): ''' ASTCircle is an :class:`ASTRegion` that represents a circle. Accepted signatures for creating an ASTCircle .. code-block::python c = ASTCircle(ast_object) # where ast_object is a starlink.Ast.Circle object c = ASTCircle(frame, center, edge_point) c = ASTCircle(frame, center, radius) :param ast_object: a circle object from the ``starlink-pyast`` module :param frame: a frame the circle is to be defined in, uses :class:`ASTICRSFrame` if `None` :param center: two elements that describe the center point of the circle in the provided frame in degrees :param edge_point: two elements that describe a point on the circumference of the circle in the provided frame in degrees :param radius: radius of the circle in degrees ''' def __init__(self, \ ast_object:starlink.Ast.Circle=None, \ frame:Union[ASTFrame,Ast.astFrame,astropy.coordinates.ICRS]=None, \ center:Union[astropy.coordinates.SkyCoord,Iterator]=None, \ edge_point:Union[np.ndarray,Iterator]=None, \ radius:Union[float,astropy.units.quantity.Quantity]=None): self._uncertainty = 4.848e-6 # defaults to 1 arcsec if ast_object: if any([x is None for x in [frame, center, edge_point, radius]]): raise Exception("ASTCircle: cannot specify both 'ast_object' and any other parameter.") if isinstance(ast_object, Ast.Circle): # make sure no other parameters are set self.astObject = ast_object return else: raise Exception("ASTCircle: The 'ast_object' provided was not of type starlink.Ast.Circle.") # check valid combination of parameters # ------------------------------------- # make sure we have a frame we can work with if frame is None: ast_frame = ASTICRSFrame().astObject #raise Exception("ASTCircle: A frame must be specified when creating an ASTCircle.") else: if isinstance(frame, ASTFrame): ast_frame = frame.astObject elif isinstance(frame, starlink.Ast.Frame): ast_frame = frame #ASTFrame.frameFromAstObject(frame) elif isinstance(frame, astropy.coordinates.ICRS): ast_frame = ASTICRSFrame().astObject else: raise Exception(f"ASTCircle: unexpected frame type specified ('{type(frame)}').") if all([x is not None for x in [edge_point, radius]]): raise ValueError("Both 'edge_point' and 'radius' cannot be simultaneously specified.") if center is None: raise ValueError("The 'center' parameter must be set.") if all([x is None for x in [edge_point, radius]]): raise ValueError("Along with 'center', a 'radius' or 'edge_point' must be specified.") # input forms: # CENTER_EDGE (0) : circle specified by center point and any point on the circumference (p1 = [float,float], p2 = [float,float]) # CENTER_RADIUS (1) : circle specified by center point and radius (p1 = [float,float], p2 = float) input_form = None if edge_point is None: input_form = CENTER_RADIUS # convert np.array types to lists so that the value can be used in 'any' and 'all' comparisons. # if isinstance(centerPoint, np.ndarray): # centerPoint = centerPoint.tolist() # if isinstance(edgePoint, np.ndarray): # edgePoint = edgePoint.tolist() # if isinstance(centerPoint, astropy.coordinates.SkyCoord): # centerPoint = [centerPoint.ra.to(u.deg).value, centerPoint.dec.to(u.deg).value] # # if all([centerPoint, edgePoint]) or all([centerPoint, radius]): # if edgePoint: # input_form = CENTER_EDGE # else: # input_form = CENTER_RADIUS # else: # raise Exception("ASTCircle: Either 'centerPoint' and 'edgePoint' OR 'centerPoint' " + \ # "and 'radius' must be specified when creating an ASTCircle.") if isinstance(center, astropy.coordinates.SkyCoord): p1 = [center.ra.to(u.rad).value, center.dec.to(u.rad).value] elif isinstance(center[0], astropy.units.quantity.Quantity): try: p1 = [center[0].to(u.rad).value, center[1].to(u.rad).value] except astropy.units.core.UnitConversionError as e: # todo: be more descriptive raise e else: p1 = [deg2rad(center[0]), deg2rad(center[1])] if input_form == CENTER_EDGE: # p1 = center point, p2 = edge point if isinstance(edge_point, astropy.coordinates.SkyCoord): p2 = [edge_point.ra.to(u.rad).value, edge_point.dec.to(u.rad).value] else: p2 = [deg2rad(edge_point[0]), deg2rad(edge_point[1])] else: # p1 = center point, p2 = radius if isinstance(radius, astropy.units.quantity.Quantity): p2 = [radius.to(u.rad).value] else: p2 = [deg2rad(radius)] self.astObject = Ast.Circle( ast_frame, input_form, p1, p2, unc=self.uncertainty ) def __repr__(self): return "<{0}.{1} {2}: center={3} deg, r={4:0.6}>".format(self.__class__.__module__, self.__class__.__name__, hex(id(self)), self.center, self.radius) def __add__(self, value): ''' Return a new ASTCircle with a radius increased by the provided :class:`astropy.units.Quantity` angular distance. :param value: new value is assumed to be in degrees if it's not an :class:`astropy.units.Quantity` object ''' try: value = float(value) * u.deg except (ValueError, TypeError): pass if value: return ASTCircle(frame=self.frame(), center=self.center, radius=self.radius + value) else: raise ValueError(f"Could not interpret provided value as a number.") def __mul__(self, value): ''' Return a new ASTCircle with a radius increased by a multiple of the value provided. :param value: numeric value to increase the radius by, e.g. 1.2 increases the radius by 20% ''' # try: # new_radius = self.radius.to_value() * value # except Exception as e: # raise ValueError(f"Could not interpret '{value}' as a number; error: {e}") if value: return ASTCircle(frame=self.frame(), center=self.center, radius=self.radius.unit * self.radius.to(u.deg).value * value) else: raise ValueError(f"Could not interpret '{value}' as a number; error: {e}") def __truediv__(self, value): ''' Return a new ASTCircle with a radius divided by a multiple of the value provided. :param value: numeric value to decrease the radius by, e.g. 2 decreases the radius by 50% ''' if value: return ASTCircle(frame=self.frame(), center=self.center, radius=self.radius.unit * self.radius.to(u.deg).value / value) else: raise ValueError(f"Could not interpret '{value}' as a number; error: {e}") @property def radius(self) -> astropy.units.quantity.Quantity: ''' The radius of this circle region in degrees. :returns: The radius as a geodesic distance in the associated coordinate system as an :class:`astropy.units.Quantity` object in degrees. ''' center = None radius = None ( center, radius, some_point_on_circumference ) = self.astObject.circlepars() return rad2deg(radius) * u.deg # TODO: possibly cache this @property def center(self): ''' The center of this circle region in degrees (a synonym for :func:`self.centre`" for the Americans). Returns ------- :returns: a list of points [x,y] that describe the centre of the circle in degrees ''' return self.centre @property def centre(self): ''' The centre of this circle region in degrees. Returns ------- :returns: a list of points [x,y] that describe the centre of the circle in degrees ''' center = None radius = None ( center, radius, some_point_on_circumference ) = self.astObject.circlepars() # ..todo:: possibly cache this # ..todo:: create another method (e.g. centerCoord) that returns an astropy.coordinates.SkyCoordinate object; would need to check if it's a sky object # example use is from boundingCircle, but that does is not a SkyFrame. return np.rad2deg(center)
[docs] def toPolygon(self, npoints=200, maxerr:astropy.units.Quantity=1.0*u.arcsec): ''' Returns a new polygon region that approximates this circle in the same frame. The algorithm used in this method leads to the new polygon being fully inscribed by the originating circle; all points generated are on the circle's circumference. Although the default number of points is 200, typically a much smaller number (e.g. 20) is sufficient. :param npoints: number of points to sample from the circle for the resulting polygon :param maxerr: ''' #old_mesh_size = self.meshSize #self.meshSize = npoints points = self.boundaryPointMesh(npoints=npoints) #logger.debug(points) return ASTPolygon(frame=self, points=points) # can get the frame from the astObject
#points = self.boundaryPointMesh(npoints=npoints) #return ASTPolygon.fromPointsOnSkyFrame(radec_pairs=points, frame=self.frame)
[docs] def boundingCircle(self) -> ASTCircle: ''' This method returns itself; a circle region is its own bounding circle. ''' return self
@property def area(self): ''' The area of the circle within its frame (e.g. on a Cartesian plane or sphere). [Not yet implemented.] ''' # see: https://math.stackexchange.com/questions/1832110/area-of-a-circle-on-sphere raise NotImplementedError()