Coverage for pygeodesy/auxilats/auxLat.py: 93%
438 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-06 12:20 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-06 12:20 -0500
2# -*- coding: utf-8 -*-
4u'''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>}
6in I{GeographicLib version 2.2+}.
8Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2022-2024) and licensed
9under the MIT/X11 License. For more information, see the U{GeographicLib
10<https://GeographicLib.SourceForge.io>} documentation.
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
16from __future__ import division as _; del _ # PYCHOK semicolon
18from pygeodesy.auxilats.auxAngle import AuxAngle, AuxBeta, AuxChi, _AuxClass, \
19 AuxMu, AuxPhi, AuxTheta, AuxXi
20from pygeodesy.auxilats.auxily import Aux, _sc, _sn
21from pygeodesy.auxilats._CX_Rs import _Rdict, _Rtuple
22from pygeodesy.basics import _reverange, _xinstanceof, _passarg
23from 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
26from pygeodesy.datums import _ellipsoidal_datum, _WGS84, \
27 Ellipsoid, _name__, _EWGS84
28# from pygeodesy.ellipsoids import Ellipsoid, _EWGS84 # from .datums
29from pygeodesy.elliptic import Elliptic as _Ef
30from pygeodesy.errors import AuxError, _xkwds_not, _xkwds_pop2, _Xorder
31# from pygeodesy.fmath import cbrt # from .karney
32from pygeodesy.fsums import Fsum, _Fsumf_, _sum
33# from pygeodesy.internals import _passarg # from .basics
34from pygeodesy.interns import NN, _not_scalar_, _UNDER_
35from 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
38from pygeodesy.props import Property, Property_RO, _update_all
39from pygeodesy.units import _isDegrees, _isRadius, Degrees, Meter
40from pygeodesy.utily import atan1, atan2
42from math import asinh, copysign, cosh, fabs, sin, sinh, sqrt
43try:
44 from math import exp2 as _exp2
45except ImportError: # Python 3.11-
47 def _exp2(x):
48 return pow(_2_0, x)
50__all__ = ()
51__version__ = '24.09.03'
53_TRIPS = 1024 # XXX 2 or 3?
56class AuxLat(AuxAngle):
57 '''Base class for accurate conversion between I{Auxiliary} latitudes
58 on an ellipsoid.
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}.
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
73 def __init__(self, a_earth=_EWGS84, f=None, b=None, **ALorder_name):
74 '''New L{AuxLat} instance on an ellipsoid or datum.
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 a_earth not in (_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))
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)
111 @Property_RO
112 def a(self):
113 '''Get the C{ellipsoid}'s equatorial radius (C{meter}, conventionally).
114 '''
115 return self.ellipsoid.a
117 equatoradius = a
119 @Property
120 def ALorder(self):
121 '''Get the I{AuxLat} order (C{int}, 4, 6 or 8).
122 '''
123 return self._mAL
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
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
143 def Authalic(self, Phi, **diff_name):
144 '''Convert I{Geographic} to I{Aunthalic} latitude.
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}.
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)
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
177 def AuthalicRadius2(self, exact=False, f_max=_0_1):
178 '''Get the I{Authalic} radius I{squared}.
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}).
184 @return: Authalic radius I{squared} (C{meter} I{squared}, same
185 units as the ellipsoid axes).
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
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)
212 polaradius = b
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
220 k = Aux._1d(auxout, auxin)
221 try: # cached
222 return AuxLat._csc[aL][k]
223 except KeyError:
224 pass
226 try:
227 Cx = self._CXcoeffs[auxout][auxin] # _Rtuple!
228 except KeyError as x:
229 raise AuxError(auxout=auxout, auxin=auxin, cause=x)
231 d = x = n = self._n
232 if Aux.use_n2(auxin) and Aux.use_n2(auxout):
233 x = self._n2
235 def _m(aL):
236 for m in _reverange(aL):
237 yield m // 2
238 else:
239 _m = _reverange # PYCHOK expected
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
253 def Conformal(self, Phi, **diff_name):
254 '''Convert I{Geographic} to I{Conformal} latitude.
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}.
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
312 d, n = _diff_name2(Phi, **diff_name)
313 Chi = AuxChi(tchi, name=n).copyquadrant(Phi)
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
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
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
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))
378 raise AuxError(auxout=auxout, Zeta_d=Zeta_d, exact=exact)
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)
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
418 @Property_RO
419 def _e(self): # unsigned, (1st) eccentricity
420 return self.ellipsoid.e # sqrt(fabs(self._e2))
422 @Property_RO
423 def _e1(self):
424 return sqrt(fabs(self._e12))
426 @Property_RO
427 def _e12(self):
428 return _over(self._e2, _1_0 - self._e2)
430 @Property_RO
431 def _e12p1(self):
432 return _1_0 / self._e2m1
434 @Property_RO
435 def _e2(self): # signed, (1st) eccentricity squared
436 return self.ellipsoid.e2
438 @Property_RO
439 def _e2m1(self): # 1 less 1st eccentricity squared
440 return self.ellipsoid.e21 # == ._fm1**2
442 @Property_RO
443 def _e2m1_sq2(self):
444 return self._e2m1 * sqrt(self._q * _0_5)
446 @Property_RO
447 def _2_e2m12(self):
448 return _2_0 / self._e2m1**2
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
455 @Property_RO
456 def ellipsoid(self):
457 '''Get the ellipsoid (L{Ellipsoid}).
458 '''
459 return self._E
461 @Property_RO
462 def f(self):
463 '''Get the C{ellipsoid}'s flattening (C{scalar}).
464 '''
465 return self.ellipsoid.f
467 flattening = f
469 @Property_RO
470 def _fm1(self): # 1 - flattening
471 return self.ellipsoid.f1
473 def _fromAux(self, Zeta, **name):
474 '''Convert I{Auxiliary} to I{Geographic} latitude.
476 @arg Zeta: Auxiliary latitude (L{AuxAngle}).
477 @kwarg name: Optional C{B{name}=NN} (C{str}).
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
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}
508 def Geocentric(self, Phi, **diff_name):
509 '''Convert I{Geographic} to I{Geocentric} latitude.
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}.
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
525 def Geodetic(self, Phi, **name): # PYCHOK no cover
526 '''Convert I{Geographic} to I{Geodetic} latitude.
528 @arg Phi: Geographic latitude (L{AuxAngle}).
529 @kwarg name: Optional C{B{name}=NN} (C{str}).
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)
538 @Property_RO
539 def _n(self): # 3rd flattening
540 return self.ellipsoid.n
542 @Property_RO
543 def _n2(self):
544 return self._n**2
546 def Parametric(self, Phi, **diff_name):
547 '''Convert I{Geographic} to I{Parametric} latitude.
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}.
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
563 Reduced = Parametric
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
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)
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
605 def Rectifying(self, Phi, **diff_name):
606 '''Convert I{Geographic} to I{Rectifying} latitude.
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}.
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 semiaxis
625 # Beta measured from larger semiaxis
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 db2 = _1_0 - kb * sb2
635 # DLMF Eq. 19.25.9
636 my = b * sb * _Ef._RFRD(cb2, db2, _1_0, kb * sb2)
637 # DLMF Eq. 19.25.10 with complementary angles
638 mx = a * cb * (_Ef.fRF(sb2, da2, _1_0) * ka1 +
639 ka * cb2 * _Ef.fRD(sb2, _1_0, da2, _3_0) * ka1 +
640 ka * sb / sqrt(da2))
641 # my + mx = 2*_Ef.fRG(a*a, b*b) = a*E(e) = b*E(i*e')
642 # mr = a*E(e)*(2/pi) = b*E(i*e')*(2/pi)
643 if self.f < 0:
644 a, b = b, a
645 my, mx = mx, my
646 mr = (my + mx) / PI_2
647 if mr:
648 my = sin(my / mr)
649 mx = sin(mx / mr) # XXX zero?
650 else: # zero Mu
651 my, mx = _0_0, _1_0
652 d, n = _diff_name2(Phi, **diff_name)
653 Mu = AuxMu(my, mx, # normalized
654 name=n).copyquadrant(Phi)
655 if d:
656 d, x = _0_0, Beta._x_normalized
657 if x and mr:
658 if Mu.x and Phi.x and not isinf(Phi.tan):
659 d = b / mr * (x / Mu.x)**2 \
660 * (x / Phi._x_normalized)
661 else:
662 d = mr / a
663 Mu._diff = self._fm1 * d
664 return Mu
666 def RectifyingRadius(self, exact=False):
667 '''Get the I{Rectifying} radius.
669 @arg exact: If C{True}, use the exact expression,
670 otherwise the I{Taylor} series.
672 @return: Rectifying radius (L{Meter}, same units
673 as the ellipsoid axes).
674 '''
675 r = self._Ef_fRG_a2b2_PI_4 if exact else self._RectifyingR
676 return Meter(r, name__=self.RectifyingRadius)
678 @Property_RO
679 def _RectifyingR(self):
680 m = self.ALorder
681 d = _polynomial(self._n2, _RRCoeffs[m], 0, m // 2)
682 return d * (self.a + self.b) * _0_5
684 def _scbeta(self, tphi):
685 return _sc(self._fm1 * tphi)
687 def _toAux(self, auxout, Phi, **diff_name):
688 '''Convert I{Geographic} to I{Auxiliary} latitude.
690 @arg auxout: I{Auxiliary} kind (C{Aux.KIND}).
691 @arg Phi: Geographic latitude (L{AuxLat}).
692 @kwarg diff_name: Use C{B{diff}=True} to set C{diff}
693 and an optional C{B{name}=NN}.
695 @return: Auxiliary latitude, I{Eta} (L{AuxLat}).
696 '''
697 _xinstanceof(AuxAngle, Phi=Phi)
698 # assert Phi._AUX == Aux.PHI
699 d, n = _diff_name2(Phi, **diff_name)
700 m = _toAuxCase.get(auxout, None)
701 if m: # callable
702 A = m(self, Phi, diff=d, name=n)
703 elif auxout == Aux.GEODETIC: # == GEOGRAPHIC
704 A = AuxPhi(Phi, name=n)
705 else: # auxout?
706 A = AuxPhi(NAN, name=n)
707 # assert A._AUX == auxout
708 return A
710 def _toZeta(self, zetaux):
711 '''Return a (lean) function to create C{AuxPhi(tphi)} and
712 convert that into C{AuxAngle} of (fixed) kind C{zetaux}
713 for use only inside the C{_Newton} loop.
714 '''
715 class _AuxPhy(AuxPhi):
716 # lean C{AuxPhi} instance.
717 # _diff = _1_0
718 # _x = _1_0
720 def __init__(self, tphi): # PYCHOK signature
721 self._y = tphi
723 m = _toAuxCase.get(zetaux, None)
724 if m: # callable
726 def _toZeta(tphi):
727 return m(self, _AuxPhy(tphi), diff=True)
729 elif zetaux == Aux.GEODETIC: # GEOGRAPHIC
730 _toZeta = _AuxPhy
732 else: # zetaux?
734 def _toZeta(unused): # PYCHOK expected
735 return _AuxPhy(NAN)
737 return _toZeta
740# switch(auxout): ...
741_toAuxCase = {Aux.AUTHALIC: AuxLat.Authalic,
742 Aux.CONFORMAL: AuxLat.Conformal,
743 Aux.GEOCENTRIC: AuxLat.Geocentric,
744 Aux.PARAMETRIC: AuxLat.Parametric,
745 Aux.RECTIFYING: AuxLat.Rectifying}
748def _Clenshaw(sinp, Zeta, cs, K):
749 sz, cz = Zeta._yx # isnormal
750 # Evaluate sum(c[k] * sin((2*k+2) * Zeta)) if sinp else
751 # sum(c[k] * cos((2*k+2) * Zeta))
752 x = _2cos2x(cz, sz) # 2 * cos(2*Zeta)
753 if isfinite(x):
754 U0, U1 = Fsum(), Fsum()
755 # assert len(cs) == K
756 for r in _reverange(K):
757 U1 -= U0 * x + cs[r]
758 U1, U0 = U0, -U1
759 # u0*f0(Zeta) - u1*fm1(Zeta)
760 # f0 = sin(2*Zeta) if sinp else cos(2*Zeta)
761 # fm1 = 0 if sinp else 1
762 if sinp:
763 U0 *= sz * cz * _2_0
764 else:
765 U0 *= x * _0_5
766 U0 -= U1
767 x = float(U0)
768 return x
771def _diff_name2(Phi, diff=False, **name):
772 '''(INTERNAL) Get C{{Bdiff}=False} and C{B{name}=NN}.
773 '''
774 n = _name__(name, _or_nameof=Phi) # if name else Phi.name
775 return diff, n
778def _Newton(tphi, Zeta, _toZeta, **name):
779 # Newton's method from AuxLat._fromAux
780 try:
781 _lg2 = _log2
782 _abs = fabs
783 tz = _abs(Zeta.tan)
784 tphi = tz / tphi # **)
785 ltz = _lg2(tz) # **)
786 ltphi = _lg2(tphi) # **)
787 ltmin = min(ltphi, MIN_EXP)
788 ltmax = max(ltphi, MAX_EXP)
789# auxin = Zeta._AUX
790 s, n, __2 = 0, 3, _0_5 # n = i + 2
791 _TOL, _xp2 = _EPSqrt, _exp2
792 for i in range(1, _TRIPS): # up to 1 Ki!
793 # _toAux(auxin, AuxPhi(tphi), diff=True)
794 Z = _toZeta(tphi)
795 # assert Z._AUX == auxin
796 t, s_ = Z.tan, s
797 if t > tz:
798 ltmax, s = ltphi, +1
799 elif t < tz:
800 ltmin, s = ltphi, -1
801 else:
802 break
803 # derivative dtan(Z)/dtan(Phi)
804 # to dlog(tan(Z))/dlog(tan(Phi))
805 d = (ltz - _lg2(t)) * t # **)
806 if d:
807 d = d / (Z.diff * tphi) # **)
808 ltphi += d
809 tphi = _xp2(ltphi)
810 if _abs(d) < _TOL:
811 i += 1
812 # _toAux(auxin, AuxPhi(tphi), diff=True)
813 Z = _toZeta(tphi)
814 tphi -= _over(Z.tan - tz, Z.diff)
815 break
816 if (i > n and (s * s_) < 0) or not ltmin < ltphi < ltmax:
817 s, n = 0, (i + 2)
818 ltphi = (ltmin + ltmax) * __2
819 tphi = _xp2(ltphi)
820 else:
821 i = _TRIPS
822 Phi = AuxPhi(tphi, **name).copyquadrant(Zeta)
823 Phi._iter = i
824 except (ValueError, ZeroDivisionError): # **) zero t, tphi, tz or Z.diff
825 Phi = AuxPhi(Zeta, **name) # diff as-as
826 Phi._iter = 0
827 # assert Phi._AUX == Aux.PHI
828 return Phi
831_AR2Coeffs = _Rdict(18,
832 _Rtuple(4, 4, '4/315, 4/105, 4/15, -1/3'),
833 _Rtuple(6, 6, '4/1287, 4/693, 4/15, 4/105, 4/315, -1/3'),
834 _Rtuple(8, 8, '4/3315, 4/2145, 4/1287, 4/693, 4/315, 4/105, 4/15, -1/3'))
836_RRCoeffs = _Rdict(9,
837 _Rtuple(4, 2, '1/64, 1/4'),
838 _Rtuple(6, 3, '1/256, 1/64, 1/4'),
839 _Rtuple(8, 4, '25/16384, 1/256, 1/64, 1/4')) # PYCHOK used!
841del _Rdict, _Rtuple
842# assert set(_AR2Coeffs.keys()) == set(_RRCoeffs.keys())
844# AuxLat._Lmax = max(_AR2Coeffs.keys()) # == max(ALorder)
846__all__ += _ALL_DOCS(AuxLat)
848# **) MIT License
849#
850# Copyright (C) 2023-2025 -- mrJean1 at Gmail -- All Rights Reserved.
851#
852# Permission is hereby granted, free of charge, to any person obtaining a
853# copy of this software and associated documentation files (the "Software"),
854# to deal in the Software without restriction, including without limitation
855# the rights to use, copy, modify, merge, publish, distribute, sublicense,
856# and/or sell copies of the Software, and to permit persons to whom the
857# Software is furnished to do so, subject to the following conditions:
858#
859# The above copyright notice and this permission notice shall be included
860# in all copies or substantial portions of the Software.
861#
862# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
863# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
864# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
865# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
866# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
867# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
868# OTHER DEALINGS IN THE SOFTWARE.