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

227 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-10 14:08 -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-2023) 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 

18from pygeodesy.basics import _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 

21from pygeodesy.errors import AuxError, _UnexpectedError, _xkwds_pop2 

22from pygeodesy.fmath import hypot, unstr 

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

24from pygeodesy.named import _name2__, _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, _update_all 

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

28from pygeodesy.units import Degrees, Radians 

29from pygeodesy.utily import atan2d, sincos2, sincos2d 

30 

31from math import asinh, atan2, copysign, degrees, fabs, radians, sinh 

32 

33__all__ = () 

34__version__ = '24.05.25' 

35 

36_0_INF_NAN_NINF = (0, _0_0) + _INF_NAN_NINF 

37_MAX_2 = MAX * _0_5 # PYCHOK used! 

38# del _INF_NAN_NINF, MAX 

39 

40 

41class AuxAngle(_Named): 

42 '''U{An accurate representation of angles 

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

44 ''' 

45 _AUX = None # overloaded/-ridden 

46 _diff = NAN # default 

47 _iter = None # like .Named._NamedBase 

48 _y = _0_0 

49 _x = _1_0 

50 

51 def __init__(self, y_angle=_0_0, x=_1_0, **name_aux): 

52 '''New L{AuxAngle}. 

53 

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

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

56 @kwarg x: The X component, ignored if C{B{y_angle}} is non-C{scalar}. 

57 @kwarg name_aux: Optional C{B{name}=NN} (C{str}) and I{Auxiliary} kind 

58 (C{B{aux}=Aux.KIND}), ignored if B{C{y_angle}} is L{AuxAngle}. 

59 

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

61 ''' 

62 name, aux = _name2__(**name_aux) 

63 try: 

64 yx = y_angle._yx 

65 if self._AUX is None: 

66 self._AUX = y_angle._AUX 

67 if self._diff != y_angle._diff: 

68 self._diff = y_angle._diff 

69 except AttributeError: 

70 yx = y_angle, x 

71 if aux: 

72 a, kwds = _xkwds_pop2(aux, aux=self._AUX) 

73 if kwds: 

74 raise _UnexpectedError(**kwds) 

75 if a is not self._AUX: 

76 if a not in _AUXClass: 

77 raise AuxError(aux=a) 

78 self._AUX = a 

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 = map(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.__name__) 

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, **name_aux): 

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

235 ''' 

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

237 

238 @staticmethod 

239 def fromLambertianDegrees(psi, **name_aux): 

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

241 ''' 

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

243 

244 @staticmethod 

245 def fromLambertianRadians(psi, **name_aux): 

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

247 ''' 

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

249 

250 @staticmethod 

251 def fromRadians(rad, **name_aux): 

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

253 ''' 

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

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_RO 

278 def _RhumbAux(self): 

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

280 ''' 

281 AuxAngle._RhumbAux = R = _MODS.rhumb.aux_.RhumbAux # overwrite property_RO 

282 return R 

283 

284 @Property_RO 

285 def tan(self): 

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

287 ''' 

288 y, x = self._yx 

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

290 

291 def toBeta(self, rhumb): 

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

293 ''' 

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

295 

296 def toChi(self, rhumb): 

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

298 ''' 

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

300 

301 @Property_RO 

302 def toDegrees(self): 

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

304 ''' 

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

306 

307 @Property_RO 

308 def toLambertianDegrees(self): # PYCHOK no cover 

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

310 ''' 

311 r = self.toLambertianRadians 

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

313 

314 @Property_RO 

315 def toLambertianRadians(self): 

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

317 ''' 

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

319 

320 def toMu(self, rhumb): 

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

322 ''' 

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

324 

325 def toPhi(self, rhumb): 

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

327 ''' 

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

329 

330 @Property_RO 

331 def toRadians(self): 

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

333 ''' 

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

335 

336 def _toRhumbAux(self, rhumb, aux): 

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

338 ''' 

339 _xinstanceof(self._RhumbAux, rhumb=rhumb) 

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

341 

342 @Property 

343 def x(self): 

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

345 ''' 

346 return self._x 

347 

348 @x.setter # PYCHOK setter! 

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

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

