1.. _working_with_angles: 2 3Working with Angles 4******************* 5 6The angular components of the various coordinate objects are represented 7by objects of the |Angle| class. While most likely to be encountered in 8the context of coordinate objects, |Angle| objects can also be used on 9their own wherever a representation of an angle is needed. 10 11.. _angle-creation: 12 13Creation 14======== 15 16The creation of an |Angle| object is quite flexible and supports a wide 17variety of input object types and formats. The type of the input angle(s) 18can be array, scalar, tuple, string, `~astropy.units.Quantity` or another 19|Angle|. This is best illustrated with a number of examples of valid ways 20to create an |Angle|. 21 22Examples 23-------- 24 25.. 26 EXAMPLE START 27 Different Ways to Create an Angle Object 28 29There are a number of ways to create an |Angle|:: 30 31 >>> import numpy as np 32 >>> from astropy import units as u 33 >>> from astropy.coordinates import Angle 34 35 >>> Angle('10.2345d') # String with 'd' abbreviation for degrees # doctest: +FLOAT_CMP 36 <Angle 10.2345 deg> 37 >>> Angle(['10.2345d', '-20d']) # Array of strings # doctest: +FLOAT_CMP 38 <Angle [ 10.2345, -20. ] deg> 39 >>> Angle('1:2:30.43 degrees') # Sexagesimal degrees # doctest: +FLOAT_CMP 40 <Angle 1.04178611 deg> 41 >>> Angle('1 2 0 hours') # Sexagesimal hours # doctest: +FLOAT_CMP 42 <Angle 1.03333333 hourangle> 43 >>> Angle(np.arange(1., 8.), unit=u.deg) # Numpy array from 1..7 in degrees # doctest: +FLOAT_CMP 44 <Angle [1., 2., 3., 4., 5., 6., 7.] deg> 45 >>> Angle('1°2′3″') # Unicode degree, arcmin and arcsec symbols # doctest: +FLOAT_CMP 46 <Angle 1.03416667 deg> 47 >>> Angle('1°2′3″N') # Unicode degree, arcmin, arcsec symbols and direction # doctest: +FLOAT_CMP 48 <Angle 1.03416667 deg> 49 >>> Angle('1d2m3.4s') # Degree, arcmin, arcsec. # doctest: +FLOAT_CMP 50 <Angle 1.03427778 deg> 51 >>> Angle('1d2m3.4sS') # Degree, arcmin, arcsec, direction. # doctest: +FLOAT_CMP 52 <Angle -1.03427778 deg> 53 >>> Angle('-1h2m3s') # Hour, minute, second # doctest: +FLOAT_CMP 54 <Angle -1.03416667 hourangle> 55 >>> Angle('-1h2m3sW') # Hour, minute, second, direction # doctest: +FLOAT_CMP 56 <Angle 1.03416667 hourangle> 57 >>> Angle((-1, 2, 3), unit=u.deg) # (degree, arcmin, arcsec) # doctest: +FLOAT_CMP 58 <Angle -1.03416667 deg> 59 >>> Angle(10.2345 * u.deg) # From a Quantity object in degrees # doctest: +FLOAT_CMP 60 <Angle 10.2345 deg> 61 >>> Angle(Angle(10.2345 * u.deg)) # From another Angle object # doctest: +FLOAT_CMP 62 <Angle 10.2345 deg> 63 64.. 65 EXAMPLE END 66 67Representation 68============== 69 70The |Angle| object also supports a variety of ways of representing the value 71of the angle, both as a floating point number and as a string. 72 73Examples 74-------- 75 76.. 77 EXAMPLE START 78 Representation of Angle Object Values 79 80There are many ways to represent the value of an |Angle|:: 81 82 >>> a = Angle(1, u.radian) 83 >>> a # doctest: +FLOAT_CMP 84 <Angle 1. rad> 85 >>> a.radian 86 1.0 87 >>> a.degree # doctest: +FLOAT_CMP 88 57.29577951308232 89 >>> a.hour # doctest: +FLOAT_CMP 90 3.8197186342054885 91 >>> a.hms # doctest: +FLOAT_CMP 92 hms_tuple(h=3.0, m=49.0, s=10.987083139758766) 93 >>> a.dms # doctest: +FLOAT_CMP 94 dms_tuple(d=57.0, m=17.0, s=44.806247096362313) 95 >>> a.signed_dms # doctest: +FLOAT_CMP 96 signed_dms_tuple(sign=1.0, d=57.0, m=17.0, s=44.806247096362313) 97 >>> (-a).dms # doctest: +FLOAT_CMP 98 dms_tuple(d=-57.0, m=-17.0, s=-44.806247096362313) 99 >>> (-a).signed_dms # doctest: +FLOAT_CMP 100 signed_dms_tuple(sign=-1.0, d=57.0, m=17.0, s=44.806247096362313) 101 >>> a.arcminute # doctest: +FLOAT_CMP 102 3437.7467707849396 103 >>> a.to_string() 104 '1rad' 105 >>> a.to_string(unit=u.degree) 106 '57d17m44.8062471s' 107 >>> a.to_string(unit=u.degree, sep=':') 108 '57:17:44.8062471' 109 >>> a.to_string(unit=u.degree, sep=('deg', 'm', 's')) 110 '57deg17m44.8062471s' 111 >>> a.to_string(unit=u.hour) 112 '3h49m10.98708314s' 113 >>> a.to_string(unit=u.hour, decimal=True) 114 '3.81972' 115 116.. 117 EXAMPLE END 118 119Usage 120===== 121 122Angles will also behave correctly for appropriate arithmetic operations. 123 124Example 125------- 126 127.. 128 EXAMPLE START 129 Arithmetic Operations Using Angle Objects 130 131To use |Angle| objects in arithmetic operations:: 132 133 >>> a = Angle(1.0, u.radian) 134 >>> a + 0.5 * u.radian + 2 * a # doctest: +FLOAT_CMP 135 <Angle 3.5 rad> 136 >>> np.sin(a / 2) # doctest: +FLOAT_CMP 137 <Quantity 0.47942554> 138 >>> a == a # doctest: +SKIP 139 array(True, dtype=bool) 140 >>> a == (a + a) # doctest: +SKIP 141 array(False, dtype=bool) 142 143.. 144 EXAMPLE END 145 146|Angle| objects can also be used for creating coordinate objects. 147 148Example 149------- 150 151.. 152 EXAMPLE START 153 Creating Coordinate Objects with Angle Objects 154 155To create a coordinate object using an |Angle|:: 156 157 >>> from astropy.coordinates import ICRS 158 >>> ICRS(Angle(1, u.deg), Angle(0.5, u.deg)) # doctest: +FLOAT_CMP 159 <ICRS Coordinate: (ra, dec) in deg 160 (1., 0.5)> 161 162.. 163 EXAMPLE END 164 165Wrapping and Bounds 166=================== 167 168There are two utility methods for working with angles that should have bounds. 169The :meth:`~astropy.coordinates.Angle.wrap_at` method allows taking an angle or 170angles and wrapping to be within a single 360 degree slice. The 171:meth:`~astropy.coordinates.Angle.is_within_bounds` method returns a 172boolean indicating whether an angle or angles is within the specified bounds. 173 174 175Longitude and Latitude Objects 176============================== 177 178|Longitude| and |Latitude| are two specialized subclasses of the |Angle| 179class that are used for all of the spherical coordinate classes. 180|Longitude| is used to represent values like right ascension, Galactic 181longitude, and azimuth (for Equatorial, Galactic, and Alt-Az coordinates, 182respectively). |Latitude| is used for declination, Galactic latitude, and 183elevation. 184 185Longitude 186--------- 187 188A |Longitude| object is distinguished from a pure |Angle| by virtue of a 189``wrap_angle`` property. The ``wrap_angle`` specifies that all angle values 190represented by the object will be in the range:: 191 192 wrap_angle - 360 * u.deg <= angle(s) < wrap_angle 193 194The default ``wrap_angle`` is 360 deg. Setting ``'wrap_angle=180 * u.deg'`` 195would instead result in values between -180 and +180 deg. Setting the 196``wrap_angle`` attribute of an existing ``Longitude`` object will result in 197re-wrapping the angle values in-place. For example:: 198 199 >>> from astropy.coordinates import Longitude 200 >>> a = Longitude([-20, 150, 350, 360] * u.deg) 201 >>> a.degree # doctest: +FLOAT_CMP 202 array([340., 150., 350., 0.]) 203 >>> a.wrap_angle = 180 * u.deg 204 >>> a.degree # doctest: +FLOAT_CMP 205 array([-20., 150., -10., 0.]) 206 207Latitude 208-------- 209 210A Latitude object is distinguished from a pure |Angle| by virtue 211of being bounded so that:: 212 213 -90.0 * u.deg <= angle(s) <= +90.0 * u.deg 214 215Any attempt to set a value outside of that range will result in a 216`ValueError`. 217 218 219Generating Angle Values 220======================= 221 222Astropy provides utility functions for generating angular or spherical 223positions, either with random sampling or with a grid of values. These functions 224all return `~astropy.coordinates.BaseRepresentation` subclass instances, which 225can be passed directly into coordinate frame classes or |SkyCoord| to create 226random or gridded coordinate objects. 227 228 229With Random Sampling 230-------------------- 231 232These functions both use standard, random `spherical point picking 233<https://mathworld.wolfram.com/SpherePointPicking.html>`_ to generate angular 234positions that are uniformly distributed on the surface of the unit sphere. To 235retrieve angular values only, use 236`~astropy.coordinates.uniform_spherical_random_surface`. For 237example, to generate 4 random angular positions:: 238 239 >>> from astropy.coordinates import uniform_spherical_random_surface 240 >>> pts = uniform_spherical_random_surface(size=4) 241 >>> pts # doctest: +SKIP 242 <UnitSphericalRepresentation (lon, lat) in rad 243 [(0.52561028, 0.38712031), (0.29900285, 0.52776066), 244 (0.98199282, 0.34247723), (2.15260367, 1.01499232)]> 245 246To generate three-dimensional positions uniformly within a spherical volume set 247by a maximum radius, instead use the 248`~astropy.coordinates.uniform_spherical_random_volume` 249function. For example, to generate 4 random 3D positions:: 250 251 >>> from astropy.coordinates import uniform_spherical_random_volume 252 >>> pts_3d = uniform_spherical_random_volume(size=4) 253 >>> pts_3d # doctest: +SKIP 254 <SphericalRepresentation (lon, lat, distance) in (rad, rad, ) 255 [(4.98504602, -0.74247419, 0.39752416), 256 (5.53281607, 0.89425191, 0.7391255 ), 257 (0.88100456, 0.21080555, 0.5531785 ), 258 (6.00879324, 0.61547168, 0.61746148)]> 259 260By default, the distance values returned are uniformly distributed within the 261unit sphere (i.e., the distance values are dimensionless). To instead generate 262random points within a sphere of a given dimensional radius, for example, 1 263parsec, pass in a |Quantity| object with the ``max_radius`` argument:: 264 265 >>> import astropy.units as u 266 >>> pts_3d = uniform_spherical_random_volume(size=4, max_radius=2*u.pc) 267 >>> pts_3d # doctest: +SKIP 268 <SphericalRepresentation (lon, lat, distance) in (rad, rad, pc) 269 [(3.36590297, -0.23085809, 1.47210093), 270 (6.14591179, 0.06840621, 0.9325143 ), 271 (2.19194797, 0.55099774, 1.19294064), 272 (5.25689272, -1.17703409, 1.63773358)]> 273 274 275On a Grid 276--------- 277 278No grid or lattice of points on the sphere can produce equal spacing between all 279grid points, but many approximate algorithms exist for generating angular grids 280with nearly even spacing (for example, `see this page 281<https://bendwavy.org/pack/pack.htm>`_). 282 283One simple and popular method in this context is the `golden spiral method 284<https://stackoverflow.com/a/44164075>`_, which is available in 285`astropy.coordinates` through the utility function 286`~astropy.coordinates.golden_spiral_grid`. This function accepts 287a single argument, ``size``, which specifies the number of points to generate in 288the grid:: 289 290 >>> from astropy.coordinates import golden_spiral_grid 291 >>> golden_pts = golden_spiral_grid(size=32) 292 >>> golden_pts # doctest: +FLOAT_CMP 293 <UnitSphericalRepresentation (lon, lat) in rad 294 [(1.94161104, 1.32014066), (5.82483312, 1.1343273 ), 295 (3.42486989, 1.004232 ), (1.02490666, 0.89666582), 296 (4.90812873, 0.80200278), (2.5081655 , 0.71583806), 297 (0.10820227, 0.63571129), (3.99142435, 0.56007531), 298 (1.59146112, 0.48787515), (5.4746832 , 0.41834639), 299 (3.07471997, 0.35090734), (0.67475674, 0.28509644), 300 (4.55797882, 0.22053326), (2.15801559, 0.15689287), 301 (6.04123767, 0.09388788), (3.64127444, 0.03125509), 302 (1.24131121, -0.03125509), (5.12453328, -0.09388788), 303 (2.72457005, -0.15689287), (0.32460682, -0.22053326), 304 (4.2078289 , -0.28509644), (1.80786567, -0.35090734), 305 (5.69108775, -0.41834639), (3.29112452, -0.48787515), 306 (0.89116129, -0.56007531), (4.77438337, -0.63571129), 307 (2.37442014, -0.71583806), (6.25764222, -0.80200278), 308 (3.85767899, -0.89666582), (1.45771576, -1.004232 ), 309 (5.34093783, -1.1343273 ), (2.9409746 , -1.32014066)]> 310 311 312 313 314Comparing Spherical Point Generation Methods 315-------------------------------------------- 316 317.. plot:: 318 :align: center 319 :context: close-figs 320 321 import matplotlib.pyplot as plt 322 from astropy.coordinates import uniform_spherical_random_surface, golden_spiral_grid 323 324 fig, axes = plt.subplots(1, 2, figsize=(10, 6), 325 subplot_kw=dict(projection='3d'), 326 constrained_layout=True) 327 328 for func, ax in zip([uniform_spherical_random_surface, 329 golden_spiral_grid], axes): 330 pts = func(size=128) 331 332 xyz = pts.to_cartesian().xyz 333 ax.plot(*xyz, ls='none') 334 335 ax.set(xlim=(-1, 1), 336 ylim=(-1, 1), 337 zlim=(-1, 1), 338 xlabel='$x$', 339 ylabel='$y$', 340 zlabel='$z$') 341 ax.set_title(func.__name__, fontsize=14) 342 343 fig.suptitle('128 points', fontsize=18) 344