Coverage for pygeodesy/auxilats/auxLat.py: 93%

1

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

3

4 u'''Class L{AuxLat} transcoded to Python from I{Karney}'s C++ class U{AuxLatitude

5<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AuxLatitude.html>}

6 in I{GeographicLib version 2.2+}.

7

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

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

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

11

12@see: U{Auxiliary latitudes<https://GeographicLib.SourceForge.io/C++/doc/auxlat.html>}

13 U{On auxiliary latitudes<https://ArXiv.org/abs/2212.05818>}.

14'''

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

16 from __future__ import division as _; del _ # noqa: E702 ;

17

18 from pygeodesy.auxilats.auxAngle import AuxAngle, AuxBeta, AuxChi, _AuxClass, \

19 AuxMu, AuxPhi, AuxTheta, AuxXi

20 from pygeodesy.auxilats.auxily import Aux, _sc, _sn

21 from pygeodesy.auxilats._CX_Rs import _Rdict, _Rkey, _Rtuple

22 from pygeodesy.basics import _isin, _reverange, _xinstanceof, _passarg

23 from pygeodesy.constants import INF, MAX_EXP, MIN_EXP, NAN, PI_2, PI_4, _EPSqrt, \

24 _0_0, _0_0s, _0_1, _0_5, _1_0, _2_0, _3_0, _360_0, \

25 _log2, _over, isfinite, isinf, isnan

26 from pygeodesy.datums import _ellipsoidal_datum, _WGS84, \

27 Ellipsoid, _name__, _EWGS84

28# from pygeodesy.ellipsoids import Ellipsoid, _EWGS84 # from .datums

29 from pygeodesy.elliptic import Elliptic as _Ef

30 from pygeodesy.errors import AuxError, _xkwds_not, _xkwds_pop2, _Xorder

31# from pygeodesy.fmath import cbrt # from .karney

32 from pygeodesy.fsums import Fsum, _Fsumf_, _sum

33# from pygeodesy.internals import _passarg # from .basics

34 from pygeodesy.interns import NN, _not_scalar_, _UNDER_

35 from pygeodesy.karney import _2cos2x, _polynomial, _ALL_DOCS, cbrt

36# from pygeodesy.lazily import _ALL_DOCS # from .karney

37# from pygeodesy.named import _name__ # from .datums

38 from pygeodesy.props import Property, Property_RO, _update_all

39 from pygeodesy.units import _isDegrees, _isRadius, Degrees, Meter

40 from pygeodesy.utily import atan1, atan2

41

42 from math import asinh, copysign, cosh, fabs, sin, sinh, sqrt

43 try:

44 from math import exp2 as _exp2

45 except ImportError: # Python 3.11-

46

47 def _exp2(x):

48 return pow(_2_0, x)

49

50 __all__ = ()

51 __version__ = '26.01.20'

52

53 _TRIPS = 1024 # XXX 2 or 3?

54

55

56 class AuxLat(AuxAngle):

57 '''Base class for accurate conversion between I{Auxiliary} latitudes

58 on an ellipsoid.

59

60 Latitudes are represented by L{AuxAngle} instances in order to

61 maintain precision near the poles, I{Authalic} latitude C{Xi},

62 I{Conformal} C{Chi}, I{Geocentric} C{Theta}, I{Geographic} C{Phi},

63 I{Parametric} C{Beta} and I{Rectifying} C{Mu}.

64

65 @see: I{Karney}'s C++ class U{AuxLatitude

66 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AuxLatitude.html>}.

67 '''

68 _csc = dict() # global coeffs cache: [aL][k], upto max(k) * (4 + 6 + 8) floats

69 _E = _EWGS84

70# _Lmax = 0 # overwritten below

71 _mAL = 6 # 4, 6 or 8 aka Lmax

72

73 def __init__(self, a_earth=_EWGS84, f=None, b=None, **ALorder_name):

74 '''New L{AuxLat} instance on an ellipsoid or datum.

75

76 @arg a_earth: Equatorial radius, semi-axis (C{meter}) or an ellipsoid or

77 datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).

78 @kwarg f: Flattening: M{(a - b) / a} (C{float}, near zero for spherical),

79 required if B{C{a_earth}} is C{scalar} and C{B{b}=None}.

80 @kwarg b: Optional polar radius, semi-axis (C{meter}, required if B{C{a_earth}}

81 is C{scalar} and C{B{f}=None}.

82 @kwarg ALorder_name: Optional C{B{name}=NN} (C{str}) and optional order of

83 this L{AuxLat} C{B{ALorder}=6}, see property C{ALorder}.

84 '''

85 if ALorder_name:

86 M = self._mAL

87 m, name = _xkwds_pop2(ALorder_name, ALorder=M)

