Coverage for pygeodesy/auxilats/auxAngle.py: 96%

225 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-04 12:01 -0400

1 

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

3 

4u'''(INTERNAL) I{Auxiliary} latitudes' base classes, constants and functions. 

5 

6Class L{AuxAngle} transcoded to Python from I{Karney}'s C++ class U{AuxAngle 

7<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AuxAngle.html>} 

8in I{GeographicLib version 2.2+}. 

9 

10Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2022-2024) and licensed 

11under the MIT/X11 License. For more information, see the U{GeographicLib 

12<https://GeographicLib.SourceForge.io>} documentation. 

13''' 

14# make sure int/int division yields float quotient, see .basics 

15from __future__ import division as _; del _ # PYCHOK semicolon 

16 

17from pygeodesy.auxilats.auxily import Aux, _Aux2Greek, AuxError 

18from pygeodesy.basics import map1, map2, _xinstanceof 

19from pygeodesy.constants import EPS, _INF_NAN_NINF, MAX, NAN, _0_0, _0_5, _1_0, \ 

20 _copysign_1_0, _over, _pos_self, isfinite, isnan 

21# from pygeodesy.errors import AuxError # from .auxilats.auxily 

22from pygeodesy.fmath import hypot, unstr 

23from pygeodesy.fsums import _add_op_, _iadd_op_, _isub_op_, _sub_op_ 

24from pygeodesy.named import _Named, _ALL_DOCS, _MODS 

25# from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS # from .named 

26from pygeodesy.props import Property, Property_RO, property_RO, property_ROver, \ 

27 _update_all 

28# from pygeodesy.streprs import unstr # from .fmath 

29from pygeodesy.units import Degrees, Radians 

30from pygeodesy.utily import atan2, atan2d, sincos2, sincos2d 

31 

32from math import asinh, copysign, degrees, fabs, radians, sinh 

33 

34__all__ = () 

35__version__ = '25.04.14' 

36 

37_0_INF_NAN_NINF = (0, _0_0) + _INF_NAN_NINF 

38_MAX_2 = MAX * _0_5 # PYCHOK used! 

39# del _INF_NAN_NINF, MAX 

40 

41 

42class AuxAngle(_Named): 

43 '''U{An accurate representation of angles 

44 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AuxAngle.html>} 

45 ''' 

46 _AUX = None # overloaded/-ridden 

47 _diff = NAN # default 

48 _iter = None # like ._NamedBase 

49 _y = _0_0 

50 _x = _1_0 

51 

52 def __init__(self, y_angle=_0_0, x=_1_0, aux=None, **name): 

53 '''New L{AuxAngle}. 

54 

55 @kwarg y_angle: The Y component (C{scalar}, including C{INF}, C{NAN} 

56 and C{NINF}) or a previous L{AuxAngle} instance. 

57 @kwarg x: The X component, required if C{B{y_angle}} is C{scalar}, 

58 ignored otherwise. 

59 @kwarg aux: I{Auxiliary} kind (C{Aux.KIND}), like B{C{x}}. 

60 @kwarg name: Optional C{B{name}=NN} see (C{str}). 

61 

62 @raise AuxError: Invalid B{C{y_angle}}, B{C{x}} or B{C{aux}}. 

63 ''' 

64 try: 

65 try: 

66 yx = y_angle._yx 

67 aux = y_angle._AUX 

68 if self._diff != y_angle._diff: 

69 self._diff = y_angle._diff 

70 except AttributeError: 

71 yx = y_angle, x 

72 if aux in _AUXClass: 

73 if self._AUX != aux: 

74 self._AUX = aux 

75 elif aux is not None: 

76 raise ValueError() # _invalid_ 

77 except Exception as X: 

78 raise AuxError(y=y_angle, x=x, aux=aux, cause=X) 

79 self._y, self._x = _yx2(yx) 

80 if name: 

81 self.name = name 

82 

83 def __abs__(self): 

84 '''Return this angle's absolute value (L{AuxAngle}). 

85 ''' 

86 a = self._copy_2(self.__abs__) 

87 a._yx = map2(fabs, self._yx) 

88 return a 

