99
1010import glass
1111import glass .healpix as hp
12+ from glass ._array_api_utils import xp_additions as uxpx
1213
1314if TYPE_CHECKING :
1415 from types import ModuleType
@@ -356,11 +357,11 @@ def test_displace_arg_complex(compare: type[Compare], xp: ModuleType) -> None:
356357
357358 # east
358359 lon , lat = glass .displace (lon0 , lat0 , xp .asarray (1j * r ))
359- compare .assert_allclose ([lon , lat ], [- d , 0.0 ], atol = 1e-15 )
360+ compare .assert_allclose ([lon , lat ], [d , 0.0 ], atol = 1e-15 )
360361
361362 # west
362363 lon , lat = glass .displace (lon0 , lat0 , xp .asarray (- 1j * r ))
363- compare .assert_allclose ([lon , lat ], [d , 0.0 ], atol = 1e-15 )
364+ compare .assert_allclose ([lon , lat ], [- d , 0.0 ], atol = 1e-15 )
364365
365366
366367def test_displace_arg_real (compare : type [Compare ], xp : ModuleType ) -> None :
@@ -382,11 +383,11 @@ def test_displace_arg_real(compare: type[Compare], xp: ModuleType) -> None:
382383
383384 # east
384385 lon , lat = glass .displace (lon0 , lat0 , xp .asarray ([0 , r ]))
385- compare .assert_allclose ([lon , lat ], [- d , 0.0 ], atol = 1e-15 )
386+ compare .assert_allclose ([lon , lat ], [d , 0.0 ], atol = 1e-15 )
386387
387388 # west
388389 lon , lat = glass .displace (lon0 , lat0 , xp .asarray ([0 , - r ]))
389- compare .assert_allclose ([lon , lat ], [d , 0.0 ], atol = 1e-15 )
390+ compare .assert_allclose ([lon , lat ], [- d , 0.0 ], atol = 1e-15 )
390391
391392
392393def test_displace_abs (
@@ -434,14 +435,14 @@ def test_displacement(
434435 data = [
435436 # equator
436437 (zero , zero , zero , five , deg5 * north ),
437- (zero , zero , - five , zero , deg5 * east ),
438+ (zero , zero , five , zero , deg5 * east ),
438439 (zero , zero , zero , - five , deg5 * south ),
439- (zero , zero , five , zero , deg5 * west ),
440+ (zero , zero , - five , zero , deg5 * west ),
440441 # pole
441442 (zero , ninety , ninety * 2 , ninety - five , deg5 * north ),
442- (zero , ninety , - ninety , ninety - five , deg5 * east ),
443+ (zero , ninety , ninety , ninety - five , deg5 * east ),
443444 (zero , ninety , zero , ninety - five , deg5 * south ),
444- (zero , ninety , ninety , ninety - five , deg5 * west ),
445+ (zero , ninety , - ninety , ninety - five , deg5 * west ),
445446 ]
446447
447448 # test each displacement individually
@@ -457,3 +458,126 @@ def test_displacement(
457458 urng .uniform (- 90.0 , 90.0 , size = 5 ),
458459 )
459460 assert alpha .shape == (20 , 5 )
461+
462+
463+ def test_displacement_zerodist (
464+ compare : type [Compare ],
465+ urng : UnifiedGenerator ,
466+ xp : ModuleType ,
467+ ) -> None :
468+ """Check that zero displacement is computed correctly."""
469+ lon = urng .uniform (- 180.0 , 180.0 , size = 100 )
470+ lat = urng .uniform (- 90.0 , 90.0 , size = 100 )
471+
472+ compare .assert_allclose (
473+ glass .displacement (lon , lat , lon , lat ),
474+ xp .zeros (100 ),
475+ )
476+
477+
478+ def test_displacement_consistent (
479+ compare : type [Compare ],
480+ urng : UnifiedGenerator ,
481+ xp : ModuleType ,
482+ ) -> None :
483+ """Check displacement is consistent with displace."""
484+ n = 1_000
485+
486+ # magnitude and angle of displacement we want to achieve
487+ r = xp .acos (urng .uniform (- 1.0 , 1.0 , size = n ))
488+ x = urng .uniform (- math .pi , math .pi , size = n )
489+
490+ # displace at random positions on the sphere
491+ from_lon = urng .uniform (- 180.0 , 180.0 , size = n )
492+ from_lat = xp .asin (urng .uniform (- 1.0 , 1.0 , size = n )) / math .pi * 180.0
493+
494+ # compute the intended displacement
495+ alpha_in = r * xp .exp (1j * x )
496+
497+ # displace random points
498+ to_lon , to_lat = glass .displace (from_lon , from_lat , alpha_in )
499+
500+ # measure displacement
501+ alpha_out = glass .displacement (from_lon , from_lat , to_lon , to_lat )
502+
503+ compare .assert_allclose (alpha_out , alpha_in , atol = 0.0 , rtol = 1e-10 )
504+
505+
506+ def test_displacement_random (
507+ compare : type [Compare ],
508+ urng : UnifiedGenerator ,
509+ xp : ModuleType ,
510+ ) -> None :
511+ """Check displacement for random points."""
512+ n = 1_000
513+
514+ # magnitude and angle of displacement we want to achieve
515+ r = xp .acos (urng .uniform (- 1.0 , 1.0 , size = n ))
516+ x = urng .uniform (- math .pi , math .pi , size = n )
517+
518+ # displacement at random positions on the sphere
519+ theta = xp .acos (urng .uniform (- 1.0 , 1.0 , size = n ))
520+ phi = urng .uniform (- math .pi , math .pi , size = n )
521+
522+ # rotation matrix that moves (0, 0, 1) to theta and phi
523+ zero = xp .zeros (n )
524+ one = xp .ones (n )
525+ rot_y = xp .stack (
526+ [
527+ xp .cos (theta ), zero , xp .sin (theta ),
528+ zero , one , zero ,
529+ - xp .sin (theta ), zero , xp .cos (theta ),
530+ ],
531+ axis = 1 ,
532+ ) # fmt: skip
533+ rot_z = xp .stack (
534+ [
535+ xp .cos (phi ), - xp .sin (phi ), zero ,
536+ xp .sin (phi ), xp .cos (phi ), zero ,
537+ zero , zero , one ,
538+ ],
539+ axis = 1 ,
540+ ) # fmt: skip
541+ rot = xp .reshape (rot_z , (n , 3 , 3 )) @ xp .reshape (rot_y , (n , 3 , 3 ))
542+
543+ # meta-check that rotation works by rotating (0, 0, 1) to theta and phi
544+ u = xp .stack (
545+ [
546+ xp .sin (theta ) * xp .cos (phi ),
547+ xp .sin (theta ) * xp .sin (phi ),
548+ xp .cos (theta ),
549+ ],
550+ axis = 1 ,
551+ )
552+ compare .assert_allclose (rot @ xp .asarray ([0.0 , 0.0 , 1.0 ]), u )
553+
554+ # meta-check that recovering theta and phi from vector works
555+ compare .assert_allclose (xp .atan2 (xp .hypot (u [:, 0 ], u [:, 1 ]), u [:, 2 ]), theta )
556+ compare .assert_allclose (xp .atan2 (u [:, 1 ], u [:, 0 ]), phi )
557+
558+ # build the displaced points near (0, 0, 1) and rotate near theta and phi
559+ v = xp .stack (
560+ [
561+ xp .sin (r ) * xp .cos (math .pi - x ),
562+ xp .sin (r ) * xp .sin (math .pi - x ),
563+ xp .cos (r ),
564+ ],
565+ axis = 1 ,
566+ )
567+ v = rot @ xp .reshape (v , (n , 3 , 1 ))
568+ v = xp .reshape (v , (n , 3 ))
569+
570+ # compute displaced theta and phi
571+ theta_d = xp .atan2 (xp .hypot (v [:, 0 ], v [:, 1 ]), v [:, 2 ])
572+ phi_d = xp .atan2 (v [:, 1 ], v [:, 0 ])
573+
574+ # compute longitude and latitude
575+ from_lon = uxpx .degrees (phi )
576+ from_lat = 90.0 - uxpx .degrees (theta )
577+ to_lon = uxpx .degrees (phi_d )
578+ to_lat = 90.0 - uxpx .degrees (theta_d )
579+
580+ # compute displacement and compare to input
581+ alpha_in = r * xp .exp (1j * x )
582+ alpha_out = glass .displacement (from_lon , from_lat , to_lon , to_lat )
583+ compare .assert_allclose (alpha_out , alpha_in , atol = 0.0 , rtol = 1e-10 )
0 commit comments