Coverage for pygeodesy/angles.py: 95%

493 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-03-25 15:01 -0400

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, \ 

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 

33 

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

35 isinf, isnan, sinh 

36 

37__all__ = _ALL_LAZY.angles 

38__version__ = '25.12.02' 

39 

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 

47 

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 

52 

53 

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) 

58 

59 

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 

68 

69 

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 

83 

84 

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 

91 

92 

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)) 

96 

97 

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

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

100 

101 

102def _rnd(x): 

103 w = _ZRND - fabs(x) 

104 if w > 0: 

105 x = _copysign(_ZRND - w, x) 

106 return x 

107 

108 

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) 

112 

113 

114class Ang(_Named): 

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

116 

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: 

120 

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

122 (a benefit shared by representing angles as degrees) 

123 

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

125 

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

127 range [-180, +180]) 

128 

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

130 angle are performed exactly. 

131 

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

133 ''' 

134 _unit = Radians # see _scnu4 

135 

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

137 '''New L{Ang}. 

138 

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

147 

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

149 

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 

163 

164 def __abs__(self): 

165 s, _ = self._float2() 

166 return self._float1(fabs(s)) 

167 

168 def __add__(self, other): 

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

170 

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

172 s, c, n = self.scn3 

173 return bool(s or c or n) 

174 

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

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

177 

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

179 s, _ = self._float2() 

180 return self._float1(_ceil(s)) 

181 

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

183 s, r = self._float2(other) 

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

185 

186 def __divmod__(self, other): 

187 s, r = self._float2(other) 

188 q, r = divmod(s, r) 

189 return q, self._float1(r) 

190 

191 def __eq__(self, other): 

192 s, r = self._float2(other) 

193 return fabs(s - r) < EPS0 

194 

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 

201 

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

203 s, _ = self._float2() 

204 return self._float1(_floor(s)) 

205 

206 def __floordiv__(self, other): 

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

208 

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

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

211 

212 def __ge__(self, other): 

213 s, r = self._float2(other) 

214 return s >= r 

215 

216 def __gt__(self, other): 

217 s, r = self._float2(other) 

218 return s > r 

219 

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__() 

224 

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) 

239 

240 def __ifloordiv__(self, other): 

241 s, r = self._float2(other) 

242 return self._ifloat(s // r) 

243 

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

245 return self._notImplemented() 

246 

247 def __imod__(self, other): 

248 s, r = self._float2(other) 

249 return self._ifloat(s % r) 

250 

251 def __imul__(self, other): 

252 s, r = self._float2(other) 

253 return self._ifloat(s * r) 

254 

255 def __int__(self): 

256 s, _ = self._float2(0) 

257 return int(s) 

258 

259 def __invert__(self): # PYCHOK no cover 

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

261 return self._notImplemented() 

262 

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)) 

266 

267 def __isub__(self, other): 

268 return self.__iadd__(-other) 

269 

270# def __iter__(self): 

271# ''' 

272# return self._NotImplemented() 

273 

274 def __itruediv__(self, other): 

275 s, r = self._float2(other) 

276 return self._ifloat(s / r) 

277 

278 def __le__(self, other): 

279 s, r = self._float2(other) 

280 return s <= r 

281 

282 def __lt__(self, other): 

283 s, r = self._float2(other) 

284 return s < r 

285 

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

287 return self._notImplemented(other) 

288 

289 def __mod__(self, other): 

290 s, r = self._float2(other) 

291 return self._float1(s % r) 

292 

293 def __mul__(self, other): 

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

295 

296 def __ne__(self, other): 

297 return not self.__eq__(other) 

298 

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 

303 

304 def __pos__(self): 

305 return self if _pos_self else self.copy() 

306 

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

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

309 

310 def __radd__(self, other): 

311 return self._other(other) + self 

312 

313 def __rdivmod__(self, other): 

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

315 

316 def __repr__(self): 

317 return self.toRepr() 

318 

319 def __rfloordiv__(self, other): 

320 return self._other(other) // self 

321 

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

323 return self._notImplemented(self, other) 