89 

90 def __add__(self, other): 

91 '''Return C{B{self} + B{other}} as an L{AuxAngle}. 

92 

93 @arg other: An L{AuxAngle}. 

94 

95 @return: The sum (L{AuxAngle}). 

96 

97 @raise TypeError: Invalid B{C{other}}. 

98 ''' 

99 a = self._copy_2(self.__add__) 

100 return a._iadd(other, _add_op_) 

101 

102 def __bool__(self): # PYCHOK not special in Python 2- 

103 '''Return C{True} if this angle is non-zero. 

104 ''' 

105 return bool(self.tan) 

106 

107 def __eq__(self, other): 

108 '''Return C{B{self} == B{other}} as C{bool}. 

109 ''' 

110 return not self.__ne__(other) 

111 

112 def __float__(self): 

113 '''Return this angle's C{tan} (C{float}). 

114 ''' 

115 return self.tan 

116 

117 def __iadd__(self, other): 

118 '''Apply C{B{self} += B{other}} to this angle. 

119 

120 @arg other: An L{AuxAngle}. 

121 

122 @return: This angle, updated (L{AuxAngle}). 

123 

124 @raise TypeError: Invalid B{C{other}}. 

125 ''' 

126 return self._iadd(other, _iadd_op_) 

127 

128 def __isub__(self, other): 

129 '''Apply C{B{self} -= B{other}} to this angle. 

130 

131 @arg other: An L{AuxAngle}. 

132 

133 @return: This instance, updated (L{AuxAngle}). 

134 

135 @raise TypeError: Invalid B{C{other}} type. 

136 ''' 

137 return self._iadd(-other, _isub_op_) 

138 

139 def __ne__(self, other): 

140 '''Return C{B{self} != B{other}} as C{bool}. 

141 ''' 

142 _xinstanceof(AuxAngle, other=other) 

143 y, x, r = self._yxr_normalized() 

144 s, c, t = other._yxr_normalized() 

145 return fabs(y - s) > EPS or fabs(x - c) > EPS \ 

146 or fabs(r - t) > EPS 

147 

148 def __neg__(self): 

149 '''Return I{a copy of} this angle, negated. 

150 ''' 

151 a = self._copy_2(self.__neg__) 

152 if a.y or not a.x: 

153 a.y = -a.y 

154 else: 

155 a.x = -a.x 

156 return a 

157 

158 def __pos__(self): 

159 '''Return this angle I{as-is}, like C{float.__pos__()}. 

160 ''' 

161 return self if _pos_self else self._copy_2(self.__pos__) 

162 

163 def __radd__(self, other): 

164 '''Return C{B{other} + B{self}} as an L{AuxAngle}. 

165 

166 @see: Method L{AuxAngle.__add__}. 

167 ''' 

168 a = self._copy_r2(other, self.__radd__) 

169 return a._iadd(self, _add_op_) 

170 

171 def __rsub__(self, other): 

172 '''Return C{B{other} - B{self}} as an L{AuxAngle}. 

173 

174 @see: Method L{AuxAngle.__sub__}. 

175 ''' 

176 a = self._copy_r2(other, self.__rsub__) 

177 return a._iadd(-self, _sub_op_) 

178 

179 def __str__(self): 

180 n = _Aux2Greek.get(self._AUX, self.classname) 

181 return unstr(n, y=self.y, x=self.x, tan=self.tan) 

182 

183 def __sub__(self, other): 

184 '''Return C{B{self} - B{other}} as an L{AuxAngle}. 

185 

186 @arg other: An L{AuxAngle}. 

187 

188 @return: The difference (L{AuxAngle}). 

189 

190 @raise TypeError: Invalid B{C{other}} type. 

191 ''' 

192 a = self._copy_2(self.__sub__) 

193 return a._iadd(-other, _sub_op_) 

194 

195 def _iadd(self, other, *unused): # op 

196 '''(INTERNAL) Apply C{B{self} += B{other}}. 

197 ''' 

198 _xinstanceof(AuxAngle, other=other) 

199 # ignore zero other to preserve signs of _y and _x 

200 if other.tan: 

