Coverage for pygeodesy/angles.py: 95%

492 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-10 16:16 -0500

1 

2# -*- coding: utf-8 -*- 

3 

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. 

6 

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>}. 

9 

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 ; 

16 

17from pygeodesy.basics import _copysign, map1, signBit, _signOf 

18from pygeodesy.constants import EPS, EPS0, NAN, PI2, _0_0, _N_0_0, _0_25, _1_0, \ 

19 _N_1_0, _4_0, _360_0, _copysign_0_0, _over, \ 

20 _copysign_1_0, _flipsign, _isfinite, _pos_self 

21from pygeodesy.errors import _xkwds, _xkwds_get, _xkwds_pop2 

22from pygeodesy.fmath import ftruncate as _fint, hypot, _ALL_LAZY, _MODS 

23# from pygeodesy.interns import NN, _COMMASPACE_ # from .streprs 

24from pygeodesy.karney import _remainder 

25# from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .fmath 

26from pygeodesy.named import _Named, _NamedTuple, _Pass 

27from pygeodesy.props import Property_RO, property_doc_, property_RO, \ 

28 _allPropertiesOf_n, _update_all 

29from pygeodesy.streprs import Fmt, fstr, unstr, NN, _COMMASPACE_ 

30from pygeodesy.units import Degrees, _isDegrees, _isRadians, Radians 

31from pygeodesy.utily import atan2, atan2d, sincos2, sincos2d, SinCos2 

32 

33from math import asinh, ceil as _ceil, fabs, floor as _floor, \ 

34 isinf, isnan, sinh 

35 

36__all__ = _ALL_LAZY.angles 

37__version__ = '26.10.28' 

38 

39_EPS03 = EPS / (1 << 20) 

40# _HD = _180_0 

41# _QD = _90_0 

42# _TD = _360_0 

43# _DM = _SM = _60_0 

44# _DS = _3600_0 

45_ZRND = _1_0 / 1024 

46 

47_CARDINAL2 = {-2: (_N_0_0, _N_1_0), 

48 -1: (_N_1_0, _0_0), 

49 1: ( _1_0, _0_0), 

50 2: ( _0_0, _N_1_0)}.get 

51 

52 

53def _ncardinal(s, c, n): 

54 if n: 

55 n *= _4_0 

56 i = (1 if (-c) < fabs(s) else 2) if signBit(c) else \ 

57 (1 if c < fabs(s) else 0) 

58 if i: 

59 n += _copysign(i, s) 

60 return n 

61 

62 

63def _normalize2(s, c): 

64 h = hypot(s, c) 

65 if _isfinite(h): 

66 sc = ((s / h), (c / h)) if h else ( 

67 # If y is +/-0 and x = -0, +/-pi is returned, 

68 # or y is +/-0 and x = +0, +/-0 is returned, 

69 # so, retain the sign of s = +/-0 

70 _orthogonal2(False, s, c)) 

71 elif isnan(h) or (isinf(s) and isinf(c)): 

72 sc = NAN, NAN 

73 else: 

74 sc = _orthogonal2(isinf(s), s, c) 

75 return sc 

76 

77 

78def _orthogonal2(pred, s, c): 

79 return (_copysign_1_0(s), _copysign_0_0(c)) if pred else \ 

80 (_copysign_0_0(s), _copysign_1_0(c)) 

81 

82 

83def _other(x, unit=Radians, **unused): 

84 # get C{x} as C{Ang} from C{Degrees}, C{Radians} or C{Lambertian} 

85 return Ang.fromLambertian(x) if unit is Lambertian else ( 

86 Ang.fromRadians(x) if _isRadians(x, iscalar=unit is Radians) else ( 

87 Ang.fromDegrees(x) if _isDegrees(x, iscalar=unit is Degrees) else 

88 _raiseError(unit, x))) # PYCHOK indent 

89 

90 

91def _raiseError(unit, arg, **kwds): 

92 raise TypeError(unstr(unit, arg, **kwds)) 

93 

94 

95def _rnd(x): 

96 w = _ZRND - fabs(x) 

97 if w > 0: 

98 x = _copysign(_ZRND - w, x) 

99 return x 

100 

101 

102def _scnu4(s, c, n, unit=Radians, **unused): # unit=Ang._unit 

103 s, c, n = map1(float, s, c, n) 

104 return _normalize2(s, c) + (n, unit) 

105 

106 

107class Ang(_Named): 

