1# -*- coding: utf-8 -*- 2# Licensed under a 3-clause BSD style license - see LICENSE.rst 3""" 4Framework and base classes for coordinate frames/"low-level" coordinate 5classes. 6""" 7 8 9# Standard library 10import copy 11import inspect 12from collections import namedtuple, defaultdict 13import warnings 14 15# Dependencies 16import numpy as np 17 18# Project 19from astropy.utils.compat.misc import override__dir__ 20from astropy.utils.decorators import lazyproperty, format_doc 21from astropy.utils.exceptions import AstropyWarning, AstropyDeprecationWarning 22from astropy import units as u 23from astropy.utils import ShapedLikeNDArray, check_broadcast 24from .transformations import TransformGraph 25from . import representation as r 26from .angles import Angle 27from .attributes import Attribute 28 29 30__all__ = ['BaseCoordinateFrame', 'frame_transform_graph', 31 'GenericFrame', 'RepresentationMapping'] 32 33 34# the graph used for all transformations between frames 35frame_transform_graph = TransformGraph() 36 37 38def _get_repr_cls(value): 39 """ 40 Return a valid representation class from ``value`` or raise exception. 41 """ 42 43 if value in r.REPRESENTATION_CLASSES: 44 value = r.REPRESENTATION_CLASSES[value] 45 elif (not isinstance(value, type) or 46 not issubclass(value, r.BaseRepresentation)): 47 raise ValueError( 48 'Representation is {!r} but must be a BaseRepresentation class ' 49 'or one of the string aliases {}'.format( 50 value, list(r.REPRESENTATION_CLASSES))) 51 return value 52 53 54def _get_diff_cls(value): 55 """ 56 Return a valid differential class from ``value`` or raise exception. 57 58 As originally created, this is only used in the SkyCoord initializer, so if 59 that is refactored, this function my no longer be necessary. 60 """ 61 62 if value in r.DIFFERENTIAL_CLASSES: 63 value = r.DIFFERENTIAL_CLASSES[value] 64 elif (not isinstance(value, type) or 65 not issubclass(value, r.BaseDifferential)): 66 raise ValueError( 67 'Differential is {!r} but must be a BaseDifferential class ' 68 'or one of the string aliases {}'.format( 69 value, list(r.DIFFERENTIAL_CLASSES))) 70 return value 71 72 73def _get_repr_classes(base, **differentials): 74 """Get valid representation and differential classes. 75 76 Parameters 77 ---------- 78 base : str or `~astropy.coordinates.BaseRepresentation` subclass 79 class for the representation of the base coordinates. If a string, 80 it is looked up among the known representation classes. 81 **differentials : dict of str or `~astropy.coordinates.BaseDifferentials` 82 Keys are like for normal differentials, i.e., 's' for a first 83 derivative in time, etc. If an item is set to `None`, it will be 84 guessed from the base class. 85 86 Returns 87 ------- 88 repr_classes : dict of subclasses 89 The base class is keyed by 'base'; the others by the keys of 90 ``diffferentials``. 91 """ 92 base = _get_repr_cls(base) 93 repr_classes = {'base': base} 94 95 for name, differential_type in differentials.items(): 96 if differential_type == 'base': 97 # We don't want to fail for this case. 98 differential_type = r.DIFFERENTIAL_CLASSES.get(base.get_name(), None) 99 100 elif differential_type in r.DIFFERENTIAL_CLASSES: 101 differential_type = r.DIFFERENTIAL_CLASSES[differential_type] 102 103 elif (differential_type is not None 104 and (not isinstance(differential_type, type) 105 or not issubclass(differential_type, r.BaseDifferential))): 106 raise ValueError( 107 'Differential is {!r} but must be a BaseDifferential class ' 108 'or one of the string aliases {}'.format( 109 differential_type, list(r.DIFFERENTIAL_CLASSES))) 110 repr_classes[name] = differential_type 111 return repr_classes 112 113 114_RepresentationMappingBase = \ 115 namedtuple('RepresentationMapping', 116 ('reprname', 'framename', 'defaultunit')) 117 118 119class RepresentationMapping(_RepresentationMappingBase): 120 """ 121 This `~collections.namedtuple` is used with the 122 ``frame_specific_representation_info`` attribute to tell frames what 123 attribute names (and default units) to use for a particular representation. 124 ``reprname`` and ``framename`` should be strings, while ``defaultunit`` can 125 be either an astropy unit, the string ``'recommended'`` (which is degrees 126 for Angles, nothing otherwise), or None (to indicate that no unit mapping 127 should be done). 128 """ 129 130 def __new__(cls, reprname, framename, defaultunit='recommended'): 131 # this trick just provides some defaults 132 return super().__new__(cls, reprname, framename, defaultunit) 133 134 135base_doc = """{__doc__} 136 Parameters 137 ---------- 138 data : `~astropy.coordinates.BaseRepresentation` subclass instance 139 A representation object or ``None`` to have no data (or use the 140 coordinate component arguments, see below). 141 {components} 142 representation_type : `~astropy.coordinates.BaseRepresentation` subclass, str, optional 143 A representation class or string name of a representation class. This 144 sets the expected input representation class, thereby changing the 145 expected keyword arguments for the data passed in. For example, passing 146 ``representation_type='cartesian'`` will make the classes expect 147 position data with cartesian names, i.e. ``x, y, z`` in most cases 148 unless overridden via ``frame_specific_representation_info``. To see this 149 frame's names, check out ``<this frame>().representation_info``. 150 differential_type : `~astropy.coordinates.BaseDifferential` subclass, str, dict, optional 151 A differential class or dictionary of differential classes (currently 152 only a velocity differential with key 's' is supported). This sets the 153 expected input differential class, thereby changing the expected keyword 154 arguments of the data passed in. For example, passing 155 ``differential_type='cartesian'`` will make the classes expect velocity 156 data with the argument names ``v_x, v_y, v_z`` unless overridden via 157 ``frame_specific_representation_info``. To see this frame's names, 158 check out ``<this frame>().representation_info``. 159 copy : bool, optional 160 If `True` (default), make copies of the input coordinate arrays. 161 Can only be passed in as a keyword argument. 162 {footer} 163""" 164 165_components = """ 166 *args, **kwargs 167 Coordinate components, with names that depend on the subclass. 168""" 169 170 171@format_doc(base_doc, components=_components, footer="") 172class BaseCoordinateFrame(ShapedLikeNDArray): 173 """ 174 The base class for coordinate frames. 175 176 This class is intended to be subclassed to create instances of specific 177 systems. Subclasses can implement the following attributes: 178 179 * `default_representation` 180 A subclass of `~astropy.coordinates.BaseRepresentation` that will be 181 treated as the default representation of this frame. This is the 182 representation assumed by default when the frame is created. 183 184 * `default_differential` 185 A subclass of `~astropy.coordinates.BaseDifferential` that will be 186 treated as the default differential class of this frame. This is the 187 differential class assumed by default when the frame is created. 188 189 * `~astropy.coordinates.Attribute` class attributes 190 Frame attributes such as ``FK4.equinox`` or ``FK4.obstime`` are defined 191 using a descriptor class. See the narrative documentation or 192 built-in classes code for details. 193 194 * `frame_specific_representation_info` 195 A dictionary mapping the name or class of a representation to a list of 196 `~astropy.coordinates.RepresentationMapping` objects that tell what 197 names and default units should be used on this frame for the components 198 of that representation. 199 200 Unless overridden via `frame_specific_representation_info`, velocity name 201 defaults are: 202 203 * ``pm_{lon}_cos{lat}``, ``pm_{lat}`` for `SphericalCosLatDifferential` 204 proper motion components 205 * ``pm_{lon}``, ``pm_{lat}`` for `SphericalDifferential` proper motion 206 components 207 * ``radial_velocity`` for any ``d_distance`` component 208 * ``v_{x,y,z}`` for `CartesianDifferential` velocity components 209 210 where ``{lon}`` and ``{lat}`` are the frame names of the angular components. 211 """ 212 213 default_representation = None 214 default_differential = None 215 216 # Specifies special names and units for representation and differential 217 # attributes. 218 frame_specific_representation_info = {} 219 220 frame_attributes = {} 221 # Default empty frame_attributes dict 222 223 def __init_subclass__(cls, **kwargs): 224 225 # We first check for explicitly set values for these: 226 default_repr = getattr(cls, 'default_representation', None) 227 default_diff = getattr(cls, 'default_differential', None) 228 repr_info = getattr(cls, 'frame_specific_representation_info', None) 229 # Then, to make sure this works for subclasses-of-subclasses, we also 230 # have to check for cases where the attribute names have already been 231 # replaced by underscore-prefaced equivalents by the logic below: 232 if default_repr is None or isinstance(default_repr, property): 233 default_repr = getattr(cls, '_default_representation', None) 234 235 if default_diff is None or isinstance(default_diff, property): 236 default_diff = getattr(cls, '_default_differential', None) 237 238 if repr_info is None or isinstance(repr_info, property): 239 repr_info = getattr(cls, '_frame_specific_representation_info', None) 240 241 repr_info = cls._infer_repr_info(repr_info) 242 243 # Make read-only properties for the frame class attributes that should 244 # be read-only to make them immutable after creation. 245 # We copy attributes instead of linking to make sure there's no 246 # accidental cross-talk between classes 247 cls._create_readonly_property('default_representation', default_repr, 248 'Default representation for position data') 249 cls._create_readonly_property('default_differential', default_diff, 250 'Default representation for differential data ' 251 '(e.g., velocity)') 252 cls._create_readonly_property('frame_specific_representation_info', 253 copy.deepcopy(repr_info), 254 'Mapping for frame-specific component names') 255 256 # Set the frame attributes. We first construct the attributes from 257 # superclasses, going in reverse order to keep insertion order, 258 # and then add any attributes from the frame now being defined 259 # (if any old definitions are overridden, this keeps the order). 260 # Note that we cannot simply start with the inherited frame_attributes 261 # since we could be a mixin between multiple coordinate frames. 262 # TODO: Should this be made to use readonly_prop_factory as well or 263 # would it be inconvenient for getting the frame_attributes from 264 # classes? 265 frame_attrs = {} 266 for basecls in reversed(cls.__bases__): 267 if issubclass(basecls, BaseCoordinateFrame): 268 frame_attrs.update(basecls.frame_attributes) 269 270 for k, v in cls.__dict__.items(): 271 if isinstance(v, Attribute): 272 frame_attrs[k] = v 273 274 cls.frame_attributes = frame_attrs 275 276 # Deal with setting the name of the frame: 277 if not hasattr(cls, 'name'): 278 cls.name = cls.__name__.lower() 279 elif (BaseCoordinateFrame not in cls.__bases__ and 280 cls.name in [getattr(base, 'name', None) 281 for base in cls.__bases__]): 282 # This may be a subclass of a subclass of BaseCoordinateFrame, 283 # like ICRS(BaseRADecFrame). In this case, cls.name will have been 284 # set by init_subclass 285 cls.name = cls.__name__.lower() 286 287 # A cache that *must be unique to each frame class* - it is 288 # insufficient to share them with superclasses, hence the need to put 289 # them in the meta 290 cls._frame_class_cache = {} 291 292 super().__init_subclass__(**kwargs) 293 294 def __init__(self, *args, copy=True, representation_type=None, 295 differential_type=None, **kwargs): 296 self._attr_names_with_defaults = [] 297 298 self._representation = self._infer_representation(representation_type, differential_type) 299 self._data = self._infer_data(args, copy, kwargs) # possibly None. 300 301 # Set frame attributes, if any 302 303 values = {} 304 for fnm, fdefault in self.get_frame_attr_names().items(): 305 # Read-only frame attributes are defined as FrameAttribute 306 # descriptors which are not settable, so set 'real' attributes as 307 # the name prefaced with an underscore. 308 309 if fnm in kwargs: 310 value = kwargs.pop(fnm) 311 setattr(self, '_' + fnm, value) 312 # Validate attribute by getting it. If the instance has data, 313 # this also checks its shape is OK. If not, we do it below. 314 values[fnm] = getattr(self, fnm) 315 else: 316 setattr(self, '_' + fnm, fdefault) 317 self._attr_names_with_defaults.append(fnm) 318 319 if kwargs: 320 raise TypeError( 321 f'Coordinate frame {self.__class__.__name__} got unexpected ' 322 f'keywords: {list(kwargs)}') 323 324 # We do ``is None`` because self._data might evaluate to false for 325 # empty arrays or data == 0 326 if self._data is None: 327 # No data: we still need to check that any non-scalar attributes 328 # have consistent shapes. Collect them for all attributes with 329 # size > 1 (which should be array-like and thus have a shape). 330 shapes = {fnm: value.shape for fnm, value in values.items() 331 if getattr(value, 'shape', ())} 332 if shapes: 333 if len(shapes) > 1: 334 try: 335 self._no_data_shape = check_broadcast(*shapes.values()) 336 except ValueError as err: 337 raise ValueError( 338 f"non-scalar attributes with inconsistent shapes: {shapes}") from err 339 340 # Above, we checked that it is possible to broadcast all 341 # shapes. By getting and thus validating the attributes, 342 # we verify that the attributes can in fact be broadcast. 343 for fnm in shapes: 344 getattr(self, fnm) 345 else: 346 self._no_data_shape = shapes.popitem()[1] 347 348 else: 349 self._no_data_shape = () 350 351 # The logic of this block is not related to the previous one 352 if self._data is not None: 353 # This makes the cache keys backwards-compatible, but also adds 354 # support for having differentials attached to the frame data 355 # representation object. 356 if 's' in self._data.differentials: 357 # TODO: assumes a velocity unit differential 358 key = (self._data.__class__.__name__, 359 self._data.differentials['s'].__class__.__name__, 360 False) 361 else: 362 key = (self._data.__class__.__name__, False) 363 364 # Set up representation cache. 365 self.cache['representation'][key] = self._data 366 367 def _infer_representation(self, representation_type, differential_type): 368 if representation_type is None and differential_type is None: 369 return {'base': self.default_representation, 's': self.default_differential} 370 371 if representation_type is None: 372 representation_type = self.default_representation 373 374 if (inspect.isclass(differential_type) 375 and issubclass(differential_type, r.BaseDifferential)): 376 # TODO: assumes the differential class is for the velocity 377 # differential 378 differential_type = {'s': differential_type} 379 380 elif isinstance(differential_type, str): 381 # TODO: assumes the differential class is for the velocity 382 # differential 383 diff_cls = r.DIFFERENTIAL_CLASSES[differential_type] 384 differential_type = {'s': diff_cls} 385 386 elif differential_type is None: 387 if representation_type == self.default_representation: 388 differential_type = {'s': self.default_differential} 389 else: 390 differential_type = {'s': 'base'} # see set_representation_cls() 391 392 return _get_repr_classes(representation_type, **differential_type) 393 394 def _infer_data(self, args, copy, kwargs): 395 # if not set below, this is a frame with no data 396 representation_data = None 397 differential_data = None 398 399 args = list(args) # need to be able to pop them 400 if (len(args) > 0) and (isinstance(args[0], r.BaseRepresentation) or 401 args[0] is None): 402 representation_data = args.pop(0) # This can still be None 403 if len(args) > 0: 404 raise TypeError( 405 'Cannot create a frame with both a representation object ' 406 'and other positional arguments') 407 408 if representation_data is not None: 409 diffs = representation_data.differentials 410 differential_data = diffs.get('s', None) 411 if ((differential_data is None and len(diffs) > 0) or 412 (differential_data is not None and len(diffs) > 1)): 413 raise ValueError('Multiple differentials are associated ' 414 'with the representation object passed in ' 415 'to the frame initializer. Only a single ' 416 'velocity differential is supported. Got: ' 417 '{}'.format(diffs)) 418 419 else: 420 representation_cls = self.get_representation_cls() 421 # Get any representation data passed in to the frame initializer 422 # using keyword or positional arguments for the component names 423 repr_kwargs = {} 424 for nmkw, nmrep in self.representation_component_names.items(): 425 if len(args) > 0: 426 # first gather up positional args 427 repr_kwargs[nmrep] = args.pop(0) 428 elif nmkw in kwargs: 429 repr_kwargs[nmrep] = kwargs.pop(nmkw) 430 431 # special-case the Spherical->UnitSpherical if no `distance` 432 433 if repr_kwargs: 434 # TODO: determine how to get rid of the part before the "try" - 435 # currently removing it has a performance regression for 436 # unitspherical because of the try-related overhead. 437 # Also frames have no way to indicate what the "distance" is 438 if repr_kwargs.get('distance', True) is None: 439 del repr_kwargs['distance'] 440 441 if (issubclass(representation_cls, 442 r.SphericalRepresentation) 443 and 'distance' not in repr_kwargs): 444 representation_cls = representation_cls._unit_representation 445 446 try: 447 representation_data = representation_cls(copy=copy, 448 **repr_kwargs) 449 except TypeError as e: 450 # this except clause is here to make the names of the 451 # attributes more human-readable. Without this the names 452 # come from the representation instead of the frame's 453 # attribute names. 454 try: 455 representation_data = ( 456 representation_cls._unit_representation( 457 copy=copy, **repr_kwargs)) 458 except Exception: 459 msg = str(e) 460 names = self.get_representation_component_names() 461 for frame_name, repr_name in names.items(): 462 msg = msg.replace(repr_name, frame_name) 463 msg = msg.replace('__init__()', 464 f'{self.__class__.__name__}()') 465 e.args = (msg,) 466 raise e 467 468 # Now we handle the Differential data: 469 # Get any differential data passed in to the frame initializer 470 # using keyword or positional arguments for the component names 471 differential_cls = self.get_representation_cls('s') 472 diff_component_names = self.get_representation_component_names('s') 473 diff_kwargs = {} 474 for nmkw, nmrep in diff_component_names.items(): 475 if len(args) > 0: 476 # first gather up positional args 477 diff_kwargs[nmrep] = args.pop(0) 478 elif nmkw in kwargs: 479 diff_kwargs[nmrep] = kwargs.pop(nmkw) 480 481 if diff_kwargs: 482 if (hasattr(differential_cls, '_unit_differential') 483 and 'd_distance' not in diff_kwargs): 484 differential_cls = differential_cls._unit_differential 485 486 elif len(diff_kwargs) == 1 and 'd_distance' in diff_kwargs: 487 differential_cls = r.RadialDifferential 488 489 try: 490 differential_data = differential_cls(copy=copy, 491 **diff_kwargs) 492 except TypeError as e: 493 # this except clause is here to make the names of the 494 # attributes more human-readable. Without this the names 495 # come from the representation instead of the frame's 496 # attribute names. 497 msg = str(e) 498 names = self.get_representation_component_names('s') 499 for frame_name, repr_name in names.items(): 500 msg = msg.replace(repr_name, frame_name) 501 msg = msg.replace('__init__()', 502 f'{self.__class__.__name__}()') 503 e.args = (msg,) 504 raise 505 506 if len(args) > 0: 507 raise TypeError( 508 '{}.__init__ had {} remaining unhandled arguments'.format( 509 self.__class__.__name__, len(args))) 510 511 if representation_data is None and differential_data is not None: 512 raise ValueError("Cannot pass in differential component data " 513 "without positional (representation) data.") 514 515 if differential_data: 516 # Check that differential data provided has units compatible 517 # with time-derivative of representation data. 518 # NOTE: there is no dimensionless time while lengths can be 519 # dimensionless (u.dimensionless_unscaled). 520 for comp in representation_data.components: 521 if (diff_comp := f'd_{comp}') in differential_data.components: 522 current_repr_unit = representation_data._units[comp] 523 current_diff_unit = differential_data._units[diff_comp] 524 expected_unit = current_repr_unit / u.s 525 if not current_diff_unit.is_equivalent(expected_unit): 526 for key, val in self.get_representation_component_names().items(): 527 if val == comp: 528 current_repr_name = key 529 break 530 for key, val in self.get_representation_component_names('s').items(): 531 if val == diff_comp: 532 current_diff_name = key 533 break 534 raise ValueError( 535 f'{current_repr_name} has unit "{current_repr_unit}" with physical ' 536 f'type "{current_repr_unit.physical_type}", but {current_diff_name} ' 537 f'has incompatible unit "{current_diff_unit}" with physical type ' 538 f'"{current_diff_unit.physical_type}" instead of the expected ' 539 f'"{(expected_unit).physical_type}".') 540 541 representation_data = representation_data.with_differentials({'s': differential_data}) 542 543 return representation_data 544 545 @classmethod 546 def _infer_repr_info(cls, repr_info): 547 # Unless overridden via `frame_specific_representation_info`, velocity 548 # name defaults are (see also docstring for BaseCoordinateFrame): 549 # * ``pm_{lon}_cos{lat}``, ``pm_{lat}`` for 550 # `SphericalCosLatDifferential` proper motion components 551 # * ``pm_{lon}``, ``pm_{lat}`` for `SphericalDifferential` proper 552 # motion components 553 # * ``radial_velocity`` for any `d_distance` component 554 # * ``v_{x,y,z}`` for `CartesianDifferential` velocity components 555 # where `{lon}` and `{lat}` are the frame names of the angular 556 # components. 557 if repr_info is None: 558 repr_info = {} 559 560 # the tuple() call below is necessary because if it is not there, 561 # the iteration proceeds in a difficult-to-predict manner in the 562 # case that one of the class objects hash is such that it gets 563 # revisited by the iteration. The tuple() call prevents this by 564 # making the items iterated over fixed regardless of how the dict 565 # changes 566 for cls_or_name in tuple(repr_info.keys()): 567 if isinstance(cls_or_name, str): 568 # TODO: this provides a layer of backwards compatibility in 569 # case the key is a string, but now we want explicit classes. 570 _cls = _get_repr_cls(cls_or_name) 571 repr_info[_cls] = repr_info.pop(cls_or_name) 572 573 # The default spherical names are 'lon' and 'lat' 574 repr_info.setdefault(r.SphericalRepresentation, 575 [RepresentationMapping('lon', 'lon'), 576 RepresentationMapping('lat', 'lat')]) 577 578 sph_component_map = {m.reprname: m.framename 579 for m in repr_info[r.SphericalRepresentation]} 580 581 repr_info.setdefault(r.SphericalCosLatDifferential, [ 582 RepresentationMapping( 583 'd_lon_coslat', 584 'pm_{lon}_cos{lat}'.format(**sph_component_map), 585 u.mas/u.yr), 586 RepresentationMapping('d_lat', 587 'pm_{lat}'.format(**sph_component_map), 588 u.mas/u.yr), 589 RepresentationMapping('d_distance', 'radial_velocity', 590 u.km/u.s) 591 ]) 592 593 repr_info.setdefault(r.SphericalDifferential, [ 594 RepresentationMapping('d_lon', 595 'pm_{lon}'.format(**sph_component_map), 596 u.mas/u.yr), 597 RepresentationMapping('d_lat', 598 'pm_{lat}'.format(**sph_component_map), 599 u.mas/u.yr), 600 RepresentationMapping('d_distance', 'radial_velocity', 601 u.km/u.s) 602 ]) 603 604 repr_info.setdefault(r.CartesianDifferential, [ 605 RepresentationMapping('d_x', 'v_x', u.km/u.s), 606 RepresentationMapping('d_y', 'v_y', u.km/u.s), 607 RepresentationMapping('d_z', 'v_z', u.km/u.s)]) 608 609 # Unit* classes should follow the same naming conventions 610 # TODO: this adds some unnecessary mappings for the Unit classes, so 611 # this could be cleaned up, but in practice doesn't seem to have any 612 # negative side effects 613 repr_info.setdefault(r.UnitSphericalRepresentation, 614 repr_info[r.SphericalRepresentation]) 615 616 repr_info.setdefault(r.UnitSphericalCosLatDifferential, 617 repr_info[r.SphericalCosLatDifferential]) 618 619 repr_info.setdefault(r.UnitSphericalDifferential, 620 repr_info[r.SphericalDifferential]) 621 622 return repr_info 623 624 @classmethod 625 def _create_readonly_property(cls, attr_name, value, doc=None): 626 private_attr = '_' + attr_name 627 628 def getter(self): 629 return getattr(self, private_attr) 630 631 setattr(cls, private_attr, value) 632 setattr(cls, attr_name, property(getter, doc=doc)) 633 634 @lazyproperty 635 def cache(self): 636 """ 637 Cache for this frame, a dict. It stores anything that should be 638 computed from the coordinate data (*not* from the frame attributes). 639 This can be used in functions to store anything that might be 640 expensive to compute but might be re-used by some other function. 641 E.g.:: 642 643 if 'user_data' in myframe.cache: 644 data = myframe.cache['user_data'] 645 else: 646 myframe.cache['user_data'] = data = expensive_func(myframe.lat) 647 648 If in-place modifications are made to the frame data, the cache should 649 be cleared:: 650 651 myframe.cache.clear() 652 653 """ 654 return defaultdict(dict) 655 656 @property 657 def data(self): 658 """ 659 The coordinate data for this object. If this frame has no data, an 660 `ValueError` will be raised. Use `has_data` to 661 check if data is present on this frame object. 662 """ 663 if self._data is None: 664 raise ValueError('The frame object "{!r}" does not have ' 665 'associated data'.format(self)) 666 return self._data 667 668 @property 669 def has_data(self): 670 """ 671 True if this frame has `data`, False otherwise. 672 """ 673 return self._data is not None 674 675 @property 676 def shape(self): 677 return self.data.shape if self.has_data else self._no_data_shape 678 679 # We have to override the ShapedLikeNDArray definitions, since our shape 680 # does not have to be that of the data. 681 def __len__(self): 682 return len(self.data) 683 684 def __bool__(self): 685 return self.has_data and self.size > 0 686 687 @property 688 def size(self): 689 return self.data.size 690 691 @property 692 def isscalar(self): 693 return self.has_data and self.data.isscalar 694 695 @classmethod 696 def get_frame_attr_names(cls): 697 return {name: getattr(cls, name) 698 for name in cls.frame_attributes} 699 700 def get_representation_cls(self, which='base'): 701 """The class used for part of this frame's data. 702 703 Parameters 704 ---------- 705 which : ('base', 's', `None`) 706 The class of which part to return. 'base' means the class used to 707 represent the coordinates; 's' the first derivative to time, i.e., 708 the class representing the proper motion and/or radial velocity. 709 If `None`, return a dict with both. 710 711 Returns 712 ------- 713 representation : `~astropy.coordinates.BaseRepresentation` or `~astropy.coordinates.BaseDifferential`. 714 """ 715 if which is not None: 716 return self._representation[which] 717 else: 718 return self._representation 719 720 def set_representation_cls(self, base=None, s='base'): 721 """Set representation and/or differential class for this frame's data. 722 723 Parameters 724 ---------- 725 base : str, `~astropy.coordinates.BaseRepresentation` subclass, optional 726 The name or subclass to use to represent the coordinate data. 727 s : `~astropy.coordinates.BaseDifferential` subclass, optional 728 The differential subclass to use to represent any velocities, 729 such as proper motion and radial velocity. If equal to 'base', 730 which is the default, it will be inferred from the representation. 731 If `None`, the representation will drop any differentials. 732 """ 733 if base is None: 734 base = self._representation['base'] 735 self._representation = _get_repr_classes(base=base, s=s) 736 737 representation_type = property( 738 fget=get_representation_cls, fset=set_representation_cls, 739 doc="""The representation class used for this frame's data. 740 741 This will be a subclass from `~astropy.coordinates.BaseRepresentation`. 742 Can also be *set* using the string name of the representation. If you 743 wish to set an explicit differential class (rather than have it be 744 inferred), use the ``set_representation_cls`` method. 745 """) 746 747 @property 748 def differential_type(self): 749 """ 750 The differential used for this frame's data. 751 752 This will be a subclass from `~astropy.coordinates.BaseDifferential`. 753 For simultaneous setting of representation and differentials, see the 754 ``set_representation_cls`` method. 755 """ 756 return self.get_representation_cls('s') 757 758 @differential_type.setter 759 def differential_type(self, value): 760 self.set_representation_cls(s=value) 761 762 @classmethod 763 def _get_representation_info(cls): 764 # This exists as a class method only to support handling frame inputs 765 # without units, which are deprecated and will be removed. This can be 766 # moved into the representation_info property at that time. 767 # note that if so moved, the cache should be acceessed as 768 # self.__class__._frame_class_cache 769 770 if cls._frame_class_cache.get('last_reprdiff_hash', None) != r.get_reprdiff_cls_hash(): 771 repr_attrs = {} 772 for repr_diff_cls in (list(r.REPRESENTATION_CLASSES.values()) + 773 list(r.DIFFERENTIAL_CLASSES.values())): 774 repr_attrs[repr_diff_cls] = {'names': [], 'units': []} 775 for c, c_cls in repr_diff_cls.attr_classes.items(): 776 repr_attrs[repr_diff_cls]['names'].append(c) 777 rec_unit = u.deg if issubclass(c_cls, Angle) else None 778 repr_attrs[repr_diff_cls]['units'].append(rec_unit) 779 780 for repr_diff_cls, mappings in cls._frame_specific_representation_info.items(): 781 782 # take the 'names' and 'units' tuples from repr_attrs, 783 # and then use the RepresentationMapping objects 784 # to update as needed for this frame. 785 nms = repr_attrs[repr_diff_cls]['names'] 786 uns = repr_attrs[repr_diff_cls]['units'] 787 comptomap = dict([(m.reprname, m) for m in mappings]) 788 for i, c in enumerate(repr_diff_cls.attr_classes.keys()): 789 if c in comptomap: 790 mapp = comptomap[c] 791 nms[i] = mapp.framename 792 793 # need the isinstance because otherwise if it's a unit it 794 # will try to compare to the unit string representation 795 if not (isinstance(mapp.defaultunit, str) 796 and mapp.defaultunit == 'recommended'): 797 uns[i] = mapp.defaultunit 798 # else we just leave it as recommended_units says above 799 800 # Convert to tuples so that this can't mess with frame internals 801 repr_attrs[repr_diff_cls]['names'] = tuple(nms) 802 repr_attrs[repr_diff_cls]['units'] = tuple(uns) 803 804 cls._frame_class_cache['representation_info'] = repr_attrs 805 cls._frame_class_cache['last_reprdiff_hash'] = r.get_reprdiff_cls_hash() 806 return cls._frame_class_cache['representation_info'] 807 808 @lazyproperty 809 def representation_info(self): 810 """ 811 A dictionary with the information of what attribute names for this frame 812 apply to particular representations. 813 """ 814 return self._get_representation_info() 815 816 def get_representation_component_names(self, which='base'): 817 out = {} 818 repr_or_diff_cls = self.get_representation_cls(which) 819 if repr_or_diff_cls is None: 820 return out 821 data_names = repr_or_diff_cls.attr_classes.keys() 822 repr_names = self.representation_info[repr_or_diff_cls]['names'] 823 for repr_name, data_name in zip(repr_names, data_names): 824 out[repr_name] = data_name 825 return out 826 827 def get_representation_component_units(self, which='base'): 828 out = {} 829 repr_or_diff_cls = self.get_representation_cls(which) 830 if repr_or_diff_cls is None: 831 return out 832 repr_attrs = self.representation_info[repr_or_diff_cls] 833 repr_names = repr_attrs['names'] 834 repr_units = repr_attrs['units'] 835 for repr_name, repr_unit in zip(repr_names, repr_units): 836 if repr_unit: 837 out[repr_name] = repr_unit 838 return out 839 840 representation_component_names = property(get_representation_component_names) 841 842 representation_component_units = property(get_representation_component_units) 843 844 def _replicate(self, data, copy=False, **kwargs): 845 """Base for replicating a frame, with possibly different attributes. 846 847 Produces a new instance of the frame using the attributes of the old 848 frame (unless overridden) and with the data given. 849 850 Parameters 851 ---------- 852 data : `~astropy.coordinates.BaseRepresentation` or None 853 Data to use in the new frame instance. If `None`, it will be 854 a data-less frame. 855 copy : bool, optional 856 Whether data and the attributes on the old frame should be copied 857 (default), or passed on by reference. 858 **kwargs 859 Any attributes that should be overridden. 860 """ 861 # This is to provide a slightly nicer error message if the user tries 862 # to use frame_obj.representation instead of frame_obj.data to get the 863 # underlying representation object [e.g., #2890] 864 if inspect.isclass(data): 865 raise TypeError('Class passed as data instead of a representation ' 866 'instance. If you called frame.representation, this' 867 ' returns the representation class. frame.data ' 868 'returns the instantiated object - you may want to ' 869 ' use this instead.') 870 if copy and data is not None: 871 data = data.copy() 872 873 for attr in self.get_frame_attr_names(): 874 if (attr not in self._attr_names_with_defaults 875 and attr not in kwargs): 876 value = getattr(self, attr) 877 if copy: 878 value = value.copy() 879 880 kwargs[attr] = value 881 882 return self.__class__(data, copy=False, **kwargs) 883 884 def replicate(self, copy=False, **kwargs): 885 """ 886 Return a replica of the frame, optionally with new frame attributes. 887 888 The replica is a new frame object that has the same data as this frame 889 object and with frame attributes overridden if they are provided as extra 890 keyword arguments to this method. If ``copy`` is set to `True` then a 891 copy of the internal arrays will be made. Otherwise the replica will 892 use a reference to the original arrays when possible to save memory. The 893 internal arrays are normally not changeable by the user so in most cases 894 it should not be necessary to set ``copy`` to `True`. 895 896 Parameters 897 ---------- 898 copy : bool, optional 899 If True, the resulting object is a copy of the data. When False, 900 references are used where possible. This rule also applies to the 901 frame attributes. 902 903 Any additional keywords are treated as frame attributes to be set on the 904 new frame object. 905 906 Returns 907 ------- 908 frameobj : `BaseCoordinateFrame` subclass instance 909 Replica of this object, but possibly with new frame attributes. 910 """ 911 return self._replicate(self.data, copy=copy, **kwargs) 912 913 def replicate_without_data(self, copy=False, **kwargs): 914 """ 915 Return a replica without data, optionally with new frame attributes. 916 917 The replica is a new frame object without data but with the same frame 918 attributes as this object, except where overridden by extra keyword 919 arguments to this method. The ``copy`` keyword determines if the frame 920 attributes are truly copied vs being references (which saves memory for 921 cases where frame attributes are large). 922 923 This method is essentially the converse of `realize_frame`. 924 925 Parameters 926 ---------- 927 copy : bool, optional 928 If True, the resulting object has copies of the frame attributes. 929 When False, references are used where possible. 930 931 Any additional keywords are treated as frame attributes to be set on the 932 new frame object. 933 934 Returns 935 ------- 936 frameobj : `BaseCoordinateFrame` subclass instance 937 Replica of this object, but without data and possibly with new frame 938 attributes. 939 """ 940 return self._replicate(None, copy=copy, **kwargs) 941 942 def realize_frame(self, data, **kwargs): 943 """ 944 Generates a new frame with new data from another frame (which may or 945 may not have data). Roughly speaking, the converse of 946 `replicate_without_data`. 947 948 Parameters 949 ---------- 950 data : `~astropy.coordinates.BaseRepresentation` 951 The representation to use as the data for the new frame. 952 953 Any additional keywords are treated as frame attributes to be set on the 954 new frame object. In particular, `representation_type` can be specified. 955 956 Returns 957 ------- 958 frameobj : `BaseCoordinateFrame` subclass instance 959 A new object in *this* frame, with the same frame attributes as 960 this one, but with the ``data`` as the coordinate data. 961 962 """ 963 return self._replicate(data, **kwargs) 964 965 def represent_as(self, base, s='base', in_frame_units=False): 966 """ 967 Generate and return a new representation of this frame's `data` 968 as a Representation object. 969 970 Note: In order to make an in-place change of the representation 971 of a Frame or SkyCoord object, set the ``representation`` 972 attribute of that object to the desired new representation, or 973 use the ``set_representation_cls`` method to also set the differential. 974 975 Parameters 976 ---------- 977 base : subclass of BaseRepresentation or string 978 The type of representation to generate. Must be a *class* 979 (not an instance), or the string name of the representation 980 class. 981 s : subclass of `~astropy.coordinates.BaseDifferential`, str, optional 982 Class in which any velocities should be represented. Must be 983 a *class* (not an instance), or the string name of the 984 differential class. If equal to 'base' (default), inferred from 985 the base class. If `None`, all velocity information is dropped. 986 in_frame_units : bool, keyword-only 987 Force the representation units to match the specified units 988 particular to this frame 989 990 Returns 991 ------- 992 newrep : BaseRepresentation-derived object 993 A new representation object of this frame's `data`. 994 995 Raises 996 ------ 997 AttributeError 998 If this object had no `data` 999 1000 Examples 1001 -------- 1002 >>> from astropy import units as u 1003 >>> from astropy.coordinates import SkyCoord, CartesianRepresentation 1004 >>> coord = SkyCoord(0*u.deg, 0*u.deg) 1005 >>> coord.represent_as(CartesianRepresentation) # doctest: +FLOAT_CMP 1006 <CartesianRepresentation (x, y, z) [dimensionless] 1007 (1., 0., 0.)> 1008 1009 >>> coord.representation_type = CartesianRepresentation 1010 >>> coord # doctest: +FLOAT_CMP 1011 <SkyCoord (ICRS): (x, y, z) [dimensionless] 1012 (1., 0., 0.)> 1013 """ 1014 1015 # For backwards compatibility (because in_frame_units used to be the 1016 # 2nd argument), we check to see if `new_differential` is a boolean. If 1017 # it is, we ignore the value of `new_differential` and warn about the 1018 # position change 1019 if isinstance(s, bool): 1020 warnings.warn("The argument position for `in_frame_units` in " 1021 "`represent_as` has changed. Use as a keyword " 1022 "argument if needed.", AstropyWarning) 1023 in_frame_units = s 1024 s = 'base' 1025 1026 # In the future, we may want to support more differentials, in which 1027 # case one probably needs to define **kwargs above and use it here. 1028 # But for now, we only care about the velocity. 1029 repr_classes = _get_repr_classes(base=base, s=s) 1030 representation_cls = repr_classes['base'] 1031 # We only keep velocity information 1032 if 's' in self.data.differentials: 1033 # For the default 'base' option in which _get_repr_classes has 1034 # given us a best guess based on the representation class, we only 1035 # use it if the class we had already is incompatible. 1036 if (s == 'base' 1037 and (self.data.differentials['s'].__class__ 1038 in representation_cls._compatible_differentials)): 1039 differential_cls = self.data.differentials['s'].__class__ 1040 else: 1041 differential_cls = repr_classes['s'] 1042 elif s is None or s == 'base': 1043 differential_cls = None 1044 else: 1045 raise TypeError('Frame data has no associated differentials ' 1046 '(i.e. the frame has no velocity data) - ' 1047 'represent_as() only accepts a new ' 1048 'representation.') 1049 1050 if differential_cls: 1051 cache_key = (representation_cls.__name__, 1052 differential_cls.__name__, in_frame_units) 1053 else: 1054 cache_key = (representation_cls.__name__, in_frame_units) 1055 1056 cached_repr = self.cache['representation'].get(cache_key) 1057 if not cached_repr: 1058 if differential_cls: 1059 # Sanity check to ensure we do not just drop radial 1060 # velocity. TODO: should Representation.represent_as 1061 # allow this transformation in the first place? 1062 if (isinstance(self.data, r.UnitSphericalRepresentation) 1063 and issubclass(representation_cls, r.CartesianRepresentation) 1064 and not isinstance(self.data.differentials['s'], 1065 (r.UnitSphericalDifferential, 1066 r.UnitSphericalCosLatDifferential, 1067 r.RadialDifferential))): 1068 raise u.UnitConversionError( 1069 'need a distance to retrieve a cartesian representation ' 1070 'when both radial velocity and proper motion are present, ' 1071 'since otherwise the units cannot match.') 1072 1073 # TODO NOTE: only supports a single differential 1074 data = self.data.represent_as(representation_cls, 1075 differential_cls) 1076 diff = data.differentials['s'] # TODO: assumes velocity 1077 else: 1078 data = self.data.represent_as(representation_cls) 1079 1080 # If the new representation is known to this frame and has a defined 1081 # set of names and units, then use that. 1082 new_attrs = self.representation_info.get(representation_cls) 1083 if new_attrs and in_frame_units: 1084 datakwargs = dict((comp, getattr(data, comp)) 1085 for comp in data.components) 1086 for comp, new_attr_unit in zip(data.components, new_attrs['units']): 1087 if new_attr_unit: 1088 datakwargs[comp] = datakwargs[comp].to(new_attr_unit) 1089 data = data.__class__(copy=False, **datakwargs) 1090 1091 if differential_cls: 1092 # the original differential 1093 data_diff = self.data.differentials['s'] 1094 1095 # If the new differential is known to this frame and has a 1096 # defined set of names and units, then use that. 1097 new_attrs = self.representation_info.get(differential_cls) 1098 if new_attrs and in_frame_units: 1099 diffkwargs = dict((comp, getattr(diff, comp)) 1100 for comp in diff.components) 1101 for comp, new_attr_unit in zip(diff.components, 1102 new_attrs['units']): 1103 # Some special-casing to treat a situation where the 1104 # input data has a UnitSphericalDifferential or a 1105 # RadialDifferential. It is re-represented to the 1106 # frame's differential class (which might be, e.g., a 1107 # dimensional Differential), so we don't want to try to 1108 # convert the empty component units 1109 if (isinstance(data_diff, 1110 (r.UnitSphericalDifferential, 1111 r.UnitSphericalCosLatDifferential)) 1112 and comp not in data_diff.__class__.attr_classes): 1113 continue 1114 1115 elif (isinstance(data_diff, r.RadialDifferential) 1116 and comp not in data_diff.__class__.attr_classes): 1117 continue 1118 1119 # Try to convert to requested units. Since that might 1120 # not be possible (e.g., for a coordinate with proper 1121 # motion but without distance, one cannot convert to a 1122 # cartesian differential in km/s), we allow the unit 1123 # conversion to fail. See gh-7028 for discussion. 1124 if new_attr_unit and hasattr(diff, comp): 1125 try: 1126 diffkwargs[comp] = diffkwargs[comp].to(new_attr_unit) 1127 except Exception: 1128 pass 1129 1130 diff = diff.__class__(copy=False, **diffkwargs) 1131 1132 # Here we have to bypass using with_differentials() because 1133 # it has a validation check. But because 1134 # .representation_type and .differential_type don't point to 1135 # the original classes, if the input differential is a 1136 # RadialDifferential, it usually gets turned into a 1137 # SphericalCosLatDifferential (or whatever the default is) 1138 # with strange units for the d_lon and d_lat attributes. 1139 # This then causes the dictionary key check to fail (i.e. 1140 # comparison against `diff._get_deriv_key()`) 1141 data._differentials.update({'s': diff}) 1142 1143 self.cache['representation'][cache_key] = data 1144 1145 return self.cache['representation'][cache_key] 1146 1147 def transform_to(self, new_frame): 1148 """ 1149 Transform this object's coordinate data to a new frame. 1150 1151 Parameters 1152 ---------- 1153 new_frame : coordinate-like or `BaseCoordinateFrame` subclass instance 1154 The frame to transform this coordinate frame into. 1155 The frame class option is deprecated. 1156 1157 Returns 1158 ------- 1159 transframe : coordinate-like 1160 A new object with the coordinate data represented in the 1161 ``newframe`` system. 1162 1163 Raises 1164 ------ 1165 ValueError 1166 If there is no possible transformation route. 1167 """ 1168 from .errors import ConvertError 1169 1170 if self._data is None: 1171 raise ValueError('Cannot transform a frame with no data') 1172 1173 if (getattr(self.data, 'differentials', None) 1174 and hasattr(self, 'obstime') and hasattr(new_frame, 'obstime') 1175 and np.any(self.obstime != new_frame.obstime)): 1176 raise NotImplementedError('You cannot transform a frame that has ' 1177 'velocities to another frame at a ' 1178 'different obstime. If you think this ' 1179 'should (or should not) be possible, ' 1180 'please comment at https://github.com/astropy/astropy/issues/6280') 1181 1182 if inspect.isclass(new_frame): 1183 warnings.warn("Transforming a frame instance to a frame class (as opposed to another " 1184 "frame instance) will not be supported in the future. Either " 1185 "explicitly instantiate the target frame, or first convert the source " 1186 "frame instance to a `astropy.coordinates.SkyCoord` and use its " 1187 "`transform_to()` method.", 1188 AstropyDeprecationWarning) 1189 # Use the default frame attributes for this class 1190 new_frame = new_frame() 1191 1192 if hasattr(new_frame, '_sky_coord_frame'): 1193 # Input new_frame is not a frame instance or class and is most 1194 # likely a SkyCoord object. 1195 new_frame = new_frame._sky_coord_frame 1196 1197 trans = frame_transform_graph.get_transform(self.__class__, 1198 new_frame.__class__) 1199 if trans is None: 1200 if new_frame is self.__class__: 1201 # no special transform needed, but should update frame info 1202 return new_frame.realize_frame(self.data) 1203 msg = 'Cannot transform from {0} to {1}' 1204 raise ConvertError(msg.format(self.__class__, new_frame.__class__)) 1205 return trans(self, new_frame) 1206 1207 def is_transformable_to(self, new_frame): 1208 """ 1209 Determines if this coordinate frame can be transformed to another 1210 given frame. 1211 1212 Parameters 1213 ---------- 1214 new_frame : `BaseCoordinateFrame` subclass or instance 1215 The proposed frame to transform into. 1216 1217 Returns 1218 ------- 1219 transformable : bool or str 1220 `True` if this can be transformed to ``new_frame``, `False` if 1221 not, or the string 'same' if ``new_frame`` is the same system as 1222 this object but no transformation is defined. 1223 1224 Notes 1225 ----- 1226 A return value of 'same' means the transformation will work, but it will 1227 just give back a copy of this object. The intended usage is:: 1228 1229 if coord.is_transformable_to(some_unknown_frame): 1230 coord2 = coord.transform_to(some_unknown_frame) 1231 1232 This will work even if ``some_unknown_frame`` turns out to be the same 1233 frame class as ``coord``. This is intended for cases where the frame 1234 is the same regardless of the frame attributes (e.g. ICRS), but be 1235 aware that it *might* also indicate that someone forgot to define the 1236 transformation between two objects of the same frame class but with 1237 different attributes. 1238 """ 1239 new_frame_cls = new_frame if inspect.isclass(new_frame) else new_frame.__class__ 1240 trans = frame_transform_graph.get_transform(self.__class__, new_frame_cls) 1241 1242 if trans is None: 1243 if new_frame_cls is self.__class__: 1244 return 'same' 1245 else: 1246 return False 1247 else: 1248 return True 1249 1250 def is_frame_attr_default(self, attrnm): 1251 """ 1252 Determine whether or not a frame attribute has its value because it's 1253 the default value, or because this frame was created with that value 1254 explicitly requested. 1255 1256 Parameters 1257 ---------- 1258 attrnm : str 1259 The name of the attribute to check. 1260 1261 Returns 1262 ------- 1263 isdefault : bool 1264 True if the attribute ``attrnm`` has its value by default, False if 1265 it was specified at creation of this frame. 1266 """ 1267 return attrnm in self._attr_names_with_defaults 1268 1269 @staticmethod 1270 def _frameattr_equiv(left_fattr, right_fattr): 1271 """ 1272 Determine if two frame attributes are equivalent. Implemented as a 1273 staticmethod mainly as a convenient location, although conceivable it 1274 might be desirable for subclasses to override this behavior. 1275 1276 Primary purpose is to check for equality of representations. This 1277 aspect can actually be simplified/removed now that representations have 1278 equality defined. 1279 1280 Secondary purpose is to check for equality of coordinate attributes, 1281 which first checks whether they themselves are in equivalent frames 1282 before checking for equality in the normal fashion. This is because 1283 checking for equality with non-equivalent frames raises an error. 1284 """ 1285 if left_fattr is right_fattr: 1286 # shortcut if it's exactly the same object 1287 return True 1288 elif left_fattr is None or right_fattr is None: 1289 # shortcut if one attribute is unspecified and the other isn't 1290 return False 1291 1292 left_is_repr = isinstance(left_fattr, r.BaseRepresentationOrDifferential) 1293 right_is_repr = isinstance(right_fattr, r.BaseRepresentationOrDifferential) 1294 if left_is_repr and right_is_repr: 1295 # both are representations. 1296 if (getattr(left_fattr, 'differentials', False) or 1297 getattr(right_fattr, 'differentials', False)): 1298 warnings.warn('Two representation frame attributes were ' 1299 'checked for equivalence when at least one of' 1300 ' them has differentials. This yields False ' 1301 'even if the underlying representations are ' 1302 'equivalent (although this may change in ' 1303 'future versions of Astropy)', AstropyWarning) 1304 return False 1305 if isinstance(right_fattr, left_fattr.__class__): 1306 # if same representation type, compare components. 1307 return np.all([(getattr(left_fattr, comp) == 1308 getattr(right_fattr, comp)) 1309 for comp in left_fattr.components]) 1310 else: 1311 # convert to cartesian and see if they match 1312 return np.all(left_fattr.to_cartesian().xyz == 1313 right_fattr.to_cartesian().xyz) 1314 elif left_is_repr or right_is_repr: 1315 return False 1316 1317 left_is_coord = isinstance(left_fattr, BaseCoordinateFrame) 1318 right_is_coord = isinstance(right_fattr, BaseCoordinateFrame) 1319 if left_is_coord and right_is_coord: 1320 # both are coordinates 1321 if left_fattr.is_equivalent_frame(right_fattr): 1322 return np.all(left_fattr == right_fattr) 1323 else: 1324 return False 1325 elif left_is_coord or right_is_coord: 1326 return False 1327 1328 return np.all(left_fattr == right_fattr) 1329 1330 def is_equivalent_frame(self, other): 1331 """ 1332 Checks if this object is the same frame as the ``other`` object. 1333 1334 To be the same frame, two objects must be the same frame class and have 1335 the same frame attributes. Note that it does *not* matter what, if any, 1336 data either object has. 1337 1338 Parameters 1339 ---------- 1340 other : :class:`~astropy.coordinates.BaseCoordinateFrame` 1341 the other frame to check 1342 1343 Returns 1344 ------- 1345 isequiv : bool 1346 True if the frames are the same, False if not. 1347 1348 Raises 1349 ------ 1350 TypeError 1351 If ``other`` isn't a `BaseCoordinateFrame` or subclass. 1352 """ 1353 if self.__class__ == other.__class__: 1354 for frame_attr_name in self.get_frame_attr_names(): 1355 if not self._frameattr_equiv(getattr(self, frame_attr_name), 1356 getattr(other, frame_attr_name)): 1357 return False 1358 return True 1359 elif not isinstance(other, BaseCoordinateFrame): 1360 raise TypeError("Tried to do is_equivalent_frame on something that " 1361 "isn't a frame") 1362 else: 1363 return False 1364 1365 def __repr__(self): 1366 frameattrs = self._frame_attrs_repr() 1367 data_repr = self._data_repr() 1368 1369 if frameattrs: 1370 frameattrs = f' ({frameattrs})' 1371 1372 if data_repr: 1373 return f'<{self.__class__.__name__} Coordinate{frameattrs}: {data_repr}>' 1374 else: 1375 return f'<{self.__class__.__name__} Frame{frameattrs}>' 1376 1377 def _data_repr(self): 1378 """Returns a string representation of the coordinate data.""" 1379 1380 if not self.has_data: 1381 return '' 1382 1383 if self.representation_type: 1384 if (hasattr(self.representation_type, '_unit_representation') 1385 and isinstance(self.data, 1386 self.representation_type._unit_representation)): 1387 rep_cls = self.data.__class__ 1388 else: 1389 rep_cls = self.representation_type 1390 1391 if 's' in self.data.differentials: 1392 dif_cls = self.get_representation_cls('s') 1393 dif_data = self.data.differentials['s'] 1394 if isinstance(dif_data, (r.UnitSphericalDifferential, 1395 r.UnitSphericalCosLatDifferential, 1396 r.RadialDifferential)): 1397 dif_cls = dif_data.__class__ 1398 1399 else: 1400 dif_cls = None 1401 1402 data = self.represent_as(rep_cls, dif_cls, in_frame_units=True) 1403 1404 data_repr = repr(data) 1405 # Generate the list of component names out of the repr string 1406 part1, _, remainder = data_repr.partition('(') 1407 if remainder != '': 1408 comp_str, _, part2 = remainder.partition(')') 1409 comp_names = comp_str.split(', ') 1410 # Swap in frame-specific component names 1411 invnames = dict([(nmrepr, nmpref) for nmpref, nmrepr 1412 in self.representation_component_names.items()]) 1413 for i, name in enumerate(comp_names): 1414 comp_names[i] = invnames.get(name, name) 1415 # Reassemble the repr string 1416 data_repr = part1 + '(' + ', '.join(comp_names) + ')' + part2 1417 1418 else: 1419 data = self.data 1420 data_repr = repr(self.data) 1421 1422 if data_repr.startswith('<' + data.__class__.__name__): 1423 # remove both the leading "<" and the space after the name, as well 1424 # as the trailing ">" 1425 data_repr = data_repr[(len(data.__class__.__name__) + 2):-1] 1426 else: 1427 data_repr = 'Data:\n' + data_repr 1428 1429 if 's' in self.data.differentials: 1430 data_repr_spl = data_repr.split('\n') 1431 if 'has differentials' in data_repr_spl[-1]: 1432 diffrepr = repr(data.differentials['s']).split('\n') 1433 if diffrepr[0].startswith('<'): 1434 diffrepr[0] = ' ' + ' '.join(diffrepr[0].split(' ')[1:]) 1435 for frm_nm, rep_nm in self.get_representation_component_names('s').items(): 1436 diffrepr[0] = diffrepr[0].replace(rep_nm, frm_nm) 1437 if diffrepr[-1].endswith('>'): 1438 diffrepr[-1] = diffrepr[-1][:-1] 1439 data_repr_spl[-1] = '\n'.join(diffrepr) 1440 1441 data_repr = '\n'.join(data_repr_spl) 1442 1443 return data_repr 1444 1445 def _frame_attrs_repr(self): 1446 """ 1447 Returns a string representation of the frame's attributes, if any. 1448 """ 1449 attr_strs = [] 1450 for attribute_name in self.get_frame_attr_names(): 1451 attr = getattr(self, attribute_name) 1452 # Check to see if this object has a way of representing itself 1453 # specific to being an attribute of a frame. (Note, this is not the 1454 # Attribute class, it's the actual object). 1455 if hasattr(attr, "_astropy_repr_in_frame"): 1456 attrstr = attr._astropy_repr_in_frame() 1457 else: 1458 attrstr = str(attr) 1459 attr_strs.append(f"{attribute_name}={attrstr}") 1460 1461 return ', '.join(attr_strs) 1462 1463 def _apply(self, method, *args, **kwargs): 1464 """Create a new instance, applying a method to the underlying data. 1465 1466 In typical usage, the method is any of the shape-changing methods for 1467 `~numpy.ndarray` (``reshape``, ``swapaxes``, etc.), as well as those 1468 picking particular elements (``__getitem__``, ``take``, etc.), which 1469 are all defined in `~astropy.utils.shapes.ShapedLikeNDArray`. It will be 1470 applied to the underlying arrays in the representation (e.g., ``x``, 1471 ``y``, and ``z`` for `~astropy.coordinates.CartesianRepresentation`), 1472 as well as to any frame attributes that have a shape, with the results 1473 used to create a new instance. 1474 1475 Internally, it is also used to apply functions to the above parts 1476 (in particular, `~numpy.broadcast_to`). 1477 1478 Parameters 1479 ---------- 1480 method : str or callable 1481 If str, it is the name of a method that is applied to the internal 1482 ``components``. If callable, the function is applied. 1483 *args : tuple 1484 Any positional arguments for ``method``. 1485 **kwargs : dict 1486 Any keyword arguments for ``method``. 1487 """ 1488 def apply_method(value): 1489 if isinstance(value, ShapedLikeNDArray): 1490 return value._apply(method, *args, **kwargs) 1491 else: 1492 if callable(method): 1493 return method(value, *args, **kwargs) 1494 else: 1495 return getattr(value, method)(*args, **kwargs) 1496 1497 new = super().__new__(self.__class__) 1498 if hasattr(self, '_representation'): 1499 new._representation = self._representation.copy() 1500 new._attr_names_with_defaults = self._attr_names_with_defaults.copy() 1501 1502 for attr in self.frame_attributes: 1503 _attr = '_' + attr 1504 if attr in self._attr_names_with_defaults: 1505 setattr(new, _attr, getattr(self, _attr)) 1506 else: 1507 value = getattr(self, _attr) 1508 if getattr(value, 'shape', ()): 1509 value = apply_method(value) 1510 elif method == 'copy' or method == 'flatten': 1511 # flatten should copy also for a single element array, but 1512 # we cannot use it directly for array scalars, since it 1513 # always returns a one-dimensional array. So, just copy. 1514 value = copy.copy(value) 1515 1516 setattr(new, _attr, value) 1517 1518 if self.has_data: 1519 new._data = apply_method(self.data) 1520 else: 1521 new._data = None 1522 shapes = [getattr(new, '_' + attr).shape 1523 for attr in new.frame_attributes 1524 if (attr not in new._attr_names_with_defaults 1525 and getattr(getattr(new, '_' + attr), 'shape', ()))] 1526 if shapes: 1527 new._no_data_shape = (check_broadcast(*shapes) 1528 if len(shapes) > 1 else shapes[0]) 1529 else: 1530 new._no_data_shape = () 1531 1532 return new 1533 1534 def __setitem__(self, item, value): 1535 if self.__class__ is not value.__class__: 1536 raise TypeError(f'can only set from object of same class: ' 1537 f'{self.__class__.__name__} vs. ' 1538 f'{value.__class__.__name__}') 1539 1540 if not self.is_equivalent_frame(value): 1541 raise ValueError('can only set frame item from an equivalent frame') 1542 1543 if value._data is None: 1544 raise ValueError('can only set frame with value that has data') 1545 1546 if self._data is None: 1547 raise ValueError('cannot set frame which has no data') 1548 1549 if self.shape == (): 1550 raise TypeError(f"scalar '{self.__class__.__name__}' frame object " 1551 f"does not support item assignment") 1552 1553 if self._data is None: 1554 raise ValueError('can only set frame if it has data') 1555 1556 if self._data.__class__ is not value._data.__class__: 1557 raise TypeError(f'can only set from object of same class: ' 1558 f'{self._data.__class__.__name__} vs. ' 1559 f'{value._data.__class__.__name__}') 1560 1561 if self._data._differentials: 1562 # Can this ever occur? (Same class but different differential keys). 1563 # This exception is not tested since it is not clear how to generate it. 1564 if self._data._differentials.keys() != value._data._differentials.keys(): 1565 raise ValueError(f'setitem value must have same differentials') 1566 1567 for key, self_diff in self._data._differentials.items(): 1568 if self_diff.__class__ is not value._data._differentials[key].__class__: 1569 raise TypeError(f'can only set from object of same class: ' 1570 f'{self_diff.__class__.__name__} vs. ' 1571 f'{value._data._differentials[key].__class__.__name__}') 1572 1573 # Set representation data 1574 self._data[item] = value._data 1575 1576 # Frame attributes required to be identical by is_equivalent_frame, 1577 # no need to set them here. 1578 1579 self.cache.clear() 1580 1581 @override__dir__ 1582 def __dir__(self): 1583 """ 1584 Override the builtin `dir` behavior to include representation 1585 names. 1586 1587 TODO: dynamic representation transforms (i.e. include cylindrical et al.). 1588 """ 1589 dir_values = set(self.representation_component_names) 1590 dir_values |= set(self.get_representation_component_names('s')) 1591 1592 return dir_values 1593 1594 def __getattr__(self, attr): 1595 """ 1596 Allow access to attributes on the representation and differential as 1597 found via ``self.get_representation_component_names``. 1598 1599 TODO: We should handle dynamic representation transforms here (e.g., 1600 `.cylindrical`) instead of defining properties as below. 1601 """ 1602 1603 # attr == '_representation' is likely from the hasattr() test in the 1604 # representation property which is used for 1605 # self.representation_component_names. 1606 # 1607 # Prevent infinite recursion here. 1608 if attr.startswith('_'): 1609 return self.__getattribute__(attr) # Raise AttributeError. 1610 1611 repr_names = self.representation_component_names 1612 if attr in repr_names: 1613 if self._data is None: 1614 self.data # this raises the "no data" error by design - doing it 1615 # this way means we don't have to replicate the error message here 1616 1617 rep = self.represent_as(self.representation_type, 1618 in_frame_units=True) 1619 val = getattr(rep, repr_names[attr]) 1620 return val 1621 1622 diff_names = self.get_representation_component_names('s') 1623 if attr in diff_names: 1624 if self._data is None: 1625 self.data # see above. 1626 # TODO: this doesn't work for the case when there is only 1627 # unitspherical information. The differential_type gets set to the 1628 # default_differential, which expects full information, so the 1629 # units don't work out 1630 rep = self.represent_as(in_frame_units=True, 1631 **self.get_representation_cls(None)) 1632 val = getattr(rep.differentials['s'], diff_names[attr]) 1633 return val 1634 1635 return self.__getattribute__(attr) # Raise AttributeError. 1636 1637 def __setattr__(self, attr, value): 1638 # Don't slow down access of private attributes! 1639 if not attr.startswith('_'): 1640 if hasattr(self, 'representation_info'): 1641 repr_attr_names = set() 1642 for representation_attr in self.representation_info.values(): 1643 repr_attr_names.update(representation_attr['names']) 1644 1645 if attr in repr_attr_names: 1646 raise AttributeError( 1647 f'Cannot set any frame attribute {attr}') 1648 1649 super().__setattr__(attr, value) 1650 1651 def __eq__(self, value): 1652 """Equality operator for frame. 1653 1654 This implements strict equality and requires that the frames are 1655 equivalent and that the representation data are exactly equal. 1656 """ 1657 is_equiv = self.is_equivalent_frame(value) 1658 1659 if self._data is None and value._data is None: 1660 # For Frame with no data, == compare is same as is_equivalent_frame() 1661 return is_equiv 1662 1663 if not is_equiv: 1664 raise TypeError(f'cannot compare: objects must have equivalent frames: ' 1665 f'{self.replicate_without_data()} vs. ' 1666 f'{value.replicate_without_data()}') 1667 1668 if ((value._data is None and self._data is not None) 1669 or (self._data is None and value._data is not None)): 1670 raise ValueError('cannot compare: one frame has data and the other ' 1671 'does not') 1672 1673 return self._data == value._data 1674 1675 def __ne__(self, value): 1676 return np.logical_not(self == value) 1677 1678 def separation(self, other): 1679 """ 1680 Computes on-sky separation between this coordinate and another. 1681 1682 .. note:: 1683 1684 If the ``other`` coordinate object is in a different frame, it is 1685 first transformed to the frame of this object. This can lead to 1686 unintuitive behavior if not accounted for. Particularly of note is 1687 that ``self.separation(other)`` and ``other.separation(self)`` may 1688 not give the same answer in this case. 1689 1690 Parameters 1691 ---------- 1692 other : `~astropy.coordinates.BaseCoordinateFrame` 1693 The coordinate to get the separation to. 1694 1695 Returns 1696 ------- 1697 sep : `~astropy.coordinates.Angle` 1698 The on-sky separation between this and the ``other`` coordinate. 1699 1700 Notes 1701 ----- 1702 The separation is calculated using the Vincenty formula, which 1703 is stable at all locations, including poles and antipodes [1]_. 1704 1705 .. [1] https://en.wikipedia.org/wiki/Great-circle_distance 1706 1707 """ 1708 from .angle_utilities import angular_separation 1709 from .angles import Angle 1710 1711 self_unit_sph = self.represent_as(r.UnitSphericalRepresentation) 1712 other_transformed = other.transform_to(self) 1713 other_unit_sph = other_transformed.represent_as(r.UnitSphericalRepresentation) 1714 1715 # Get the separation as a Quantity, convert to Angle in degrees 1716 sep = angular_separation(self_unit_sph.lon, self_unit_sph.lat, 1717 other_unit_sph.lon, other_unit_sph.lat) 1718 return Angle(sep, unit=u.degree) 1719 1720 def separation_3d(self, other): 1721 """ 1722 Computes three dimensional separation between this coordinate 1723 and another. 1724 1725 Parameters 1726 ---------- 1727 other : `~astropy.coordinates.BaseCoordinateFrame` 1728 The coordinate system to get the distance to. 1729 1730 Returns 1731 ------- 1732 sep : `~astropy.coordinates.Distance` 1733 The real-space distance between these two coordinates. 1734 1735 Raises 1736 ------ 1737 ValueError 1738 If this or the other coordinate do not have distances. 1739 """ 1740 1741 from .distances import Distance 1742 1743 if issubclass(self.data.__class__, r.UnitSphericalRepresentation): 1744 raise ValueError('This object does not have a distance; cannot ' 1745 'compute 3d separation.') 1746 1747 # do this first just in case the conversion somehow creates a distance 1748 other_in_self_system = other.transform_to(self) 1749 1750 if issubclass(other_in_self_system.__class__, r.UnitSphericalRepresentation): 1751 raise ValueError('The other object does not have a distance; ' 1752 'cannot compute 3d separation.') 1753 1754 # drop the differentials to ensure they don't do anything odd in the 1755 # subtraction 1756 self_car = self.data.without_differentials().represent_as(r.CartesianRepresentation) 1757 other_car = other_in_self_system.data.without_differentials().represent_as(r.CartesianRepresentation) 1758 dist = (self_car - other_car).norm() 1759 if dist.unit == u.one: 1760 return dist 1761 else: 1762 return Distance(dist) 1763 1764 @property 1765 def cartesian(self): 1766 """ 1767 Shorthand for a cartesian representation of the coordinates in this 1768 object. 1769 """ 1770 1771 # TODO: if representations are updated to use a full transform graph, 1772 # the representation aliases should not be hard-coded like this 1773 return self.represent_as('cartesian', in_frame_units=True) 1774 1775 @property 1776 def cylindrical(self): 1777 """ 1778 Shorthand for a cylindrical representation of the coordinates in this 1779 object. 1780 """ 1781 1782 # TODO: if representations are updated to use a full transform graph, 1783 # the representation aliases should not be hard-coded like this 1784 return self.represent_as('cylindrical', in_frame_units=True) 1785 1786 @property 1787 def spherical(self): 1788 """ 1789 Shorthand for a spherical representation of the coordinates in this 1790 object. 1791 """ 1792 1793 # TODO: if representations are updated to use a full transform graph, 1794 # the representation aliases should not be hard-coded like this 1795 return self.represent_as('spherical', in_frame_units=True) 1796 1797 @property 1798 def sphericalcoslat(self): 1799 """ 1800 Shorthand for a spherical representation of the positional data and a 1801 `SphericalCosLatDifferential` for the velocity data in this object. 1802 """ 1803 1804 # TODO: if representations are updated to use a full transform graph, 1805 # the representation aliases should not be hard-coded like this 1806 return self.represent_as('spherical', 'sphericalcoslat', 1807 in_frame_units=True) 1808 1809 @property 1810 def velocity(self): 1811 """ 1812 Shorthand for retrieving the Cartesian space-motion as a 1813 `CartesianDifferential` object. This is equivalent to calling 1814 ``self.cartesian.differentials['s']``. 1815 """ 1816 if 's' not in self.data.differentials: 1817 raise ValueError('Frame has no associated velocity (Differential) ' 1818 'data information.') 1819 1820 return self.cartesian.differentials['s'] 1821 1822 @property 1823 def proper_motion(self): 1824 """ 1825 Shorthand for the two-dimensional proper motion as a 1826 `~astropy.units.Quantity` object with angular velocity units. In the 1827 returned `~astropy.units.Quantity`, ``axis=0`` is the longitude/latitude 1828 dimension so that ``.proper_motion[0]`` is the longitudinal proper 1829 motion and ``.proper_motion[1]`` is latitudinal. The longitudinal proper 1830 motion already includes the cos(latitude) term. 1831 """ 1832 if 's' not in self.data.differentials: 1833 raise ValueError('Frame has no associated velocity (Differential) ' 1834 'data information.') 1835 1836 sph = self.represent_as('spherical', 'sphericalcoslat', 1837 in_frame_units=True) 1838 pm_lon = sph.differentials['s'].d_lon_coslat 1839 pm_lat = sph.differentials['s'].d_lat 1840 return np.stack((pm_lon.value, 1841 pm_lat.to(pm_lon.unit).value), axis=0) * pm_lon.unit 1842 1843 @property 1844 def radial_velocity(self): 1845 """ 1846 Shorthand for the radial or line-of-sight velocity as a 1847 `~astropy.units.Quantity` object. 1848 """ 1849 if 's' not in self.data.differentials: 1850 raise ValueError('Frame has no associated velocity (Differential) ' 1851 'data information.') 1852 1853 sph = self.represent_as('spherical', in_frame_units=True) 1854 return sph.differentials['s'].d_distance 1855 1856 1857class GenericFrame(BaseCoordinateFrame): 1858 """ 1859 A frame object that can't store data but can hold any arbitrary frame 1860 attributes. Mostly useful as a utility for the high-level class to store 1861 intermediate frame attributes. 1862 1863 Parameters 1864 ---------- 1865 frame_attrs : dict 1866 A dictionary of attributes to be used as the frame attributes for this 1867 frame. 1868 """ 1869 1870 name = None # it's not a "real" frame so it doesn't have a name 1871 1872 def __init__(self, frame_attrs): 1873 self.frame_attributes = {} 1874 for name, default in frame_attrs.items(): 1875 self.frame_attributes[name] = Attribute(default) 1876 setattr(self, '_' + name, default) 1877 1878 super().__init__(None) 1879 1880 def __getattr__(self, name): 1881 if '_' + name in self.__dict__: 1882 return getattr(self, '_' + name) 1883 else: 1884 raise AttributeError(f'no {name}') 1885 1886 def __setattr__(self, name, value): 1887 if name in self.get_frame_attr_names(): 1888 raise AttributeError(f"can't set frame attribute '{name}'") 1889 else: 1890 super().__setattr__(name, value) 1891