201 s, c = other._yx 

202 y, x = self._yx 

203 self._yx = (y * c + x * s), \ 

204 (x * c - y * s) 

205 return self 

206 

207 def _copy_2(self, which): 

208 '''(INTERNAL) Copy for I{dyadic} operators. 

209 ''' 

210 return _Named.copy(self, deep=False, name__=which) 

211 

212 def _copy_r2(self, other, which): 

213 '''(INTERNAL) Copy for I{reverse-dyadic} operators. 

214 ''' 

215 _xinstanceof(AuxAngle, other=other) 

216 return other._copy_2(which) 

217 

218 def copyquadrant(self, other): 

219 '''Copy an B{C{other}} angle's quadrant into this angle (L{auxAngle}). 

220 ''' 

221 _xinstanceof(AuxAngle, other=other) 

222 self._yx = copysign(self.y, other.y), \ 

223 copysign(self.x, other.x) 

224 return self 

225 

226 @Property_RO 

227 def diff(self): 

228 '''Get derivative C{dtan(Eta) / dtan(Phi)} (C{float} or C{NAN}). 

229 ''' 

230 return self._diff 

231 

232 @staticmethod 

233 def fromDegrees(deg, **aux_name): 

234 '''Get an L{AuxAngle} from degrees. 

235 ''' 

236 return _AuxClass(**aux_name)(*sincos2d(deg), **aux_name) 

237 

238 @staticmethod 

239 def fromLambertianDegrees(psi, **aux_name): 

240 '''Get an L{AuxAngle} from I{Lambertian} degrees. 

241 ''' 

242 return _AuxClass(**aux_name)(sinh(radians(psi)), **aux_name) 

243 

244 @staticmethod 

245 def fromLambertianRadians(psi, **aux_name): 

246 '''Get an L{AuxAngle} from I{Lambertian} radians. 

247 ''' 

248 return _AuxClass(**aux_name)(sinh(psi), **aux_name) 

249 

250 @staticmethod 

251 def fromRadians(rad, **aux_name): 

252 '''Get an L{AuxAngle} from radians. 

253 ''' 

254 return _AuxClass(**aux_name)(*sincos2(rad), **aux_name) 

255 

256 @Property_RO 

257 def iteration(self): 

258 '''Get the iteration (C{int} or C{None}). 

259 ''' 

260 return self._iter 

261 

262 def normal(self): 

263 '''Normalize this angle I{in-place}. 

264 

265 @return: This angle, normalized (L{AuxAngle}). 

266 ''' 

267 self._yx = self._yx_normalized 

268 return self 

269 

270 @Property_RO 

271 def normalized(self): 

272 '''Get a normalized copy of this angle (L{AuxAngle}). 

273 ''' 

274 y, x = self._yx_normalized 

275 return self.classof(y, x, name=self.name, aux=self._AUX) 

276 

277 @property_ROver 

278 def _RhumbAux(self): 

279 '''(INTERNAL) Import the L{RhumbAux} class, I{once}. 

280 ''' 

281 return _MODS.rhumb.aux_.RhumbAux # overwrite property_ROver 

282 

283 @Property_RO 

284 def tan(self): 

285 '''Get this angle's C{tan} (C{float}). 

286 ''' 

287 y, x = self._yx 

288 return _over(y, x) if isfinite(y) and y else y 

289 

290 def toBeta(self, rhumb): 

291 '''Short for C{rhumb.auxDLat.convert(Aux.BETA, self, exact=rhumb.exact)} 

292 ''' 

293 return self._toRhumbAux(rhumb, Aux.BETA) 

294 

295 def toChi(self, rhumb): 

296 '''Short for C{rhumb.auxDLat.convert(Aux.CHI, self, exact=rhumb.exact)} 

297 ''' 

298 return self._toRhumbAux(rhumb, Aux.CHI) 

299 

300 @Property_RO 

301 def toDegrees(self): 

302 '''Get this angle as L{Degrees}. 

303 ''' 

304 return Degrees(atan2d(*self._yx), name=self.name) 

305 

306 @Property_RO 

307 def toLambertianDegrees(self): # PYCHOK no cover 

