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