88 if m != M:

89 self.ALorder = m

90 else:

91 name = NN

92 try:

93 if not _isin(a_earth, _EWGS84, _WGS84):

94 n = _name__(name, name__=AuxLat)

95 if b is f is None:

96 E = _ellipsoidal_datum(a_earth, name=n).ellipsoid # XXX raiser=_earth_

97 elif _isRadius(a_earth):

98 E = Ellipsoid(a_earth, f=f, b=b, name=_UNDER_(NN, n))

99 else:

100 raise ValueError(_not_scalar_)

101 self._E = E

102 elif not (b is f is None):

103 # turn _UnexpectedError into AuxError

104 name = _name__(name, **_xkwds_not(None, b=b, f=f))

105

106 if name:

107 self.name = name

108 except Exception as x:

109 raise AuxError(a_earth=a_earth, f=f, b=b, cause=x)

110

111 @Property_RO

112 def a(self):

113 '''Get the C{ellipsoid}'s equatorial radius (C{meter}, conventionally).

114 '''

115 return self.ellipsoid.a

116

117 equatoradius = a

118

119 @Property

120 def ALorder(self):

121 '''Get the I{AuxLat} order (C{int}, 4, 6 or 8).

122 '''

123 return self._mAL

124

125 @ALorder.setter # PYCHOK setter!

126 def ALorder(self, order):

127 '''Set the I{AuxLat} order (C{int}, 4, 6 or 8).

128 '''

129 m = _Xorder(_AR2Coeffs, AuxError, ALorder=order)

130 if self._mAL != m:

131 _update_all(self)

132 self._mAL = m

133

134 def _atanhee(self, tphi): # see Ellipsoid._es_atanh, .albers._atanhee

135 # atanh(e * sphi) = asinh(e' * sbeta)

136 f = self.f

137 s = _sn(self._fm1 * tphi) if f > 0 else _sn(tphi)

138 if f: # atanh(e * sphi) = asinh(e' * sbeta)

139 e = self._e

140 s = _over(atan1(e * s) if f < 0 else asinh(self._e1 * s), e)

141 return s

142

143 def Authalic(self, Phi, **diff_name):

144 '''Convert I{Geographic} to I{Aunthalic} latitude.

145

146 @arg Phi: Geographic latitude (L{AuxAngle}).

147 @kwarg diff_name: Use C{B{diff}=True} to set C{diff}

148 optional C{B{name}=NN}.

149

150 @return: Parametric latitude, C{Beta} (L{AuxAngle}).

151 '''

152 _xinstanceof(AuxAngle, Phi=Phi)

153 # assert Phi._AUX == Aux.PHI

154 tphi = fabs(Phi.tan)

155 if isfinite(tphi) and tphi and self.f:

156 y, x = Phi._yx_normalized

157 q = self._q

158 qv = self._qf(tphi)

159 Dq2 = self._Dq(tphi)

160 Dq2 *= (q + qv) / (fabs(y) + _1_0) # _Dq(-tphi)

161 d, n = _diff_name2(Phi, **diff_name)

162 Xi = AuxXi(copysign(qv, Phi.y), x * sqrt(Dq2), name=n)

163

164 if d:

165 if isnan(tphi):

166 d = self._e2m1_sq2

167 else:

168 c = self.Parametric(Phi)._x_normalized

169 d = _over(c, Xi._x_normalized)**3

170 d *= _over(c, x) * _over(_2_0, q)

171 Xi._diff = d

172 else:

173 Xi = AuxXi(*Phi._yx) # diff default

174 # assert Xi._AUX == Aux.XI

175 return Xi

176

177 def AuthalicRadius2(self, exact=False, f_max=_0_1):

178 '''Get the I{Authalic} radius I{squared}.

179

180 @kwarg exact: If C{True}, use the exact expression, otherwise

181 the I{Taylor} series.

182 @kwarg f_max: C{Flattening} not to exceed (C{float}).

183

184 @return: Authalic radius I{squared} (C{meter} I{squared}, same

185 units as the ellipsoid axes).

186

187 @raise AuxError: If C{B{exact}=False} and C{abs(flattening)}

188 exceeds C{f_max}.

189 '''

190 f = self.f

191 if exact or not f:

192 c2 = self.ellipsoid.b2 * self._q # == ellipsoid.c2x * 2

193 elif fabs(f) < f_max:

194 # Using a * (a + b) / 2 as the multiplying factor leads to a rapidly

195 # converging series in n. Of course, using this series isn't really

196 # necessary, since the exact expression is simple to evaluate. However,

197 # we do it for consistency with RectifyingRadius and, presumably, the

198 # roundoff error is smaller compared to that for the exact expression.

