Source code for astropy.coordinates.coordsystems

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

"""
This module contains the base classes and frameworks for coordinate objects.
"""
from abc import ABCMeta, abstractproperty, abstractmethod

from .. import units as u
from .angles import RA, Dec, Angle, AngularSeparation
from .distances import *

__all__ = ['SphericalCoordinatesBase']


[docs]class SphericalCoordinatesBase(object): """ Abstract superclass for all coordinate classes representing points in three dimensions. Notes ----- Subclasses must implement `__init__`, and define the `latangle` and `lonangle` properties. They may also override the `equinox` property, or leave it unaltered to indicate the coordinates are equinoxless. `_initialize_latlon` is provided to implement typical initialization features, and should be called from a subclass' `__init__`. See the classes in `astropy.coordinates.builtin_systems` for examples of this. """ __metaclass__ = ABCMeta @abstractmethod def __init__(self, *args, **kwargs): """ Subclasses must override this, but they should also call this to set up internal state. """ self._distance = None self._cartpoint = None def __eq__(self, other): try: return (self.latangle == other.latangle and self.lonangle == other.lonangle and self.distance == other.distance and self.equinox == other.equinox) except AttributeError: return False _init_docstring_param_templ = """coordstr : str A single string with the coordinates. Cannot be used with `{latnm}` and `{lonnm}` nor `x`/`y`/`z`. {lonnm} : `~astropy.coordinates.angle.Angle`, float, int, str This must be given with `{latnm}`. {latnm} : `~astropy.coordinates.angle.Angle`, float, int, str This must be given with `{lonnm}`. distance : `~astropy.coordinates.coordsystems.Distance`, optional This may be given with `{latnm}` and `{lonnm}` or `coordstr` and not `x`, `y`, or `z`. If not given, `None` (unit sphere) will be assumed. x : number The first cartesian coordinate. Must be given with `y` and `z` and not with `{lonnm}` or `{latnm}` nor `coordstr`. y : number The second cartesian coordinate. Must be given with `x` and `z` and not with `{lonnm}` or `{latnm}` nor `coordstr`. z : number The third cartesian coordinate. Must be given with `x` and `y` and not with `{lonnm}` or `{latnm}` nor `coordstr`. cartpoint : `~astropy.coordinates.distance.CartesianPoints` A cartesian point with the coordinates. Cannot be used with any other arguments. unit The `unit` parameter's interpretation depends on what other parameters are given: * If `{lonnm}` and `{latnm}` or `coordstr` are given: `unit` must be a length-2 sequence specifying the units of `{lonnm}` and `{latnm}`, respectively. They can be either `~astropy.units.UnitBase` objects or strings that will be converted using `~astropy.units.Unit`. They can also be None to attempt to automatically interpret the units (see `~astropy.coordinates.angles.Angle` for details.) If `unit` is just `None`, this will be interpreted the same as ``(None, None)``. * If `x`, `y`, and `z` are given: `unit` must be a single unit with dimensions of length""" def _initialize_latlon(self, lonname, latname, useradec, initargs, initkwargs): """ Subclasses should use this to initialize standard lat/lon-style coordinates. This recognizes both the lat/lon style and the cartesian form. Parameters ---------- lonname : str The name of the longitude-like coordinate attribute latname : str The name of the latitude-like coordinate attribute useradec : bool If True, the `RA` and `Dec` classes will be used for the angles. Otherwise, a basic `Angle` will be used. initargs : list The ``*args`` from the initializer initkwargs : dict The ``**kwargs`` from the initializer """ initkwargs = dict(initkwargs) # copy nargs = len(initargs) sclsnm = self.__class__.__name__ if nargs == 1: if isinstance(initargs[0], CartesianPoints): initkwargs['cartpoint'] = initargs[0] else: initkwargs['coordstr'] = initargs[0] if nargs > 1: if lonname in initkwargs: raise TypeError("{0} got multiple values for keyword argument " "'{1}'".format(sclsnm, lonname)) initkwargs[lonname] = initargs[0] if nargs >= 2: if latname in initkwargs: raise TypeError("{0} got multiple values for keyword argument " "'{1}'".format(sclsnm, latname)) initkwargs[latname] = initargs[1] if nargs > 2: raise TypeError('{0} takes up to 2 positional arguments ' '({1} given)'.format(sclsnm, len(initargs))) unit = initkwargs.pop('unit', None) coordstr = initkwargs.pop('coordstr', None) lonval = initkwargs.pop(lonname, None) latval = initkwargs.pop(latname, None) distval = initkwargs.pop('distance', None) cartpoint = initkwargs.pop('cartpoint', None) x = initkwargs.pop('x', None) y = initkwargs.pop('y', None) z = initkwargs.pop('z', None) if len(initkwargs) > 0: raise TypeError('{0} got unexpected keyword argument' ' {1}'.format(sclsnm, initkwargs.keys())) angleinit = ((lonval is not None and latval is not None) or coordstr is not None) cartinit = ((x is not None and y is not None and z is not None) or cartpoint is not None) if angleinit and not cartinit: # lat/lon-style initialization for v in [x, y, z, cartpoint]: if v is not None: raise ValueError('Cannot give both angular and cartesian ' 'coordinates while initializing ' + sclsnm) try: # this raises a TypeError if `unit` is not None or iterable units = [None, None] if unit is None else list(unit) except TypeError: raise ValueError('Must give a sequence of 2 units or None ' 'while initializing {0}. Instead got a ' 'non-sequence {1}'.format(sclsnm, unit)) if len(units) == 2: try: if units[0] is not None: units[0] = u.Unit(units[0]) if units[1] is not None: units[1] = u.Unit(units[1]) except ValueError: raise ValueError('Could not convert units to unit objects ' 'while initializing ' + sclsnm) else: raise ValueError('Must give a sequence of 2 units or None ' 'while initializing {0}. Instead got a sequence ' 'of {1}.'.format(sclsnm, len(units))) if coordstr is not None: # need to try to parse the coordinate from a single argument # populates latval and lonval variables, which then get made # into coordinates below if isinstance(coordstr, basestring): if "," in coordstr: lonval, latval = coordstr.split(",") else: coosplit = coordstr.split() if len(coosplit) == 6: lonval = " ".join(coosplit[0:3]) latval = " ".join(coosplit[3:]) elif len(coosplit) == 2: lonval, latval = coosplit else: msg = ("Could not parse {lonname}/{latname} values " "from the string provided: '{coordstr}'.") raise ValueError(msg.format(lonname=lonname, latname=latname, coordstr=coordstr)) else: raise ValueError("A {0} cannot be created with a single value of type " "'{1}', must be a string.".format(sclsnm, type(coordstr).__name__)) # now actually create the angle objects if useradec: lonang = RA(lonval, unit=units[0]) latang = Dec(latval, unit=units[1]) else: if isinstance(lonval, RA): raise TypeError('Cannot provide an RA object to non-RA/Dec system {0}'.format(sclsnm)) if isinstance(latval, Dec): raise TypeError('Cannot provide a Dec object to non-RA/Dec system {0}'.format(sclsnm)) lonang = Angle(lonval, unit=units[0]) latang = Angle(latval, unit=units[1]) dist = None if distval is None else Distance(distval) # copy elif cartinit and not angleinit: # cartesian-style initialization for v in [coordstr, lonval, latval, distval]: if v is not None: raise ValueError('Cannot give both angular and cartesian ' 'coordinates while initializing ' + sclsnm) if cartpoint is not None: for v in [x, y, z, unit]: if v is not None: raise ValueError('Cannot give both a CartesianPoints ' 'and x/y/z/unit parameters while ' 'initializing ' + sclsnm) x = cartpoint.x y = cartpoint.y z = cartpoint.z unit = cartpoint.unit r, latval, lonval = cartesian_to_spherical(x, y, z) if useradec: lonang = RA(lonval, unit=u.radian) latang = Dec(latval, unit=u.radian) else: lonang = Angle(lonval, unit=u.radian) latang = Angle(latval, unit=u.radian) dist = None if unit is None else Distance(r, unit) else: raise TypeError('Must initialize {coordnm} with ' '{latname}/{lonname}/(distance) or x/y/z ' ''.format(coordnm=sclsnm, latname=latname, lonname=lonname)) # now actually set the values setattr(self, lonname, lonang) setattr(self, latname, latang) self._distance = dist @abstractproperty
[docs] def latangle(self): """ The latitudinal/elevation angle for these coordinates as an `~astropy.coorinates.angles.Angle` object. .. note :: This should be overridden in subclasses as a read-only property that just returns an attribute a way to abstract the exact choice of names for the coordiantes. E.g., `ICRSCoordinates` implements this by doing ``return self.ra``. """
@abstractproperty
[docs] def lonangle(self): """ The longitudinal/azimuthal angle for these coordinates as an `~astropy.coorinates.angles.Angle` object. .. note :: This should be overridden in subclasses as a read-only property that just returns an attribute a way to abstract the exact choice of names for the coordinates. E.g., `ICRSCoordinates` implements this by doing ``return self.dec``. """
@property
[docs] def equinox(self): """ The equinox of this system, or None to indicate no equinox specified. """ return None
@property def distance(self): """ The radial distance for this coordinate object as an `~astropy.coordinates.coordsystems.Distance` object. If set as a tuple, the tuple will be passed into the `~astropy.coordinates.coordsystems.Distance` constructor. Alternatively, this may be `None`, indicating an unknown/not given distance. Where necessary, this object will be interpreted as angles on the unit sphere. """ return self._distance @distance.setter
[docs] def distance(self, val): if val is None: self._distance = None elif isinstance(val, tuple): self._distance = Distance(*val) elif isinstance(val, Distance): self._distance = val else: raise TypeError( 'Spherical coordinate distance must be a Distance object, a ' 'tuple that can be used to instantiate a Distance object, or ' 'None.')
@property
[docs] def x(self): self._make_cart() return self._cartpoint.x
@property
[docs] def y(self): self._make_cart() return self._cartpoint.y
@property
[docs] def z(self): self._make_cart() return self._cartpoint.z
@property
[docs] def cartesian(self): self._make_cart() return self._cartpoint
def _make_cart(self, override=False): if override or self._cartpoint is None: if self._distance is None: r = 1 runit = None else: r = self._distance._value runit = self._distance._unit x, y, z = spherical_to_cartesian(r, self.latangle.radians, self.lonangle.radians) self._cartpoint = CartesianPoints(x, y, z, runit)
[docs] def separation(self, other): """ Computes on-sky separation between this coordinate and another. See the `~astropy.coordinates.angles.AngularSeparation` docstring for further details on the actual calculation. Parameters ---------- other : `~astropy.coordinates.coordsystems.SphericalCoordinatesBase` The coordinate system to get the separation to. Returns ------- sep : `~astropy.coordinates.angles.AngularSeparation` The on-sky separation between this and the `other` coordinate. """ other_in_self_system = other.transform_to(self.__class__) lat1 = self.latangle.radians lat2 = other_in_self_system.latangle.radians lon1 = self.lonangle.radians lon2 = other_in_self_system.lonangle.radians return AngularSeparation(lat1, lon1, lat2, lon2, u.radian)
[docs] def separation_3d(self, other): """ Computes three dimensional separation between this coordinate and another. Parameters ---------- other : `~astropy.coordinates.coordsystems.SphericalCoordinatesBase` The coordinate system to get the distance to. Returns ------- sep : `~astropy.coordinates.coordsystems.Distance` The real-space distance between these two coordinates. Raises ------ ValueError If this or the other coordinate do not have distances. """ if self._distance is None: raise ValueError('This object does not have a distance; cannot ' 'compute 3d separation.') # do this first just in case the conversion somehow creates a distance other_in_self_system = other.transform_to(self.__class__) if other_in_self_system._distance is None: raise ValueError('The other object does not have a distance; ' 'cannot compute 3d separation.') dscale = other_in_self_system._distance._unit.to(self._distance._unit, 1) dx = self.x - other_in_self_system.x * dscale dy = self.y - other_in_self_system.y * dscale dz = self.z - other_in_self_system.z * dscale distval = (dx ** 2 + dy ** 2 + dz ** 2) ** 0.5 return Distance(distval, self._distance._unit) #<------------transformation-related stuff here-------------------->
[docs] def transform_to(self, tosys): """ Transform this coordinate to a new system. Parameters ---------- tosys : class The system to transform this coordinate into. Returns ------- transcoord A new object with this coordinate represented in the `tosys` system. Raises ------ ValueError If there is no possible transformation route. """ from copy import deepcopy from .transformations import master_transform_graph from .errors import ConvertError if tosys is self.__class__: return deepcopy(self) trans = master_transform_graph.get_transform(self.__class__, tosys) if trans is None: raise ConvertError('Cannot transform from {0} to ' '{1}'.format(self.__class__, tosys)) return trans(self)
[docs] def is_transformable_to(self, tosys): """ Determines if this coordinate can be transformed to a particular system. Parameters ---------- tosys : class The system to transform this coordinate into. Returns ------- transformable : bool or str True if this can be trasnformed to `tosys`, False if not. The string 'same' if `tosys` is the same system as this object (i.e. no transformation is needed). """ from .transformations import master_transform_graph if self.__class__ is tosys: return 'same' else: trans = master_transform_graph.get_transform(self.__class__, tosys) return trans is not None
def __getattr__(self, name): """ Overrides getattr to return coordinates that this can be transformed to, based on the alias name in the master transform graph. """ from .transformations import master_transform_graph nmsys = master_transform_graph.lookup_name(name) if self.__class__ is nmsys: return self if nmsys is not None and self.is_transformable_to(nmsys): return self.transform_to(nmsys) else: msg = "'{0}' object has no attribute '{1}', nor a transform." raise AttributeError(msg.format(self.__class__.__name__, name)) def __dir__(self): """ Overriding the builtin `dir` behavior allows us to add the transforms available by aliases. This also allows ipython tab-completion to know about the transforms. """ from .transformations import master_transform_graph # the stuff `dir` normally gives dir_items = dir(type(self)) + self.__dict__.keys() # determine the aliases that this can be transformed to. for alias in master_transform_graph.get_aliases(): tosys = master_transform_graph.lookup_name(alias) if self.is_transformable_to(tosys): dir_items.append(alias) return sorted(set(dir_items))

Page Contents