308 '''Get this angle's I{Lambertian} in L{Degrees}. 

309 ''' 

310 r = self.toLambertianRadians 

311 return Degrees(degrees(r), name=r.name) 

312 

313 @Property_RO 

314 def toLambertianRadians(self): 

315 '''Get this angle's I{Lambertian} in L{Radians}. 

316 ''' 

317 return Radians(asinh(self.tan), name=self.name) 

318 

319 def toMu(self, rhumb): 

320 '''Short for C{rhumb.auxDLat.convert(Aux.MU, self, exact=rhumb.exact)} 

321 ''' 

322 return self._toRhumbAux(rhumb, Aux.MU) 

323 

324 def toPhi(self, rhumb): 

325 '''Short for C{rhumb.auxDLat.convert(Aux.PHI, self, exact=rhumb.exact)} 

326 ''' 

327 return self._toRhumbAux(rhumb, Aux.PHI) 

328 

329 @Property_RO 

330 def toRadians(self): 

331 '''Get this angle as L{Radians}. 

332 ''' 

333 return Radians(atan2(*self._yx), name=self.name) 

334 

335 def _toRhumbAux(self, rhumb, aux): 

336 '''(INTERNAL) Create an C{aux}-KIND angle from this angle. 

337 ''' 

338 _xinstanceof(self._RhumbAux, rhumb=rhumb) 

339 return rhumb._auxD.convert(aux, self, exact=rhumb.exact) 

340 

341 @Property 

342 def x(self): 

343 '''Get this angle's C{x} (C{float}). 

344 ''' 

345 return self._x 

346 

347 @x.setter # PYCHOK setter! 

348 def x(self, x): # PYCHOK no cover 

349 '''Set this angle's C{x} (C{float}). 

350 ''' 

351 x = float(x) 

352 if self._x != x: 

353 _update_all(self) 

354 self._x = x 

355 

356 @property_RO 

357 def _x_normalized(self): 

358 '''(INTERNAL) Get the I{normalized} C{x}. 

359 ''' 

360 _, x = self._yx_normalized 

361 return x 

362 

363 @Property 

364 def y(self): 

365 '''Get this angle's C{y} (C{float}). 

366 ''' 

367 return self._y 

368 

369 @y.setter # PYCHOK setter! 

370 def y(self, y): # PYCHOK no cover 

371 '''Set this angle's C{y} (C{float}). 

372 ''' 

373 y = float(y) 

374 if self.y != y: 

375 _update_all(self) 

376 self._y = y 

377 

378 @Property 

379 def _yx(self): 

380 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}. 

381 ''' 

382 return self._y, self._x 

383 

384 @_yx.setter # PYCHOK setter! 

385 def _yx(self, yx): 

386 '''(INTERNAL) Set this angle's C{y} and C{x}. 

387 ''' 

388 yx = _yx2(yx) 

389 if self._yx != yx: 

390 _update_all(self) 

391 self._y, self._x = yx 

392 

393 @Property_RO 

394 def _yx_normalized(self): 

395 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}, I{normalized}. 

396 ''' 

397 y, x = self._yx 

398 if isfinite(y) and fabs(y) < _MAX_2 \ 

399 and fabs(x) < _MAX_2 \ 

400 and isfinite(self.tan): 

401 h = hypot(y, x) 

402 if h > 0 and y: 

403 y = y / h # /= chokes PyChecker 

404 x = x / h 

405 if isnan(y): # PYCHOK no cover 

406 y = _copysign_1_0(self.y) 

407 if isnan(x): # PYCHOK no cover 

408 x = _copysign_1_0(self.x) 

409 else: # scalar 0 

410 y, x = _0_0, _copysign_1_0(y * x) 

411 else: # scalar NAN 

412 y, x = NAN, _copysign_1_0(y * x) 

413 return y, x 

414 

415 def _yxr_normalized(self, abs_y=False): 

416 '''(INTERNAL) Get 3-tuple C{(y, x, r)}, I{normalized} 

417 with C{y} or C{abs(y)} and C{r} as C{.toRadians}. 

418 ''' 

419 y, x = self._yx_normalized 