199 m = self.ALorder

200 c2 = _polynomial(self._n, _AR2Coeffs[m], 0, m)

201 c2 *= self.a * (self.a + self.b)

202 else:

203 raise AuxError(exact=exact, f=f, f_max=f_max)

204 return c2 * _0_5

205

206 @Property_RO

207 def b(self):

208 '''Get the C{ellipsoid}'s polar radius (C{meter}, conventionally).

209 '''

210 return self.ellipsoid.b # a * (_1_0 - f)

211

212 polaradius = b

213

214 def _coeffs(self, auxout, auxin):

215 # Get the polynomial coefficients as 4-, 6- or 8-tuple

216 aL = self.ALorder # aka Lmax

217 if auxout == auxin:

218 return _0_0s(aL) # uncached

219

220 k = Aux._1d(auxout, auxin)

221 try: # cached

222 return AuxLat._csc[aL][k]

223 except KeyError:

224 pass

225

226 try:

227 Cx = self._CXcoeffs[auxout][auxin] # _Rtuple!

228 except KeyError as x:

229 raise AuxError(auxout=auxout, auxin=auxin, cause=x)

230

231 d = x = n = self._n

232 if Aux.use_n2(auxin) and Aux.use_n2(auxout):

233 x = self._n2

234

235 def _m(aL):

236 for m in _reverange(aL):

237 yield m // 2

238 else:

239 _m = _reverange # PYCHOK expected

240

241 i = 0

242 cs = []

243 _p = _polynomial

244 for m in _m(aL):

245 j = i + m + 1 # order m = j - i - 1

246 cs.append(_p(x, Cx, i, j) * d)

247 d *= n

248 i = j

249 # assert i == len(Cx) and len(cs) == aL

250 AuxLat._csc.setdefault(aL, {})[k] = cs = tuple(cs)

251 return cs

252

253 def Conformal(self, Phi, **diff_name):

254 '''Convert I{Geographic} to I{Conformal} latitude.

255

256 @arg Phi: Geographic latitude (L{AuxAngle}).

257 @kwarg diff_name: Use C{B{diff}=True} to set C{diff}

258 and an optional C{B{name}=NN}.

259

260 @return: Conformal latitude, C{Chi} (L{AuxAngle}).

261 '''

262 _xinstanceof(AuxAngle, Phi=Phi)

263 # assert Phi._AUX == Aux.PHI

264 tphi = tchi = fabs(Phi.tan)

265 if isfinite(tphi) and tphi and self.f:

266 sig = sinh(self._atanhee(tphi) * self._e2)

267 scsig = _sc(sig)

268 scphi = _sc(tphi)

269 if self.f > 0:

270 # The general expression for tchi is

271 # tphi * scsig - sig * scphi

272 # This involves cancellation if f > 0, so change to

273 # (tphi - sig) * (tphi + sig) / (tphi * scsig + sig * scphi)

274 # To control overflow, write as (sigtphi = sig / tphi)

275 # (tphi - sig) * (1 + sigtphi) / (scsig + sigtphi * scphi)

276 sigtphi = sig / tphi

277 if sig < (tphi * _0_5):

278 t = tphi - sig

279 else:

280 def _asinh_2(x):

281 return asinh(x) * _0_5

282 # Still possibly dangerous cancellation in tphi - sig.

283 # Write tphi - sig = (1 - e) * Dg(1, e)

284 # Dg(x, y) = (g(x) - g(y)) / (x - y)

285 # g(x) = sinh(x * atanh(sphi * x))

286 # Note sinh(atanh(sphi)) = tphi

287 # Turn the crank on divided differences, substitute

288 # sphi = tphi / _sc(tphi)

289 # atanh(x) = asinh(x / sqrt(1 - x^2))

290 e = self._e

291 em1 = self._e2m1 / (_1_0 + e)

292 # assert em1 != 0

293 scb = self._scbeta(tphi)

294 scphib = scphi / scb # sec(phi) / sec(beta)

295 atphib = _asinh_2(tphi * e / scb) # atanh(e * sphi)

296 atphi = _asinh_2(tphi) # atanh(sphi)

297 t = _asinh_2(em1 * (tphi * scphib)) / em1

298 try:

299 Dg = _Fsumf_(atphi, atphib, t, e * t)

300 except ValueError: # Fsum(NAN) exception

301 Dg = _sum((atphi, atphib, t, e * t))

302 e *= atphib

303 t = atphi - e

304 if t: # sinh(0) == 0

305 Dg *= sinh(t) / t * cosh(atphi + e) * em1

306 t = float(Dg) # tphi - sig

307 tchi = _over(t * (_1_0 + sigtphi),

308 scsig + scphi * sigtphi) if t else _0_0

