Coverage for pygeodesy / angles.py: 95%

494 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-08-20 12:21 -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 NN, _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, NN, _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__ = '26.08.06' 

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 _orthogonal2(pred, s, c): 

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

87 (_copysign_0_0(s), _copysign_1_0(c)) 

88 

89 

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

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

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

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

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

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

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: # Error=... from _NamedTuple.toUnits() 

157 u = _xkwds_get(unit_name, unit=u) 

158 name = _xkwds_get(unit_name, name=NN) 

159 if name: 

160 self.name = name 

161 self._n = _fint(n) 

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

163 self.unit = u 

164 

165 def __abs__(self): 

166 s, _ = self._float2() 

167 return self._float1(fabs(s)) 

168 

169 def __add__(self, other): 

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

171 

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

173 s, c, n = self.scn3 

174 return bool(s or c or n) 

175 

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

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

178 

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

180 s, _ = self._float2() 

181 return self._float1(_ceil(s)) 

182 

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

184 s, r = self._float2(other) 

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

186 

187 def __divmod__(self, other): 

188 s, r = self._float2(other) 

189 q, r = divmod(s, r) 

190 return q, self._float1(r) 

191 

192 def __eq__(self, other): 

193 s, r = self._float2(other) 

194 return fabs(s - r) < EPS0 

195 

196 def __float__(self): 

197 u = self.unit 