420 if abs_y: 

421 y = fabs(y) # only y, not x 

422 return y, x, atan2(y, x) # .toRadians 

423 

424 

425class AuxBeta(AuxAngle): 

426 '''A I{Parametric, Auxiliary} latitude. 

427 ''' 

428 _AUX = Aux.BETA 

429 

430 @staticmethod 

431 def fromDegrees(deg, **name): 

432 '''Get an L{AuxBeta} from degrees. 

433 ''' 

434 return AuxBeta(*sincos2d(deg), **name) 

435 

436 @staticmethod 

437 def fromRadians(rad, **name): 

438 '''Get an L{AuxBeta} from radians. 

439 ''' 

440 return AuxBeta(*sincos2(rad), **name) 

441 

442 

443class AuxChi(AuxAngle): 

444 '''A I{Conformal, Auxiliary} latitude. 

445 ''' 

446 _AUX = Aux.CHI 

447 

448 @staticmethod 

449 def fromDegrees(deg, **name): 

450 '''Get an L{AuxChi} from degrees. 

451 ''' 

452 return AuxChi(*sincos2d(deg), **name) 

453 

454 

455class AuxMu(AuxAngle): 

456 '''A I{Rectifying, Auxiliary} latitude. 

457 ''' 

458 _AUX = Aux.MU 

459 

460 @staticmethod 

461 def fromDegrees(deg, **name): 

462 '''Get an L{AuxMu} from degrees. 

463 ''' 

464 return AuxMu(*sincos2d(deg), **name) 

465 

466 

467class AuxPhi(AuxAngle): 

468 '''A I{Geodetic or Geographic, Auxiliary} latitude. 

469 ''' 

470 _AUX = Aux.PHI 

471 _diff = _1_0 # see .auxLat._Newton 

472 

473 @staticmethod 

474 def fromDegrees(deg, **name): 

475 '''Get an L{AuxPhi} from degrees. 

476 ''' 

477 return AuxPhi(*sincos2d(deg), **name) 

478 

479 

480class AuxTheta(AuxAngle): 

481 '''A I{Geocentric, Auxiliary} latitude. 

482 ''' 

483 _AUX = Aux.THETA 

484 

485 @staticmethod 

486 def fromDegrees(deg, **name): 

487 '''Get an L{AuxTheta} from degrees. 

488 ''' 

489 return AuxTheta(*sincos2d(deg), **name) 

490 

491 

492class AuxXi(AuxAngle): 

493 '''An I{Authalic, Auxiliary} latitude. 

494 ''' 

495 _AUX = Aux.XI 

496 

497 @staticmethod 

498 def fromDegrees(deg, **name): 

499 '''Get an L{AuxXi} from degrees. 

500 ''' 

501 return AuxXi(*sincos2d(deg), **name) 

502 

503 

504_AUXClass = {Aux.BETA: AuxBeta, 

505 Aux.CHI: AuxChi, 

506 Aux.MU: AuxMu, 

507 Aux.PHI: AuxPhi, 

508 Aux.THETA: AuxTheta, 

509 Aux.XI: AuxXi} 

510 

511def _AuxClass(aux=None, **unused): # PYCHOK C{classof(aux)} 

512 return _AUXClass.get(aux, AuxAngle) 

513 

514 

515def _yx2(yx): 

516 '''(INTERNAL) Validate 2-tuple C{(y, x)}. 

517 ''' 

518 try: 

519 y, x = yx 

520 y, x = map1(float, y, x) 

521 if y in _0_INF_NAN_NINF: 

522 x = _copysign_1_0(x) 

523 except (TypeError, ValueError) as X: 

524 raise AuxError(y=y, x=x, cause=X) 

525 return y, x 

526 

527 

528__all__ += _ALL_DOCS(AuxAngle, AuxBeta, AuxChi, AuxMu, AuxPhi, AuxTheta, AuxXi) 

529 

530# **) MIT License 

531# 

532# Copyright (C) 2023-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

533# 

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

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

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

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

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

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

540# 

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

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

543# 

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

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

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

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

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

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

550# OTHER DEALINGS IN THE SOFTWARE.