309 else:

310 tchi = tphi * scsig - sig * scphi

311

312 d, n = _diff_name2(Phi, **diff_name)

313 Chi = AuxChi(tchi, name=n).copyquadrant(Phi)

314

315 if d:

316 if isinf(tphi): # PYCHOK np cover

317 d = self._conformal_diff

318 else:

319 d = self.Parametric(Phi)._x_normalized

320 if d:

321 d = _over(d, Chi._x_normalized) * \

322 _over(d, Phi._x_normalized) * self._e2m1

323 Chi._diff = d

324 # assrt Chi._AUX == Aux.CHI

325 return Chi

326

327 @Property_RO

328 def _conformal_diff(self): # PYCHOK no cover

329 '''(INTERNAL) Constant I{Conformal} diff.

330 '''

331 e = self._e

332 if self.f > 0:

333 ss = sinh(asinh(self._e1) * e)

334 d = _over(_1_0, _sc(ss) + ss)

335 elif e:

336 ss = sinh(-atan1(e) * e)

337 d = _sc(ss) - ss

338 else:

339 d = _1_0

340 return d

341

342 def convert(self, auxout, Zeta_d, exact=False):

343 # Convert I{Auxiliary} or I{scalar} latitude

344 Z = d = Zeta_d

345 if isinstance(Z, AuxAngle):

346 A, auxin = _AuxClass(auxout), Z._AUX

347 if auxin == auxout:

348 pass

349 elif not (isfinite(Z.tan) and Z.tan): # XXX

350 Z = A(*Z._yx, aux=auxout, name=Z.name)

351 elif exact:

352 p = Aux.power(auxout, auxin)

353 if p is None:

354 P = self._fromAux(Z) # Phi

355 Z = self._toAux(auxout, P)

356 Z._iter = P.iteration

357 else:

358 y, x = Z._yx

359 if p:

360 y *= pow(self._fm1, p)

361 Z = A(y, x, aux=auxout, name=Z.name)

362 else:

363 cs = self._coeffs(auxout, auxin)

364 yx = Z._yx_normalized

365 Z = A(*yx, aux=auxout, name=Z.name)

366 # assert Z._yx == yx

367 r = _Clenshaw(True, Z, cs, self.ALorder)

368 Z += AuxAngle.fromRadians(r, aux=auxout)

369 # assert Z._AUX == auxout

370 return Z

371

372 elif _isDegrees(d):

373 Z = AuxPhi.fromDegrees(d)

374 d = round((d - Z.toDegrees) / _360_0) * _360_0

375 d += self.convert(auxout, Z, exact).toDegrees

376 return Degrees(d, name=Aux.Greek(auxout))

377

378 raise AuxError(auxout=auxout, Zeta_d=Zeta_d, exact=exact)

379

380 @Property_RO

381 def _CXcoeffs(self): # in .auxilats.__main__, .testAuxilats

382 '''(INTERNAL) Get the C{CX_4}, C{_6} or C{_8} coefficients.

383 '''

384 return Aux._CXcoeffs(self.ALorder)

385

386 def _Dq(self, tphi):

387 # I{Divided Difference} of (q(1) - q(sphi)) / (1 - sphi).

388 sphi = _sn(tphi)

389 if tphi > 0:

390 scphi = _sc(tphi)

391 d = _1_0 / (scphi**2 * (_1_0 + sphi)) # XXX - sphi

392 if d:

393 # General expression for _Dq(1, sphi) is

394 # atanh(e * d / (1 - e2 * sphi)) / (e * d) +

395 # (1 + e2 * sphi) / ((1 - e2 * sphi * sphi) * e2m1)

396 # with atanh(e * d / (1 - e2 * sphi)) =

397 # atanh(e * d * scphi / (scphi - e2 * tphi))

398 e2m1, ed = self._e2m1, (self._e * d)

399 if e2m1 and ed:

400 e2 = self._e2

401 if e2 > 0: # assert self.f > 0

402 scb = self._scbeta(tphi)

403 q = scphib = scphi / scb

404 q *= (scphi + tphi * e2) / (e2m1 * scb)

405 q += asinh(self._e1 * d * scphib) / ed

406 else:

407 s2 = sphi * e2

408 q = (_1_0 + s2) / ((_1_0 - sphi * s2) * e2m1)

409 q += (atan2(ed, _1_0 - s2) / ed) if e2 < 0 else _1_0

410 else: # PYCHOK no cover

411 q = INF

412 else: # PYCHOK no cover

413 q = self._2_e2m12

414 else: # not reached, open-coded in .Authalic

415 q = _over(self._q - self._qf(tphi), _1_0 - sphi)

416 return q

417

418 @Property_RO