351 ''' 

352 x = float(x) 

353 if self._x != x: 

354 _update_all(self) 

355 self._x = x 

356 

357 @property_RO 

358 def _x_normalized(self): 

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

360 ''' 

361 _, x = self._yx_normalized 

362 return x 

363 

364 @Property 

365 def y(self): 

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

367 ''' 

368 return self._y 

369 

370 @y.setter # PYCHOK setter! 

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

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

373 ''' 

374 y = float(y) 

375 if self.y != y: 

376 _update_all(self) 

377 self._y = y 

378 

379 @Property 

380 def _yx(self): 

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

382 ''' 

383 return self._y, self._x 

384 

385 @_yx.setter # PYCHOK setter! 

386 def _yx(self, yx): 

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

388 ''' 

389 yx = _yx2(yx) 

390 if self._yx != yx: 

391 _update_all(self) 

392 self._y, self._x = yx 

393 

394 @Property_RO 

395 def _yx_normalized(self): 

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

397 ''' 

398 y, x = self._yx 

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

400 and fabs(x) < _MAX_2 \ 

401 and isfinite(self.tan): 

402 h = hypot(y, x) 

403 if h > 0 and y: 

404 y = y / h # /= chokes PyChecker 

405 x = x / h 

406 if isnan(y): # PYCHOK no cover 

407 y = _copysign_1_0(self.y) 

408 if isnan(x): # PYCHOK no cover 

409 x = _copysign_1_0(self.x) 

410 else: # scalar 0 

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

412 else: # scalar NAN 

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

414 return y, x 

415 

416 def _yxr_normalized(self, abs_y=False): 

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

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

419 ''' 

420 y, x = self._yx_normalized 

421 if abs_y: 

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

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

424 

425 

426class AuxBeta(AuxAngle): 

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

428 ''' 

429 _AUX = Aux.BETA 

430 

431 @staticmethod 

432 def fromDegrees(deg, **name): 

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

434 ''' 

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

436 

437 @staticmethod 

438 def fromRadians(rad, **name): 

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

440 ''' 

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

442 

443 

444class AuxChi(AuxAngle): 

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

446 ''' 

447 _AUX = Aux.CHI 

448 

449 @staticmethod 

450 def fromDegrees(deg, **name): 

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

452 ''' 

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

454 

455 

456class AuxMu(AuxAngle): 

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

458 ''' 

459 _AUX = Aux.MU 

460 

461 @staticmethod 

462 def fromDegrees(deg, **name): 

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

464 ''' 

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

466 

467 

468class AuxPhi(AuxAngle): 

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

470 ''' 

471 _AUX = Aux.PHI 

472 _diff = _1_0 # see .auxLat._Newton 

473 

474 @staticmethod 

475 def fromDegrees(deg, **name): 

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

477 ''' 

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

479 

480 

481class AuxTheta(AuxAngle): 

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

483 ''' 

484 _AUX = Aux.THETA 

485 

486 @staticmethod 

487 def fromDegrees(deg, **name): 

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

489 ''' 

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

491 

492 

493class AuxXi(AuxAngle): 

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

495 ''' 

496 _AUX = Aux.XI 

497 

498 @staticmethod 

499 def fromDegrees(deg, **name): 

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

501 ''' 

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

503 

504 

505_AUXClass = {Aux.BETA: AuxBeta, 

506 Aux.CHI: AuxChi, 

507 Aux.MU: AuxMu, 

508 Aux.PHI: AuxPhi, 

509 Aux.THETA: AuxTheta, 

510 Aux.XI: AuxXi} 

511 

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

513 return _AUXClass.get(aux, AuxAngle) 

514 

515 

516def _yx2(yx): 

517 try: 

518 y, x = map(float, yx) 

519 if y in _0_INF_NAN_NINF: 

520 x = _copysign_1_0(x) 

521 except (TypeError, ValueError) as e: 

522 y, x = yx 

523 raise AuxError(y=y, x=x, cause=e) 

524 return y, x 

525 

526 

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

528 

529# **) MIT License 

530# 

531# Copyright (C) 2023-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

532# 

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

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

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

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

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

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

539# 

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

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

542# 

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

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

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

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

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

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

549# OTHER DEALINGS IN THE SOFTWARE.