@@ -510,6 +510,8 @@ def wrap_0_pi(theta):
510510 This is used to fold angles of colatitude. If zero is the angle of the
511511 north pole, colatitude increases to :math:`\pi` at the south pole then
512512 decreases to :math:`0` as we head back to the north pole.
513+
514+ :seealso: :func:`wrap_mpi2_pi2` :func:`wrap_0_2pi` :func:`wrap_mpi_pi` :func:`angle_wrap`
513515 """
514516 n = (theta / np .pi )
515517 if isinstance (n , np .ndarray ):
@@ -519,6 +521,26 @@ def wrap_0_pi(theta):
519521
520522 return np .where (n & 1 == 0 , theta - n * np .pi , (n + 1 ) * np .pi - theta )
521523
524+ def wrap_mpi2_pi2 (theta ):
525+ r"""
526+ Wrap angle to range :math:`[-\pi/2, \pi/2]`
527+
528+ :param theta: input angle
529+ :type theta: scalar or ndarray
530+ :return: angle wrapped into range :math:`[-\pi/2, \pi/2]`
531+
532+ This is used to fold angles of latitude.
533+
534+ :seealso: :func:`wrap_0_pi` :func:`wrap_0_2pi` :func:`wrap_mpi_pi` :func:`angle_wrap`
535+
536+ """
537+ n = (theta / np .pi )
538+ if isinstance (n , np .ndarray ):
539+ n = n .astype (int )
540+ else :
541+ n = int (n )
542+
543+ return np .where (n & 1 == 0 , theta - n * np .pi , (n + 1 ) * np .pi - theta )
522544
523545def wrap_0_2pi (theta ):
524546 r"""
@@ -527,17 +549,22 @@ def wrap_0_2pi(theta):
527549 :param theta: input angle
528550 :type theta: scalar or ndarray
529551 :return: angle wrapped into range :math:`[0, 2\pi)`
552+
553+ :seealso: :func:`wrap_mpi_pi` :func:`wrap_0_pi` :func:`wrap_mpi2_pi2` :func:`angle_wrap`
530554 """
555+
531556 return theta - 2.0 * math .pi * np .floor (theta / 2.0 / np .pi )
532557
533558
534559def wrap_mpi_pi (angle ):
535560 r"""
536- Wrap angle to range :math:`[\- pi, \pi)`
561+ Wrap angle to range :math:`[-\ pi, \pi)`
537562
538563 :param theta: input angle
539564 :type theta: scalar or ndarray
540565 :return: angle wrapped into range :math:`[-\pi, \pi)`
566+
567+ :seealso: :func:`wrap_0_2pi` :func:`wrap_0_pi` :func:`wrap_mpi2_pi2` :func:`angle_wrap`
541568 """
542569 return np .mod (angle + math .pi , 2 * math .pi ) - np .pi
543570
@@ -575,12 +602,122 @@ def angdiff(a, b=None):
575602 >>> angdiff(0.9 * pi, -0.9 * pi) / pi
576603 >>> angdiff(3 * pi)
577604
605+ :seealso: :func:`vector_diff` :func:`wrap_mpi_pi`
578606 """
579607 if b is None :
580608 return np .mod (a + math .pi , 2 * math .pi ) - math .pi
581609 else :
582610 return np .mod (a - b + math .pi , 2 * math .pi ) - math .pi
583611
612+ def angle_std (theta ):
613+ r"""
614+ Standard deviation of angular values
615+
616+ :param theta: angular values
617+ :type theta: array_like
618+ :return: circular standard deviation
619+ :rtype: float
620+
621+ .. math::
622+
623+ \sigma_{\theta} = \sqrt{-2 \log \| \left[ \frac{\sum \sin \theta_i}{N}, \frac{\sum \sin \theta_i}{N} \right] \|} \in [0, \infty)
624+
625+ :seealso: :func:`angle_mean`
626+ """
627+ X = np .cos (theta ).mean ()
628+ Y = np .sin (theta ).mean ()
629+ R = np .sqrt (X ** 2 + Y ** 2 )
630+
631+ return np .sqrt (- 2 * np .log (R ))
632+
633+ def angle_mean (theta ):
634+ r"""
635+ Mean of angular values
636+
637+ :param theta: angular values
638+ :type v: array_like
639+ :return: circular mean
640+ :rtype: float
641+
642+ The circular mean is given by
643+
644+ .. math::
645+
646+ \bar{\theta} = \tan^{-1} \frac{\sum \sin \theta_i}{\sum \cos \theta_i} \in [-\pi, \pi)]
647+
648+ :seealso: :func:`angle_std`
649+ """
650+ X = np .cos (theta ).sum ()
651+ Y = np .sin (theta ).sum ()
652+ return np .artan2 (Y , X )
653+
654+ def angle_wrap (theta , mode = '-pi:pi' ):
655+ """
656+ Generalized angle-wrapping
657+
658+ :param v: angles to wrap
659+ :type v: array_like
660+ :param mode: wrapping mode, one of: ``"0:2pi"``, ``"0:pi"``, ``"-pi/2:pi/2"`` or ``"-pi:pi"`` [default]
661+ :type mode: str, optional
662+ :return: wrapped angles
663+ :rtype: ndarray
664+
665+ .. note:: The modes ``"0:pi"`` and ``"-pi/2:pi/2"`` are used to wrap angles of
666+ colatitude and latitude respectively.
667+
668+ :seealso: :func:`wrap_0_2pi` :func:`wrap_mpi_pi` :func:`wrap_0_pi` :func:`wrap_mpi2_pi2`
669+ """
670+ if mode == "0:2pi" :
671+ return wrap_0_2pi (theta )
672+ elif mode == "-pi:pi" :
673+ return wrap_mpi_pi (theta )
674+ elif mode == "0:pi" :
675+ return wrap_0_pi (theta )
676+ elif mode == "0:pi" :
677+ return wrap_mpi2_pi2 (theta )
678+ else :
679+ raise ValueError ('bad method specified' )
680+
681+ def vector_diff (v1 , v2 , mode ):
682+ """
683+ Generalized vector differnce
684+
685+ :param v1: first vector
686+ :type v1: array_like(n)
687+ :param v2: second vector
688+ :type v2: array_like(n)
689+ :param mode: subtraction mode
690+ :type mode: str of length n
691+
692+ ============== ====================================
693+ mode character purpose
694+ ============== ====================================
695+ r real number, don't wrap
696+ c angle on circle, wrap to [-π, π)
697+ C angle on circle, wrap to [0, 2π)
698+ l latitude angle, wrap to [-π/2, π/2]
699+ L colatitude angle, wrap to [0, π]
700+ ============== ====================================
701+
702+ :seealso: :func:`angdiff` :func:`wrap_0_2pi` :func:`wrap_mpi_pi` :func:`wrap_0_pi` :func:`wrap_mpi2_pi2`
703+ """
704+ v = getvector (v1 ) - getvector (v2 )
705+ for i , m in enumerate (mode ):
706+ if m == "r" :
707+ pass
708+ elif m == "c" :
709+ v [i ] = wrap_mpi_pi (v [i ])
710+ elif m == "C" :
711+ v [i ] = wrap_0_2pi (v [i ])
712+ elif m == "l" :
713+ v [i ] = wrap_mpi2_pi2 (v [i ])
714+ elif m == "L" :
715+ v [i ] = wrap_0_pi (v [i ])
716+ else :
717+ raise ValueError ('bad mode character' )
718+
719+ return v
720+
584721
585722def removesmall (v , tol = 100 ):
586723 """
0 commit comments