419 def _e(self): # unsigned, (1st) eccentricity

420 return self.ellipsoid.e # sqrt(fabs(self._e2))

421

422 @Property_RO

423 def _e1(self):

424 return sqrt(fabs(self._e12))

425

426 @Property_RO

427 def _e12(self):

428 return _over(self._e2, _1_0 - self._e2)

429

430 @Property_RO

431 def _e12p1(self):

432 return _1_0 / self._e2m1

433

434 @Property_RO

435 def _e2(self): # signed, (1st) eccentricity squared

436 return self.ellipsoid.e2

437

438 @Property_RO

439 def _e2m1(self): # 1 less 1st eccentricity squared

440 return self.ellipsoid.e21 # == ._fm1**2

441

442 @Property_RO

443 def _e2m1_sq2(self):

444 return self._e2m1 * sqrt(self._q * _0_5)

445

446 @Property_RO

447 def _2_e2m12(self):

448 return _2_0 / self._e2m1**2

449

450 @Property_RO

451 def _Ef_fRG_a2b2_PI_4(self):

452 E = self.ellipsoid

453 return _Ef.fRG(E.a2, E.b2) / PI_4

454

455 @Property_RO

456 def ellipsoid(self):

457 '''Get the ellipsoid (L{Ellipsoid}).

458 '''

459 return self._E

460

461 @Property_RO

462 def f(self):

463 '''Get the C{ellipsoid}'s flattening (C{scalar}).

464 '''

465 return self.ellipsoid.f

466

467 flattening = f

468

469 @Property_RO

470 def _fm1(self): # 1 - flattening

471 return self.ellipsoid.f1

472

473 def _fromAux(self, Zeta, **name):

474 '''Convert I{Auxiliary} to I{Geographic} latitude.

475

476 @arg Zeta: Auxiliary latitude (L{AuxAngle}).

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

478

479 @return: Geographic latitude, I{Phi} (L{AuxAngle}).

480 '''

481 _xinstanceof(AuxAngle, Zeta=Zeta)

482 aux = Zeta._AUX

483 n = _name__(name, _or_nameof=Zeta)

484 f = self._fromAuxCase.get(aux, None)

485 if f is None:

486 Phi = AuxPhi(NAN, name=n)

487 elif callable(f):

488 t = self._fm1

489 t *= f(t)

490 Phi = _Newton(t, Zeta, self._toZeta(aux), name=n)

491 else: # assert isscalar(f)

492 y, x = Zeta._yx

493 Phi = AuxPhi(y / f, x, name=n)

494 # assert Phi._AUX == Aux.PHI

495 return Phi

496

497 @Property_RO

498 def _fromAuxCase(self):

499 '''(INTERNAL) switch(auxin): ...

500 '''

501 return {Aux.AUTHALIC: cbrt,

502 Aux.CONFORMAL: _passarg,

503 Aux.GEOCENTRIC: self._e2m1,

504 Aux.GEOGRAPHIC: _1_0,

505 Aux.PARAMETRIC: self._fm1,

506 Aux.RECTIFYING: sqrt}

507

508 def Geocentric(self, Phi, **diff_name):

509 '''Convert I{Geographic} to I{Geocentric} latitude.

510

511 @arg Phi: Geographic latitude (L{AuxAngle}).

512 @kwarg diff_name: Use C{B{diff}=True} to set C{diff}

513 and an optional C{B{name}=NN}.

514

515 @return: Geocentric latitude, C{Phi} (L{AuxAngle}).

516 '''

517 _xinstanceof(AuxAngle, Phi=Phi)

518 # assert Phi._AUX == Aux.PHI

519 d, n = _diff_name2(Phi, **diff_name)

520 Theta = AuxTheta(Phi.y * self._e2m1, Phi.x, name=n)

521 if d:

522 Theta._diff = self._e2m1

523 return Theta

524

525 def Geodetic(self, Phi, **name): # PYCHOK no cover

526 '''Convert I{Geographic} to I{Geodetic} latitude.

527

528 @arg Phi: Geographic latitude (L{AuxAngle}).

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

530

531 @return: Geodetic latitude, C{Phi} (L{AuxAngle}).

532 '''

533 _xinstanceof(AuxAngle, Phi=Phi)

534 # assert Phi._AUX == Aux.PHI

535 _, n = _diff_name2(Phi, **name)

536 return AuxPhi(Phi, name=n)

537

538 @Property_RO

539 def _n(self): # 3rd flattening

540 return self.ellipsoid.n

541

542 @Property_RO

543 def _n2(self):

544 return self._n**2

545

546 def Parametric(self, Phi, **diff_name):

