Coverage for pygeodesy/angles.py: 95%
493 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-25 15:01 -0400
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-25 15:01 -0400
2# -*- coding: utf-8 -*-
4u'''Classes L{Ang}, L{Deg}, L{Rad} and L{Lambertian} accurately representing an angle
5as a 3-tuple C{(sine, cosine, turns)}, with C{turns} the number of full turns.
7Transcoded to pure Python from I{Karney}'s GeographicLib 2.7 C++ class U{AngleT
8<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AngleT.html>}.
10Copyright (C) U{Charles Karney <mailto:Karney@Alum.MIT.edu>} (2024-2025) and licensed
11under the MIT/X11 License. For more information, see the U{GeographicLib 2.7
12<https://GeographicLib.SourceForge.io/>} documentation.
13'''
14# make sure int/int division yields float quotient, see .basics
15from __future__ import division as _; del _ # noqa: E702 ;
17from pygeodesy.basics import _copysign, map1, signBit, _signOf
18from pygeodesy.constants import EPS, EPS0, NAN, PI2, _0_0, _N_0_0, \
19 _0_25, _1_0, _N_1_0, _4_0, _360_0, \
20 _copysign_0_0, _copysign_1_0, \
21 _flipsign, float_, _isfinite, \
22 _over, _pos_self, remainder
23from pygeodesy.errors import _xkwds, _xkwds_get, _xkwds_pop2
24from pygeodesy.fmath import hypot, _ALL_LAZY, _MODS
25# from pygeodesy.interns import _COMMASPACE_ # from .streprs
26# from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .fmath
27from pygeodesy.named import _Named, _NamedTuple, _Pass
28from pygeodesy.props import Property_RO, property_doc_, property_RO, \
29 _allPropertiesOf_n, _update_all
30from pygeodesy.streprs import Fmt, fstr, unstr, _COMMASPACE_
31from pygeodesy.units import Degrees, _isDegrees, _isRadians, Radians
32from pygeodesy.utily import atan2, atan2d, sincos2, sincos2d, SinCos2
34from math import asinh, ceil as _ceil, fabs, floor as _floor, \
35 isinf, isnan, sinh
37__all__ = _ALL_LAZY.angles
38__version__ = '25.12.02'
40_EPS03 = EPS / (1 << 20)
41# _HD = _180_0
42# _QD = _90_0
43# _TD = _360_0
44# _DM = _SM = _60_0
45# _DS = _3600_0
46_ZRND = _1_0 / 1024
48_CARDINAL2 = {-2: (_N_0_0, _N_1_0),
49 -1: (_N_1_0, _0_0),
50 1: ( _1_0, _0_0),
51 2: ( _0_0, _N_1_0)}.get
54def _fint(f):
55 # float of C{int(f)} preserving signed C{0}.
56 i = int(f)
57 return float_(i) if i else _copysign_0_0(f)
60def _ncardinal(s, c, n):
61 if n:
62 n *= _4_0
63 i = (1 if (-c) < fabs(s) else 2) if signBit(c) else \
64 (1 if c < fabs(s) else 0)
65 if i:
66 n += _copysign(i, s)
67 return n
70def _normalize2(s, c):
71 h = hypot(s, c)
72 if _isfinite(h):
73 sc = ((s / h), (c / h)) if h else (
74 # If y is +/-0 and x = -0, +/-pi is returned,
75 # or y is +/-0 and x = +0, +/-0 is returned,
76 # so, retain the sign of s = +/-0
77 _orthogonal2(False, s, c))
78 elif isnan(h) or (isinf(s) and isinf(c)):
79 sc = NAN, NAN
80 else:
81 sc = _orthogonal2(isinf(s), s, c)
82 return sc
85def _other(x, unit=Radians, **unused):
86 # get C{x} as C{Ang} from C{Degrees}, C{Radians} or C{Lambertian}
87 return Ang.fromLambertian(x) if unit is Lambertian else (
88 Ang.fromRadians(x) if _isRadians(x, iscalar=unit is Radians) else (
89 Ang.fromDegrees(x) if _isDegrees(x, iscalar=unit is Degrees) else
90 _raiseError(unit, x))) # PYCHOK indent
93def _orthogonal2(pred, s, c):
94 return (_copysign_1_0(s), _copysign_0_0(c)) if pred else \
95 (_copysign_0_0(s), _copysign_1_0(c))
98def _raiseError(unit, arg, **kwds):
99 raise TypeError(unstr(unit, arg, **kwds))
102def _rnd(x):
103 w = _ZRND - fabs(x)
104 if w > 0:
105 x = _copysign(_ZRND - w, x)
106 return x
109def _scnu4(s, c, n, unit=Radians, **unused): # unit=Ang._unit
110 s, c, n = map1(float, s, c, n)
111 return _normalize2(s, c) + (n, unit)
114class Ang(_Named):
115 '''An accurate representation of angles, as 3-tuple C{(s, c, n)}.
117 This class represents an angle via its sine C{s}, cosine C{c} and
118 the number of full turns C{n}. The angle is then C{atan2(s, c) +
119 n * PI2}. This representation offers several advantages:
121 - cardinal directions (multiples of 90 degrees) are exactly represented
122 (a benefit shared by representing angles as degrees)
124 - angles very close to any cardinal direction are accurately represented
126 - there's no loss of precision with large angles (outside the "normal"
127 range [-180, +180])
129 - various operations, such as adding a multiple of 90 degrees to an
130 angle are performed exactly.
132 @note: B{C{n}} is a C{float}, this allows it to be NAN, INF or NINF.
133 '''
134 _unit = Radians # see _scnu4
136 def __init__(self, s_ang=0, c=None, n=0, normal=True, **unit_name):
137 '''New L{Ang}.
139 @kwarg s_ang: A previous L{Ang}, C{Degrees}, C{Radians} if C{B{c}
140 is None}, otherwise the sine component (C{float}).
141 @kwarg c: The cosine component (C{float}) iff C{not None}.
142 @kwarg n: The number of L{PI2} turns (C{float}).
143 @kwarg normal: If C{True}, B{C{s}} and B{C{c}} are normalized, i.e.
144 on the unit circle (C{boo}).
145 @kwarg unit_name: Type C{B{unit}=}L{Radians} or L{Degrees} of scalar
146 scalar values (L{Degrees} or L{Radians}).
148 @note: Either B{C{s}} or B{C{c}} can be INF or NINF, but not both.
150 @note: By default, the point B{C{(s, c)}} is scaled to lie on the
151 unit circle.
152 '''
153 s, c, n, u = s_ang.scnu4 if isAng(s_ang) else (
154 _other(s_ang, **unit_name).scnu4 if c is None else
155 _scnu4(s_ang, c, n, **unit_name))
156 if unit_name:
157 u, name = _xkwds_pop2(unit_name, unit=u)
158 if name:
159 self.name = name
160 self._n = _fint(n)
161 self._s, self._c = (s, c) if normal else _normalize2(s, c)
162 self.unit = u
164 def __abs__(self):
165 s, _ = self._float2()
166 return self._float1(fabs(s))
168 def __add__(self, other):
169 return self.copy().__iadd__(other)
171 def __bool__(self): # PYCHOK Python 3+
172 s, c, n = self.scn3
173 return bool(s or c or n)
175# def __call__(self, *args, **kwds): # PYCHOK no cover
176# return self._NotImplemented(*args, **kwds)
178 def __ceil__(self): # PYCHOK not special in Python 2-
179 s, _ = self._float2()
180 return self._float1(_ceil(s))
182 def __cmp__(self, other): # PYCHOK no cover
183 s, r = self._float2(other)
184 return _signOf(s, r) # -1, 0, +1
186 def __divmod__(self, other):
187 s, r = self._float2(other)
188 q, r = divmod(s, r)
189 return q, self._float1(r)
191 def __eq__(self, other):
192 s, r = self._float2(other)
193 return fabs(s - r) < EPS0
195 def __float__(self):
196 u = self.unit
197 return self.radians if u is Radians else (
198 self.degrees if u is Degrees else (
199 self.lambertian if u is Lambertian else
200 _raiseError(float, u))) # PYCHOK indent
202 def __floor__(self): # PYCHOK not special in Python 2-
203 s, _ = self._float2()
204 return self._float1(_floor(s))
206 def __floordiv__(self, other):
207 return self.copy().__ifloordiv__(other)
209# def __format__(self, *other): # PYCHOK no cover
210# return self._NotImplemented(self, *other)
212 def __ge__(self, other):
213 s, r = self._float2(other)
214 return s >= r
216 def __gt__(self, other):
217 s, r = self._float2(other)
218 return s > r
220 def __hash__(self): # PYCHOK no cover
221 # @see: U{Notes for type implementors<https://docs.Python.org/
222 # 3/library/numbers.html#numbers.Rational>}
223 return hash(self.scn3) # tuple.__hash__()
225 def __iadd__(self, other):
226 p = self._other(other)
227 q = p.ncardinal + self.ncardinal
228 s, c, n = self.scn3
229 s, c = _normalize2(s * p.c + c * p.s,
230 c * p.c - s * p.s)
231 q -= _ncardinal(s, c, n)
232 n = _fint(q * _0_25) + p.n
233 if n:
234 self._n += n
235 self._s = s
236 self._c = c
237 _update_all(self)
238 return self._update(s, c)
240 def __ifloordiv__(self, other):
241 s, r = self._float2(other)
242 return self._ifloat(s // r)
244 def __imatmul__(self, other): # PYCHOK no cover
245 return self._notImplemented()
247 def __imod__(self, other):
248 s, r = self._float2(other)
249 return self._ifloat(s % r)
251 def __imul__(self, other):
252 s, r = self._float2(other)
253 return self._ifloat(s * r)
255 def __int__(self):
256 s, _ = self._float2(0)
257 return int(s)
259 def __invert__(self): # PYCHOK no cover
260 # Luciano Ramalho, "Fluent Python", O'Reilly, 2nd Ed, 2022 p. 567
261 return self._notImplemented()
263 def __ipow__(self, other, *mod): # PYCHOK 2 vs 3 args
264 s, r = self._float2(other)
265 return self._ifloat(pow(s, r, *mod))
267 def __isub__(self, other):
268 return self.__iadd__(-other)
270# def __iter__(self):
271# '''
272# return self._NotImplemented()
274 def __itruediv__(self, other):
275 s, r = self._float2(other)
276 return self._ifloat(s / r)
278 def __le__(self, other):
279 s, r = self._float2(other)
280 return s <= r
282 def __lt__(self, other):
283 s, r = self._float2(other)
284 return s < r
286 def __matmul__(self, other): # PYCHOK no cover
287 return self._notImplemented(other)
289 def __mod__(self, other):
290 s, r = self._float2(other)
291 return self._float1(s % r)
293 def __mul__(self, other):
294 return self.copy().__imul__(other)
296 def __ne__(self, other):
297 return not self.__eq__(other)
299 def __neg__(self):
300 s, c, n = self.scn3
301 s, n = _flipsign(s), _flipsign(n)
302 return self._Ang(s, c, n) # normal=True
304 def __pos__(self):
305 return self if _pos_self else self.copy()
307 def __pow__(self, other, *mod): # PYCHOK 2 vs 3 args
308 return self.copy().__ipow__(other, *mod)
310 def __radd__(self, other):
311 return self._other(other) + self
313 def __rdivmod__(self, other):
314 return divmod(self._other(other), self)
316 def __repr__(self):
317 return self.toRepr()
319 def __rfloordiv__(self, other):
320 return self._other(other) // self
322 def __rmatmul__(self, other): # PYCHOK no cover
323 return self._notImplemented(self, other)
325 def __rmod__(self, other):
326 return self._other(other) % self
328 def __rmul__(self, other):
329 return self._other(other) * self
331 def __round__(self, *ndigits): # PYCHOK Python 3+
332 return self.round(*ndigits)
334 def __rpow__(self, other, *mod):
335 return pow(self._other(other), self, *mod)
337 def __rsub__(self, other):
338 return self._other(other) - self
340 def __rtruediv__(self, other):
341 return self._other(other) / self
343 def __str__(self):
344 return self.toStr(0) # ignore turns
346 def __sub__(self, other):
347 return self.copy().__isub__(other)
349 def __truediv__(self, other):
350 return self.copy().__itruediv__(other)
352 __trunc__ = __int__
354 if _MODS.sys_version_info2 < (3, 0): # PYCHOK no cover
355 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
356 __div__ = __truediv__
357 __idiv__ = __itruediv__
358 __long__ = __int__
359 __nonzero__ = __bool__
360 __rdiv__ = __rtruediv__
362 def _Ang(self, s, *cn, **normal_unit_name):
363 # return an C{Ang} like C{self}
364 return Ang(s, *cn, **self._kwds(normal_unit_name))
366 def base(self, *center):
367 '''Return this C{Angle}'s base, optionally centered.
368 '''
369 r = self.copy()
370 if center:
371 c = self._other(center[0])
372 b = self - c
373 b = b.base()
374 b += c
375 r.n0 = b.n0
376 else:
377 r.n = 0
378 return r
380 @property_RO
381 def c(self):
382 '''Get the cosine of this C{Angle} (C{float}).
383 '''
384 return self._c
386 @staticmethod
387 def cardinal(q=0, **unit_name):
388 '''A cardinal direction.
390 @kwarg q: The number of I{quarter} turns (C{scalar}).
392 @return: An C{Ang} equivalent to B{C{q}} quarter turns.
394 @note: B{C{q}} is truncated to an integer and signed
395 C{0} is distinguished. C{Ang.NAN} is returned
396 if B{C{q}} is not finite.
397 '''
398 if _isfinite(q):
399 if q:
400 q = _fint(q)
401 i = int(remainder(q, _4_0)) # i is in [-2, 2]
402 n = _fint((q - i) * _0_25)
403 s, c = _CARDINAL2(i, ((_0_0 if q else q), _1_0))
404 t = s is not q
405 else:
406 s, c, n, t = _copysign_0_0(q), 1, 0, True
407 r = Ang(s, c, n, normal=t, **unit_name)
408 else:
409 r = Ang.NAN(**unit_name)
410 return r
412 def copy(self, **unit_name): # PYCHOK signature
413 '''Return a copy of this C{Ang}.
414 '''
415 return self._Ang(self, **self._kwds(unit_name))
417 @Property_RO
418 def degrees(self):
419 '''Get this C{Ang} in C{degrees}.
420 '''
421 d = self.degrees0
422 if self.n:
423 d += self.n * _360_0
424 return d # XXX Degrees(d, self.name)
426 @Property_RO
427 def degrees0(self):
428 '''Get this C{Ang} in C{degrees} ignoring the turns.
429 '''
430 return atan2d(*self.sc2) # XXX Degrees(d, self.name)
432 divmod = __divmod__
434 @staticmethod
435 def EPS0(**unit_name):
436 '''Get a tiny C{Ang}.
438 @note: This allows angles extremely close to the cardinal
439 directions to be generated. The C{.round} method
440 will flush this angle to C{0}.
441 '''
442 return Ang(_EPS03, 1, **unit_name)
444 @staticmethod
445 def _flip(bet, omg, alp=None):
446 '''(INTERNAL) Reflect C{bet}, C{omg} and C{alp} inplace.
447 ''' # Ellipsoid3.Flip
448 bet.reflect(flipc=True)
449 omg.reflect(flips=True)
450 if alp:
451 alp.reflect(flips=True, flipc=True)
453 def flipsign(self, mul=-1, **name):
454 '''Copy this C{Ang} with sign flipped.
455 '''
456 r = (-self) if signBit(mul) else self
457 return self._Ang(r, **name) if name else r
459 def _float1(self, f, **name):
460 # return C{f} as C{Ang} in this C{unit}
461 return _Ang_from[self.unit](f, **name)
463 def _float2(self, other=None):
464 # get self and C{other} as floats
465 r = other if other is None or isinstance(other, int) else \
466 float(_Ang_from[self.unit](other))
467 return float(self), r
469 def _ifloat(self, f): # PYCHOK expected
470 # set self to C{f} degrees or radians
471 scn = self._float1(f).scn3
472 self._s, self._c, self._n = scn
473 return self._update()
475 @staticmethod
476 def fromDegrees(deg, **unit_name):
477 '''Get an C{Ang} from degrees.
478 '''
479 if isAng(deg):
480 s, c, n = deg.scn3
481 d = deg.degrees0
482 elif _isDegrees(deg, iscalar=True):
483 s, c = sincos2d(deg)
484 d = atan2d(s, c)
485 n = round((deg - d) / _360_0)
486 else:
487 _raiseError(Ang.fromDegrees, deg, **unit_name)
488 a = Ang(s, c, n, **_xkwds(unit_name, unit=Degrees))
489 a.__dict__.update(degrees0=d) # Property_RO
490 return a
492 @staticmethod
493 def fromLambertian(psi, **unit_name):
494 '''Get an C{Ang} from C{lamberterian} radians.
495 '''
496 s = psi.lambertian if isAng(psi) else sinh(psi)
497 return Ang(s, 1, normal=False, **_xkwds(unit_name, unit=Lambertian))
499 @staticmethod
500 def fromRadians(rad, **unit_name):
501 '''Get an C{Ang} from radians.
502 '''
503 if isAng(rad):
504 s, c, n = rad.scn3
505 r = rad.radians0
506 elif _isRadians(rad, iscalar=True):
507 s, c = sincos2(rad)
508 r = atan2(s, c)
509 n = round((rad - r) / PI2)
510 else:
511 _raiseError(Ang.fromRadians, rad, **unit_name)
512 a = Ang(s, c, n, **_xkwds(unit_name, unit=Radians))
513 a.__dict__.update(radians0=r) # Property_RO
514 return a
516 @staticmethod
517 def fromScalar(ang, **unit_name):
518 '''Get an C{Ang} from C{Degrees}, C{Radians} or another C{Ang}.
519 '''
520 if isAng(ang):
521 r = Ang(ang, **_xkwds(unit_name, unit=ang.unit))
522 else:
523 u = _xkwds_get(unit_name, unit=None)
524 if u is Lambertian:
525 r = Ang.fromLambertian(ang, **unit_name)
526 elif _isDegrees(ang, iscalar=u is Degrees):
527 r = Ang.fromDegrees(ang, **unit_name)
528 elif _isRadians(ang, iscalar=u is Radians):
529 r = Ang.fromRadians(ang, **unit_name)
530 else:
531 _raiseError(Ang.fromScalar, ang, **unit_name)
532 return r
534 def is_integer(self, *n):
535 '''Is this C{Ang}'s degrees C{integer}? (C{bool}).
536 '''
537 return self.toDegrees(*n).is_integer()
539 def isnear0(self, eps0=EPS0): # aka zerop
540 '''Is this C{Ang} near C{0} within a tolerance?
541 '''
542 s, c, n = self.scn3
543 return bool(n == 0 and c > 0 and fabs(s) <= eps0)
545 def _kwds(self, kwds, **dflt):
546 return _xkwds(kwds, **_xkwds(dflt, unit=self.unit,
547 name=self.name))
549 @Property_RO
550 def lambertian(self):
551 '''Get this C{Ang}'s Lambertian, C{asinh(tan(radians))}.
552 '''
553 return asinh(self.t) # XXX Lambertian(self.t)
555 def mod(self, mul=_1_0, **unit_name):
556 '''Return the I{reduced latitude} C{atan(B{mul} *
557 tan(B{this}))} as an C{Ang}.
559 @arg mul: Factor (C{scalar}, positive).
561 @note: The quadrant of the result tracks that of
562 this C{Ang} through multiples turns.
563 '''
564 kwds = self._kwds(unit_name)
565 if signBit(mul):
566 r = self._Ang(Ang.NAN(), **kwds)
567 else:
568 s, c, n = self.scn3
569 if mul > 1:
570 c = c / mul # /= chokes PyChecker
571 else: # mul <= 1
572 s *= mul
573 r = self._Ang(s, c, n, normal=False, **kwds)
574 return r
576 @staticmethod
577 def N(**unit_name):
578 '''Get North C{Ang}.
579 '''
580 return Ang(0, 1, **unit_name)
582 @property
583 def n(self):
584 '''Return the number of turns (C{float}) or C{0.0}.
585 '''
586 return self._n or _0_0
588 @n.setter # PYCHOK setter!
589 def n(self, n):
590 self._n_0(_fint(n))
592 def _n_0(self, n):
593 '''(INTERNAL) Set C{n} or C{n0}.
594 '''
595 if self._n != n:
596 self._n, n = n, self._n
597 self._update()
598 return n
600 @property
601 def n0(self):
602 '''Return the number of turns, treating C{-180} as C{180 - 1 turn} (C{float}).
603 '''
604 return (self.n - self._n01) or _0_0
606 @n0.setter # PYCHOK setter!
607 def n0(self, n):
608 self._n_0(_fint(n) + self._n01)
610 @Property_RO
611 def _n01(self):
612 s, c = self.sc2
613 return int(c < 0 and s == 0 and signBit(s))
615 @staticmethod
616 def NAN(**unit_name):
617 '''Get an invalid C{Ang}.
618 '''
619 return Ang(NAN, NAN, **unit_name)
621 @Property_RO
622 def ncardinal(self):
623 '''Get the nearest cardinal direction (C{float_int}).
625 @note: This is the reverse of C{cardinal}.
626 '''
627 return _ncardinal(*self.scn3)
629 def nearest(self, ind=0, **name):
630 '''Return the closest cardinal direction (C{Ang}).
632 @arg ind: An indicator, if C{B{ind}=0} the closest cardinal
633 direction, otherwise, if B{C{ind}} is even, the
634 closest even (N/S) cardinal direction or if B{C{ind}}
635 is odd, the closest odd (E/W) cardinal direction.
636 '''
637 s, c, n = self.scn3
638 p = (ind == 0 and fabs(s) > fabs(c)) or (ind & 1)
639 s, c = _orthogonal2(p, s, c)
640 return self._Ang(s, c, n, **self._kwds(name))
642 @staticmethod
643 def _norm(bet, omg, alp=None, alt=False):
644 '''(INTERNAL) Put C{bet}, C{ong} and C{alp} in range.
645 ''' # Ellipsoid3.AngNorm
646 flip = signBit(omg.s if alt else bet.c)
647 if flip:
648 Ang._flip(bet, omg, alp)
649 return flip
651 def normalize(self, *n):
652 '''Re-normalize this C{Ang}, optionally replacing turns.
653 '''
654 sc = _normalize2(*self.sc2)
655 if n:
656 self.n, n = n[0], self.n
657 if self.n != n: # updated
658 self._s, self._c = sc
659 return self
660 return self._update(*sc)
662 def _other(self, other):
663 # get C{other} as C{Ang} from C{unit}
664 return other if isAng(other) else _other(other, self.unit)
666 pow = __pow__
668 @Property_RO
669 def _quadrant(self):
670 s, c = map(int, map(signBit, self.sc2))
671 return s + s + (c ^ s)
673 @property_doc_("this C{Ang}'s quadrant (C{int} 0..3)")
674 def quadrant(self):
675 return self._quadrant
677 @quadrant.setter # PYCHOK setter!
678 def quadrant(self, quadrant):
679 s, c = map(fabs, self.sc2)
680 q = int(quadrant)
681 if (q & 2):
682 s = -s # _copysign(self.s, -1 if (q & 2) else 1)
683 if (((q >> 1) ^ q) & 1):
684 c = -c # _copysign(self.c, -1 if (((q >> 1) ^ q) & 1) else 1)
685 self._update(s, c)
687 @Property_RO
688 def radians(self):
689 '''Get this C{Ang} in C{radians}.
690 '''
691 r = self.radians0
692 if self.n:
693 r += self.n * PI2
694 return r # XXX Radians(r, self.name)
696 @Property_RO
697 def radians0(self):
698 '''Get this C{Ang} in C{radians} ignoring the turns.
699 '''
700 return atan2(*self.sc2) # XXX Radians(r, self.name)
702 def reflect(self, flips=False, flipc=False, swapsc=False):
703 '''Reflect this C{Ang} in various ways.
705 @kwarg flips: Flip the sign of C{s}.
706 @kwarg flipc: Flip the sign of C{c}.
707 @kwarg swapsc: Swap C{s} and C{c}.
709 @note: The operations are carried out in the order
710 of the arguments.
711 '''
712 s, c = self.sc2
713 if flips:
714 s = -s
715 if flipc:
716 c = -c
717 if swapsc:
718 s, c = c, s
719 return self._update(s, c)
721 def round(self, *ndigits, **name):
722 '''Return this C{Ang}, optionally rounded to C{ndigits} (C{Ang}).
723 '''
724 s, c, n = self.scn3
725 if ndigits:
726 s = round(s, *ndigits)
727 c = round(c, *ndigits)
728 else:
729 s, c = map1(_rnd, s, c)
730 return self._Ang(s, c, n, **self._kwds(name))
732 @property_RO
733 def s(self):
734 '''Get the sine of this C{Ang} (C{float}).
735 '''
736 return self._s
738 @property_RO
739 def sc2(self):
740 '''Get the 2-tuple C{(s, c)}.
741 '''
742 return self.s, self.c
744 @Property_RO
745 def scn3(self):
746 '''Get the 3-tuple C{(s, c, n)}.
747 '''
748 return self.s, self.c, self.n
750 @property_RO
751 def scnu4(self):
752 '''Get the 4-tuple C{(s, c, n, unit)}.
753 '''
754 return self.s, self.c, self.n, self.unit
756 def shift(self, q=0, **unit_name):
757 '''Shift this C{Ang} by C{q} I{quarter} turns (C{scalar}).
758 '''
759 kwds = self._kwds(unit_name)
760 if _isfinite(q):
761 s = self.copy(**kwds)
762 if q:
763 s -= Ang.cardinal(q)
764 else:
765 s = Ang.NAN(**kwds)
766 return s
768 def signOf(self, *n):
769 '''Determine this C{Ang}'s sign, optionally replacing the turns.
771 @return: The sign (C{int}, -1, 0 or +1).
772 '''
773 return _signOf(self.toDegrees(*n), 0)
775 @Property_RO
776 def t(self):
777 '''Get the tangent of this C{Ang} (C{float}).
778 '''
779 return _over(*self.sc2)
781 def toDegrees(self, *n):
782 '''Return this C{Ang} as C{Degrees}, optionally replacing the turns.
783 '''
784 if n:
785 d = self.degrees0
786 n = float(n[0])
787 if n:
788 d += n * _360_0
789 else:
790 d = self.degrees
791 return Degrees(d, self.name)
793 def toLambertian(self, **name):
794 '''Return this C{Ang} as L{Lambertian}.
795 '''
796 name = _xkwds(name, name=self.name)
797 return Lambertian(self.lambertian, **name)
799 def toRadians(self, *n):
800 '''Return this C{Ang} as C{Radians}, optionally replacing the turns.
801 '''
802 if n:
803 r = self.radians0
804 n = float(n[0])
805 if n:
806 r += n * PI2
807 else:
808 r = self.radians
809 return Radians(r, self.name)
811 def toRepr(self, *n, **prec_fmt): # PYCHOK signature
812 '''Return this C{Ang} as C{"<name>(<value>)"} with/out turns (C{str}).
813 '''
814 return self.toUnit(*n).toRepr(**prec_fmt)
816 def toStr(self, *n, **prec_fmt): # PYCHOK signature
817 '''Return this C{Ang} as C{"<value>"} with/out turns (C{str}).
818 '''
819 return self.toUnit(*n).toStr(**prec_fmt)
821 def toTuple(self, **prec_fmt_sep):
822 '''Return string C{"(s, c, n)"} or tuple C{('s', 'c', 'n')} if C{sep is None}.
823 '''
824 return fstr(self.scn3, **prec_fmt_sep)
826 def toUnit(self, *n):
827 '''Return this C{Ang} as C{self.unit}s, optionally replacing the turns.
828 '''
829 u = self.unit
830 return self.toRadians(*n) if u is Radians else (
831 self.toDegrees(*n) if u is Degrees else (
832 self.toLambertian() if u is Lambertian else
833 _raiseError(self.toUnit, u))) # PYCHOK indent
835 @property_doc_(' the scalar unit to L{Degrees} or L{Radians}')
836 def unit(self):
837 return self._unit
839 @unit.setter # PYCHOK setter!
840 def unit(self, unit):
841 if unit not in _Ang_types: # PYCHOK no cover
842 _raiseError(Ang.unit, unit)
843 if self._unit != unit:
844 self._unit = unit
846 def _update(self, *sc):
847 if sc:
848 if sc == self.sc2:
849 return self
850 self._s, self._c = sc
851 _update_all(self)
852 return self
854_allPropertiesOf_n(14, Ang) # PYCHOK assert
857class _Ang3Tuple(_NamedTuple):
858 '''(INTERNAL) Methods C{.toDegrees}, C{.toLambertian}, C{.toRadians} and C{.toUnit}.
859 '''
860 _Names_ = (Ang.__name__,) * 3 # needed for ...
861 _Units_ = Ang, Ang, _Pass # ...testNamedTuples
863 def toDegrees(self, *n, **fmt_prec_sep):
864 '''Change any C{Ang} to C{unit Degrees} or to C{Degrees.toStr} if any B{C{fmt_prec_sep}}.
865 '''
866 t = self.toUnit(Degrees, *n)
867 if fmt_prec_sep: # see C{Degrees.toStr}
868 sep, fmt_prec = _xkwds_pop2(fmt_prec_sep, sep=_COMMASPACE_)
869 s = self.toStr(sep=None) if sep else self
870 t = (a.toStr(**fmt_prec) if isAng(a) else s for a, s in zip(t, s))
871 t = Fmt.PAREN(sep.join(t)) if sep else tuple(t)
872 return t
874 def toLambertian(self):
875 '''Change any C{Ang} to C{unit Lambertian}.
876 '''
877 return self.toUnit(Lambertian)
879 def toRadians(self, *n):
880 '''Change any C{Ang} to C{unit Radians}.
881 '''
882 return self.toUnit(Radians, *n)
884 def toUnit(self, unit, *n):
885 '''Change any C{Ang} to C{unit}, .
886 '''
887 for a in self:
888 if isAng(a): # and a.unit is not unit:
889 a.unit = unit
890 if n:
891 a.n = n[0]
892 return self
895class Lambertian(Radians):
896 '''A C{Lambertian} in C{radians}.
897 '''
898 def __new__(cls, *args, **kwds):
899 return Radians.__new__(cls, *args, **_xkwds(kwds, name='psi'))
902_Ang_from = {Radians: Ang.fromRadians,
903 Degrees: Ang.fromDegrees,
904 Lambertian: Ang.fromLambertian}
905_Ang_types = tuple(_Ang_from.keys()) # PYCHOK used!
908def Ang_(s, c=None, n=1, **unit_name):
909 '''(INTERNAL) New, non-normal C{Ang}.
910 '''
911 return Ang(s, c, n, **_xkwds(unit_name, normal=False))
914def Deg(deg, **name):
915 '''Return an L{Ang} from C{deg} degrees or an other L{Ang}.
916 '''
917 return Ang(deg, unit=Degrees, **name)
920def isAng(ang):
921 '''Is C{ang} an L{Ang} instance?
922 '''
923 return isinstance(ang, Ang)
926def Rad(rad, **name):
927 '''Return an L{Ang} from C{rad} radians or an other L{Ang}.
928 '''
929 return Ang(rad, unit=Radians, **name)
932def _SinCos2(ang, *unit):
933 '''Get C{sin} and C{cos} of an L{Ang}, any I{typed} C{ang}le
934 or C{unit} if C{ang}le is scalar.
936 @see: Function L{SinCos2<pygeodesy.utily.SinCos2>}.
937 '''
938 return ang.sc2 if isAng(ang) else SinCos2(ang, *unit)
940# **) MIT License
941#
942# Copyright (C) 2025-2026 -- mrJean1 at Gmail -- All Rights Reserved.
943#
944# Permission is hereby granted, free of charge, to any person obtaining a
945# copy of this software and associated documentation files (the "Software"),
946# to deal in the Software without restriction, including without limitation
947# the rights to use, copy, modify, merge, publish, distribute, sublicense,
948# and/or sell copies of the Software, and to permit persons to whom the
949# Software is furnished to do so, subject to the following conditions:
950#
951# The above copyright notice and this permission notice shall be included
952# in all copies or substantial portions of the Software.
953#
954# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
955# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
956# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
957# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
958# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
959# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
960# OTHER DEALINGS IN THE SOFTWARE.