1# -*- coding: utf-8 -*-
2# Licensed under a 3-clause BSD style license - see LICENSE.rst
3
4from copy import deepcopy
5
6import pytest
7import numpy as np
8from numpy.testing import assert_allclose, assert_array_equal
9
10from astropy import units as u
11from astropy.tests.helper import (assert_quantity_allclose as
12                                  assert_allclose_quantity)
13from astropy.utils import isiterable
14from astropy.utils.exceptions import DuplicateRepresentationWarning
15from astropy.coordinates.angles import Longitude, Latitude, Angle
16from astropy.coordinates.distances import Distance
17from astropy.coordinates.matrix_utilities import rotation_matrix
18from astropy.coordinates.representation import (
19    REPRESENTATION_CLASSES, DIFFERENTIAL_CLASSES, DUPLICATE_REPRESENTATIONS,
20    BaseRepresentation, SphericalRepresentation, UnitSphericalRepresentation,
21    SphericalCosLatDifferential, CartesianRepresentation, RadialRepresentation,
22    RadialDifferential, CylindricalRepresentation,
23    PhysicsSphericalRepresentation, CartesianDifferential,
24    SphericalDifferential, CylindricalDifferential,
25    PhysicsSphericalDifferential, UnitSphericalDifferential,
26    UnitSphericalCosLatDifferential)
27
28
29# create matrices for use in testing ``.transform()`` methods
30matrices = {
31    "rotation": rotation_matrix(-10, "z", u.deg),
32    "general": np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
33}
34
35
36# Preserve the original REPRESENTATION_CLASSES dict so that importing
37#   the test file doesn't add a persistent test subclass (LogDRepresentation)
38def setup_function(func):
39    func.REPRESENTATION_CLASSES_ORIG = deepcopy(REPRESENTATION_CLASSES)
40    func.DUPLICATE_REPRESENTATIONS_ORIG = deepcopy(DUPLICATE_REPRESENTATIONS)
41
42
43def teardown_function(func):
44    REPRESENTATION_CLASSES.clear()
45    REPRESENTATION_CLASSES.update(func.REPRESENTATION_CLASSES_ORIG)
46    DUPLICATE_REPRESENTATIONS.clear()
47    DUPLICATE_REPRESENTATIONS.update(func.DUPLICATE_REPRESENTATIONS_ORIG)
48
49
50def components_equal(rep1, rep2):
51    result = True
52    if type(rep1) is not type(rep2):
53        return False
54    for component in rep1.components:
55        result &= getattr(rep1, component) == getattr(rep2, component)
56    return result
57
58
59def components_allclose(rep1, rep2):
60    result = True
61    if type(rep1) is not type(rep2):
62        return False
63    for component in rep1.components:
64        result &= u.allclose(getattr(rep1, component), getattr(rep2, component))
65    return result
66
67
68def representation_equal(rep1, rep2):
69    result = True
70    if type(rep1) is not type(rep2):
71        return False
72    if getattr(rep1, '_differentials', False):
73        if rep1._differentials.keys() != rep2._differentials.keys():
74            return False
75        for key, diff1 in rep1._differentials.items():
76            result &= components_equal(diff1, rep2._differentials[key])
77    elif getattr(rep2, '_differentials', False):
78        return False
79
80    return result & components_equal(rep1, rep2)
81
82
83def representation_equal_up_to_angular_type(rep1, rep2):
84    result = True
85    if type(rep1) is not type(rep2):
86        return False
87    if getattr(rep1, '_differentials', False):
88        if rep1._differentials.keys() != rep2._differentials.keys():
89            return False
90        for key, diff1 in rep1._differentials.items():
91            result &= components_allclose(diff1, rep2._differentials[key])
92    elif getattr(rep2, '_differentials', False):
93        return False
94
95    return result & components_allclose(rep1, rep2)
96
97
98class TestRadialRepresentation:
99
100    def test_transform(self):
101        """Test the ``transform`` method. Only multiplication matrices pass."""
102        rep = RadialRepresentation(distance=10 * u.kpc)
103
104        # a rotation matrix does not work
105        matrix = rotation_matrix(10 * u.deg)
106        with pytest.raises(ValueError, match="scaled identity matrix"):
107            rep.transform(matrix)
108
109        # only a scaled identity matrix
110        matrix = 3 * np.identity(3)
111        newrep = rep.transform(matrix)
112        assert newrep.distance == 30 * u.kpc
113
114        # let's also check with differentials
115        dif = RadialDifferential(d_distance=-3 * u.km / u.s)
116        rep = rep.with_differentials(dict(s=dif))
117
118        newrep = rep.transform(matrix)
119        assert newrep.distance == 30 * u.kpc
120        assert newrep.differentials["s"].d_distance == -9 * u.km / u.s
121
122
123class TestSphericalRepresentation:
124
125    def test_name(self):
126        assert SphericalRepresentation.get_name() == 'spherical'
127        assert SphericalRepresentation.get_name() in REPRESENTATION_CLASSES
128
129    def test_empty_init(self):
130        with pytest.raises(TypeError) as exc:
131            s = SphericalRepresentation()
132
133    def test_init_quantity(self):
134
135        s3 = SphericalRepresentation(lon=8 * u.hourangle, lat=5 * u.deg, distance=10 * u.kpc)
136        assert s3.lon == 8. * u.hourangle
137        assert s3.lat == 5. * u.deg
138        assert s3.distance == 10 * u.kpc
139
140        assert isinstance(s3.lon, Longitude)
141        assert isinstance(s3.lat, Latitude)
142        assert isinstance(s3.distance, Distance)
143
144    def test_init_no_mutate_input(self):
145
146        lon = -1 * u.hourangle
147        s = SphericalRepresentation(lon=lon, lat=-1 * u.deg, distance=1 * u.kpc, copy=True)
148
149        # The longitude component should be wrapped at 24 hours
150        assert_allclose_quantity(s.lon, 23 * u.hourangle)
151
152        # The input should not have been mutated by the constructor
153        assert_allclose_quantity(lon, -1 * u.hourangle)
154
155    def test_init_lonlat(self):
156
157        s2 = SphericalRepresentation(Longitude(8, u.hour),
158                                     Latitude(5, u.deg),
159                                     Distance(10, u.kpc))
160
161        assert s2.lon == 8. * u.hourangle
162        assert s2.lat == 5. * u.deg
163        assert s2.distance == 10. * u.kpc
164
165        assert isinstance(s2.lon, Longitude)
166        assert isinstance(s2.lat, Latitude)
167        assert isinstance(s2.distance, Distance)
168
169        # also test that wrap_angle is preserved
170        s3 = SphericalRepresentation(Longitude(-90, u.degree,
171                                               wrap_angle=180*u.degree),
172                                     Latitude(-45, u.degree),
173                                     Distance(1., u.Rsun))
174        assert s3.lon == -90. * u.degree
175        assert s3.lon.wrap_angle == 180 * u.degree
176
177    def test_init_subclass(self):
178        class Longitude180(Longitude):
179            _default_wrap_angle = 180*u.degree
180
181        s = SphericalRepresentation(Longitude180(-90, u.degree),
182                                    Latitude(-45, u.degree),
183                                    Distance(1., u.Rsun))
184        assert isinstance(s.lon, Longitude180)
185        assert s.lon == -90. * u.degree
186        assert s.lon.wrap_angle == 180 * u.degree
187
188    def test_init_array(self):
189
190        s1 = SphericalRepresentation(lon=[8, 9] * u.hourangle,
191                                     lat=[5, 6] * u.deg,
192                                     distance=[1, 2] * u.kpc)
193
194        assert_allclose(s1.lon.degree, [120, 135])
195        assert_allclose(s1.lat.degree, [5, 6])
196        assert_allclose(s1.distance.kpc, [1, 2])
197
198        assert isinstance(s1.lon, Longitude)
199        assert isinstance(s1.lat, Latitude)
200        assert isinstance(s1.distance, Distance)
201
202    def test_init_array_nocopy(self):
203
204        lon = Longitude([8, 9] * u.hourangle)
205        lat = Latitude([5, 6] * u.deg)
206        distance = Distance([1, 2] * u.kpc)
207
208        s1 = SphericalRepresentation(lon=lon, lat=lat, distance=distance, copy=False)
209
210        lon[:] = [1, 2] * u.rad
211        lat[:] = [3, 4] * u.arcmin
212        distance[:] = [8, 9] * u.Mpc
213
214        assert_allclose_quantity(lon, s1.lon)
215        assert_allclose_quantity(lat, s1.lat)
216        assert_allclose_quantity(distance, s1.distance)
217
218    def test_init_float32_array(self):
219        """Regression test against #2983"""
220        lon = Longitude(np.float32([1., 2.]), u.degree)
221        lat = Latitude(np.float32([3., 4.]), u.degree)
222        s1 = UnitSphericalRepresentation(lon=lon, lat=lat, copy=False)
223        assert s1.lon.dtype == np.float32
224        assert s1.lat.dtype == np.float32
225        assert s1._values['lon'].dtype == np.float32
226        assert s1._values['lat'].dtype == np.float32
227
228    def test_reprobj(self):
229
230        s1 = SphericalRepresentation(lon=8 * u.hourangle, lat=5 * u.deg, distance=10 * u.kpc)
231
232        s2 = SphericalRepresentation.from_representation(s1)
233
234        assert_allclose_quantity(s2.lon, 8. * u.hourangle)
235        assert_allclose_quantity(s2.lat, 5. * u.deg)
236        assert_allclose_quantity(s2.distance, 10 * u.kpc)
237
238        s3 = SphericalRepresentation(s1)
239
240        assert representation_equal(s1, s3)
241
242    def test_broadcasting(self):
243
244        s1 = SphericalRepresentation(lon=[8, 9] * u.hourangle,
245                                     lat=[5, 6] * u.deg,
246                                     distance=10 * u.kpc)
247
248        assert_allclose_quantity(s1.lon, [120, 135] * u.degree)
249        assert_allclose_quantity(s1.lat, [5, 6] * u.degree)
250        assert_allclose_quantity(s1.distance, [10, 10] * u.kpc)
251
252    def test_broadcasting_mismatch(self):
253
254        with pytest.raises(ValueError) as exc:
255            s1 = SphericalRepresentation(lon=[8, 9, 10] * u.hourangle,
256                                         lat=[5, 6] * u.deg,
257                                         distance=[1, 2] * u.kpc)
258        assert exc.value.args[0] == "Input parameters lon, lat, and distance cannot be broadcast"
259
260    def test_readonly(self):
261
262        s1 = SphericalRepresentation(lon=8 * u.hourangle,
263                                     lat=5 * u.deg,
264                                     distance=1. * u.kpc)
265
266        with pytest.raises(AttributeError):
267            s1.lon = 1. * u.deg
268
269        with pytest.raises(AttributeError):
270            s1.lat = 1. * u.deg
271
272        with pytest.raises(AttributeError):
273            s1.distance = 1. * u.kpc
274
275    def test_getitem_len_iterable(self):
276
277        s = SphericalRepresentation(lon=np.arange(10) * u.deg,
278                                    lat=-np.arange(10) * u.deg,
279                                    distance=1 * u.kpc)
280
281        s_slc = s[2:8:2]
282
283        assert_allclose_quantity(s_slc.lon, [2, 4, 6] * u.deg)
284        assert_allclose_quantity(s_slc.lat, [-2, -4, -6] * u.deg)
285        assert_allclose_quantity(s_slc.distance, [1, 1, 1] * u.kpc)
286
287        assert len(s) == 10
288        assert isiterable(s)
289
290    def test_getitem_len_iterable_scalar(self):
291
292        s = SphericalRepresentation(lon=1 * u.deg,
293                                    lat=-2 * u.deg,
294                                    distance=3 * u.kpc)
295
296        with pytest.raises(TypeError):
297            s_slc = s[0]
298        with pytest.raises(TypeError):
299            len(s)
300        assert not isiterable(s)
301
302    def test_setitem(self):
303        s = SphericalRepresentation(lon=np.arange(5) * u.deg,
304                                    lat=-np.arange(5) * u.deg,
305                                    distance=1 * u.kpc)
306        s[:2] = SphericalRepresentation(lon=10.*u.deg, lat=2.*u.deg,
307                                        distance=5.*u.kpc)
308        assert_allclose_quantity(s.lon, [10, 10, 2, 3, 4] * u.deg)
309        assert_allclose_quantity(s.lat, [2, 2, -2, -3, -4] * u.deg)
310        assert_allclose_quantity(s.distance, [5, 5, 1, 1, 1] * u.kpc)
311
312    def test_negative_distance(self):
313        """Only allowed if explicitly passed on."""
314        with pytest.raises(ValueError, match='allow_negative'):
315            SphericalRepresentation(10*u.deg, 20*u.deg, -10*u.m)
316
317        s1 = SphericalRepresentation(10*u.deg, 20*u.deg,
318                                     Distance(-10*u.m, allow_negative=True))
319
320        assert s1.distance == -10.*u.m
321
322    def test_nan_distance(self):
323        """ This is a regression test: calling represent_as() and passing in the
324            same class as the object shouldn't round-trip through cartesian.
325        """
326
327        sph = SphericalRepresentation(1*u.deg, 2*u.deg, np.nan*u.kpc)
328        new_sph = sph.represent_as(SphericalRepresentation)
329        assert_allclose_quantity(new_sph.lon, sph.lon)
330        assert_allclose_quantity(new_sph.lat, sph.lat)
331
332        dif = SphericalCosLatDifferential(1*u.mas/u.yr, 2*u.mas/u.yr,
333                                          3*u.km/u.s)
334        sph = sph.with_differentials(dif)
335        new_sph = sph.represent_as(SphericalRepresentation)
336        assert_allclose_quantity(new_sph.lon, sph.lon)
337        assert_allclose_quantity(new_sph.lat, sph.lat)
338
339    def test_raise_on_extra_arguments(self):
340        with pytest.raises(TypeError, match='got multiple values'):
341            SphericalRepresentation(1*u.deg, 2*u.deg, 1.*u.kpc, lat=10)
342
343        with pytest.raises(TypeError, match='unexpected keyword.*parrot'):
344            SphericalRepresentation(1*u.deg, 2*u.deg, 1.*u.kpc, parrot=10)
345
346    def test_representation_shortcuts(self):
347        """Test that shortcuts in ``represent_as`` don't fail."""
348        difs = SphericalCosLatDifferential(4*u.mas/u.yr,5*u.mas/u.yr,6*u.km/u.s)
349        sph = SphericalRepresentation(1*u.deg, 2*u.deg, 3*u.kpc,
350                                      differentials={'s': difs})
351
352        got = sph.represent_as(PhysicsSphericalRepresentation,
353                               PhysicsSphericalDifferential)
354        assert np.may_share_memory(sph.lon, got.phi)
355        assert np.may_share_memory(sph.distance, got.r)
356        expected = BaseRepresentation.represent_as(
357            sph, PhysicsSphericalRepresentation, PhysicsSphericalDifferential)
358        # equal up to angular type
359        assert representation_equal_up_to_angular_type(got, expected)
360
361        got = sph.represent_as(UnitSphericalRepresentation,
362                               UnitSphericalDifferential)
363        assert np.may_share_memory(sph.lon, got.lon)
364        assert np.may_share_memory(sph.lat, got.lat)
365        expected = BaseRepresentation.represent_as(
366            sph, UnitSphericalRepresentation, UnitSphericalDifferential)
367        assert representation_equal_up_to_angular_type(got, expected)
368
369    def test_transform(self):
370        """Test ``.transform()`` on rotation and general matrices."""
371        # set up representation
372        ds1 = SphericalDifferential(
373            d_lon=[1, 2] * u.mas / u.yr, d_lat=[3, 4] * u.mas / u.yr,
374            d_distance=[-5, 6] * u.km / u.s)
375        s1 = SphericalRepresentation(lon=[1, 2] * u.deg, lat=[3, 4] * u.deg,
376                                     distance=[5, 6] * u.kpc, differentials=ds1)
377
378        # transform representation & get comparison (thru CartesianRep)
379        s2 = s1.transform(matrices["rotation"])
380        ds2 =  s2.differentials["s"]
381
382        dexpected = SphericalDifferential.from_cartesian(
383            ds1.to_cartesian(base=s1).transform(matrices["rotation"]), base=s2)
384
385        assert_allclose_quantity(s2.lon, s1.lon + 10 * u.deg)
386        assert_allclose_quantity(s2.lat, s1.lat)
387        assert_allclose_quantity(s2.distance, s1.distance)
388        # check differentials. they shouldn't have changed.
389        assert_allclose_quantity(ds2.d_lon, ds1.d_lon)
390        assert_allclose_quantity(ds2.d_lat, ds1.d_lat)
391        assert_allclose_quantity(ds2.d_distance, ds1.d_distance)
392        assert_allclose_quantity(ds2.d_lon, dexpected.d_lon)
393        assert_allclose_quantity(ds2.d_lat, dexpected.d_lat)
394        assert_allclose_quantity(ds2.d_distance, dexpected.d_distance)
395
396        # now with a non rotation matrix
397        # transform representation & get comparison (thru CartesianRep)
398        s3 = s1.transform(matrices["general"])
399        ds3 = s3.differentials["s"]
400
401        expected = (s1.represent_as(CartesianRepresentation,
402                                    CartesianDifferential)
403                    .transform(matrices["general"])
404                    .represent_as(SphericalRepresentation,
405                                  SphericalDifferential))
406        dexpected = expected.differentials["s"]
407
408        assert_allclose_quantity(s3.lon, expected.lon)
409        assert_allclose_quantity(s3.lat, expected.lat)
410        assert_allclose_quantity(s3.distance, expected.distance)
411        assert_allclose_quantity(ds3.d_lon, dexpected.d_lon)
412        assert_allclose_quantity(ds3.d_lat, dexpected.d_lat)
413        assert_allclose_quantity(ds3.d_distance, dexpected.d_distance)
414
415    def test_transform_with_NaN(self):
416        # all over again, but with a NaN in the distance
417
418        ds1 = SphericalDifferential(
419            d_lon=[1, 2] * u.mas / u.yr, d_lat=[3, 4] * u.mas / u.yr,
420            d_distance=[-5, 6] * u.km / u.s)
421        s1 = SphericalRepresentation(lon=[1, 2] * u.deg, lat=[3, 4] * u.deg,
422                                     distance=[5, np.nan] * u.kpc,
423                                     differentials=ds1)
424
425        # transform representation & get comparison (thru CartesianRep)
426        s2 = s1.transform(matrices["rotation"])
427        ds2 =  s2.differentials["s"]
428
429        dexpected = SphericalDifferential.from_cartesian(
430            ds1.to_cartesian(base=s1).transform(matrices["rotation"]), base=s2)
431
432        assert_allclose_quantity(s2.lon, s1.lon + 10 * u.deg)
433        assert_allclose_quantity(s2.lat, s1.lat)
434        assert_allclose_quantity(s2.distance, s1.distance)
435        assert_allclose_quantity(ds2.d_lon, dexpected.d_lon)
436        assert_allclose_quantity(ds2.d_lat, dexpected.d_lat)
437        assert_allclose_quantity(ds2.d_distance, dexpected.d_distance)
438        # the 2nd component is NaN since the 2nd distance is NaN
439        # TODO! this will change when ``.transform`` skips Cartesian
440        assert_array_equal(np.isnan(ds2.d_lon), (False, True))
441        assert_array_equal(np.isnan(ds2.d_lat), (False, True))
442        assert_array_equal(np.isnan(ds2.d_distance), (False, True))
443
444        # now with a non rotation matrix
445        s3 = s1.transform(matrices["general"])
446        ds3 = s3.differentials["s"]
447
448        thruC = (s1.represent_as(CartesianRepresentation,
449                                CartesianDifferential)
450                    .transform(matrices["general"])
451                    .represent_as(SphericalRepresentation,
452                              differential_class=SphericalDifferential))
453        dthruC = thruC.differentials["s"]
454
455        # s3 should not propagate Nan.
456        assert_array_equal(np.isnan(s3.lon), (False, False))
457        assert_array_equal(np.isnan(s3.lat), (False, False))
458        assert_array_equal(np.isnan(s3.distance), (False, True))
459        # ds3 does b/c currently aren't any shortcuts on the transform
460        assert_array_equal(np.isnan(ds3.d_lon), (False, True))
461        assert_array_equal(np.isnan(ds3.d_lat), (False, True))
462        assert_array_equal(np.isnan(ds3.d_distance), (False, True))
463
464        # through Cartesian should
465        assert_array_equal(np.isnan(thruC.lon), (False, True))
466        assert_array_equal(np.isnan(thruC.lat), (False, True))
467        assert_array_equal(np.isnan(thruC.distance), (False, True))
468        assert_array_equal(np.isnan(dthruC.d_lon), (False, True))
469        assert_array_equal(np.isnan(dthruC.d_lat), (False, True))
470        assert_array_equal(np.isnan(dthruC.d_distance), (False, True))
471        # test that they are close on the first value
472        assert_allclose_quantity(s3.lon[0], thruC.lon[0])
473        assert_allclose_quantity(s3.lat[0], thruC.lat[0])
474        assert_allclose_quantity(ds3.d_lon[0], dthruC.d_lon[0])
475        assert_allclose_quantity(ds3.d_lat[0], dthruC.d_lat[0])
476
477
478class TestUnitSphericalRepresentation:
479
480    def test_name(self):
481        assert UnitSphericalRepresentation.get_name() == 'unitspherical'
482        assert UnitSphericalRepresentation.get_name() in REPRESENTATION_CLASSES
483
484    def test_empty_init(self):
485        with pytest.raises(TypeError) as exc:
486            s = UnitSphericalRepresentation()
487
488    def test_init_quantity(self):
489
490        s3 = UnitSphericalRepresentation(lon=8 * u.hourangle, lat=5 * u.deg)
491        assert s3.lon == 8. * u.hourangle
492        assert s3.lat == 5. * u.deg
493
494        assert isinstance(s3.lon, Longitude)
495        assert isinstance(s3.lat, Latitude)
496
497    def test_init_lonlat(self):
498
499        s2 = UnitSphericalRepresentation(Longitude(8, u.hour),
500                                         Latitude(5, u.deg))
501
502        assert s2.lon == 8. * u.hourangle
503        assert s2.lat == 5. * u.deg
504
505        assert isinstance(s2.lon, Longitude)
506        assert isinstance(s2.lat, Latitude)
507
508    def test_init_array(self):
509
510        s1 = UnitSphericalRepresentation(lon=[8, 9] * u.hourangle,
511                                         lat=[5, 6] * u.deg)
512
513        assert_allclose(s1.lon.degree, [120, 135])
514        assert_allclose(s1.lat.degree, [5, 6])
515
516        assert isinstance(s1.lon, Longitude)
517        assert isinstance(s1.lat, Latitude)
518
519    def test_init_array_nocopy(self):
520
521        lon = Longitude([8, 9] * u.hourangle)
522        lat = Latitude([5, 6] * u.deg)
523
524        s1 = UnitSphericalRepresentation(lon=lon, lat=lat, copy=False)
525
526        lon[:] = [1, 2] * u.rad
527        lat[:] = [3, 4] * u.arcmin
528
529        assert_allclose_quantity(lon, s1.lon)
530        assert_allclose_quantity(lat, s1.lat)
531
532    def test_reprobj(self):
533
534        s1 = UnitSphericalRepresentation(lon=8 * u.hourangle, lat=5 * u.deg)
535
536        s2 = UnitSphericalRepresentation.from_representation(s1)
537
538        assert_allclose_quantity(s2.lon, 8. * u.hourangle)
539        assert_allclose_quantity(s2.lat, 5. * u.deg)
540
541        s3 = UnitSphericalRepresentation(s1)
542
543        assert representation_equal(s3, s1)
544
545    def test_broadcasting(self):
546
547        s1 = UnitSphericalRepresentation(lon=[8, 9] * u.hourangle,
548                                         lat=[5, 6] * u.deg)
549
550        assert_allclose_quantity(s1.lon, [120, 135] * u.degree)
551        assert_allclose_quantity(s1.lat, [5, 6] * u.degree)
552
553    def test_broadcasting_mismatch(self):
554
555        with pytest.raises(ValueError) as exc:
556            s1 = UnitSphericalRepresentation(lon=[8, 9, 10] * u.hourangle,
557                                             lat=[5, 6] * u.deg)
558        assert exc.value.args[0] == "Input parameters lon and lat cannot be broadcast"
559
560    def test_readonly(self):
561
562        s1 = UnitSphericalRepresentation(lon=8 * u.hourangle,
563                                         lat=5 * u.deg)
564
565        with pytest.raises(AttributeError):
566            s1.lon = 1. * u.deg
567
568        with pytest.raises(AttributeError):
569            s1.lat = 1. * u.deg
570
571    def test_getitem(self):
572
573        s = UnitSphericalRepresentation(lon=np.arange(10) * u.deg,
574                                        lat=-np.arange(10) * u.deg)
575
576        s_slc = s[2:8:2]
577
578        assert_allclose_quantity(s_slc.lon, [2, 4, 6] * u.deg)
579        assert_allclose_quantity(s_slc.lat, [-2, -4, -6] * u.deg)
580
581    def test_getitem_scalar(self):
582
583        s = UnitSphericalRepresentation(lon=1 * u.deg,
584                                        lat=-2 * u.deg)
585
586        with pytest.raises(TypeError):
587            s_slc = s[0]
588
589    def test_representation_shortcuts(self):
590        """Test that shortcuts in ``represent_as`` don't fail."""
591        # TODO! representation transformations with differentials cannot
592        # (currently) be implemented due to a mismatch between the UnitSpherical
593        # expected keys (e.g. "s") and that expected in the other class
594        # (here "s / m"). For more info, see PR #11467
595        # We leave the test code commented out for future use.
596        # diffs = UnitSphericalCosLatDifferential(4*u.mas/u.yr, 5*u.mas/u.yr,
597        #                                         6*u.km/u.s)
598        sph = UnitSphericalRepresentation(1*u.deg, 2*u.deg)
599                                          # , differentials={'s': diffs}
600        got = sph.represent_as(PhysicsSphericalRepresentation)
601                               # , PhysicsSphericalDifferential)
602        assert np.may_share_memory(sph.lon, got.phi)
603        expected = BaseRepresentation.represent_as(
604            sph, PhysicsSphericalRepresentation)  # PhysicsSphericalDifferential
605        assert representation_equal_up_to_angular_type(got, expected)
606
607        got = sph.represent_as(SphericalRepresentation)
608                               # , SphericalDifferential)
609        assert np.may_share_memory(sph.lon, got.lon)
610        assert np.may_share_memory(sph.lat, got.lat)
611        expected = BaseRepresentation.represent_as(
612            sph, SphericalRepresentation) # , SphericalDifferential)
613        assert representation_equal_up_to_angular_type(got, expected)
614
615    def test_transform(self):
616        """Test ``.transform()`` on rotation and general matrices."""
617        # set up representation
618        ds1 = UnitSphericalDifferential(d_lon=[1, 2] * u.mas / u.yr,
619                                        d_lat=[3, 4] * u.mas / u.yr,)
620        s1 = UnitSphericalRepresentation(lon=[1, 2] * u.deg, lat=[3, 4] * u.deg,
621                                         differentials=ds1)
622
623        # transform representation & get comparison (thru CartesianRep)
624        s2 = s1.transform(matrices["rotation"])
625        ds2 =  s2.differentials["s"]
626
627        dexpected = UnitSphericalDifferential.from_cartesian(
628            ds1.to_cartesian(base=s1).transform(matrices["rotation"]), base=s2)
629
630        assert_allclose_quantity(s2.lon, s1.lon + 10 * u.deg)
631        assert_allclose_quantity(s2.lat, s1.lat)
632        # compare differentials. they should be unchanged (ds1).
633        assert_allclose_quantity(ds2.d_lon, ds1.d_lon)
634        assert_allclose_quantity(ds2.d_lat, ds1.d_lat)
635        assert_allclose_quantity(ds2.d_lon, dexpected.d_lon)
636        assert_allclose_quantity(ds2.d_lat, dexpected.d_lat)
637        assert not hasattr(ds2, "d_distance")
638
639        # now with a non rotation matrix
640        # note that the result will be a Spherical, not UnitSpherical
641        s3 = s1.transform(matrices["general"])
642        ds3 = s3.differentials["s"]
643
644        expected = (s1.represent_as(CartesianRepresentation,
645                                    CartesianDifferential)
646                    .transform(matrices["general"])
647                    .represent_as(SphericalRepresentation,
648                                  differential_class=SphericalDifferential))
649        dexpected = expected.differentials["s"]
650
651        assert_allclose_quantity(s3.lon, expected.lon)
652        assert_allclose_quantity(s3.lat, expected.lat)
653        assert_allclose_quantity(s3.distance, expected.distance)
654        assert_allclose_quantity(ds3.d_lon, dexpected.d_lon)
655        assert_allclose_quantity(ds3.d_lat, dexpected.d_lat)
656        assert_allclose_quantity(ds3.d_distance, dexpected.d_distance)
657
658
659class TestPhysicsSphericalRepresentation:
660
661    def test_name(self):
662        assert PhysicsSphericalRepresentation.get_name() == 'physicsspherical'
663        assert PhysicsSphericalRepresentation.get_name() in REPRESENTATION_CLASSES
664
665    def test_empty_init(self):
666        with pytest.raises(TypeError) as exc:
667            s = PhysicsSphericalRepresentation()
668
669    def test_init_quantity(self):
670
671        s3 = PhysicsSphericalRepresentation(phi=8 * u.hourangle, theta=5 * u.deg, r=10 * u.kpc)
672        assert s3.phi == 8. * u.hourangle
673        assert s3.theta == 5. * u.deg
674        assert s3.r == 10 * u.kpc
675
676        assert isinstance(s3.phi, Angle)
677        assert isinstance(s3.theta, Angle)
678        assert isinstance(s3.r, Distance)
679
680    def test_init_phitheta(self):
681
682        s2 = PhysicsSphericalRepresentation(Angle(8, u.hour),
683                                            Angle(5, u.deg),
684                                            Distance(10, u.kpc))
685
686        assert s2.phi == 8. * u.hourangle
687        assert s2.theta == 5. * u.deg
688        assert s2.r == 10. * u.kpc
689
690        assert isinstance(s2.phi, Angle)
691        assert isinstance(s2.theta, Angle)
692        assert isinstance(s2.r, Distance)
693
694    def test_init_array(self):
695
696        s1 = PhysicsSphericalRepresentation(phi=[8, 9] * u.hourangle,
697                                            theta=[5, 6] * u.deg,
698                                            r=[1, 2] * u.kpc)
699
700        assert_allclose(s1.phi.degree, [120, 135])
701        assert_allclose(s1.theta.degree, [5, 6])
702        assert_allclose(s1.r.kpc, [1, 2])
703
704        assert isinstance(s1.phi, Angle)
705        assert isinstance(s1.theta, Angle)
706        assert isinstance(s1.r, Distance)
707
708    def test_init_array_nocopy(self):
709
710        phi = Angle([8, 9] * u.hourangle)
711        theta = Angle([5, 6] * u.deg)
712        r = Distance([1, 2] * u.kpc)
713
714        s1 = PhysicsSphericalRepresentation(phi=phi, theta=theta, r=r, copy=False)
715
716        phi[:] = [1, 2] * u.rad
717        theta[:] = [3, 4] * u.arcmin
718        r[:] = [8, 9] * u.Mpc
719
720        assert_allclose_quantity(phi, s1.phi)
721        assert_allclose_quantity(theta, s1.theta)
722        assert_allclose_quantity(r, s1.r)
723
724    def test_reprobj(self):
725
726        s1 = PhysicsSphericalRepresentation(phi=8 * u.hourangle, theta=5 * u.deg, r=10 * u.kpc)
727
728        s2 = PhysicsSphericalRepresentation.from_representation(s1)
729
730        assert_allclose_quantity(s2.phi, 8. * u.hourangle)
731        assert_allclose_quantity(s2.theta, 5. * u.deg)
732        assert_allclose_quantity(s2.r, 10 * u.kpc)
733
734        s3 = PhysicsSphericalRepresentation(s1)
735
736        assert representation_equal(s3, s1)
737
738    def test_broadcasting(self):
739
740        s1 = PhysicsSphericalRepresentation(phi=[8, 9] * u.hourangle,
741                                            theta=[5, 6] * u.deg,
742                                            r=10 * u.kpc)
743
744        assert_allclose_quantity(s1.phi, [120, 135] * u.degree)
745        assert_allclose_quantity(s1.theta, [5, 6] * u.degree)
746        assert_allclose_quantity(s1.r, [10, 10] * u.kpc)
747
748    def test_broadcasting_mismatch(self):
749
750        with pytest.raises(ValueError) as exc:
751            s1 = PhysicsSphericalRepresentation(phi=[8, 9, 10] * u.hourangle,
752                                                theta=[5, 6] * u.deg,
753                                                r=[1, 2] * u.kpc)
754        assert exc.value.args[0] == "Input parameters phi, theta, and r cannot be broadcast"
755
756    def test_readonly(self):
757
758        s1 = PhysicsSphericalRepresentation(phi=[8, 9] * u.hourangle,
759                                            theta=[5, 6] * u.deg,
760                                            r=[10, 20] * u.kpc)
761
762        with pytest.raises(AttributeError):
763            s1.phi = 1. * u.deg
764
765        with pytest.raises(AttributeError):
766            s1.theta = 1. * u.deg
767
768        with pytest.raises(AttributeError):
769            s1.r = 1. * u.kpc
770
771    def test_getitem(self):
772
773        s = PhysicsSphericalRepresentation(phi=np.arange(10) * u.deg,
774                                           theta=np.arange(5, 15) * u.deg,
775                                           r=1 * u.kpc)
776
777        s_slc = s[2:8:2]
778
779        assert_allclose_quantity(s_slc.phi, [2, 4, 6] * u.deg)
780        assert_allclose_quantity(s_slc.theta, [7, 9, 11] * u.deg)
781        assert_allclose_quantity(s_slc.r, [1, 1, 1] * u.kpc)
782
783    def test_getitem_scalar(self):
784
785        s = PhysicsSphericalRepresentation(phi=1 * u.deg,
786                                           theta=2 * u.deg,
787                                           r=3 * u.kpc)
788
789        with pytest.raises(TypeError):
790            s_slc = s[0]
791
792    def test_representation_shortcuts(self):
793        """Test that shortcuts in ``represent_as`` don't fail."""
794        difs = PhysicsSphericalDifferential(4*u.mas/u.yr,5*u.mas/u.yr,6*u.km/u.s)
795        sph = PhysicsSphericalRepresentation(1*u.deg, 2*u.deg, 3*u.kpc,
796                                             differentials={'s': difs})
797
798        got = sph.represent_as(SphericalRepresentation,
799                               SphericalDifferential)
800        assert np.may_share_memory(sph.phi, got.lon)
801        assert np.may_share_memory(sph.r, got.distance)
802        expected = BaseRepresentation.represent_as(
803            sph, SphericalRepresentation, SphericalDifferential)
804        assert representation_equal_up_to_angular_type(got, expected)
805
806        got = sph.represent_as(UnitSphericalRepresentation,
807                               UnitSphericalDifferential)
808        assert np.may_share_memory(sph.phi, got.lon)
809        expected = BaseRepresentation.represent_as(
810            sph, UnitSphericalRepresentation, UnitSphericalDifferential)
811        assert representation_equal_up_to_angular_type(got, expected)
812
813    def test_initialize_with_nan(self):
814        # Regression test for gh-11558: initialization used to fail.
815        psr = PhysicsSphericalRepresentation([1., np.nan]*u.deg, [np.nan, 2.]*u.deg,
816                                             [3., np.nan]*u.m)
817        assert_array_equal(np.isnan(psr.phi), [False, True])
818        assert_array_equal(np.isnan(psr.theta), [True, False])
819        assert_array_equal(np.isnan(psr.r), [False, True])
820
821    def test_transform(self):
822        """Test ``.transform()`` on rotation and general transform matrices."""
823        # set up representation
824        ds1 = PhysicsSphericalDifferential(
825            d_phi=[1, 2] * u.mas / u.yr, d_theta=[3, 4] * u.mas / u.yr,
826            d_r=[-5, 6] * u.km / u.s)
827        s1 = PhysicsSphericalRepresentation(
828            phi=[1, 2] * u.deg, theta=[3, 4] * u.deg, r=[5, 6] * u.kpc,
829            differentials=ds1)
830
831        # transform representation & get comparison (thru CartesianRep)
832        s2 = s1.transform(matrices["rotation"])
833        ds2 = s2.differentials["s"]
834
835        dexpected = PhysicsSphericalDifferential.from_cartesian(
836            ds1.to_cartesian(base=s1).transform(matrices["rotation"]), base=s2)
837
838        assert_allclose_quantity(s2.phi, s1.phi + 10 * u.deg)
839        assert_allclose_quantity(s2.theta, s1.theta)
840        assert_allclose_quantity(s2.r, s1.r)
841        # compare differentials. should be unchanged (ds1).
842        assert_allclose_quantity(ds2.d_phi, ds1.d_phi)
843        assert_allclose_quantity(ds2.d_theta, ds1.d_theta)
844        assert_allclose_quantity(ds2.d_r, ds1.d_r)
845        assert_allclose_quantity(ds2.d_phi, dexpected.d_phi)
846        assert_allclose_quantity(ds2.d_theta, dexpected.d_theta)
847        assert_allclose_quantity(ds2.d_r, dexpected.d_r)
848
849        # now with a non rotation matrix
850        # transform representation & get comparison (thru CartesianRep)
851        s3 = s1.transform(matrices["general"])
852        ds3 = s3.differentials["s"]
853
854        expected = (s1.represent_as(CartesianRepresentation,
855                                    CartesianDifferential)
856                    .transform(matrices["general"])
857                    .represent_as(PhysicsSphericalRepresentation,
858                                  PhysicsSphericalDifferential))
859        dexpected = expected.differentials["s"]
860
861        assert_allclose_quantity(s3.phi, expected.phi)
862        assert_allclose_quantity(s3.theta, expected.theta)
863        assert_allclose_quantity(s3.r, expected.r)
864        assert_allclose_quantity(ds3.d_phi, dexpected.d_phi)
865        assert_allclose_quantity(ds3.d_theta, dexpected.d_theta)
866        assert_allclose_quantity(ds3.d_r, dexpected.d_r)
867
868    def test_transform_with_NaN(self):
869        # all over again, but with a NaN in the distance
870
871        ds1 = PhysicsSphericalDifferential(
872            d_phi=[1, 2] * u.mas / u.yr, d_theta=[3, 4] * u.mas / u.yr,
873            d_r=[-5, 6] * u.km / u.s)
874        s1 = PhysicsSphericalRepresentation(
875            phi=[1, 2] * u.deg, theta=[3, 4] * u.deg, r=[5, np.nan] * u.kpc,
876            differentials=ds1)
877
878        # transform representation & get comparison (thru CartesianRep)
879        s2 = s1.transform(matrices["rotation"])
880        ds2 =  s2.differentials["s"]
881
882        dexpected = PhysicsSphericalDifferential.from_cartesian(
883            ds1.to_cartesian(base=s1).transform(matrices["rotation"]), base=s2)
884
885        assert_allclose_quantity(s2.phi, s1.phi + 10 * u.deg)
886        assert_allclose_quantity(s2.theta, s1.theta)
887        assert_allclose_quantity(s2.r, s1.r)
888        assert_allclose_quantity(ds2.d_phi, dexpected.d_phi)
889        assert_allclose_quantity(ds2.d_theta, dexpected.d_theta)
890        assert_allclose_quantity(ds2.d_r, dexpected.d_r)
891
892        # now with a non rotation matrix
893        s3 = s1.transform(matrices["general"])
894        ds3 = s3.differentials["s"]
895
896        thruC = (s1.represent_as(CartesianRepresentation,
897                                 CartesianDifferential)
898                    .transform(matrices["general"])
899                    .represent_as(PhysicsSphericalRepresentation,
900                                  PhysicsSphericalDifferential))
901        dthruC = thruC.differentials["s"]
902
903        # s3 should not propagate Nan.
904        assert_array_equal(np.isnan(s3.phi), (False, False))
905        assert_array_equal(np.isnan(s3.theta), (False, False))
906        assert_array_equal(np.isnan(s3.r), (False, True))
907        # ds3 does b/c currently aren't any shortcuts on the transform
908        assert_array_equal(np.isnan(ds3.d_phi), (False, True))
909        assert_array_equal(np.isnan(ds3.d_theta), (False, True))
910        assert_array_equal(np.isnan(ds3.d_r), (False, True))
911
912        # through Cartesian does
913        assert_array_equal(np.isnan(thruC.phi), (False, True))
914        assert_array_equal(np.isnan(thruC.theta), (False, True))
915        assert_array_equal(np.isnan(thruC.r), (False, True))
916        # so only test on the first value
917        assert_allclose_quantity(s3.phi[0], thruC.phi[0])
918        assert_allclose_quantity(s3.theta[0], thruC.theta[0])
919        assert_allclose_quantity(ds3.d_phi[0], dthruC.d_phi[0])
920        assert_allclose_quantity(ds3.d_theta[0], dthruC.d_theta[0])
921
922
923class TestCartesianRepresentation:
924
925    def test_name(self):
926        assert CartesianRepresentation.get_name() == 'cartesian'
927        assert CartesianRepresentation.get_name() in REPRESENTATION_CLASSES
928
929    def test_empty_init(self):
930        with pytest.raises(TypeError) as exc:
931            s = CartesianRepresentation()
932
933    def test_init_quantity(self):
934
935        s1 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc)
936
937        assert s1.x.unit is u.kpc
938        assert s1.y.unit is u.kpc
939        assert s1.z.unit is u.kpc
940
941        assert_allclose(s1.x.value, 1)
942        assert_allclose(s1.y.value, 2)
943        assert_allclose(s1.z.value, 3)
944
945    def test_init_singleunit(self):
946
947        s1 = CartesianRepresentation(x=1, y=2, z=3, unit=u.kpc)
948
949        assert s1.x.unit is u.kpc
950        assert s1.y.unit is u.kpc
951        assert s1.z.unit is u.kpc
952
953        assert_allclose(s1.x.value, 1)
954        assert_allclose(s1.y.value, 2)
955        assert_allclose(s1.z.value, 3)
956
957    def test_init_array(self):
958
959        s1 = CartesianRepresentation(x=[1, 2, 3] * u.pc,
960                                     y=[2, 3, 4] * u.Mpc,
961                                     z=[3, 4, 5] * u.kpc)
962
963        assert s1.x.unit is u.pc
964        assert s1.y.unit is u.Mpc
965        assert s1.z.unit is u.kpc
966
967        assert_allclose(s1.x.value, [1, 2, 3])
968        assert_allclose(s1.y.value, [2, 3, 4])
969        assert_allclose(s1.z.value, [3, 4, 5])
970
971    def test_init_one_array(self):
972
973        s1 = CartesianRepresentation(x=[1, 2, 3] * u.pc)
974
975        assert s1.x.unit is u.pc
976        assert s1.y.unit is u.pc
977        assert s1.z.unit is u.pc
978
979        assert_allclose(s1.x.value, 1)
980        assert_allclose(s1.y.value, 2)
981        assert_allclose(s1.z.value, 3)
982
983        r = np.arange(27.).reshape(3, 3, 3) * u.kpc
984        s2 = CartesianRepresentation(r, xyz_axis=0)
985        assert s2.shape == (3, 3)
986        assert s2.x.unit == u.kpc
987        assert np.all(s2.x == r[0])
988        assert np.all(s2.xyz == r)
989        assert np.all(s2.get_xyz(xyz_axis=0) == r)
990        s3 = CartesianRepresentation(r, xyz_axis=1)
991        assert s3.shape == (3, 3)
992        assert np.all(s3.x == r[:, 0])
993        assert np.all(s3.y == r[:, 1])
994        assert np.all(s3.z == r[:, 2])
995        assert np.all(s3.get_xyz(xyz_axis=1) == r)
996        s4 = CartesianRepresentation(r, xyz_axis=2)
997        assert s4.shape == (3, 3)
998        assert np.all(s4.x == r[:, :, 0])
999        assert np.all(s4.get_xyz(xyz_axis=2) == r)
1000        s5 = CartesianRepresentation(r, unit=u.pc)
1001        assert s5.x.unit == u.pc
1002        assert np.all(s5.xyz == r)
1003        s6 = CartesianRepresentation(r.value, unit=u.pc, xyz_axis=2)
1004        assert s6.x.unit == u.pc
1005        assert np.all(s6.get_xyz(xyz_axis=2).value == r.value)
1006
1007    def test_init_one_array_size_fail(self):
1008        with pytest.raises(ValueError) as exc:
1009            CartesianRepresentation(x=[1, 2, 3, 4] * u.pc)
1010        assert exc.value.args[0].startswith("too many values to unpack")
1011
1012    def test_init_xyz_but_more_than_one_array_fail(self):
1013        with pytest.raises(ValueError) as exc:
1014            CartesianRepresentation(x=[1, 2, 3] * u.pc, y=[2, 3, 4] * u.pc,
1015                                    z=[3, 4, 5] * u.pc, xyz_axis=0)
1016        assert 'xyz_axis should only be set' in str(exc.value)
1017
1018    def test_init_one_array_yz_fail(self):
1019        with pytest.raises(ValueError) as exc:
1020            CartesianRepresentation(x=[1, 2, 3, 4] * u.pc, y=[1, 2] * u.pc)
1021        assert exc.value.args[0] == ("x, y, and z are required to instantiate "
1022                                     "CartesianRepresentation")
1023
1024    def test_init_array_nocopy(self):
1025
1026        x = [8, 9, 10] * u.pc
1027        y = [5, 6, 7] * u.Mpc
1028        z = [2, 3, 4] * u.kpc
1029
1030        s1 = CartesianRepresentation(x=x, y=y, z=z, copy=False)
1031
1032        x[:] = [1, 2, 3] * u.kpc
1033        y[:] = [9, 9, 8] * u.kpc
1034        z[:] = [1, 2, 1] * u.kpc
1035
1036        assert_allclose_quantity(x, s1.x)
1037        assert_allclose_quantity(y, s1.y)
1038        assert_allclose_quantity(z, s1.z)
1039
1040    def test_xyz_is_view_if_possible(self):
1041        xyz = np.arange(1., 10.).reshape(3, 3)
1042        s1 = CartesianRepresentation(xyz, unit=u.kpc, copy=False)
1043        s1_xyz = s1.xyz
1044        assert s1_xyz.value[0, 0] == 1.
1045        xyz[0, 0] = 0.
1046        assert s1.x[0] == 0.
1047        assert s1_xyz.value[0, 0] == 0.
1048        # Not possible: we don't check that tuples are from the same array
1049        xyz = np.arange(1., 10.).reshape(3, 3)
1050        s2 = CartesianRepresentation(*xyz, unit=u.kpc, copy=False)
1051        s2_xyz = s2.xyz
1052        assert s2_xyz.value[0, 0] == 1.
1053        xyz[0, 0] = 0.
1054        assert s2.x[0] == 0.
1055        assert s2_xyz.value[0, 0] == 1.
1056
1057    def test_reprobj(self):
1058
1059        s1 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc)
1060
1061        s2 = CartesianRepresentation.from_representation(s1)
1062
1063        assert s2.x == 1 * u.kpc
1064        assert s2.y == 2 * u.kpc
1065        assert s2.z == 3 * u.kpc
1066
1067        s3 = CartesianRepresentation(s1)
1068
1069        assert representation_equal(s3, s1)
1070
1071    def test_broadcasting(self):
1072
1073        s1 = CartesianRepresentation(x=[1, 2] * u.kpc, y=[3, 4] * u.kpc, z=5 * u.kpc)
1074
1075        assert s1.x.unit == u.kpc
1076        assert s1.y.unit == u.kpc
1077        assert s1.z.unit == u.kpc
1078
1079        assert_allclose(s1.x.value, [1, 2])
1080        assert_allclose(s1.y.value, [3, 4])
1081        assert_allclose(s1.z.value, [5, 5])
1082
1083    def test_broadcasting_mismatch(self):
1084
1085        with pytest.raises(ValueError) as exc:
1086            s1 = CartesianRepresentation(x=[1, 2] * u.kpc, y=[3, 4] * u.kpc, z=[5, 6, 7] * u.kpc)
1087        assert exc.value.args[0] == "Input parameters x, y, and z cannot be broadcast"
1088
1089    def test_readonly(self):
1090
1091        s1 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc)
1092
1093        with pytest.raises(AttributeError):
1094            s1.x = 1. * u.kpc
1095
1096        with pytest.raises(AttributeError):
1097            s1.y = 1. * u.kpc
1098
1099        with pytest.raises(AttributeError):
1100            s1.z = 1. * u.kpc
1101
1102    def test_xyz(self):
1103
1104        s1 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc)
1105
1106        assert isinstance(s1.xyz, u.Quantity)
1107        assert s1.xyz.unit is u.kpc
1108
1109        assert_allclose(s1.xyz.value, [1, 2, 3])
1110
1111    def test_unit_mismatch(self):
1112
1113        q_len = u.Quantity([1], u.km)
1114        q_nonlen = u.Quantity([1], u.kg)
1115
1116        with pytest.raises(u.UnitsError) as exc:
1117            s1 = CartesianRepresentation(x=q_nonlen, y=q_len, z=q_len)
1118        assert exc.value.args[0] == "x, y, and z should have matching physical types"
1119
1120        with pytest.raises(u.UnitsError) as exc:
1121            s1 = CartesianRepresentation(x=q_len, y=q_nonlen, z=q_len)
1122        assert exc.value.args[0] == "x, y, and z should have matching physical types"
1123
1124        with pytest.raises(u.UnitsError) as exc:
1125            s1 = CartesianRepresentation(x=q_len, y=q_len, z=q_nonlen)
1126        assert exc.value.args[0] == "x, y, and z should have matching physical types"
1127
1128    def test_unit_non_length(self):
1129
1130        s1 = CartesianRepresentation(x=1 * u.kg, y=2 * u.kg, z=3 * u.kg)
1131
1132        s2 = CartesianRepresentation(x=1 * u.km / u.s, y=2 * u.km / u.s, z=3 * u.km / u.s)
1133
1134        banana = u.def_unit('banana')
1135        s3 = CartesianRepresentation(x=1 * banana, y=2 * banana, z=3 * banana)
1136
1137    def test_getitem(self):
1138
1139        s = CartesianRepresentation(x=np.arange(10) * u.m,
1140                                    y=-np.arange(10) * u.m,
1141                                    z=3 * u.km)
1142
1143        s_slc = s[2:8:2]
1144
1145        assert_allclose_quantity(s_slc.x, [2, 4, 6] * u.m)
1146        assert_allclose_quantity(s_slc.y, [-2, -4, -6] * u.m)
1147        assert_allclose_quantity(s_slc.z, [3, 3, 3] * u.km)
1148
1149    def test_getitem_scalar(self):
1150
1151        s = CartesianRepresentation(x=1 * u.m,
1152                                    y=-2 * u.m,
1153                                    z=3 * u.km)
1154
1155        with pytest.raises(TypeError):
1156            s_slc = s[0]
1157
1158    def test_transform(self):
1159
1160        ds1 = CartesianDifferential(d_x=[1, 2] * u.km / u.s,
1161                                    d_y=[3, 4] * u.km / u.s,
1162                                    d_z=[5, 6] * u.km / u.s)
1163        s1 = CartesianRepresentation(x=[1, 2] * u.kpc, y=[3, 4] * u.kpc,
1164                                     z=[5, 6] * u.kpc, differentials=ds1)
1165
1166        # transform representation & get comparison (thru CartesianRep)
1167        s2 = s1.transform(matrices["general"])
1168        ds2 =  s2.differentials["s"]
1169
1170        dexpected = CartesianDifferential.from_cartesian(
1171            ds1.to_cartesian(base=s1).transform(matrices["general"]), base=s2)
1172
1173        assert_allclose_quantity(ds2.d_x, dexpected.d_x)
1174        assert_allclose_quantity(ds2.d_y, dexpected.d_y)
1175        assert_allclose_quantity(ds2.d_z, dexpected.d_z)
1176
1177        # also explicitly calculate, since we can
1178        assert_allclose(s2.x.value, [1 * 1 + 2 * 3 + 3 * 5, 1 * 2 + 2 * 4 + 3 * 6])
1179        assert_allclose(s2.y.value, [4 * 1 + 5 * 3 + 6 * 5, 4 * 2 + 5 * 4 + 6 * 6])
1180        assert_allclose(s2.z.value, [7 * 1 + 8 * 3 + 9 * 5, 7 * 2 + 8 * 4 + 9 * 6])
1181        assert_allclose(ds2.d_x.value, [1 * 1 + 2 * 3 + 3 * 5, 1 * 2 + 2 * 4 + 3 * 6])
1182        assert_allclose(ds2.d_y.value, [4 * 1 + 5 * 3 + 6 * 5, 4 * 2 + 5 * 4 + 6 * 6])
1183        assert_allclose(ds2.d_z.value, [7 * 1 + 8 * 3 + 9 * 5, 7 * 2 + 8 * 4 + 9 * 6])
1184
1185        assert s2.x.unit is u.kpc
1186        assert s2.y.unit is u.kpc
1187        assert s2.z.unit is u.kpc
1188        assert ds2.d_x.unit == u.km / u.s
1189        assert ds2.d_y.unit == u.km / u.s
1190        assert ds2.d_z.unit == u.km / u.s
1191
1192
1193class TestCylindricalRepresentation:
1194
1195    def test_name(self):
1196        assert CylindricalRepresentation.get_name() == 'cylindrical'
1197        assert CylindricalRepresentation.get_name() in REPRESENTATION_CLASSES
1198
1199    def test_empty_init(self):
1200        with pytest.raises(TypeError) as exc:
1201            s = CylindricalRepresentation()
1202
1203    def test_init_quantity(self):
1204
1205        s1 = CylindricalRepresentation(rho=1 * u.kpc, phi=2 * u.deg, z=3 * u.kpc)
1206
1207        assert s1.rho.unit is u.kpc
1208        assert s1.phi.unit is u.deg
1209        assert s1.z.unit is u.kpc
1210
1211        assert_allclose(s1.rho.value, 1)
1212        assert_allclose(s1.phi.value, 2)
1213        assert_allclose(s1.z.value, 3)
1214
1215    def test_init_array(self):
1216
1217        s1 = CylindricalRepresentation(rho=[1, 2, 3] * u.pc,
1218                                       phi=[2, 3, 4] * u.deg,
1219                                       z=[3, 4, 5] * u.kpc)
1220
1221        assert s1.rho.unit is u.pc
1222        assert s1.phi.unit is u.deg
1223        assert s1.z.unit is u.kpc
1224
1225        assert_allclose(s1.rho.value, [1, 2, 3])
1226        assert_allclose(s1.phi.value, [2, 3, 4])
1227        assert_allclose(s1.z.value, [3, 4, 5])
1228
1229    def test_init_array_nocopy(self):
1230
1231        rho = [8, 9, 10] * u.pc
1232        phi = [5, 6, 7] * u.deg
1233        z = [2, 3, 4] * u.kpc
1234
1235        s1 = CylindricalRepresentation(rho=rho, phi=phi, z=z, copy=False)
1236
1237        rho[:] = [9, 2, 3] * u.kpc
1238        phi[:] = [1, 2, 3] * u.arcmin
1239        z[:] = [-2, 3, 8] * u.kpc
1240
1241        assert_allclose_quantity(rho, s1.rho)
1242        assert_allclose_quantity(phi, s1.phi)
1243        assert_allclose_quantity(z, s1.z)
1244
1245    def test_reprobj(self):
1246
1247        s1 = CylindricalRepresentation(rho=1 * u.kpc, phi=2 * u.deg, z=3 * u.kpc)
1248
1249        s2 = CylindricalRepresentation.from_representation(s1)
1250
1251        assert s2.rho == 1 * u.kpc
1252        assert s2.phi == 2 * u.deg
1253        assert s2.z == 3 * u.kpc
1254
1255        s3 = CylindricalRepresentation(s1)
1256
1257        assert representation_equal(s3, s1)
1258
1259    def test_broadcasting(self):
1260
1261        s1 = CylindricalRepresentation(rho=[1, 2] * u.kpc, phi=[3, 4] * u.deg, z=5 * u.kpc)
1262
1263        assert s1.rho.unit == u.kpc
1264        assert s1.phi.unit == u.deg
1265        assert s1.z.unit == u.kpc
1266
1267        assert_allclose(s1.rho.value, [1, 2])
1268        assert_allclose(s1.phi.value, [3, 4])
1269        assert_allclose(s1.z.value, [5, 5])
1270
1271    def test_broadcasting_mismatch(self):
1272
1273        with pytest.raises(ValueError) as exc:
1274            s1 = CylindricalRepresentation(rho=[1, 2] * u.kpc, phi=[3, 4] * u.deg, z=[5, 6, 7] * u.kpc)
1275        assert exc.value.args[0] == "Input parameters rho, phi, and z cannot be broadcast"
1276
1277    def test_readonly(self):
1278
1279        s1 = CylindricalRepresentation(rho=1 * u.kpc,
1280                                       phi=20 * u.deg,
1281                                       z=3 * u.kpc)
1282
1283        with pytest.raises(AttributeError):
1284            s1.rho = 1. * u.kpc
1285
1286        with pytest.raises(AttributeError):
1287            s1.phi = 20 * u.deg
1288
1289        with pytest.raises(AttributeError):
1290            s1.z = 1. * u.kpc
1291
1292    def unit_mismatch(self):
1293
1294        q_len = u.Quantity([1], u.kpc)
1295        q_nonlen = u.Quantity([1], u.kg)
1296
1297        with pytest.raises(u.UnitsError) as exc:
1298            s1 = CylindricalRepresentation(rho=q_nonlen, phi=10 * u.deg, z=q_len)
1299        assert exc.value.args[0] == "rho and z should have matching physical types"
1300
1301        with pytest.raises(u.UnitsError) as exc:
1302            s1 = CylindricalRepresentation(rho=q_len, phi=10 * u.deg, z=q_nonlen)
1303        assert exc.value.args[0] == "rho and z should have matching physical types"
1304
1305    def test_getitem(self):
1306
1307        s = CylindricalRepresentation(rho=np.arange(10) * u.pc,
1308                                      phi=-np.arange(10) * u.deg,
1309                                      z=1 * u.kpc)
1310
1311        s_slc = s[2:8:2]
1312
1313        assert_allclose_quantity(s_slc.rho, [2, 4, 6] * u.pc)
1314        assert_allclose_quantity(s_slc.phi, [-2, -4, -6] * u.deg)
1315        assert_allclose_quantity(s_slc.z, [1, 1, 1] * u.kpc)
1316
1317    def test_getitem_scalar(self):
1318
1319        s = CylindricalRepresentation(rho=1 * u.pc,
1320                                      phi=-2 * u.deg,
1321                                      z=3 * u.kpc)
1322
1323        with pytest.raises(TypeError):
1324            s_slc = s[0]
1325
1326    def test_transform(self):
1327
1328        s1 = CylindricalRepresentation(phi=[1, 2] * u.deg, z=[3, 4] * u.pc,
1329                                       rho=[5, 6] * u.kpc)
1330
1331        s2 = s1.transform(matrices["rotation"])
1332
1333        assert_allclose_quantity(s2.phi, s1.phi + 10 * u.deg)
1334        assert_allclose_quantity(s2.z, s1.z)
1335        assert_allclose_quantity(s2.rho, s1.rho)
1336
1337        assert s2.phi.unit is u.rad
1338        assert s2.z.unit is u.kpc
1339        assert s2.rho.unit is u.kpc
1340
1341        # now with a non rotation matrix
1342        s3 = s1.transform(matrices["general"])
1343        expected = (s1.to_cartesian().transform(matrices["general"])
1344                    ).represent_as(CylindricalRepresentation)
1345
1346        assert_allclose_quantity(s3.phi, expected.phi)
1347        assert_allclose_quantity(s3.z, expected.z)
1348        assert_allclose_quantity(s3.rho, expected.rho)
1349
1350
1351class TestUnitSphericalCosLatDifferential:
1352
1353    @pytest.mark.parametrize("matrix", list(matrices.values()))
1354    def test_transform(self, matrix):
1355        """Test ``.transform()`` on rotation and general matrices."""
1356        # set up representation
1357        ds1 = UnitSphericalCosLatDifferential(d_lon_coslat=[1, 2] * u.mas / u.yr,
1358                                              d_lat=[3, 4] * u.mas / u.yr,)
1359        s1 = UnitSphericalRepresentation(lon=[1, 2] * u.deg, lat=[3, 4] * u.deg)
1360
1361        # transform representation & get comparison (thru CartesianRep)
1362        s2 = s1.transform(matrix)
1363        ds2 = ds1.transform(matrix, s1, s2)
1364
1365        dexpected = UnitSphericalCosLatDifferential.from_cartesian(
1366            ds1.to_cartesian(base=s1).transform(matrix), base=s2)
1367
1368        assert_allclose_quantity(ds2.d_lon_coslat, dexpected.d_lon_coslat)
1369        assert_allclose_quantity(ds2.d_lat, dexpected.d_lat)
1370
1371
1372def test_cartesian_spherical_roundtrip():
1373
1374    s1 = CartesianRepresentation(x=[1, 2000.] * u.kpc,
1375                                 y=[3000., 4.] * u.pc,
1376                                 z=[5., 6000.] * u.pc)
1377
1378    s2 = SphericalRepresentation.from_representation(s1)
1379
1380    s3 = CartesianRepresentation.from_representation(s2)
1381
1382    s4 = SphericalRepresentation.from_representation(s3)
1383
1384    assert_allclose_quantity(s1.x, s3.x)
1385    assert_allclose_quantity(s1.y, s3.y)
1386    assert_allclose_quantity(s1.z, s3.z)
1387
1388    assert_allclose_quantity(s2.lon, s4.lon)
1389    assert_allclose_quantity(s2.lat, s4.lat)
1390    assert_allclose_quantity(s2.distance, s4.distance)
1391
1392
1393def test_cartesian_setting_with_other():
1394
1395    s1 = CartesianRepresentation(x=[1, 2000.] * u.kpc,
1396                                 y=[3000., 4.] * u.pc,
1397                                 z=[5., 6000.] * u.pc)
1398    s1[0] = SphericalRepresentation(0.*u.deg, 0.*u.deg, 1*u.kpc)
1399    assert_allclose_quantity(s1.x, [1., 2000.] * u.kpc)
1400    assert_allclose_quantity(s1.y, [0., 4.] * u.pc)
1401    assert_allclose_quantity(s1.z, [0., 6000.] * u.pc)
1402
1403    with pytest.raises(ValueError, match='loss of information'):
1404        s1[1] = UnitSphericalRepresentation(0.*u.deg, 10.*u.deg)
1405
1406
1407def test_cartesian_physics_spherical_roundtrip():
1408
1409    s1 = CartesianRepresentation(x=[1, 2000.] * u.kpc,
1410                                 y=[3000., 4.] * u.pc,
1411                                 z=[5., 6000.] * u.pc)
1412
1413    s2 = PhysicsSphericalRepresentation.from_representation(s1)
1414
1415    s3 = CartesianRepresentation.from_representation(s2)
1416
1417    s4 = PhysicsSphericalRepresentation.from_representation(s3)
1418
1419    assert_allclose_quantity(s1.x, s3.x)
1420    assert_allclose_quantity(s1.y, s3.y)
1421    assert_allclose_quantity(s1.z, s3.z)
1422
1423    assert_allclose_quantity(s2.phi, s4.phi)
1424    assert_allclose_quantity(s2.theta, s4.theta)
1425    assert_allclose_quantity(s2.r, s4.r)
1426
1427
1428def test_spherical_physics_spherical_roundtrip():
1429
1430    s1 = SphericalRepresentation(lon=3 * u.deg, lat=4 * u.deg, distance=3 * u.kpc)
1431
1432    s2 = PhysicsSphericalRepresentation.from_representation(s1)
1433
1434    s3 = SphericalRepresentation.from_representation(s2)
1435
1436    s4 = PhysicsSphericalRepresentation.from_representation(s3)
1437
1438    assert_allclose_quantity(s1.lon, s3.lon)
1439    assert_allclose_quantity(s1.lat, s3.lat)
1440    assert_allclose_quantity(s1.distance, s3.distance)
1441
1442    assert_allclose_quantity(s2.phi, s4.phi)
1443    assert_allclose_quantity(s2.theta, s4.theta)
1444    assert_allclose_quantity(s2.r, s4.r)
1445
1446    assert_allclose_quantity(s1.lon, s4.phi)
1447    assert_allclose_quantity(s1.lat, 90. * u.deg - s4.theta)
1448    assert_allclose_quantity(s1.distance, s4.r)
1449
1450
1451def test_cartesian_cylindrical_roundtrip():
1452
1453    s1 = CartesianRepresentation(x=np.array([1., 2000.]) * u.kpc,
1454                                 y=np.array([3000., 4.]) * u.pc,
1455                                 z=np.array([5., 600.]) * u.cm)
1456
1457    s2 = CylindricalRepresentation.from_representation(s1)
1458
1459    s3 = CartesianRepresentation.from_representation(s2)
1460
1461    s4 = CylindricalRepresentation.from_representation(s3)
1462
1463    assert_allclose_quantity(s1.x, s3.x)
1464    assert_allclose_quantity(s1.y, s3.y)
1465    assert_allclose_quantity(s1.z, s3.z)
1466
1467    assert_allclose_quantity(s2.rho, s4.rho)
1468    assert_allclose_quantity(s2.phi, s4.phi)
1469    assert_allclose_quantity(s2.z, s4.z)
1470
1471
1472def test_unit_spherical_roundtrip():
1473
1474    s1 = UnitSphericalRepresentation(lon=[10., 30.] * u.deg,
1475                                     lat=[5., 6.] * u.arcmin)
1476
1477    s2 = CartesianRepresentation.from_representation(s1)
1478
1479    s3 = SphericalRepresentation.from_representation(s2)
1480
1481    s4 = UnitSphericalRepresentation.from_representation(s3)
1482
1483    assert_allclose_quantity(s1.lon, s4.lon)
1484    assert_allclose_quantity(s1.lat, s4.lat)
1485
1486
1487def test_no_unnecessary_copies():
1488
1489    s1 = UnitSphericalRepresentation(lon=[10., 30.] * u.deg,
1490                                     lat=[5., 6.] * u.arcmin)
1491    s2 = s1.represent_as(UnitSphericalRepresentation)
1492    assert s2 is s1
1493    assert np.may_share_memory(s1.lon, s2.lon)
1494    assert np.may_share_memory(s1.lat, s2.lat)
1495    s3 = s1.represent_as(SphericalRepresentation)
1496    assert np.may_share_memory(s1.lon, s3.lon)
1497    assert np.may_share_memory(s1.lat, s3.lat)
1498    s4 = s1.represent_as(CartesianRepresentation)
1499    s5 = s4.represent_as(CylindricalRepresentation)
1500    assert np.may_share_memory(s5.z, s4.z)
1501
1502
1503def test_representation_repr():
1504    r1 = SphericalRepresentation(lon=1 * u.deg, lat=2.5 * u.deg, distance=1 * u.kpc)
1505    assert repr(r1) == ('<SphericalRepresentation (lon, lat, distance) in (deg, deg, kpc)\n'
1506                        '    (1., 2.5, 1.)>')
1507
1508    r2 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc)
1509    assert repr(r2) == ('<CartesianRepresentation (x, y, z) in kpc\n'
1510                        '    (1., 2., 3.)>')
1511
1512    r3 = CartesianRepresentation(x=[1, 2, 3] * u.kpc, y=4 * u.kpc, z=[9, 10, 11] * u.kpc)
1513    assert repr(r3) == ('<CartesianRepresentation (x, y, z) in kpc\n'
1514                        '    [(1., 4.,  9.), (2., 4., 10.), (3., 4., 11.)]>')
1515
1516
1517def test_representation_repr_multi_d():
1518    """Regression test for #5889."""
1519    cr = CartesianRepresentation(np.arange(27).reshape(3, 3, 3), unit='m')
1520    assert repr(cr) == (
1521        '<CartesianRepresentation (x, y, z) in m\n'
1522        '    [[(0.,  9., 18.), (1., 10., 19.), (2., 11., 20.)],\n'
1523        '     [(3., 12., 21.), (4., 13., 22.), (5., 14., 23.)],\n'
1524        '     [(6., 15., 24.), (7., 16., 25.), (8., 17., 26.)]]>')
1525    # This was broken before.
1526    assert repr(cr.T) == (
1527        '<CartesianRepresentation (x, y, z) in m\n'
1528        '    [[(0.,  9., 18.), (3., 12., 21.), (6., 15., 24.)],\n'
1529        '     [(1., 10., 19.), (4., 13., 22.), (7., 16., 25.)],\n'
1530        '     [(2., 11., 20.), (5., 14., 23.), (8., 17., 26.)]]>')
1531
1532
1533def test_representation_str():
1534    r1 = SphericalRepresentation(lon=1 * u.deg, lat=2.5 * u.deg, distance=1 * u.kpc)
1535    assert str(r1) == '(1., 2.5, 1.) (deg, deg, kpc)'
1536
1537    r2 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc)
1538    assert str(r2) == '(1., 2., 3.) kpc'
1539
1540    r3 = CartesianRepresentation(x=[1, 2, 3] * u.kpc, y=4 * u.kpc, z=[9, 10, 11] * u.kpc)
1541    assert str(r3) == '[(1., 4.,  9.), (2., 4., 10.), (3., 4., 11.)] kpc'
1542
1543
1544def test_representation_str_multi_d():
1545    """Regression test for #5889."""
1546    cr = CartesianRepresentation(np.arange(27).reshape(3, 3, 3), unit='m')
1547    assert str(cr) == (
1548        '[[(0.,  9., 18.), (1., 10., 19.), (2., 11., 20.)],\n'
1549        ' [(3., 12., 21.), (4., 13., 22.), (5., 14., 23.)],\n'
1550        ' [(6., 15., 24.), (7., 16., 25.), (8., 17., 26.)]] m')
1551    # This was broken before.
1552    assert str(cr.T) == (
1553        '[[(0.,  9., 18.), (3., 12., 21.), (6., 15., 24.)],\n'
1554        ' [(1., 10., 19.), (4., 13., 22.), (7., 16., 25.)],\n'
1555        ' [(2., 11., 20.), (5., 14., 23.), (8., 17., 26.)]] m')
1556
1557
1558def test_subclass_representation():
1559    from astropy.coordinates.builtin_frames import ICRS
1560
1561    class Longitude180(Longitude):
1562        def __new__(cls, angle, unit=None, wrap_angle=180 * u.deg, **kwargs):
1563            self = super().__new__(cls, angle, unit=unit, wrap_angle=wrap_angle,
1564                                   **kwargs)
1565            return self
1566
1567    class SphericalWrap180Representation(SphericalRepresentation):
1568        attr_classes = {'lon': Longitude180,
1569                        'lat': Latitude,
1570                        'distance': u.Quantity}
1571
1572    class ICRSWrap180(ICRS):
1573        frame_specific_representation_info = ICRS._frame_specific_representation_info.copy()
1574        frame_specific_representation_info[SphericalWrap180Representation] = \
1575            frame_specific_representation_info[SphericalRepresentation]
1576        default_representation = SphericalWrap180Representation
1577
1578    c = ICRSWrap180(ra=-1 * u.deg, dec=-2 * u.deg, distance=1 * u.m)
1579    assert c.ra.value == -1
1580    assert c.ra.unit is u.deg
1581    assert c.dec.value == -2
1582    assert c.dec.unit is u.deg
1583
1584
1585def test_minimal_subclass():
1586    # Basically to check what we document works;
1587    # see doc/coordinates/representations.rst
1588    class LogDRepresentation(BaseRepresentation):
1589        attr_classes = {'lon': Longitude,
1590                        'lat': Latitude,
1591                        'logd': u.Dex}
1592
1593        def to_cartesian(self):
1594            d = self.logd.physical
1595            x = d * np.cos(self.lat) * np.cos(self.lon)
1596            y = d * np.cos(self.lat) * np.sin(self.lon)
1597            z = d * np.sin(self.lat)
1598            return CartesianRepresentation(x=x, y=y, z=z, copy=False)
1599
1600        @classmethod
1601        def from_cartesian(cls, cart):
1602            s = np.hypot(cart.x, cart.y)
1603            r = np.hypot(s, cart.z)
1604            lon = np.arctan2(cart.y, cart.x)
1605            lat = np.arctan2(cart.z, s)
1606            return cls(lon=lon, lat=lat, logd=u.Dex(r), copy=False)
1607
1608    ld1 = LogDRepresentation(90.*u.deg, 0.*u.deg, 1.*u.dex(u.kpc))
1609    ld2 = LogDRepresentation(lon=90.*u.deg, lat=0.*u.deg, logd=1.*u.dex(u.kpc))
1610    assert np.all(ld1.lon == ld2.lon)
1611    assert np.all(ld1.lat == ld2.lat)
1612    assert np.all(ld1.logd == ld2.logd)
1613    c = ld1.to_cartesian()
1614    assert_allclose_quantity(c.xyz, [0., 10., 0.] * u.kpc, atol=1.*u.npc)
1615    ld3 = LogDRepresentation.from_cartesian(c)
1616    assert np.all(ld3.lon == ld2.lon)
1617    assert np.all(ld3.lat == ld2.lat)
1618    assert np.all(ld3.logd == ld2.logd)
1619    s = ld1.represent_as(SphericalRepresentation)
1620    assert_allclose_quantity(s.lon, ld1.lon)
1621    assert_allclose_quantity(s.distance, 10.*u.kpc)
1622    assert_allclose_quantity(s.lat, ld1.lat)
1623
1624    with pytest.raises(TypeError):
1625        LogDRepresentation(0.*u.deg, 1.*u.deg)
1626    with pytest.raises(TypeError):
1627        LogDRepresentation(0.*u.deg, 1.*u.deg, 1.*u.dex(u.kpc), lon=1.*u.deg)
1628    with pytest.raises(TypeError):
1629        LogDRepresentation(0.*u.deg, 1.*u.deg, 1.*u.dex(u.kpc), True, False)
1630    with pytest.raises(TypeError):
1631        LogDRepresentation(0.*u.deg, 1.*u.deg, 1.*u.dex(u.kpc), foo='bar')
1632
1633    # if we define it a second time, even the qualnames are the same,
1634    # so we raise
1635    with pytest.raises(ValueError):
1636        class LogDRepresentation(BaseRepresentation):
1637            attr_classes = {'lon': Longitude,
1638                            'lat': Latitude,
1639                            'logr': u.Dex}
1640
1641
1642def test_duplicate_warning():
1643    from astropy.coordinates.representation import DUPLICATE_REPRESENTATIONS
1644    from astropy.coordinates.representation import REPRESENTATION_CLASSES
1645
1646    with pytest.warns(DuplicateRepresentationWarning):
1647        class UnitSphericalRepresentation(BaseRepresentation):
1648            attr_classes = {'lon': Longitude,
1649                            'lat': Latitude}
1650
1651    assert 'unitspherical' in DUPLICATE_REPRESENTATIONS
1652    assert 'unitspherical' not in REPRESENTATION_CLASSES
1653    assert 'astropy.coordinates.representation.UnitSphericalRepresentation' in REPRESENTATION_CLASSES
1654    assert __name__ + '.test_duplicate_warning.<locals>.UnitSphericalRepresentation' in REPRESENTATION_CLASSES
1655
1656
1657class TestCartesianRepresentationWithDifferential:
1658
1659    def test_init_differential(self):
1660
1661        diff = CartesianDifferential(d_x=1 * u.km/u.s,
1662                                     d_y=2 * u.km/u.s,
1663                                     d_z=3 * u.km/u.s)
1664
1665        # Check that a single differential gets turned into a 1-item dict.
1666        s1 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc,
1667                                     differentials=diff)
1668
1669        assert s1.x.unit is u.kpc
1670        assert s1.y.unit is u.kpc
1671        assert s1.z.unit is u.kpc
1672        assert len(s1.differentials) == 1
1673        assert s1.differentials['s'] is diff
1674
1675        # can also pass in an explicit dictionary
1676        s1 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc,
1677                                     differentials={'s': diff})
1678        assert len(s1.differentials) == 1
1679        assert s1.differentials['s'] is diff
1680
1681        # using the wrong key will cause it to fail
1682        with pytest.raises(ValueError):
1683            s1 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc,
1684                                         differentials={'1 / s2': diff})
1685
1686        # make sure other kwargs are handled properly
1687        s1 = CartesianRepresentation(x=1, y=2, z=3,
1688                                     differentials=diff, copy=False, unit=u.kpc)
1689        assert len(s1.differentials) == 1
1690        assert s1.differentials['s'] is diff
1691
1692        with pytest.raises(TypeError):  # invalid type passed to differentials
1693            CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc,
1694                                    differentials='garmonbozia')
1695
1696        # And that one can add it to another representation.
1697        s1 = CartesianRepresentation(
1698            CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc),
1699            differentials=diff)
1700        assert len(s1.differentials) == 1
1701        assert s1.differentials['s'] is diff
1702
1703        # make sure differentials can't accept differentials
1704        with pytest.raises(TypeError):
1705            CartesianDifferential(d_x=1 * u.km/u.s, d_y=2 * u.km/u.s,
1706                                  d_z=3 * u.km/u.s, differentials=diff)
1707
1708    def test_init_differential_compatible(self):
1709        # TODO: more extensive checking of this
1710
1711        # should fail - representation and differential not compatible
1712        diff = SphericalDifferential(d_lon=1 * u.mas/u.yr,
1713                                     d_lat=2 * u.mas/u.yr,
1714                                     d_distance=3 * u.km/u.s)
1715        with pytest.raises(TypeError):
1716            CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc,
1717                                    differentials=diff)
1718
1719        # should succeed - representation and differential are compatible
1720        diff = SphericalCosLatDifferential(d_lon_coslat=1 * u.mas/u.yr,
1721                                           d_lat=2 * u.mas/u.yr,
1722                                           d_distance=3 * u.km/u.s)
1723
1724        r1 = SphericalRepresentation(lon=15*u.deg, lat=21*u.deg,
1725                                     distance=1*u.pc,
1726                                     differentials=diff)
1727
1728    def test_init_differential_multiple_equivalent_keys(self):
1729        d1 = CartesianDifferential(*[1, 2, 3] * u.km/u.s)
1730        d2 = CartesianDifferential(*[4, 5, 6] * u.km/u.s)
1731
1732        # verify that the check against expected_unit validates against passing
1733        # in two different but equivalent keys
1734        with pytest.raises(ValueError):
1735            r1 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc,
1736                                         differentials={'s': d1, 'yr': d2})
1737
1738    def test_init_array_broadcasting(self):
1739
1740        arr1 = np.arange(8).reshape(4, 2) * u.km/u.s
1741        diff = CartesianDifferential(d_x=arr1, d_y=arr1, d_z=arr1)
1742
1743        # shapes aren't compatible
1744        arr2 = np.arange(27).reshape(3, 9) * u.kpc
1745        with pytest.raises(ValueError):
1746            rep = CartesianRepresentation(x=arr2, y=arr2, z=arr2,
1747                                          differentials=diff)
1748
1749        arr2 = np.arange(8).reshape(4, 2) * u.kpc
1750        rep = CartesianRepresentation(x=arr2, y=arr2, z=arr2,
1751                                      differentials=diff)
1752
1753        assert rep.x.unit is u.kpc
1754        assert rep.y.unit is u.kpc
1755        assert rep.z.unit is u.kpc
1756        assert len(rep.differentials) == 1
1757        assert rep.differentials['s'] is diff
1758
1759        assert rep.xyz.shape == rep.differentials['s'].d_xyz.shape
1760
1761    def test_reprobj(self):
1762
1763        # should succeed - representation and differential are compatible
1764        diff = SphericalCosLatDifferential(d_lon_coslat=1 * u.mas/u.yr,
1765                                           d_lat=2 * u.mas/u.yr,
1766                                           d_distance=3 * u.km/u.s)
1767
1768        r1 = SphericalRepresentation(lon=15*u.deg, lat=21*u.deg,
1769                                     distance=1*u.pc,
1770                                     differentials=diff)
1771
1772        r2 = CartesianRepresentation.from_representation(r1)
1773        assert r2.get_name() == 'cartesian'
1774        assert not r2.differentials
1775
1776        r3 = SphericalRepresentation(r1)
1777        assert r3.differentials
1778        assert representation_equal(r3, r1)
1779
1780    def test_readonly(self):
1781
1782        s1 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc)
1783
1784        with pytest.raises(AttributeError):  # attribute is not settable
1785            s1.differentials = 'thing'
1786
1787    def test_represent_as(self):
1788
1789        diff = CartesianDifferential(d_x=1 * u.km/u.s,
1790                                     d_y=2 * u.km/u.s,
1791                                     d_z=3 * u.km/u.s)
1792        rep1 = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc,
1793                                       differentials=diff)
1794
1795        # Only change the representation, drop the differential
1796        new_rep = rep1.represent_as(SphericalRepresentation)
1797        assert new_rep.get_name() == 'spherical'
1798        assert not new_rep.differentials  # dropped
1799
1800        # Pass in separate classes for representation, differential
1801        new_rep = rep1.represent_as(SphericalRepresentation,
1802                                    SphericalCosLatDifferential)
1803        assert new_rep.get_name() == 'spherical'
1804        assert new_rep.differentials['s'].get_name() == 'sphericalcoslat'
1805
1806        # Pass in a dictionary for the differential classes
1807        new_rep = rep1.represent_as(SphericalRepresentation,
1808                                    {'s': SphericalCosLatDifferential})
1809        assert new_rep.get_name() == 'spherical'
1810        assert new_rep.differentials['s'].get_name() == 'sphericalcoslat'
1811
1812        # make sure represent_as() passes through the differentials
1813        for name in REPRESENTATION_CLASSES:
1814            if name == 'radial':
1815                # TODO: Converting a CartesianDifferential to a
1816                #       RadialDifferential fails, even on `main`
1817                continue
1818            elif name.endswith("geodetic"):
1819                # TODO: Geodetic representations do not have differentials yet
1820                continue
1821            new_rep = rep1.represent_as(REPRESENTATION_CLASSES[name],
1822                                        DIFFERENTIAL_CLASSES[name])
1823            assert new_rep.get_name() == name
1824            assert len(new_rep.differentials) == 1
1825            assert new_rep.differentials['s'].get_name() == name
1826
1827        with pytest.raises(ValueError) as excinfo:
1828            rep1.represent_as('name')
1829        assert 'use frame object' in str(excinfo.value)
1830
1831    @pytest.mark.parametrize('sph_diff,usph_diff', [
1832        (SphericalDifferential, UnitSphericalDifferential),
1833        (SphericalCosLatDifferential, UnitSphericalCosLatDifferential)])
1834    def test_represent_as_unit_spherical_with_diff(self, sph_diff, usph_diff):
1835        """Test that differential angles are correctly reduced."""
1836        diff = CartesianDifferential(d_x=1 * u.km/u.s,
1837                                     d_y=2 * u.km/u.s,
1838                                     d_z=3 * u.km/u.s)
1839        rep = CartesianRepresentation(x=1 * u.kpc, y=2 * u.kpc, z=3 * u.kpc,
1840                                      differentials=diff)
1841        sph = rep.represent_as(SphericalRepresentation, sph_diff)
1842        usph = rep.represent_as(UnitSphericalRepresentation, usph_diff)
1843        assert components_equal(usph, sph.represent_as(UnitSphericalRepresentation))
1844        assert components_equal(usph.differentials['s'],
1845                                sph.differentials['s'].represent_as(usph_diff))
1846        # Just to be sure components_equal and the represent_as work as advertised,
1847        # a sanity check: d_lat is always defined and should be the same.
1848        assert_array_equal(sph.differentials['s'].d_lat,
1849                           usph.differentials['s'].d_lat)
1850
1851    def test_getitem(self):
1852
1853        d = CartesianDifferential(d_x=np.arange(10) * u.m/u.s,
1854                                  d_y=-np.arange(10) * u.m/u.s,
1855                                  d_z=1. * u.m/u.s)
1856        s = CartesianRepresentation(x=np.arange(10) * u.m,
1857                                    y=-np.arange(10) * u.m,
1858                                    z=3 * u.km,
1859                                    differentials=d)
1860
1861        s_slc = s[2:8:2]
1862        s_dif = s_slc.differentials['s']
1863
1864        assert_allclose_quantity(s_slc.x, [2, 4, 6] * u.m)
1865        assert_allclose_quantity(s_slc.y, [-2, -4, -6] * u.m)
1866        assert_allclose_quantity(s_slc.z, [3, 3, 3] * u.km)
1867
1868        assert_allclose_quantity(s_dif.d_x, [2, 4, 6] * u.m/u.s)
1869        assert_allclose_quantity(s_dif.d_y, [-2, -4, -6] * u.m/u.s)
1870        assert_allclose_quantity(s_dif.d_z, [1, 1, 1] * u.m/u.s)
1871
1872    def test_setitem(self):
1873        d = CartesianDifferential(d_x=np.arange(5) * u.m/u.s,
1874                                  d_y=-np.arange(5) * u.m/u.s,
1875                                  d_z=1. * u.m/u.s)
1876        s = CartesianRepresentation(x=np.arange(5) * u.m,
1877                                    y=-np.arange(5) * u.m,
1878                                    z=3 * u.km,
1879                                    differentials=d)
1880        s[:2] = s[2]
1881        assert_array_equal(s.x, [2, 2, 2, 3, 4] * u.m)
1882        assert_array_equal(s.y, [-2, -2, -2, -3, -4] * u.m)
1883        assert_array_equal(s.z, [3, 3, 3, 3, 3] * u.km)
1884        assert_array_equal(s.differentials['s'].d_x,
1885                           [2, 2, 2, 3, 4] * u.m/u.s)
1886        assert_array_equal(s.differentials['s'].d_y,
1887                           [-2, -2, -2, -3, -4] * u.m/u.s)
1888        assert_array_equal(s.differentials['s'].d_z,
1889                           [1, 1, 1, 1, 1] * u.m/u.s)
1890
1891        s2 = s.represent_as(SphericalRepresentation,
1892                            SphericalDifferential)
1893
1894        s[0] = s2[3]
1895        assert_allclose_quantity(s.x, [3, 2, 2, 3, 4] * u.m)
1896        assert_allclose_quantity(s.y, [-3, -2, -2, -3, -4] * u.m)
1897        assert_allclose_quantity(s.z, [3, 3, 3, 3, 3] * u.km)
1898        assert_allclose_quantity(s.differentials['s'].d_x,
1899                                 [3, 2, 2, 3, 4] * u.m/u.s)
1900        assert_allclose_quantity(s.differentials['s'].d_y,
1901                                 [-3, -2, -2, -3, -4] * u.m/u.s)
1902        assert_allclose_quantity(s.differentials['s'].d_z,
1903                                 [1, 1, 1, 1, 1] * u.m/u.s)
1904
1905        s3 = CartesianRepresentation(s.xyz, differentials={
1906            's': d,
1907            's2': CartesianDifferential(np.ones((3, 5))*u.m/u.s**2)})
1908        with pytest.raises(ValueError, match='same differentials'):
1909            s[0] = s3[2]
1910
1911        s4 = SphericalRepresentation(0.*u.deg, 0.*u.deg, 1.*u.kpc,
1912                                     differentials=RadialDifferential(
1913                                         10*u.km/u.s))
1914        with pytest.raises(ValueError, match='loss of information'):
1915            s[0] = s4
1916
1917    def test_transform(self):
1918        d1 = CartesianDifferential(d_x=[1, 2] * u.km/u.s,
1919                                   d_y=[3, 4] * u.km/u.s,
1920                                   d_z=[5, 6] * u.km/u.s)
1921        r1 = CartesianRepresentation(x=[1, 2] * u.kpc,
1922                                     y=[3, 4] * u.kpc,
1923                                     z=[5, 6] * u.kpc,
1924                                     differentials=d1)
1925
1926        r2 = r1.transform(matrices["general"])
1927        d2 = r2.differentials['s']
1928        assert_allclose_quantity(d2.d_x, [22., 28]*u.km/u.s)
1929        assert_allclose_quantity(d2.d_y, [49, 64]*u.km/u.s)
1930        assert_allclose_quantity(d2.d_z, [76, 100.]*u.km/u.s)
1931
1932    def test_with_differentials(self):
1933        # make sure with_differential correctly creates a new copy with the same
1934        # differential
1935        cr = CartesianRepresentation([1, 2, 3]*u.kpc)
1936        diff = CartesianDifferential([.1, .2, .3]*u.km/u.s)
1937        cr2 = cr.with_differentials(diff)
1938        assert cr.differentials != cr2.differentials
1939        assert cr2.differentials['s'] is diff
1940
1941        # make sure it works even if a differential is present already
1942        diff2 = CartesianDifferential([.1, .2, .3]*u.m/u.s)
1943        cr3 = CartesianRepresentation([1, 2, 3]*u.kpc, differentials=diff)
1944        cr4 = cr3.with_differentials(diff2)
1945        assert cr4.differentials['s'] != cr3.differentials['s']
1946        assert cr4.differentials['s'] == diff2
1947
1948        # also ensure a *scalar* differential will works
1949        cr5 = cr.with_differentials(diff)
1950        assert len(cr5.differentials) == 1
1951        assert cr5.differentials['s'] == diff
1952
1953        # make sure we don't update the original representation's dict
1954        d1 = CartesianDifferential(*np.random.random((3, 5)), unit=u.km/u.s)
1955        d2 = CartesianDifferential(*np.random.random((3, 5)), unit=u.km/u.s**2)
1956        r1 = CartesianRepresentation(*np.random.random((3, 5)), unit=u.pc,
1957                                     differentials=d1)
1958
1959        r2 = r1.with_differentials(d2)
1960        assert r1.differentials['s'] is r2.differentials['s']
1961        assert 's2' not in r1.differentials
1962        assert 's2' in r2.differentials
1963
1964
1965def test_repr_with_differentials():
1966    diff = CartesianDifferential([.1, .2, .3]*u.km/u.s)
1967    cr = CartesianRepresentation([1, 2, 3]*u.kpc, differentials=diff)
1968    assert "has differentials w.r.t.: 's'" in repr(cr)
1969
1970
1971def test_to_cartesian():
1972    """
1973    Test that to_cartesian drops the differential.
1974    """
1975    sd = SphericalDifferential(d_lat=1*u.deg, d_lon=2*u.deg, d_distance=10*u.m)
1976    sr = SphericalRepresentation(lat=1*u.deg, lon=2*u.deg, distance=10*u.m,
1977                                 differentials=sd)
1978
1979    cart = sr.to_cartesian()
1980    assert cart.get_name() == 'cartesian'
1981    assert not cart.differentials
1982
1983
1984@pytest.fixture
1985def unitphysics():
1986    """
1987    This fixture is used
1988    """
1989    had_unit = False
1990    if hasattr(PhysicsSphericalRepresentation, '_unit_representation'):
1991        orig = PhysicsSphericalRepresentation._unit_representation
1992        had_unit = True
1993
1994    class UnitPhysicsSphericalRepresentation(BaseRepresentation):
1995        attr_classes = {'phi': Angle,
1996                        'theta': Angle}
1997
1998        def __init__(self, *args, copy=True, **kwargs):
1999            super().__init__(*args, copy=copy, **kwargs)
2000
2001            # Wrap/validate phi/theta
2002            if copy:
2003                self._phi = self._phi.wrap_at(360 * u.deg)
2004            else:
2005                # necessary because the above version of `wrap_at` has to be a copy
2006                self._phi.wrap_at(360 * u.deg, inplace=True)
2007
2008            if np.any(self._theta < 0.*u.deg) or np.any(self._theta > 180.*u.deg):
2009                raise ValueError('Inclination angle(s) must be within '
2010                                 '0 deg <= angle <= 180 deg, '
2011                                 'got {}'.format(self._theta.to(u.degree)))
2012
2013        @property
2014        def phi(self):
2015            return self._phi
2016
2017        @property
2018        def theta(self):
2019            return self._theta
2020
2021        def unit_vectors(self):
2022            sinphi, cosphi = np.sin(self.phi), np.cos(self.phi)
2023            sintheta, costheta = np.sin(self.theta), np.cos(self.theta)
2024            return {
2025                'phi': CartesianRepresentation(-sinphi, cosphi, 0., copy=False),
2026                'theta': CartesianRepresentation(costheta*cosphi,
2027                                                 costheta*sinphi,
2028                                                 -sintheta, copy=False)}
2029
2030        def scale_factors(self):
2031            sintheta = np.sin(self.theta)
2032            l = np.broadcast_to(1.*u.one, self.shape, subok=True)
2033            return {'phi', sintheta,
2034                    'theta', l}
2035
2036        def to_cartesian(self):
2037            x = np.sin(self.theta) * np.cos(self.phi)
2038            y = np.sin(self.theta) * np.sin(self.phi)
2039            z = np.cos(self.theta)
2040
2041            return CartesianRepresentation(x=x, y=y, z=z, copy=False)
2042
2043        @classmethod
2044        def from_cartesian(cls, cart):
2045            """
2046            Converts 3D rectangular cartesian coordinates to spherical polar
2047            coordinates.
2048            """
2049            s = np.hypot(cart.x, cart.y)
2050
2051            phi = np.arctan2(cart.y, cart.x)
2052            theta = np.arctan2(s, cart.z)
2053
2054            return cls(phi=phi, theta=theta, copy=False)
2055
2056        def norm(self):
2057            return u.Quantity(np.ones(self.shape), u.dimensionless_unscaled,
2058                              copy=False)
2059
2060    PhysicsSphericalRepresentation._unit_representation = UnitPhysicsSphericalRepresentation
2061    yield UnitPhysicsSphericalRepresentation
2062
2063    if had_unit:
2064        PhysicsSphericalRepresentation._unit_representation = orig
2065    else:
2066        del PhysicsSphericalRepresentation._unit_representation
2067
2068    # remove from the module-level representations, if present
2069    REPRESENTATION_CLASSES.pop(UnitPhysicsSphericalRepresentation.get_name(), None)
2070
2071
2072def test_unitphysics(unitphysics):
2073    obj = unitphysics(phi=0*u.deg, theta=10*u.deg)
2074    objkw = unitphysics(phi=0*u.deg, theta=10*u.deg)
2075    assert objkw.phi == obj.phi
2076    assert objkw.theta == obj.theta
2077
2078    asphys = obj.represent_as(PhysicsSphericalRepresentation)
2079    assert asphys.phi == obj.phi
2080    assert_allclose(asphys.theta, obj.theta)
2081    assert_allclose_quantity(asphys.r, 1*u.dimensionless_unscaled)
2082
2083    assph = obj.represent_as(SphericalRepresentation)
2084    assert assph.lon == obj.phi
2085    assert assph.lat == 80*u.deg
2086    assert_allclose_quantity(assph.distance, 1*u.dimensionless_unscaled)
2087
2088    with pytest.raises(TypeError, match='got multiple values'):
2089        unitphysics(1*u.deg, 2*u.deg, theta=10)
2090
2091    with pytest.raises(TypeError, match='unexpected keyword.*parrot'):
2092        unitphysics(1*u.deg, 2*u.deg, parrot=10)
2093
2094
2095def test_distance_warning(recwarn):
2096    SphericalRepresentation(1*u.deg, 2*u.deg, 1*u.kpc)
2097    with pytest.raises(ValueError) as excinfo:
2098        SphericalRepresentation(1*u.deg, 2*u.deg, -1*u.kpc)
2099    assert 'Distance must be >= 0' in str(excinfo.value)
2100    # second check is because the "originating" ValueError says the above,
2101    # while the representation one includes the below
2102    assert 'you must explicitly pass' in str(excinfo.value)
2103
2104
2105def test_dtype_preservation_in_indexing():
2106    # Regression test for issue #8614 (fixed in #8876)
2107    xyz = np.array([[1, 0, 0], [0.9, 0.1, 0]], dtype='f4')
2108    cr = CartesianRepresentation(xyz, xyz_axis=-1, unit="km")
2109    assert cr.xyz.dtype == xyz.dtype
2110    cr0 = cr[0]
2111    # This used to fail.
2112    assert cr0.xyz.dtype == xyz.dtype
2113
2114
2115class TestInfo:
2116    def setup_class(cls):
2117        cls.rep = SphericalRepresentation([0, 1]*u.deg, [2, 3]*u.deg,
2118                                          10*u.pc)
2119        cls.diff = SphericalDifferential([10, 20]*u.mas/u.yr,
2120                                         [30, 40]*u.mas/u.yr,
2121                                         [50, 60]*u.km/u.s)
2122        cls.rep_w_diff = SphericalRepresentation(cls.rep,
2123                                                 differentials=cls.diff)
2124
2125    def test_info_unit(self):
2126        assert self.rep.info.unit == 'deg, deg, pc'
2127        assert self.diff.info.unit == 'mas / yr, mas / yr, km / s'
2128        assert self.rep_w_diff.info.unit == 'deg, deg, pc'
2129
2130    @pytest.mark.parametrize('item', ['rep', 'diff', 'rep_w_diff'])
2131    def test_roundtrip(self, item):
2132        rep_or_diff = getattr(self, item)
2133        as_dict = rep_or_diff.info._represent_as_dict()
2134        new = rep_or_diff.__class__.info._construct_from_dict(as_dict)
2135        assert np.all(representation_equal(new, rep_or_diff))
2136
2137
2138@pytest.mark.parametrize('cls',
2139                         [SphericalDifferential,
2140                          SphericalCosLatDifferential,
2141                          CylindricalDifferential,
2142                          PhysicsSphericalDifferential,
2143                          UnitSphericalDifferential,
2144                          UnitSphericalCosLatDifferential])
2145def test_differential_norm_noncartesian(cls):
2146    # The norm of a non-Cartesian differential without specifying `base` should error
2147    rep = cls(0, 0, 0)
2148    with pytest.raises(ValueError, match=r"`base` must be provided .* " + cls.__name__):
2149        rep.norm()
2150
2151
2152def test_differential_norm_radial():
2153    # Unlike most non-Cartesian differentials, the norm of a radial differential does not require `base`
2154    rep = RadialDifferential(1*u.km/u.s)
2155    assert_allclose_quantity(rep.norm(), 1*u.km/u.s)
2156