547 '''Convert I{Geographic} to I{Parametric} latitude.

548

549 @arg Phi: Geographic latitude (L{AuxAngle}).

550 @kwarg diff_name: Use C{B{diff}=True} to set C{diff}

551 and an optional C{B{name}=NN}.

552

553 @return: Parametric latitude, C{Beta} (L{AuxAngle}).

554 '''

555 _xinstanceof(AuxAngle, Phi=Phi)

556 # assert Phi._AUX == Aux.PHI

557 d, n = _diff_name2(Phi, **diff_name)

558 Beta = AuxBeta(Phi.y * self._fm1, Phi.x, name=n)

559 if d:

560 Beta._diff = self._fm1

561 return Beta

562

563 Reduced = Parametric

564

565 @Property_RO

566 def _q(self): # constant _q

567 q, f = self._e12p1, self.f

568 if f:

569 e = self._e

570 q += _over(asinh(self._e1) if f > 0 else atan1(e), e)

571 else:

572 q += _1_0

573 return q

574

575 def _qf(self, tphi):

576 # function _q: atanh(e * sphi) / e + sphi / (1 - (e * sphi)^2)

577 scb = self._scbeta(tphi)

578 return self._atanhee(tphi) + (tphi / scb) * (_sc(tphi) / scb)

579

580 def _qIntegrand(self, beta):

581 # pbeta(beta) = integrate(q(beta), beta), with beta in radians

582 # q(beta) = (1-f) * (sin(xi) - sin(chi)) / cos(phi)

583 # = (1-f) * (cos(chi) - cos(xi)) / cos(phi) *

584 # (cos(xi) + cos(chi)) / (sin(xi) + sin(chi))

585 # Fit q(beta)/cos(beta) with Fourier transform

586 # q(beta)/cos(beta) = sum(c[k] * sin((2*k+1)*beta), k, 0, K-1)

587 # then the integral is

588 # pbeta = sum(d[k] * cos((2*k+2)*beta), k, 0, K-1)

589 # where

590 # d[k] = -1/(4*(k+1)) * (c[k] + c[k+1]) for k in 0..K-2

591 # d[K-1] = -1/(4*K) * c[K-1]

592 Beta = AuxBeta.fromRadians(beta)

593 if Beta.x: # and self._fm1:

594 Ax, _cv = Aux, self.convert

595 Phi = _cv(Ax.PHI, Beta, exact=True)

596 schi, cchi = _cv(Ax.CHI, Phi, exact=True)._yx_normalized

597 sxi, cxi = _cv(Ax.XI, Phi, exact=True)._yx_normalized

598 r = (sxi - schi) if fabs(schi) < fabs(cchi) else \

599 _over(_2cos2x(cchi, cxi), (sxi + schi) * _2_0)

600 r *= _over(self._fm1, Phi._x_normalized * Beta._x_normalized)

601 else: # beta == PI_2, PI3_2, ...

602 r = _0_0 # XXX 0 avoids NAN summation exceptions

603 return r

604

605 def Rectifying(self, Phi, **diff_name):

606 '''Convert I{Geographic} to I{Rectifying} latitude.

607

608 @arg Phi: Geographic latitude (L{AuxAngle}).

609 @kwarg diff_name: Use C{B{diff}=True} to set C{diff}

610 and an optional C{B{name}=NN}.

611

612 @return: Rectifying latitude, C{Mu} (L{AuxAngle}).

613 '''

614 Beta = self.Parametric(Phi)

615 # assert Beta._AUX == Aux.BETA

616 sb, cb = map(fabs, Beta._yx_normalized)

617 a, ka, ka1 = _1_0, self._e2, self._e2m1

618 b, kb, kb1 = self._fm1, -self._e12, self._e12p1

619 if self.f < 0:

620 a, b = b, a

621 ka, kb = kb, ka

622 ka1, kb1 = kb1, ka1

623 sb, cb = cb, sb

624 # now a, b = larger/smaller semi-axis

625 # Beta measured from larger semi-axis

626 # kb, ka = modulus-squared for distance from Beta = 0, pi/2

627 # NB kb <= 0; 0 <= ka <= 1

628 # sa = b*E(Beta, sqrt(kb))

629 # sb = a*E(Beta',sqrt(ka))

630 # 1 - ka * (1 - sb2) = 1 - ka + ka*sb2

631 sb2 = sb**2

632 cb2 = cb**2

633 da2 = ka1 + ka * sb2

634 # DLMF Eq. 19.25.9

635 my = b * sb * _Ef._fRF3RD(cb2, _1_0, kb * sb2)

636 # DLMF Eq. 19.25.10 with complementary angles

637 mx = a * cb * (_Ef.fRF(sb2, da2, _1_0) * ka1 +

638 ka * cb2 * _Ef.fRD(sb2, _1_0, da2, _3_0) * ka1 +

639 ka * sb / sqrt(da2))