198 return self.radians if u is Radians else ( 

199 self.degrees if u is Degrees else ( 

200 self.lambertian if u is Lambertian else 

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

202 

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

204 s, _ = self._float2() 

205 return self._float1(_floor(s)) 

206 

207 def __floordiv__(self, other): 

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

209 

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

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

212 

213 def __ge__(self, other): 

214 s, r = self._float2(other) 

215 return s >= r 

216 

217 def __gt__(self, other): 

218 s, r = self._float2(other) 

219 return s > r 

220 

221 def __hash__(self): # PYCHOK no cover 

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

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

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

225 

226 def __iadd__(self, other): 

227 p = self._other(other) 

228 q = p.ncardinal + self.ncardinal 

229 s, c, n = self.scn3 

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

231 c * p.c - s * p.s) 

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

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

234 if n: 

235 self._n += n 

236 self._s = s 

237 self._c = c 

238 _update_all(self) 

239 return self._update(s, c) 

240 

241 def __ifloordiv__(self, other): 

242 s, r = self._float2(other) 

243 return self._ifloat(s // r) 

244 

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

246 return self._notImplemented() 

247 

248 def __imod__(self, other): 

249 s, r = self._float2(other) 

250 return self._ifloat(s % r) 

251 

252 def __imul__(self, other): 

253 s, r = self._float2(other) 

254 return self._ifloat(s * r) 

255 

256 def __int__(self): 

257 s, _ = self._float2(0) 

258 return int(s) 

259 

260 def __invert__(self): # PYCHOK no cover 

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

262 return self._notImplemented() 

263 

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

265 s, r = self._float2(other) 

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

267 

268 def __isub__(self, other): 

269 return self.__iadd__(-other) 

270 

271# def __iter__(self): 

272# ''' 

273# return self._NotImplemented() 

274 

275 def __itruediv__(self, other): 

276 s, r = self._float2(other) 

277 return self._ifloat(s / r) 

278 

279 def __le__(self, other): 

280 s, r = self._float2(other) 

281 return s <= r 

282 

283 def __lt__(self, other): 

284 s, r = self._float2(other) 

285 return s < r 

286 

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

288 return self._notImplemented(other) 

289 

290 def __mod__(self, other): 

291 s, r = self._float2(other) 

292 return self._float1(s % r) 

293 

294 def __mul__(self, other): 

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

296 

297 def __ne__(self, other): 

298 return not self.__eq__(other) 

299 

300 def __neg__(self): 

301 s, c, n = self.scn3 

302 s, n = _flipsign(s), _flipsign(n) 

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

304 

305 def __pos__(self): 

306 return self if _pos_self else self.copy() 

307 

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

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

310 

311 def __radd__(self, other): 

312 return self._other(other) + self 

313 

314 def __rdivmod__(self, other): 

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

316 

317 def __repr__(self): 

318 return self.toRepr() 

319 

320 def __rfloordiv__(self, other): 

321 return self._other(other) // self 

322 

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

324 return self._notImplemented(self, other) 

325 

326 def __rmod__(self, other): 

327 return self._other(other) % self 

328 

329 def __rmul__(self, other): 

330 return self._other(other) * self 

331 

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

333 return self.round(*ndigits) 

334 

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

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

337 

338 def __rsub__(self, other): 

339 return self._other(other) - self 

340 

341 def __rtruediv__(self, other): 

342 return self._other(other) / self 

343 

344 def __str__(self): 

345 return self.toStr(0) # ignore turns 

346 

347 def __sub__(self, other): 

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

349 

350 def __truediv__(self, other): 

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

352 

353 __trunc__ = __int__ 

354 

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

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

357 __div__ = __truediv__ 

358 __idiv__ = __itruediv__ 

359 __long__ = __int__ 

360 __nonzero__ = __bool__ 

361 __rdiv__ = __rtruediv__ 

362 

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

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

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

366 

367 def base(self, *center): 

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

369 ''' 

370 r = self.copy() 

371 if center: 

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

373 b = self - c 

374 b = b.base() 

375 b += c 

376 r.n0 = b.n0 

377 else: 

378 r.n = 0 

379 return r 

380 

381 @property_RO 

382 def c(self): 

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

384 ''' 

385 return self._c 

386 

387 @staticmethod 

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

389 '''A cardinal direction. 

390 

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

392 

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

394 

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

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

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

398 ''' 

399 if _isfinite(q): 

400 if q: 

401 q = _fint(q) 

402 i = int(remainder(q, _4_0)) # i is in [-2, 2] 

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

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

405 t = s is not q 

406 else: 

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

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

409 else: 

410 r = Ang.NAN(**unit_name) 

411 return r 

412 

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

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

415 ''' 

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

417 

418 @Property_RO 

419 def degrees(self): 

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

421 ''' 

422 d = self.degrees0 

423 if self.n: 

424 d += self.n * _360_0 

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

426 

427 @Property_RO 

428 def degrees0(self): 

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

430 ''' 

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

432 

433 divmod = __divmod__ 

434 

435 @staticmethod 

436 def EPS0(**unit_name): 

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

438 

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

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

441 will flush this angle to C{0}. 

442 ''' 

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

444 

445 @staticmethod 

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

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

448 ''' # Ellipsoid3.Flip 

449 bet.reflect(flipc=True) 

450 omg.reflect(flips=True) 

451 if alp: 

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

453 

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

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

456 ''' 

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

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

459 

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

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

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

463 

464 def _float2(self, other=None): 

465 # get self and C{other} as floats 

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

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

468 return float(self), r 

469 

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

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

472 scn = self._float1(f).scn3 

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

474 return self._update() 

475 

476 @staticmethod 

477 def fromDegrees(deg, **unit_name): 

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

479 ''' 

480 if isAng(deg): 

481 s, c, n = deg.scn3 

482 d = deg.degrees0 

483 elif _isDegrees(deg, iscalar=True): 

484 s, c = sincos2d(deg) 

485 d = atan2d(s, c) 

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

487 else: 

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

489 a = Ang(s, c, n, **_xkwds(unit_name, unit=Degrees)) 

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

491 return a 

492 

493 @staticmethod 

494 def fromLambertian(psi, **unit_name): 

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

496 ''' 

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

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

499 

500 @staticmethod 

501 def fromRadians(rad, **unit_name): 

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

503 ''' 

504 if isAng(rad): 

505 s, c, n = rad.scn3 

506 r = rad.radians0 

507 elif _isRadians(rad, iscalar=True): 

508 s, c = sincos2(rad) 

509 r = atan2(s, c) 

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

511 else: 

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

513 a = Ang(s, c, n, **_xkwds(unit_name, unit=Radians)) 

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

515 return a 

516 

517 @staticmethod 

518 def fromScalar(ang, **unit_name): 

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

520 ''' 

521 if isAng(ang): 

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

523 else: 

524 u = _xkwds_get(unit_name, unit=None) 

525 if u is Lambertian: 

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

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

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

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

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

531 else: 

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

533 return r 

534 

535 def is_integer(self, *n): 

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

537 ''' 

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

539 

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

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

542 ''' 

543 s, c, n = self.scn3 

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

545 

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

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

548 name=self.name)) 