108 '''An accurate representation of angles, as 3-tuple C{(s, c, n)}. 

109 

110 This class represents an angle via its sine C{s}, cosine C{c} and 

111 the number of full turns C{n}. The angle is then C{atan2(s, c) + 

112 n * PI2}. This representation offers several advantages: 

113 

114 - cardinal directions (multiples of 90 degrees) are exactly represented 

115 (a benefit shared by representing angles as degrees) 

116 

117 - angles very close to any cardinal direction are accurately represented 

118 

119 - there's no loss of precision with large angles (outside the "normal" 

120 range [-180, +180]) 

121 

122 - various operations, such as adding a multiple of 90 degrees to an 

123 angle are performed exactly. 

124 

125 @note: B{C{n}} is a C{float}, this allows it to be NAN, INF or NINF. 

126 ''' 

127 _unit = Radians # see _scnu4 

128 

129 def __init__(self, s_ang=0, c=None, n=0, normal=True, **unit_name): 

130 '''New L{Ang}. 

131 

132 @kwarg s_ang: A previous L{Ang}, C{Degrees}, C{Radians} if C{B{c} 

133 is None}, otherwise the sine component (C{float}). 

134 @kwarg c: The cosine component (C{float}) iff C{not None}. 

135 @kwarg n: The number of L{PI2} turns (C{float}). 

136 @kwarg normal: If C{True}, B{C{s}} and B{C{c}} are normalized, i.e. 

137 on the unit circle (C{boo}). 

138 @kwarg unit_name: Type C{B{unit}=}L{Radians} or L{Degrees} of scalar 

139 scalar values (L{Degrees} or L{Radians}). 

140 

141 @note: Either B{C{s}} or B{C{c}} can be INF or NINF, but not both. 

142 

143 @note: By default, the point B{C{(s, c)}} is scaled to lie on the 

144 unit circle. 

145 ''' 

146 s, c, n, u = s_ang.scnu4 if isAng(s_ang) else ( 

147 _other(s_ang, **unit_name).scnu4 if c is None else 

148 _scnu4(s_ang, c, n, **unit_name)) 

149 if unit_name: # Error=... from _NamedTuple.toUnits() 

150 u = _xkwds_get(unit_name, unit=u) 

151 name = _xkwds_get(unit_name, name=NN) 

152 if name: 

153 self.name = name 

154 self._n = _fint(n) 

155 self._s, self._c = (s, c) if normal else _normalize2(s, c) 

156 self.unit = u 

157 

158 def __abs__(self): 

159 s, _ = self._float2() 

160 return self._float1(fabs(s)) 

161 

162 def __add__(self, other): 

163 return self.copy().__iadd__(other) 

164 

165 def __bool__(self): # PYCHOK Python 3+ 

166 s, c, n = self.scn3 

167 return bool(s or c or n) 

168 

169# def __call__(self, *args, **kwds): # PYCHOK no cover 

170# return self._NotImplemented(*args, **kwds) 

171 

172 def __ceil__(self): # PYCHOK not special in Python 2- 

173 s, _ = self._float2() 

174 return self._float1(_ceil(s)) 

175 

176 def __cmp__(self, other): # PYCHOK no cover 

177 s, r = self._float2(other) 

178 return _signOf(s, r) # -1, 0, +1 

179 

180 def __divmod__(self, other): 

181 s, r = self._float2(other) 

182 q, r = divmod(s, r) 

183 return q, self._float1(r) 

184 

185 def __eq__(self, other): 

186 s, r = self._float2(other) 

187 return fabs(s - r) < EPS0 

188 

189 def __float__(self): 

190 u = self.unit 

191 return self.radians if u is Radians else ( 

192 self.degrees if u is Degrees else ( 

193 self.lambertian if u is Lambertian else 

194 _raiseError(float, u))) # PYCHOK indent 

195 

196 def __floor__(self): # PYCHOK not special in Python 2- 

197 s, _ = self._float2() 

198 return self._float1(_floor(s)) 

199 

200 def __floordiv__(self, other): 

201 return self.copy().__ifloordiv__(other) 

202 

203# def __format__(self, *other): # PYCHOK no cover 

204# return self._NotImplemented(self, *other) 

205 

206 def __ge__(self, other): 

207 s, r = self._float2(other) 

208 return s >= r 

209 

210 def __gt__(self, other): 

211 s, r = self._float2(other) 

212 return s > r 

213 

214 def __hash__(self): # PYCHOK no cover 

215 # @see: U{Notes for type implementors<https://docs.Python.org/ 

216 # 3/library/numbers.html#numbers.Rational>} 

217 return hash(self.scn3) # tuple.__hash__() 

218 