640 # my + mx = 2*_Ef.fRG(a*a, b*b) = a*E(e) = b*E(i*e')

641 # mr = a*E(e)*(2/pi) = b*E(i*e')*(2/pi)

642 if self.f < 0:

643 a, b = b, a

644 my, mx = mx, my

645 mr = (my + mx) / PI_2

646 if mr:

647 my = sin(my / mr)

648 mx = sin(mx / mr) # XXX zero?

649 else: # zero Mu

650 my, mx = _0_0, _1_0

651 d, n = _diff_name2(Phi, **diff_name)

652 Mu = AuxMu(my, mx, # normalized

653 name=n).copyquadrant(Phi)

654 if d:

655 d, x = _0_0, Beta._x_normalized

656 if x and mr:

657 if Mu.x and Phi.x and not isinf(Phi.tan):

658 d = b / mr * (x / Mu.x)**2 \

659 * (x / Phi._x_normalized)

660 else:

661 d = mr / a

662 Mu._diff = self._fm1 * d

663 return Mu

664

665 def RectifyingRadius(self, exact=False):

666 '''Get the I{Rectifying} radius.

667

668 @arg exact: If C{True}, use the exact expression,

669 otherwise the I{Taylor} series.

670

671 @return: Rectifying radius (L{Meter}, same units

672 as the ellipsoid axes).

673 '''

674 r = self._Ef_fRG_a2b2_PI_4 if exact else self._RectifyingR

675 return Meter(r, name__=self.RectifyingRadius)

676

677 @Property_RO

678 def _RectifyingR(self):

679 m = self.ALorder