549 

550 @Property_RO 

551 def lambertian(self): 

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

553 ''' 

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

555 

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

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

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

559 

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

561 

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

563 this C{Ang} through multiples turns. 

564 ''' 

565 kwds = self._kwds(unit_name) 

566 if signBit(mul): 

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

568 else: 

569 s, c, n = self.scn3 

570 if mul > 1: 

571 c = c / mul # /= chokes PyChecker 

572 else: # mul <= 1 

573 s *= mul 

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

575 return r 

576 

577 @staticmethod 

578 def N(**unit_name): 

579 '''Get North C{Ang}. 

580 ''' 

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

582 

583 @property 

584 def n(self): 

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

586 ''' 

587 return self._n or _0_0 

588 

589 @n.setter # PYCHOK setter! 

590 def n(self, n): 

591 self._n_0(_fint(n)) 

592 

593 def _n_0(self, n): 

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

595 ''' 

596 if self._n != n: 

597 self._n, n = n, self._n 

598 self._update() 

599 return n 

600 

601 @property 

602 def n0(self): 

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

604 ''' 

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

606 

607 @n0.setter # PYCHOK setter! 

608 def n0(self, n): 

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

610 

611 @Property_RO 

612 def _n01(self): 

613 s, c = self.sc2 

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

615 

616 @staticmethod 

617 def NAN(**unit_name): 

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

619 ''' 

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

621 

622 @Property_RO 

623 def ncardinal(self): 

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

625 

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

627 ''' 

628 return _ncardinal(*self.scn3) 

629 

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

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

632 

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

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

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

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

637 ''' 

638 s, c, n = self.scn3 

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

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

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

642 

643 @staticmethod 

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

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

646 ''' # Ellipsoid3.AngNorm 

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

648 if flip: 

649 Ang._flip(bet, omg, alp) 

650 return flip 

651 

652 def normalize(self, *n): 

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

654 ''' 

655 sc = _normalize2(*self.sc2) 

656 if n: 

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

658 if self.n != n: # updated 

659 self._s, self._c = sc 

660 return self 

661 return self._update(*sc) 

662 

663 def _other(self, other): 

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

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

666 

667 pow = __pow__ 

668 

669 @Property_RO 

670 def _quadrant(self): 

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

672 return s + s + (c ^ s) 

673 

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

675 def quadrant(self): 

676 return self._quadrant 

677 

678 @quadrant.setter # PYCHOK setter! 

679 def quadrant(self, quadrant): 

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

681 q = int(quadrant) 

682 if (q & 2): 

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

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

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

686 self._update(s, c) 

687 

688 @Property_RO 

689 def radians(self): 

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

691 ''' 

692 r = self.radians0 

693 if self.n: 

694 r += self.n * PI2 

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

696 

697 @Property_RO 

698 def radians0(self): 

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

700 ''' 

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

702 

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

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

705 

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

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

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

709 

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

711 of the arguments. 

712 ''' 

713 s, c = self.sc2 

714 if flips: 

715 s = -s 

716 if flipc: 

717 c = -c 

718 if swapsc: 

719 s, c = c, s 

720 return self._update(s, c) 

721 

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

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

724 ''' 

725 s, c, n = self.scn3 

726 if ndigits: 

727 s = round(s, *ndigits) 

728 c = round(c, *ndigits) 

729 else: 

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

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

732 

733 @property_RO 

734 def s(self): 

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