219 def __iadd__(self, other): 

220 p = self._other(other) 

221 q = p.ncardinal + self.ncardinal 

222 s, c, n = self.scn3 

223 s, c = _normalize2(s * p.c + c * p.s, 

224 c * p.c - s * p.s) 

225 q -= _ncardinal(s, c, n) 

226 n = _fint(q * _0_25) + p.n 

227 if n: 

228 self._n += n 

229 self._s = s 

230 self._c = c 

231 _update_all(self) 

232 return self._update(s, c) 

233 

234 def __ifloordiv__(self, other): 

235 s, r = self._float2(other) 

236 return self._ifloat(s // r) 

237 

238 def __imatmul__(self, other): # PYCHOK no cover 

239 return self._notImplemented() 

240 

241 def __imod__(self, other): 

242 s, r = self._float2(other) 

243 return self._ifloat(s % r) 

244 

245 def __imul__(self, other): 

246 s, r = self._float2(other) 

247 return self._ifloat(s * r) 

248 

249 def __int__(self): 

250 s, _ = self._float2(0) 

251 return int(s) 

252 

253 def __invert__(self): # PYCHOK no cover 

254 # Luciano Ramalho, "Fluent Python", O'Reilly, 2nd Ed, 2022 p. 567 

255 return self._notImplemented() 

256 

257 def __ipow__(self, other, *mod): # PYCHOK 2 vs 3 args 

258 s, r = self._float2(other) 

259 return self._ifloat(pow(s, r, *mod)) 

260 

261 def __isub__(self, other): 

262 return self.__iadd__(-other) 

263 

264# def __iter__(self): 

265# ''' 

266# return self._NotImplemented() 

267 

268 def __itruediv__(self, other): 

269 s, r = self._float2(other) 

270 return self._ifloat(s / r) 

271 

272 def __le__(self, other): 

273 s, r = self._float2(other) 

274 return s <= r 

275 

276 def __lt__(self, other): 

277 s, r = self._float2(other) 

278 return s < r 

279 

280 def __matmul__(self, other): # PYCHOK no cover 

281 return self._notImplemented(other) 

282 

283 def __mod__(self, other): 

284 s, r = self._float2(other) 

285 return self._float1(s % r) 

286 

287 def __mul__(self, other): 

288 return self.copy().__imul__(other) 

289 

290 def __ne__(self, other): 

291 return not self.__eq__(other) 

292 

293 def __neg__(self): 

294 s, c, n = self.scn3 

295 s, n = _flipsign(s), _flipsign(n) 

296 return self._Ang(s, c, n) # normal=True 

297 

298 def __pos__(self): 

299 return self if _pos_self else self.copy() 

300 

301 def __pow__(self, other, *mod): # PYCHOK 2 vs 3 args 

302 return self.copy().__ipow__(other, *mod) 

303 

304 def __radd__(self, other): 

305 return self._other(other) + self 

306 

307 def __rdivmod__(self, other): 

308 return divmod(self._other(other), self) 

309 

310 def __repr__(self): 

311 return self.toRepr() 

312 

313 def __rfloordiv__(self, other): 

314 return self._other(other) // self 

315 

316 def __rmatmul__(self, other): # PYCHOK no cover 

317 return self._notImplemented(self, other) 

318 

319 def __rmod__(self, other): 

320 return self._other(other) % self 

321 

322 def __rmul__(self, other): 

323 return self._other(other) * self 

324 

325 def __round__(self, *ndigits): # PYCHOK Python 3+ 

326 return self.round(*ndigits) 

327 

328 def __rpow__(self, other, *mod): 

329 return pow(self._other(other), self, *mod) 

330 

331 def __rsub__(self, other): 

332 return self._other(other) - self 

333 

334 def __rtruediv__(self, other): 

335 return self._other(other) / self 

336 

337 def __str__(self): 

338 return self.toStr(0) # ignore turns 

339 

340 def __sub__(self, other): 

341 return self.copy().__isub__(other) 

342 

343 def __truediv__(self, other): 

344 return self.copy().__itruediv__(other) 

345 

346 __trunc__ = __int__ 

347 

348 if _MODS.sys_version_info2 < (3, 0): # PYCHOK no cover 

349 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions> 

350 __div__ = __truediv__ 

351 __idiv__ = __itruediv__ 

352 __long__ = __int__ 

353 __nonzero__ = __bool__ 

354 __rdiv__ = __rtruediv__ 

355 

356 def _Ang(self, s, *cn, **normal_unit_name): 

357 # return an C{Ang} like C{self} 

358 return Ang(s, *cn, **self._kwds(normal_unit_name)) 

359 

360 def base(self, *center): 

361 '''Return this C{Angle}'s base, optionally centered. 

362 ''' 

363 r = self.copy() 

364 if center: 

365 c = self._other(center[0]) 

366 b = self - c 

367 b = b.base() 

368 b += c 

369 r.n0 = b.n0 

370 else: 

371 r.n = 0 

372 return r 

373 

374 @property_RO 

375 def c(self): 

376 '''Get the cosine of this C{Angle} (C{float}). 

377 ''' 

378 return self._c 

379 

380 @staticmethod 

381 def cardinal(q=0, **unit_name): 

382 '''A cardinal direction. 

383 

384 @kwarg q: The number of I{quarter} turns (C{scalar}). 

385 

386 @return: An C{Ang} equivalent to B{C{q}} quarter turns. 

387 

388 @note: B{C{q}} is truncated to an integer and signed 

389 C{0} is distinguished. C{Ang.NAN} is returned 

390 if B{C{q}} is not finite. 

391 ''' 

392 if _isfinite(q): 

393 if q: 

394 q = _fint(q) 

395 i = int(_remainder(q, _4_0)) # i is in [-2, 2] 

396 n = _fint((q - i) * _0_25) 

397 s, c = _CARDINAL2(i, ((_0_0 if q else q), _1_0)) 

398 t = s is not q 

399 else: 

400 s, c, n, t = _copysign_0_0(q), 1, 0, True 

401 r = Ang(s, c, n, normal=t, **unit_name) 

402 else: 

403 r = Ang.NAN(**unit_name) 

404 return r 

405 

406 def copy(self, **unit_name): # PYCHOK signature 

407 '''Return a copy of this C{Ang}. 

408 ''' 

409 return self._Ang(self, **self._kwds(unit_name)) 

410 

411 @Property_RO 

412 def degrees(self): 

413 '''Get this C{Ang} in C{degrees}. 

414 ''' 

415 d = self.degrees0 

416 if self.n: 

417 d += self.n * _360_0 

418 return d # XXX Degrees(d, self.name) 

419 

420 @Property_RO 

421 def degrees0(self): 

422 '''Get this C{Ang} in C{degrees} ignoring the turns. 

423 ''' 

424 return atan2d(*self.sc2) # XXX Degrees(d, self.name) 

425 

426 divmod = __divmod__ 

427 

428 @staticmethod 

429 def EPS0(**unit_name): 

430 '''Get a tiny C{Ang}. 

431 

432 @note: This allows angles extremely close to the cardinal 

433 directions to be generated. The C{.round} method 

434 will flush this angle to C{0}. 

435 ''' 

436 return Ang(_EPS03, 1, **unit_name) 

437 

438 @staticmethod 

439 def _flip(bet, omg, alp=None): 

440 '''(INTERNAL) Reflect C{bet}, C{omg} and C{alp} inplace. 

441 ''' # Ellipsoid3.Flip 

442 bet.reflect(flipc=True) 

443 omg.reflect(flips=True) 

444 if alp: 

445 alp.reflect(flips=True, flipc=True) 

446 

447 def flipsign(self, mul=-1, **name): 

448 '''Copy this C{Ang} with sign flipped. 

449 ''' 

450 r = (-self) if signBit(mul) else self 

451 return self._Ang(r, **name) if name else r 

452 

453 def _float1(self, f, **name): 

454 # return C{f} as C{Ang} in this C{unit} 

455 return _Ang_from[self.unit](f, **name) 

456 

457 def _float2(self, other=None): 

458 # get self and C{other} as floats 

459 r = other if other is None or isinstance(other, int) else \ 

460 float(_Ang_from[self.unit](other)) 

461 return float(self), r 

462 

463 def _ifloat(self, f): # PYCHOK expected 

464 # set self to C{f} degrees or radians 

465 scn = self._float1(f).scn3 

466 self._s, self._c, self._n = scn 

467 return self._update() 

468 

469 @staticmethod 

470 def fromDegrees(deg, **unit_name): 

471 '''Get an C{Ang} from degrees. 

472 ''' 

473 if isAng(deg): 

474 s, c, n = deg.scn3 

475 d = deg.degrees0 

476 elif _isDegrees(deg, iscalar=True): 

477 s, c = sincos2d(deg) 

478 d = atan2d(s, c) 

479 n = round((deg - d) / _360_0) 

480 else: 

481 _raiseError(Ang.fromDegrees, deg, **unit_name) 

482 a = Ang(s, c, n, **_xkwds(unit_name, unit=Degrees)) # PYLINT E0606 

483 a.__dict__.update(degrees0=d) # Property_RO 

484 return a 

485 

486 @staticmethod 

487 def fromLambertian(psi, **unit_name): 

488 '''Get an C{Ang} from C{lamberterian} radians. 

489 ''' 

490 s = psi.lambertian if isAng(psi) else sinh(psi) 

491 return Ang(s, 1, normal=False, **_xkwds(unit_name, unit=Lambertian)) 

492 

493 @staticmethod 

494 def fromRadians(rad, **unit_name): 

495 '''Get an C{Ang} from radians. 

496 ''' 

497 if isAng(rad): 

498 s, c, n = rad.scn3 

499 r = rad.radians0 

500 elif _isRadians(rad, iscalar=True): 

501 s, c = sincos2(rad) 

502 r = atan2(s, c) 

503 n = round((rad - r) / PI2) 

504 else: 

505 _raiseError(Ang.fromRadians, rad, **unit_name) 

506 a = Ang(s, c, n, **_xkwds(unit_name, unit=Radians)) # PYLINT E0606 

507 a.__dict__.update(radians0=r) # Property_RO 

508 return a 

509 

510 @staticmethod 

511 def fromScalar(ang, **unit_name): 

512 '''Get an C{Ang} from C{Degrees}, C{Radians} or another C{Ang}. 

513 ''' 

514 if isAng(ang): 

515 r = Ang(ang, **_xkwds(unit_name, unit=ang.unit)) 

516 else: 

517 u = _xkwds_get(unit_name, unit=None) 

518 if u is Lambertian: 

519 r = Ang.fromLambertian(ang, **unit_name) 

520 elif _isDegrees(ang, iscalar=u is Degrees): 

521 r = Ang.fromDegrees(ang, **unit_name) 

522 elif _isRadians(ang, iscalar=u is Radians): 

523 r = Ang.fromRadians(ang, **unit_name) 

524 else: 

525 _raiseError(Ang.fromScalar, ang, **unit_name) 

526 return r # PYLINT E0606 

527 

528 def is_integer(self, *n): 

529 '''Is this C{Ang}'s degrees C{integer}? (C{bool}). 

530 ''' 

531 return self.toDegrees(*n).is_integer() 

532 

533 def isnear0(self, eps0=EPS0): # aka zerop 

534 '''Is this C{Ang} near C{0} within a tolerance? 

535 ''' 

536 s, c, n = self.scn3 

537 return bool(n == 0 and c > 0 and fabs(s) <= eps0) 

538 

539 def _kwds(self, kwds, **dflt): 

540 return _xkwds(kwds, **_xkwds(dflt, unit=self.unit, 

541 name=self.name)) 

542 

543 @Property_RO 

544 def lambertian(self): 

545 '''Get this C{Ang}'s Lambertian, C{asinh(tan(radians))}. 

546 ''' 

547 return asinh(self.t) # XXX Lambertian(self.t) 

548 

549 def mod(self, mul=_1_0, **unit_name): 

550 '''Return the I{reduced latitude} C{atan(B{mul} * 

551 tan(B{this}))} as an C{Ang}. 

552 

553 @arg mul: Factor (C{scalar}, positive). 

554 

555 @note: The quadrant of the result tracks that of 

556 this C{Ang} through multiples turns. 

557 ''' 

558 kwds = self._kwds(unit_name) 

559 if signBit(mul): 

560 r = self._Ang(Ang.NAN(), **kwds) 

561 else: 

562 s, c, n = self.scn3 

563 if mul > 1: 

564 c = c / mul # /= chokes PyChecker 

565 else: # mul <= 1 

566 s *= mul 

567 r = self._Ang(s, c, n, normal=False, **kwds) 

568 return r 

569 

570 @staticmethod 

571 def N(**unit_name): 

572 '''Get North C{Ang}. 

573 ''' 

574 return Ang(0, 1, **unit_name) 

575 

576 @property 

577 def n(self): 

578 '''Return the number of turns (C{float}) or C{0.0}. 

579 ''' 

580 return self._n or _0_0 

581 

582 @n.setter # PYCHOK setter! 

583 def n(self, n): 

584 self._n_0(_fint(n)) 

585 

586 def _n_0(self, n): 

587 '''(INTERNAL) Set C{n} or C{n0}. 

588 ''' 

589 if self._n != n: 

590 self._n, n = n, self._n 

591 self._update() 

592 return n 

593 

594 @property 

595 def n0(self): 

596 '''Return the number of turns, treating C{-180} as C{180 - 1 turn} (C{float}). 

597 ''' 

598 return (self.n - self._n01) or _0_0 

599 

600 @n0.setter # PYCHOK setter! 

601 def n0(self, n): 

602 self._n_0(_fint(n) + self._n01) 

603 

604 @Property_RO 

605 def _n01(self): 

606 s, c = self.sc2 

607 return int(c < 0 and s == 0 and signBit(s)) 

608 

609 @staticmethod 

610 def NAN(**unit_name): 

611 '''Get an invalid C{Ang}. 

612 ''' 

613 return Ang(NAN, NAN, **unit_name) 

614 

615 @Property_RO 

616 def ncardinal(self): 

617 '''Get the nearest cardinal direction (C{float_int}). 

618 

619 @note: This is the reverse of C{cardinal}. 

620 ''' 

621 return _ncardinal(*self.scn3) 

622 

623 def nearest(self, ind=0, **name): 

624 '''Return the closest cardinal direction (C{Ang}). 

625 

626 @arg ind: An indicator, if C{B{ind}=0} the closest cardinal 

627 direction, otherwise, if B{C{ind}} is even, the 

628 closest even (N/S) cardinal direction or if B{C{ind}} 

629 is odd, the closest odd (E/W) cardinal direction. 

630 ''' 

631 s, c, n = self.scn3 

632 p = (ind == 0 and fabs(s) > fabs(c)) or (ind & 1) 

633 s, c = _orthogonal2(p, s, c) 

634 return self._Ang(s, c, n, **self._kwds(name)) 

635 

636 @staticmethod 

637 def _norm(bet, omg, alp=None, alt=False): 

638 '''(INTERNAL) Put C{bet}, C{ong} and C{alp} in range. 

639 ''' # Ellipsoid3.AngNorm 

640 flip = signBit(omg.s if alt else bet.c) 

641 if flip: 

642 Ang._flip(bet, omg, alp) 

643 return flip 

644 

645 def normalize(self, *n): 

646 '''Re-normalize this C{Ang}, optionally replacing turns. 

647 ''' 

648 sc = _normalize2(*self.sc2) 

649 if n: 

650 self.n, n = n[0], self.n 

651 if self.n != n: # updated 

652 self._s, self._c = sc 

653 return self 

654 return self._update(*sc) 

655 

656 def _other(self, other): 

657 # get C{other} as C{Ang} from C{unit} 

658 return other if isAng(other) else _other(other, self.unit) 

659 

660 pow = __pow__ 

661 

662 @Property_RO 

663 def _quadrant(self): 

664 s, c = map(int, map(signBit, self.sc2)) 

665 return s + s + (c ^ s) 

666 

667 @property_doc_("this C{Ang}'s quadrant (C{int} 0..3)") 

668 def quadrant(self): 

669 return self._quadrant 

670 

671 @quadrant.setter # PYCHOK setter! 

672 def quadrant(self, quadrant): 

673 s, c = map(fabs, self.sc2) 

674 q = int(quadrant) 

675 if (q & 2): 

676 s = -s # _copysign(self.s, -1 if (q & 2) else 1) 

677 if (((q >> 1) ^ q) & 1): 

678 c = -c # _copysign(self.c, -1 if (((q >> 1) ^ q) & 1) else 1) 

679 self._update(s, c) 

680 

681 @Property_RO 

682 def radians(self): 

683 '''Get this C{Ang} in C{radians}. 

684 ''' 

685 r = self.radians0 

686 if self.n: 

687 r += self.n * PI2 

688 return r # XXX Radians(r, self.name) 

689 

690 @Property_RO 

691 def radians0(self): 

692 '''Get this C{Ang} in C{radians} ignoring the turns. 

693 ''' 

694 return atan2(*self.sc2) # XXX Radians(r, self.name) 

695 

696 def reflect(self, flips=False, flipc=False, swapsc=False): 

697 '''Reflect this C{Ang} in various ways. 

698 

699 @kwarg flips: Flip the sign of C{s}. 

700 @kwarg flipc: Flip the sign of C{c}. 

701 @kwarg swapsc: Swap C{s} and C{c}. 

702 

703 @note: The operations are carried out in the order 

704 of the arguments. 

705 ''' 

706 s, c = self.sc2 

707 if flips: 

708 s = -s 

709 if flipc: 

710 c = -c 

711 if swapsc: 

712 s, c = c, s 

713 return self._update(s, c) 

714 

715 def round(self, *ndigits, **name): 

716 '''Return this C{Ang}, optionally rounded to C{ndigits} (C{Ang}). 

717 ''' 

718 s, c, n = self.scn3 

719 if ndigits: 

720 s = round(s, *ndigits) 

721 c = round(c, *ndigits) 

722 else: 

723 s, c = map1(_rnd, s, c) 

724 return self._Ang(s, c, n, **self._kwds(name)) 

725 

726 @property_RO 

727 def s(self): 

728 '''Get the sine of this C{Ang} (C{float}). 

729 ''' 

730 return self._s 

731 

732 @property_RO 

733 def sc2(self): 

734 '''Get the 2-tuple C{(s, c)}. 

735 ''' 

736 return self.s, self.c 

737 

738 @Property_RO 

739 def scn3(self): 

740 '''Get the 3-tuple C{(s, c, n)}. 

741 ''' 

742 return self.s, self.c, self.n 

743 

744 @property_RO 

745 def scnu4(self): 

746 '''Get the 4-tuple C{(s, c, n, unit)}. 

747 ''' 

748 return self.s, self.c, self.n, self.unit 

749 

750 def shift(self, q=0, **unit_name): 

751 '''Shift this C{Ang} by C{q} I{quarter} turns (C{scalar}). 

752 ''' 

753 kwds = self._kwds(unit_name) 

754 if _isfinite(q): 

755 s = self.copy(**kwds) 

756 if q: 

757 s -= Ang.cardinal(q) 

758 else: 

759 s = Ang.NAN(**kwds) 

760 return s 

761 

762 def signOf(self, *n): 

763 '''Determine this C{Ang}'s sign, optionally replacing the turns. 

764 

765 @return: The sign (C{int}, -1, 0 or +1). 

766 ''' 

767 return _signOf(self.toDegrees(*n), 0) 

768 

769 @Property_RO 

770 def t(self): 

771 '''Get the tangent of this C{Ang} (C{float}). 

772 ''' 

773 return _over(*self.sc2) 

774 

775 def toDegrees(self, *n): 

776 '''Return this C{Ang} as C{Degrees}, optionally replacing the turns. 

777 ''' 

778 if n: 

779 d = self.degrees0 

780 n = float(n[0]) 

781 if n: 

782 d += n * _360_0 

783 else: 

784 d = self.degrees 

785 return Degrees(d, self.name) 

786 

787 def toLambertian(self, **name): 

788 '''Return this C{Ang} as L{Lambertian}. 

789 ''' 

790 name = _xkwds(name, name=self.name) 

791 return Lambertian(self.lambertian, **name) 

792 

793 def toRadians(self, *n): 

794 '''Return this C{Ang} as C{Radians}, optionally replacing the turns. 

795 ''' 

796 if n: 

797 r = self.radians0 

798 n = float(n[0]) 

799 if n: 

800 r += n * PI2 

801 else: 

802 r = self.radians 

803 return Radians(r, self.name) 

804 

805 def toRepr(self, *n, **prec_fmt): # PYCHOK signature 

806 '''Return this C{Ang} as C{"<name>(<value>)"} with/out turns (C{str}). 

807 ''' 

808 return self.toUnit(*n).toRepr(**prec_fmt) 

809 

810 def toStr(self, *n, **prec_fmt): # PYCHOK signature 

811 '''Return this C{Ang} as C{"<value>"} with/out turns (C{str}). 

812 ''' 

813 return self.toUnit(*n).toStr(**prec_fmt) 

814 

815 def toTuple(self, **prec_fmt_sep): 

816 '''Return string C{"(s, c, n)"} or tuple C{('s', 'c', 'n')} if C{sep is None}. 

817 ''' 

818 return fstr(self.scn3, **prec_fmt_sep) 

819 

820 def toUnit(self, *n): 

821 '''Return this C{Ang} as C{self.unit}s, optionally replacing the turns. 

822 ''' 

823 u = self.unit 

824 return self.toRadians(*n) if u is Radians else ( 

825 self.toDegrees(*n) if u is Degrees else ( 

826 self.toLambertian() if u is Lambertian else 

827 _raiseError(self.toUnit, u))) # PYCHOK indent 

828 

829 @property_doc_(' the scalar unit to L{Degrees} or L{Radians}') 

830 def unit(self): 

831 return self._unit 

832 

833 @unit.setter # PYCHOK setter! 

834 def unit(self, unit): 

835 if unit not in _Ang_types: # PYCHOK no cover 

836 _raiseError(Ang.unit, unit) 

837 if self._unit != unit: 

838 self._unit = unit 

839 

840 def _update(self, *sc): 

841 if sc: 

842 if sc == self.sc2: 

843 return self 

844 self._s, self._c = sc 

845 _update_all(self) 

846 return self 

847 

848_allPropertiesOf_n(14, Ang) # PYCHOK assert 

849 

850 

851class _Ang3Tuple(_NamedTuple): 

852 '''(INTERNAL) Methods C{.toDegrees}, C{.toLambertian}, C{.toRadians} and C{.toUnit}. 

853 ''' 

854 _Names_ = (Ang.__name__,) * 3 # needed for ... 

855 _Units_ = Ang, Ang, _Pass # ...testNamedTuples 

856 

857 def toDegrees(self, *n, **fmt_prec_sep): 

858 '''Change any C{Ang} to C{unit Degrees} or to C{Degrees.toStr} if any B{C{fmt_prec_sep}}. 

859 ''' 

860 t = self.toUnit(Degrees, *n) 

861 if fmt_prec_sep: # see C{Degrees.toStr} 

862 sep, fmt_prec = _xkwds_pop2(fmt_prec_sep, sep=_COMMASPACE_) 

863 s = self.toStr(sep=None) if sep else self 

864 t = (a.toStr(**fmt_prec) if isAng(a) else s for a, s in zip(t, s)) 

865 t = Fmt.PAREN(sep.join(t)) if sep else tuple(t) 

866 return t 

867 

868 def toLambertian(self): 

869 '''Change any C{Ang} to C{unit Lambertian}. 

870 ''' 

871 return self.toUnit(Lambertian) 

872 

873 def toRadians(self, *n): 

874 '''Change any C{Ang} to C{unit Radians}. 

875 ''' 

876 return self.toUnit(Radians, *n) 

877 

878 def toUnit(self, unit, *n): 

879 '''Change any C{Ang} to C{unit}, optional name C{n}. 

880 ''' 

881 for a in self: 

882 if isAng(a): # and a.unit is not unit: 

883 a.unit = unit 

884 if n: 

885 a.n = n[0] 

886 return self 

887 

888 

889class Lambertian(Radians): 

890 '''A C{Lambertian} in C{radians}. 

891 ''' 

892 def __new__(cls, *args, **kwds): 

893 return Radians.__new__(cls, *args, **_xkwds(kwds, name='psi')) 

894 

895 

896_Ang_from = {Radians: Ang.fromRadians, 

897 Degrees: Ang.fromDegrees, 

898 Lambertian: Ang.fromLambertian} 

899_Ang_types = tuple(_Ang_from.keys()) # PYCHOK used! 

900 

901 

902def Ang_(s, c=None, n=1, **unit_name): 

903 '''(INTERNAL) New, non-normal C{Ang}. 

904 ''' 

905 return Ang(s, c, n, **_xkwds(unit_name, normal=False)) 

906 

907 

908def Deg(deg, **name): 

909 '''Return an L{Ang} from C{deg} degrees or an other L{Ang}. 

910 ''' 

911 return Ang(deg, unit=Degrees, **name) 

912 

913 

914def isAng(ang): 

915 '''Is C{ang} an L{Ang} instance? 

916 ''' 

917 return isinstance(ang, Ang) 

918 

919 

920def Rad(rad, **name): 

921 '''Return an L{Ang} from C{rad} radians or an other L{Ang}. 

922 ''' 

923 return Ang(rad, unit=Radians, **name) 

924 

925 

926def _SinCos2(ang, *unit): 

927 '''Get C{sin} and C{cos} of an L{Ang}, any I{typed} C{ang}le 

928 or C{unit} if C{ang}le is scalar. 

929 

930 @see: Function L{SinCos2<pygeodesy.utily.SinCos2>}. 

931 ''' 

932 return ang.sc2 if isAng(ang) else SinCos2(ang, *unit) 

933 

934# **) MIT License 

935# 

936# Copyright (C) 2025-2027 -- mrJean1 at Gmail -- All Rights Reserved. 

937# 

938# Permission is hereby granted, free of charge, to any person obtaining a 

939# copy of this software and associated documentation files (the "Software"), 

940# to deal in the Software without restriction, including without limitation 

941# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

942# and/or sell copies of the Software, and to permit persons to whom the 

943# Software is furnished to do so, subject to the following conditions: 

944# 

945# The above copyright notice and this permission notice shall be included 

946# in all copies or substantial portions of the Software. 

947# 

948# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

949# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

950# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

951# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

952# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

953# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

954# OTHER DEALINGS IN THE SOFTWARE.