324 

325 def __rmod__(self, other): 

326 return self._other(other) % self 

327 

328 def __rmul__(self, other): 

329 return self._other(other) * self 

330 

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

332 return self.round(*ndigits) 

333 

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

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

336 

337 def __rsub__(self, other): 

338 return self._other(other) - self 

339 

340 def __rtruediv__(self, other): 

341 return self._other(other) / self 

342 

343 def __str__(self): 

344 return self.toStr(0) # ignore turns 

345 

346 def __sub__(self, other): 

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

348 

349 def __truediv__(self, other): 

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

351 

352 __trunc__ = __int__ 

353 

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__ 

361 

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)) 

365 

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 

379 

380 @property_RO 

381 def c(self): 

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

383 ''' 

384 return self._c 

385 

386 @staticmethod 

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

388 '''A cardinal direction. 

389 

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

391 

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

393 

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 

411 

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)) 

416 

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) 

425 

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) 

431 

432 divmod = __divmod__ 

433 

434 @staticmethod 

435 def EPS0(**unit_name): 

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

437 

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) 

443 

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) 

452 

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 

458 

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) 

462 

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 

468 

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() 

474 

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 

491 

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)) 

498 

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 

515 

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 

533 

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() 

538 

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) 

544 

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

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

547 name=self.name)) 

548 

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) 

554 

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

558 

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

560 

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 

575 

576 @staticmethod 

577 def N(**unit_name): 

578 '''Get North C{Ang}. 

579 ''' 

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

581 

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 

587 

588 @n.setter # PYCHOK setter! 

589 def n(self, n): 

590 self._n_0(_fint(n)) 

591 

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 

599 

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 

605 

606 @n0.setter # PYCHOK setter! 

607 def n0(self, n): 

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

609 

610 @Property_RO 

611 def _n01(self): 

612 s, c = self.sc2 

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

614 

615 @staticmethod 

616 def NAN(**unit_name): 

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

618 ''' 

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

620 

621 @Property_RO 

622 def ncardinal(self): 

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

624 

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

626 ''' 

627 return _ncardinal(*self.scn3) 

628 

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

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

631 

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)) 

641 

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 

650 

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) 

661 

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) 

665 

666 pow = __pow__ 

667 

668 @Property_RO 

669 def _quadrant(self): 

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

671 return s + s + (c ^ s) 

672 

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

674 def quadrant(self): 

675 return self._quadrant 

676 

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) 

686 

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) 

695 

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) 

701 

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

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

704 

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

708 

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) 

720 

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)) 

731 

732 @property_RO 

733 def s(self): 

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

735 ''' 

736 return self._s 

737 

738 @property_RO 

739 def sc2(self): 

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

741 ''' 

742 return self.s, self.c 

743 

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 

749 

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 

755 

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 

767 

768 def signOf(self, *n): 

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

770 

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

772 ''' 

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

774 

775 @Property_RO 

776 def t(self): 

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

778 ''' 

779 return _over(*self.sc2) 

780 

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) 

792 

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) 

798 

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) 

810 

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) 

815 

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) 

820 

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) 

825 

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 

834 

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

836 def unit(self): 

837 return self._unit 

838 

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 

845 

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 

853 

854_allPropertiesOf_n(14, Ang) # PYCHOK assert 

855 

856 

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 

862 

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 

873 

874 def toLambertian(self): 

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

876 ''' 

877 return self.toUnit(Lambertian) 

878 

879 def toRadians(self, *n): 

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

881 ''' 

882 return self.toUnit(Radians, *n) 

883 

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 

893 

894 

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')) 

900 

901 

902_Ang_from = {Radians: Ang.fromRadians, 

903 Degrees: Ang.fromDegrees, 

904 Lambertian: Ang.fromLambertian} 

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

906 

907 

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)) 

912 

913 

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) 

918 

919 

920def isAng(ang): 

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

922 ''' 

923 return isinstance(ang, Ang) 

924 

925 

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) 

930 

931 

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. 

935 

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

937 ''' 

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

939 

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.