736 ''' 

737 return self._s 

738 

739 @property_RO 

740 def sc2(self): 

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

742 ''' 

743 return self.s, self.c 

744 

745 @Property_RO 

746 def scn3(self): 

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

748 ''' 

749 return self.s, self.c, self.n 

750 

751 @property_RO 

752 def scnu4(self): 

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

754 ''' 

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

756 

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

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

759 ''' 

760 kwds = self._kwds(unit_name) 

761 if _isfinite(q): 

762 s = self.copy(**kwds) 

763 if q: 

764 s -= Ang.cardinal(q) 

765 else: 

766 s = Ang.NAN(**kwds) 

767 return s 

768 

769 def signOf(self, *n): 

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

771 

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

773 ''' 

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

775 

776 @Property_RO 

777 def t(self): 

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

779 ''' 

780 return _over(*self.sc2) 

781 

782 def toDegrees(self, *n): 

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

784 ''' 

785 if n: 

786 d = self.degrees0 

787 n = float(n[0]) 

788 if n: 

789 d += n * _360_0 

790 else: 

791 d = self.degrees 

792 return Degrees(d, self.name) 

793 

794 def toLambertian(self, **name): 

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

796 ''' 

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

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

799 

800 def toRadians(self, *n): 

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

802 ''' 

803 if n: 

804 r = self.radians0 

805 n = float(n[0]) 

806 if n: 

807 r += n * PI2 

808 else: 

809 r = self.radians 

810 return Radians(r, self.name) 

811 

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

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

814 ''' 

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

816 

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

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

819 ''' 

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

821 

822 def toTuple(self, **prec_fmt_sep): 

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

824 ''' 

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

826 

827 def toUnit(self, *n): 

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

829 ''' 

830 u = self.unit 

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

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

833 self.toLambertian() if u is Lambertian else 

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

835 

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

837 def unit(self): 

838 return self._unit 

839 

840 @unit.setter # PYCHOK setter! 

841 def unit(self, unit): 

842 if unit not in _Ang_types: # PYCHOK no cover 

843 _raiseError(Ang.unit, unit) 

844 if self._unit != unit: 

845 self._unit = unit 

846 

847 def _update(self, *sc): 

848 if sc: 

849 if sc == self.sc2: 

850 return self 

851 self._s, self._c = sc 

852 _update_all(self) 

853 return self 

854 

855_allPropertiesOf_n(14, Ang) # PYCHOK assert 

856 

857 

858class _Ang3Tuple(_NamedTuple): 

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

860 ''' 

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

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

863 

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

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

866 ''' 

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

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

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

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

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

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

873 return t 

874 

875 def toLambertian(self): 

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

877 ''' 

878 return self.toUnit(Lambertian) 

879 

880 def toRadians(self, *n): 

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

882 ''' 

883 return self.toUnit(Radians, *n) 

884 

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

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

887 ''' 

888 for a in self: 

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

890 a.unit = unit 

891 if n: 

892 a.n = n[0] 

893 return self 

894 

895 

896class Lambertian(Radians): 

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

898 ''' 

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

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

901 

902 

903_Ang_from = {Radians: Ang.fromRadians, 

904 Degrees: Ang.fromDegrees, 

905 Lambertian: Ang.fromLambertian} 

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

907 

908 

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

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

911 ''' 

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

913 

914 

915def Deg(deg, **name): 

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

917 ''' 

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

919 

920 

921def isAng(ang): 

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

923 ''' 

924 return isinstance(ang, Ang) 

925 

926 

927def Rad(rad, **name): 

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

929 ''' 

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

931 

932 

933def _SinCos2(ang, *unit): 

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

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

936 

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

938 ''' 

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

940 

941# **) MIT License 

942# 

943# Copyright (C) 2025-2026 -- mrJean1 at Gmail -- All Rights Reserved. 

944# 

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

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

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

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

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

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

951# 

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

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

954# 

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

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

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

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

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

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

961# OTHER DEALINGS IN THE SOFTWARE.