680 d = _polynomial(self._n2, _RRCoeffs[m], 0, m // 2)

681 return d * (self.a + self.b) * _0_5

682

683 def _scbeta(self, tphi):

684 return _sc(self._fm1 * tphi)

685

686 def _toAux(self, auxout, Phi, **diff_name):

687 '''Convert I{Geographic} to I{Auxiliary} latitude.

688

689 @arg auxout: I{Auxiliary} kind (C{Aux.KIND}).

690 @arg Phi: Geographic latitude (L{AuxLat}).

691 @kwarg diff_name: Use C{B{diff}=True} to set C{diff}

692 and an optional C{B{name}=NN}.

693

694 @return: Auxiliary latitude, I{Eta} (L{AuxLat}).

695 '''

696 _xinstanceof(AuxAngle, Phi=Phi)

697 # assert Phi._AUX == Aux.PHI

698 d, n = _diff_name2(Phi, **diff_name)

699 m = _toAuxCase.get(auxout, None)

700 if m: # callable

701 A = m(self, Phi, diff=d, name=n)

702 elif auxout == Aux.GEODETIC: # == GEOGRAPHIC

703 A = AuxPhi(Phi, name=n)

704 else: # auxout?

705 A = AuxPhi(NAN, name=n)

706 # assert A._AUX == auxout

707 return A

708

709 def _toZeta(self, zetaux):

710 '''Return a (lean) function to create C{AuxPhi(tphi)} and

711 convert that into C{AuxAngle} of (fixed) kind C{zetaux}

712 for use only inside the C{_Newton} loop.

713 '''

714 class _AuxPhy(AuxPhi):

715 # lean C{AuxPhi} instance.

716 # _diff = _1_0

717 # _x = _1_0

718

719 def __init__(self, tphi): # PYCHOK signature

720 self._y = tphi

721

722 m = _toAuxCase.get(zetaux, None)

723 if m: # callable

724

725 def _toZeta(tphi):

726 return m(self, _AuxPhy(tphi), diff=True)

727

728 elif zetaux == Aux.GEODETIC: # GEOGRAPHIC

729 _toZeta = _AuxPhy

730

731 else: # zetaux?

732

733 def _toZeta(unused): # PYCHOK expected

734 return _AuxPhy(NAN)

735

736 return _toZeta

737

738

739# switch(auxout): ...

740 _toAuxCase = {Aux.AUTHALIC: AuxLat.Authalic,

741 Aux.CONFORMAL: AuxLat.Conformal,

742 Aux.GEOCENTRIC: AuxLat.Geocentric,

743 Aux.PARAMETRIC: AuxLat.Parametric,

744 Aux.RECTIFYING: AuxLat.Rectifying}

745

746

747 def _Clenshaw(sinp, Zeta, cs, K):

748 sz, cz = Zeta._yx # isnormal

749 # Evaluate sum(c[k] * sin((2*k+2) * Zeta)) if sinp else

750 # sum(c[k] * cos((2*k+2) * Zeta))

751 x = _2cos2x(cz, sz) # 2 * cos(2*Zeta)

752 if isfinite(x):

753 U0, U1 = Fsum(), Fsum()

754 # assert len(cs) == K

755 for r in _reverange(K):

756 U1 -= U0 * x + cs[r]

757 U1, U0 = U0, -U1

758 # u0*f0(Zeta) - u1*fm1(Zeta)

759 # f0 = sin(2*Zeta) if sinp else cos(2*Zeta)

760 # fm1 = 0 if sinp else 1

761 if sinp:

762 U0 *= sz * cz * _2_0

763 else:

764 U0 *= x * _0_5

765 U0 -= U1

766 x = float(U0)

767 return x

768

769

770 def _diff_name2(Phi, diff=False, **name):

771 '''(INTERNAL) Get C{{Bdiff}=False} and C{B{name}=NN}.

772 '''

773 n = _name__(name, _or_nameof=Phi) # if name else Phi.name

774 return diff, n

775

776

777 def _Newton(tphi, Zeta, _toZeta, **name):

778 # Newton's method from AuxLat._fromAux

779 try:

780 _lg2 = _log2

781 _abs = fabs

782 tz = _abs(Zeta.tan)

783 tphi = tz / tphi # **)

784 ltz = _lg2(tz) # **)

785 ltphi = _lg2(tphi) # **)

786 ltmin = min(ltphi, MIN_EXP)

787 ltmax = max(ltphi, MAX_EXP)

788# auxin = Zeta._AUX

789 s, n, __2 = 0, 3, _0_5 # n = i + 2

790 _TOL, _xp2 = _EPSqrt, _exp2

791 for i in range(1, _TRIPS): # up to 1 Ki!

792 # _toAux(auxin, AuxPhi(tphi), diff=True)

793 Z = _toZeta(tphi)

794 # assert Z._AUX == auxin

795 t, s_ = Z.tan, s

796 if t > tz:

797 ltmax, s = ltphi, +1

798 elif t < tz:

799 ltmin, s = ltphi, -1

800 else:

801 break

802 # derivative dtan(Z)/dtan(Phi)

803 # to dlog(tan(Z))/dlog(tan(Phi))

804 d = (ltz - _lg2(t)) * t # **)

805 if d:

806 d = d / (Z.diff * tphi) # **)

807 ltphi += d

808 tphi = _xp2(ltphi)

809 if _abs(d) < _TOL:

810 i += 1

811 # _toAux(auxin, AuxPhi(tphi), diff=True)

812 Z = _toZeta(tphi)

813 tphi -= _over(Z.tan - tz, Z.diff)

814 break

815 if (i > n and (s * s_) < 0) or not ltmin < ltphi < ltmax:

816 s, n = 0, (i + 2)

817 ltphi = (ltmin + ltmax) * __2

818 tphi = _xp2(ltphi)

819 else:

820 i = _TRIPS

821 Phi = AuxPhi(tphi, **name).copyquadrant(Zeta)

822 Phi._iter = i

823 except (ValueError, ZeroDivisionError): # **) zero t, tphi, tz or Z.diff

824 Phi = AuxPhi(Zeta, **name) # diff as-as

825 Phi._iter = 0

826 # assert Phi._AUX == Aux.PHI

827 return Phi

828

829

830 _AR2Coeffs = _Rdict(18,

831 _Rtuple(_Rkey(4), 4, '4/315, 4/105, 4/15, -1/3'),

832 _Rtuple(_Rkey(6), 6, '4/1287, 4/693, 4/15, 4/105, 4/315, -1/3'),

833 _Rtuple(_Rkey(8), 8, '4/3315, 4/2145, 4/1287, 4/693, 4/315, 4/105, 4/15, -1/3'))

834

835 _RRCoeffs = _Rdict(9,

836 _Rtuple(_Rkey(4), 2, '1/64, 1/4'),

837 _Rtuple(_Rkey(6), 3, '1/256, 1/64, 1/4'),

838 _Rtuple(_Rkey(8), 4, '25/16384, 1/256, 1/64, 1/4')) # PYCHOK used!

839

840 del _Rdict, _Rkey, _Rtuple

841# assert set(_AR2Coeffs.keys()) == set(_RRCoeffs.keys())

842

843# AuxLat._Lmax = max(_AR2Coeffs.keys()) # == max(ALorder)

844

845 __all__ += _ALL_DOCS(AuxLat)

846

847# **) MIT License

848#

849# Copyright (C) 2023-2026 -- mrJean1 at Gmail -- All Rights Reserved.

850#

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

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

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

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

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

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

857#

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

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

860#

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

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

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

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

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

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

867# OTHER DEALINGS IN THE SOFTWARE.