Coverage for pygeodesy/triaxials/bases.py: 91%
471 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-15 15:48 -0500
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-15 15:48 -0500
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) Base classes for I{ordered} triaxial ellipsoid classes L{Conformal}, L{Conformal3},
5L{Triaxial}, L{Triaxial3} and I{unordered} L{Triaxial_}.
7Transcoded to pure Python from I{Karney}'s GeographicLib 2.7 C++ classes U{Ellipsoid3<https://
8GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Triaxial_1_1Ellipsoid3.html>},
9U{Cartesian3<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Triaxial_1_1Cartesian3.html>} and
10U{Conformal3<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Triaxial_1_1Conformal3.html>}.
12GeographicLib 2.5.2 C++ class U{JacobiConformal<https://GeographicLib.SourceForge.io/C++/doc/
13classGeographicLib_1_1JacobiConformal.html#details>}.
15Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2008-2024, 2025) and licensed under the MIT/X11 License.
16For more information, see the U{GeographicLib 2.5.2 and 2.7<https://GeographicLib.SourceForge.io/>} documentation.
18Enum-like C{Lat-/Longitude Kinds (LLK)}, see I{Karney}'s U{coord<https://GeographicLib.SourceForge.io/
19C++/doc/classGeographicLib_1_1Triaxial_1_1Cartesian3.html>}:
21@var LLK.CONFORMAL: Jacobi conformal X and Y projection
22@var LLK.ELLIPSOIDAL: Ellipsoidal lat-, longitude and heading C{bet}, C{omg}, C{alp} (L{Ang})
23@var LLK.GEOCENTRIC: Geocentric lat-, longitude and heading C{phi}", C{lam}" and C{zet} (L{Ang})
24@var LLK.GEOCENTRIC_X: Geocentric with pole along major X axis
25@var LLK.GEODETIC: Geodetic lat-, longitude and heading C{phi}, C{lam} and C{zet} (L{Ang})
26@var LLK.GEODETIC_X: Geodetic with pole along major X axis
27@var LLK.GEODETIC_LON0: Geodetic lat-, longitude I{- lon0} and heading C{phi}, C{lam} and C{zet} (L{Ang})
28@var LLK.GEOGRAPHIC = LLK.GEODETIC
29@var LLK.PARAMETRIC: Parametric lat-, longitude and heading C{phi}', C{lam}' and C{zet} (L{Ang})
30@var LLK.PARAMETRIC_X: Parametric with pole along major X axis
31@var LLK.PLANETODETIC = LLK.GEODETIC
32@var LLK.PLANETOCENTRIC = LLK.GEOCENTRIC
33'''
34# make sure int/int division yields float quotient, see .basics
35from __future__ import division as _; del _ # noqa: E702 ;
37# from pygeodesy.angles import Ang, isAng # _MODS
38# from pygeodesy.basics import map1 # from .namedTuples
39from pygeodesy.constants import EPS, EPS0, EPS02, EPS4, INT0, NAN, PI_3, PI4, \
40 _EPS2e4, _isfinite, float0_, _1_over, _0_0, \
41 _1_0, _N_1_0, _3_0, _4_0 # PYCHOK used!
42# from pygeodesy.ellipses import Ellipse, _isFlat # _MODS
43# from pygeodesy.ellipsoids import Ellipsoid, _EWGS84 # _MODS
44# from pygeodesy.elliptic import Elliptic # _MODS
45# from pygeodesy.errors import _ValueError, _xkwds # from .utily
46from pygeodesy.fmath import cbrt, fmean_, hypot, norm2, sqrt0, fabs, sqrt
47from pygeodesy.fsums import _Fsumf_, fsumf_
48# from pygeodesy.internals import typename # _MODS
49from pygeodesy.interns import _a_, _b_, _c_, _inside_, _not_, _NOTEQUAL_, _null_, \
50 _outside_, _scale_, _SPACE_, _spherical_, _x_, _y_, _z_
51from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS, _FOR_DOCS
52from pygeodesy.named import _NamedEnum, _NamedEnumItem, _NamedTuple, _Pass # _MODS
53from pygeodesy.namedTuples import Vector4Tuple, map1
54from pygeodesy.props import Property_RO, property_doc_, property_RO, \
55 deprecated_method, deprecated_property_RO
56# from pygeodesy.streprs import Fmt # _MODS
57from pygeodesy.units import Degrees, Easting, Float, Height, Height_, Meter, \
58 Meter2, Meter3, Northing, Radius_, Scalar
59from pygeodesy.utily import km2m, m2km, _ValueError, _xkwds
60from pygeodesy.vector3d import _otherV3d, Vector3d
62# from math import fabs, sqrt # from .fmath
64__all__ = _ALL_LAZY.triaxials_bases
65__version__ = '26.02.15'
67_bet_ = 'bet' # PYCHOK shared
68_llk_ = 'llk' # PYCHOK shared
69_KTpFlat = 1.5849625007
70_MAXIT = 33 # 20 # PYCHOK shared
71_not_ordered_ = _not_('ordered')
72_omg_ = 'omg' # PYCHOK shared
75class Conformal5Tuple(_NamedTuple): # see .Forward4Tuple
76 '''5-Tuple C{(x, y, z, scale, llk)} with the easting C{x} and
77 northing C{y} projection, C{scale} or C{NAN} I{but with}
78 C{z=INT0} I{and kind} C{llk=LLK.CONFORMAL} I{always}.
79 '''
80 _Names_ = (_x_, _y_, _z_, _scale_, _llk_)
81 _Units_ = ( Easting, Northing, _Pass, Scalar, _Pass)
83 def __new__(cls, x, y, z=INT0, scale=NAN, llk=None, **kwds): # **iteration_name
84 args = x, y, (z or INT0), scale, (llk or LLK.CONFORMAL)
85 return _NamedTuple.__new__(cls, args, **kwds)
88class _LLK(str):
89 '''(INTERNAL) Lat-/Longitude Kind.
90 '''
91 def __init__(self, llk): # aka C++ alt
92 self._X = bool(llk.endswith('_X'))
93 str.__init__(llk)
96class LLK(object):
97 '''Enum-like C{Lat-/Longitude Kinds (LLK)}, see U{coord<https://GeographicLib.
98 SourceForge.io/C++/doc/classGeographicLib_1_1Triaxial_1_1Cartesian3.html>}.
99 '''
100 CONFORMAL = _LLK('CONFORMAL')
102 ELLIPSOIDAL = _LLK('ELLIPSOIDAL') # bet, omg, alp
103 GEOCENTRIC = _LLK('GEOCENTRIC') # phi2p, lam2p, zet
104 GEOCENTRIC_X = _LLK('GEOCENTRIC_X')
105 GEODETIC = _LLK('GEODETIC') # phi, lam, zet
106 GEODETIC_LON0 = _LLK('GEODETIC_LON0')
107 GEODETIC_X = _LLK('GEODETIC_X')
108 GEOGRAPHIC = GEODETIC
109 PARAMETRIC = _LLK('PARAMETRIC') # phi1p, lam1p, zet
110 PARAMETRIC_X = _LLK('PARAMETRIC_X')
111 PLANETODETIC = GEODETIC
112 PLANETOCENTRIC = GEOCENTRIC
114 _CENTRICS = (GEOCENTRIC, GEOCENTRIC_X, PLANETOCENTRIC)
115 _DETICS = (GEODETIC, GEODETIC_X, GEODETIC_LON0, GEOGRAPHIC, PLANETODETIC)
116 _METRICS = (PARAMETRIC, PARAMETRIC_X)
117 _NOIDAL = (None, ELLIPSOIDAL)
118# _XCLUDE = (CONFORMAL, GEOGRAPHIC, PLANETOCENTRIC, PLANETODETIC)
120 def __getitem__(self, name):
121 llk = self.get(name, None)
122 if llk is None:
123 t = _MODS.internals.typename(self)
124 t = _MODS.streprs.Fmt.SQUARE(t, name)
125 raise _ValueError(t, name)
126 return llk
128 def get(self, name, dflt=None):
129 '''Get an C{LLK} by C{name}.
130 '''
131 llk = getattr(self, name, None)
132 return llk if isinstance(llk, _LLK) else dflt
134 def items(self):
135 '''Yield all C{LLK (name, value)} pairs.
136 '''
137 for n, llk in LLK.__class__.__dict__.items():
138 if isinstance(llk, _LLK):
139 yield n, llk
141 def keys(self):
142 '''Yield all C{LLK} names.
143 '''
144 for n, _ in self.items():
145 yield n
147 def values(self):
148 '''Yield all C{LLK} values.
149 '''
150 for _, llk in self.items():
151 yield llk
153if not _FOR_DOCS: # PYCHOK force epydoc
154 LLK = LLK() # singleton
155del _FOR_DOCS
158def _HeightINT0(h):
159 return h if h is INT0 else Height(h=h)
162class _UnOrderedTriaxialBase(_NamedEnumItem):
163 '''(INTERNAL) Base class for all I{unordered} triaxial classes.
164 '''
165 _ijk = _kji = None
166 _unordered = True
168 def __init__(self, a_triaxial, b=None, c=None, **name):
169 '''New I{unordered} C{Triaxial_}.
171 @arg a_triaxial: Large, C{X} semi-axis (C{scalar}, conventionally in
172 C{meter}) or an other L{Triaxial}, L{Triaxial_} or
173 L{TriaxialB} instance.
174 @kwarg b: Middle, C{Y} semi-axis (C{meter}, same units as B{C{a}}),
175 required if C{B{a_triaxial} is scalar}, ignored otherwise.
176 @kwarg c: Small, C{Z} semi-axis (C{meter}, like B{C{b}}).
177 @kwarg name: Optional C{B{name}=NN} (C{str}).
179 @raise TriaxialError: Invalid semi-axis or -axes.
180 '''
181 try:
182 try:
183 a = a_triaxial
184 t = a._abc3
185 name = _xkwds(name, name=a.name)
186 except AttributeError:
187 t = Radius_(a=a), Radius_(b=b), Radius_(c=c)
188 except (TypeError, ValueError) as x:
189 raise TriaxialError(a=a, b=b, c=c, cause=x)
190 if name:
191 self.name = name
193 a, b, c = self._abc3 = t
194 if self._unordered: # == not isinstance(self, Triaxial)
195 s, _, t = sorted(t)
196 if not (_isfinite(t) and _isfinite(s) and s > 0):
197 raise TriaxialError(a=a, b=b, c=c) # txt=_invalid_
198 elif not (_isfinite(a) and a >= b >= c > 0): # see TriaxialB
199 raise TriaxialError(a=a, b=b, c=c, txt=_not_ordered_)
200 elif not (a > c and self._a2c2 > 0 and self.e2ac > 0):
201 raise TriaxialError(a=a, c=c, e2ac=self.e2ac, txt=_spherical_)
203# def __str__(self): # in _NamedEnumItem
204# return self.toStr()
206 @Property_RO
207 def a(self):
208 '''Get the C{largest, x} semi-axis (C{meter}, conventionally).
209 '''
210 a, _, _ = self._abc3
211 return a
213 @Property_RO
214 def a2(self):
215 '''Get C{a**2}.
216 '''
217 return self.a**2
219 @Property_RO
220 def _a2b2(self):
221 '''(INTERNAL) Get C{a**2 - b**2} == E_sub_e**2.
222 '''
223 a, b, _ = self._abc3
224 d = a - b
225 return (d * (a + b)) if d else _0_0
227 @Property_RO
228 def _a2_b2(self):
229 '''(INTERNAL) Get C{(a / b)**2}.
230 '''
231 a, b, _ = self._abc3
232 return (a / b)**2 if a != b else _1_0
234 @Property_RO
235 def abc3(self): # in geed3solve._a12d
236 '''Get the semi-axes as 3-tuple C{(a, b, c)}.
237 '''
238 return self._abc3
240 @Property_RO
241 def _a2b2c23(self):
242 '''(INTERNAL) Get 3-tuple C{(a**2, b**2, c**2)}.
243 '''
244 a, b, c = self._abc3
245 return self.a2, self.b2, self.c2
247 @Property_RO
248 def _a2c2(self):
249 '''(INTERNAL) Get C{a**2 - c**2} == E_sub_x**2.
250 '''
251 a, _, c = self._abc3
252 d = a - c
253 return (d * (a + c)) if d else _0_0
255 @Property_RO
256 def area(self):
257 '''Get the surface area (C{meter} I{squared}).
258 '''
259 return self.areaKT(_KTpFlat) if self.isFlat else self.areaRG
261 def areaKT(self, *p):
262 '''I{Approximate} the surface area using U{Knud Thomson's
263 <https://WikiPedia.org/wiki/Ellipsoid#Approximate_formula>}
264 formula (C{meter} I{squared}).
266 @arg p: Exponent (C{scalar} > 0), 1.6075 for near-spherical
267 or 1.5849625007 for "near-flat" triaxials.
268 '''
269 a, b, c = self._abc3
270 if a == b == c:
271 a *= a
272 else:
273 _p = pow
274 p = p[0] if p else (_KTpFlat if self.isFlat else 1.6075)
275 a = _p(fmean_(_p(a * b, p), _p(a * c, p), _p(b * c, p)), _1_over(p))
276 return Meter2(areaKT=a * PI4)
278 @deprecated_method
279 def area_p(self, p=1.6075):
280 '''DEPRECATED on 2026-02-15, use method L{areaKT<Triaxial_.areaKT>}.'''
281 return Meter2(area_p=self.areaKT(p))
283 @Property_RO
284 def areaRG(self):
285 '''Get the surface area using U{Carlson's symmetric RG
286 <https://WikiPedia.org/wiki/Ellipsoid#Surface_Area>}
287 form (C{meter} I{squared}).
288 '''
289 t = self.a2, self.b2, self.c2
290 r = _MODS.elliptic._rG3(*map(_1_over, t))
291 return Meter2(areaRG=self.volume * r * _3_0)
293 @Property_RO
294 def b(self):
295 '''Get the C{middle, y} semi-axis (C{meter}, same units as B{C{a}}).
296 '''
297 _, b, _ = self._abc3
298 return b
300 @Property_RO
301 def b2(self):
302 '''Get C{b**2}.
303 '''
304 return self.b**2
306 @Property_RO
307 def _b2_a2(self):
308 '''(INTERNAL) Get C{(b / a)**2}.
309 '''
310 a, b, _ = self._abc3
311 return (b / a)**2 if a != b else _1_0
313 @Property_RO
314 def _b2c2(self):
315 '''(INTERNAL) Get C{b**2 - c**2} == E_sub_y**2.
316 '''
317 _, b, c = self._abc3
318 d = b - c
319 return (d * (b + c)) if d else _0_0
321 @Property_RO
322 def c(self):
323 '''Get the C{smallest, z} semi-axis (C{meter}, same units as B{C{a}}).
324 '''
325 _, _, c = self._abc3
326 return c
328 @Property_RO
329 def c2(self):
330 '''Get C{c**2}.
331 '''
332 return self.c**2
334 @Property_RO
335 def _c2_a2(self):
336 '''(INTERNAL) Get C{(c / a)**2}.
337 '''
338 a, _, c = self._abc3
339 return (c / a)**2 if a != c else _1_0
341 @Property_RO
342 def _c2_b2(self):
343 '''(INTERNAL) Get C{(c / b)**2}.
344 '''
345 _, b, c = self._abc3
346 return (c / b)**2 if b != c else _1_0
348 @Property_RO
349 def e2ab(self):
350 '''Get the C{ab} ellipse' I{(1st) eccentricity squared} (C{scalar}), M{1 - (b/a)**2}.
351 '''
352 return Float(e2ab=(_1_0 - self._b2_a2) or _0_0)
354# _1e2ab = _b2_a2 # == C{1 - e2ab} == C{(b/a)**2}
356 @Property_RO
357 def e2ac(self):
358 '''Get the C{ac} ellipse' I{(1st) eccentricity squared} (C{scalar}), M{1 - (c/a)**2}.
359 '''
360 return Float(e2ac=(_1_0 - self._c2_a2) or _0_0)
362# _1e2ac = _c2_a2 # == C{1 - e2ac} == C{(c/a)**2}
364 @Property_RO
365 def e2bc(self):
366 '''Get the C{bc} ellipse' I{(1st) eccentricity squared} (C{scalar}), M{1 - (c/b)**2}.
367 '''
368 return Float(e2bc=(_1_0 - self._c2_b2) or _0_0)
370# _1e2bc = _c2_b2 # == C{1 - e2bc} == C{(c/b)**2}
372 def hartzell4(self, pov, los=False, **name):
373 '''Compute the intersection of this triaxial's surface with a Line-Of-Sight
374 from a Point-Of-View in space.
376 @see: Function L{hartzell4<triaxials.triaxial5.hartzell4>} for further details.
377 '''
378 return _MODS.triaxials.hartzell4(pov, los=los, tri_biax=self, **name)
380 def height4(self, x_xyz, y=None, z=None, normal=True, eps=EPS, **name):
381 '''Compute the projection on and the height above or below this triaxial's surface.
383 @see: Function L{height4<triaxials.triaxial5.height4>} for further details.
384 '''
385 return _MODS.triaxials.height4(x_xyz, y=y, z=z, tri_biax=self, normal=normal, eps=eps, **name)
387 @Property_RO
388 def isFlat(self):
389 '''Is this triaxial "flat", too pro-/oblate (C{bool})?
390 '''
391 _f = _MODS.ellipses._isFlat
392 c, b, a = sorted(self._abc3)
393 return _f(a, c) or _f(b, c) or _f(a, b)
395 @Property_RO
396 def isOblate(self):
397 '''Is this triaxial oblate (C{bool})?
398 '''
399 return not (self.isProlate or self.isSpherical)
401 @Property_RO
402 def isOrdered(self):
403 '''Is this triaxial I{ordered} and I{not spherical} (C{bool})?
404 '''
405 a, b, c = self._abc3
406 return bool(a >= b > c) # b > c!
408 @Property_RO
409 def isProlate(self):
410 '''Is this triaxial prolate (C{bool})?
411 '''
412 a, b, c = self._abc3
413 return a < b or b < c or a < c
415 @Property_RO
416 def isSpherical(self):
417 '''Is this triaxial I{spherical} (C{Radius} or INT0)?
418 '''
419 a, b, c = self._abc3
420 return a if a == b == c else INT0
422 def _norm2(self, s, c, *a):
423 '''(INTERNAL) Normalize C{s} and C{c} iff not already.
424 '''
425 if fabs(_hypot2_1(s, c)) > EPS02:
426 s, c = norm2(s, c)
427 if a:
428 s, c = norm2(s * self.b, c * a[0])
429 return float0_(s, c)
431 def normal3d(self, x_xyz, y=None, z=None, length=_1_0):
432 '''Get a 3-D vector I{on and perpendicular to} this triaxial's surface.
434 @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},
435 L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).
436 @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}, ignored
437 otherwise.
438 @kwarg z: Z component (C{scalar}), like B{C{y}}.
439 @kwarg length: Optional, signed length in out-/inward direction (C{scalar}).
441 @return: A C{Vector3d(x_, y_, z_)} normalized to B{C{length}}, pointing out-
442 or inward for postive respectively negative B{C{length}}.
444 @raise TriaxialError: Zero length cartesian or vector.
446 @note: Cartesian C{(B{x}, B{y}, B{z})} I{must be on} this triaxial's surface,
447 use method L{Triaxial.sideOf} to validate.
449 @see: Methods L{Triaxial.height4} and L{Triaxial.sideOf}.
450 '''
451 # n = 2 * (x / a2, y / b2, z / c2)
452 # == 2 * (x, y * a2 / b2, z * a2 / c2) / a2 # iff ordered
453 # == 2 * (x, y / _b2_a2, z / _c2_a2) / a2
454 # == unit(x, y / _b2_a2, z / _c2_a2).times(length)
455 x, y, z = _otherV3d_(x_xyz, y, z).xyz3
456 n = Vector3d(x, y / self._b2_a2,
457 z / self._c2_a2, name__=self.normal3d)
458 u = n.length
459 if u < EPS0:
460 raise TriaxialError(x=x_xyz, y=y, z=z, txt=_null_)
461 return n.times(length / u)
463 def normal4(self, x_xyz, y=None, z=None, height=0, normal=True):
464 '''Compute a cartesian at a height above or below this triaxial's surface.
466 @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian}, L{Ecef9Tuple},
467 L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).
468 @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}, ignored
469 otherwise.
470 @kwarg z: Z component (C{scalar}), like B{C{y}}.
472 @kwarg normal: If C{True}, the B{C{height}} is I{perpendicular, plumb} to the
473 triaxial's surface, otherwise C{radially} to the center of this
474 triaxial (C{bool}).
475 @kwarg name: Optional C{B{name}="normal4"} (C{str}).
477 @return: L{Vector4Tuple}C{(x, y, z, h)} with the cartesian coordinates C{x},
478 C{y} and C{z} and C{h} the I{signed, normal distance} to the triaxial's
479 surface in C{meter}, conventionally. Positive C{h} indicates, the
480 cartesian is outside the triaxial, negative C{h} means inside.
482 @raise TriaxialError: Zero length cartesian or vector.
484 @note: Cartesian C{(B{x}, B{y}, B{z})} I{must be on} this triaxial's surface,
485 use method L{Triaxial.sideOf} to validate.
487 @see: Methods L{Triaxial.normal3d} and L{Triaxial.height4}.
488 '''
489 v, h = _otherV3d_(x_xyz, y, z), Height_(height, low=None)
490 if h:
491 if v.length < EPS0:
492 raise TriaxialError(x=x_xyz, y=y, z=z, txt=_null_)
493 if normal:
494 n = self.normal3d(v, length=h)
495 h = n.length
496 n += v
497 else:
498 h = h / v.length
499 n = v.times(h + _1_0)
500 else:
501 n = v
502 return Vector4Tuple(n.x, n.y, n.z, h, name__=self.normal4)
504 def _order3(self, *abc, **reverse): # reverse=False
505 '''(INTERNAL) Un-/Order C{a}, C{b} and C{c}.
507 @return: 3-Tuple C{(a, b, c)} ordered by or un-ordered
508 (reverse-ordered) C{ijk} if C{B{reverse}=True}.
509 '''
510 ijk = self._order_ijk(**reverse)
511 return _getitems(abc, *ijk) if ijk else abc
513 def _order3d(self, v, **reverse): # reverse=False
514 '''(INTERNAL) Un-/Order a C{Vector3d}.
516 @return: Vector3d(x, y, z) un-/ordered.
517 '''
518 ijk = self._order_ijk(**reverse)
519 return v.classof(*_getitems(v.xyz3, *ijk)) if ijk else v
521 @Property_RO
522 def _ordered4(self):
523 '''(INTERNAL) Helper for C{_hartzell3} and C{_plumbTo5}.
524 '''
525 def _order2(reverse, a, b, c):
526 '''(INTERNAL) Un-Order C{a}, C{b} and C{c}.
528 @return: 2-Tuple C{((a, b, c), ijk)} with C{a} >= C{b} >= C{c}
529 and C{ijk} a 3-tuple with the initial indices.
530 '''
531 i, j, k = range(3)
532 if a < b:
533 a, b, i, j = b, a, j, i
534 if a < c:
535 a, c, i, k = c, a, k, i
536 if b < c:
537 b, c, j, k = c, b, k, j
538 # reverse (k, j, i) since (a, b, c) is reversed-sorted
539 ijk = (k, j, i) if reverse else (None if i < j < k else (i, j, k))
540 return (a, b, c), ijk
542 abc, T = self._abc3, self
543 if not self.isOrdered:
544 abc, ijk = _order2(False, *abc)
545 if ijk:
546 _, kji = _order2(True, *ijk)
547 T = _UnOrderedTriaxialBase(*abc)
548 T._ijk, T._kji = ijk, kji
549 return abc + (T,)
551 def _order_ijk(self, reverse=False):
552 '''(INTERNAL) Get the un-/order indices.
553 '''
554 return self._kji if reverse else self._ijk
556 @deprecated_property_RO
557 def perimeter4ab(self):
558 '''DEPRECATED on 2026.02.09, use property L{Ellipse<pygeodesy.Ellipse>}C{(a, b).perimeter2k_}.'''
559 a, b, _ = self._abc3
560 return Meter(perimeter4ab=_MODS.ellipses.Ellipse(a, b).perimeter2k_)
562 @deprecated_property_RO
563 def perimeter4ac(self):
564 '''DEPRECATED on 2026.02.09, use property L{Ellipse<pygeodesy.Ellipse>}C{(a, c).perimeter2k_}.'''
565 a, _, c = self._abc3
566 return Meter(perimeter4ac=_MODS.ellipses.Ellipse(a, c).perimeter2k_)
568 @deprecated_property_RO
569 def perimeter4bc(self):
570 '''DEPRECATED on 2026.02.09, use property L{Ellipse<pygeodesy.Ellipse>}C{(b, c).perimeter2k_}.'''
571 _, b, c = self._abc3
572 return Meter(perimeter4bc=_MODS.ellipses.Ellipse(b, c).perimeter2k_)
574 @Property_RO
575 def R2(self):
576 '''Get the I{authalic} earth radius (C{meter}), M{sqrt(area / PI4)}.
577 '''
578 r = self.isSpherical
579 return Meter(R2=r if r else sqrt(self.area / PI4)) # Radius
581 Rauthalic = R2
583 @Property_RO
584 def R3(self):
585 '''Get the I{volumetric} earth radius (C{meter}), M{(a * b * c)**(1/3)}.
586 '''
587 a, b, c = self._abc3
588 return Meter(R3=a if a == b == c else cbrt(a * b * c)) # Radius
590 Rvolumetric = R3
592 def _radialTo3(self, sbeta, cbeta, somega, comega):
593 '''(INTERNAL) I{Unordered} helper for C{.height4}.
594 '''
595 def _rphi(a, b, sphi, cphi):
596 # <https://WikiPedia.org/wiki/Ellipse#Polar_form_relative_to_focus>
597 # polar form: radius(phi) = a * b / hypot(a * sphi, b * cphi)
598 return (b / hypot(sphi, b / a * cphi)) if a > b else (
599 (a / hypot(cphi, a / b * sphi)) if a < b else a)
601 sa, ca = self._norm2(sbeta, cbeta)
602 sb, cb = self._norm2(somega, comega)
604 a, b, c = self._abc3
605 if a != b:
606 a = _rphi(a, b, sb, cb)
607 if a != c:
608 c = _rphi(a, c, sa, ca)
609 t = c * ca
610 return (t * cb), (t * sb), (c * sa)
612 def sideOf(self, x_xyz, y=None, z=None, eps=EPS4):
613 '''Is a cartesian on, above or below the surface of this triaxial?
615 @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},
616 L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).
617 @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} is C{scalar},
618 ignored otherwise.
619 @kwarg z: Z component (C{scalar}), like B{C{y}}.
620 @kwarg eps: On-surface tolerance (C{scalar}, distance I{squared}).
622 @return: C{INT0} if C{(B{x}, B{y}, B{z})} is near this triaxial's surface
623 within tolerance B{C{eps}}, otherwise the signed, radial distance
624 I{squared} (C{float}), nega-/positive for in- respectively outside
625 this triaxial.
627 @see: Methods L{Triaxial.height4} and L{Triaxial.normal3d}.
628 '''
629 v = _otherV3d_(x_xyz, y, z)
630 s = fsumf_(_N_1_0, *map(_over02, v.xyz3, self._abc3))
631 return INT0 if fabs(s) < eps else s
633 def _sideOn(self, v, eps=_EPS2e4):
634 s = self.sideOf(v.xyz, eps=eps)
635 if s: # PYCHOK no cover
636 t = _SPACE_((_inside_ if s < 0 else _outside_), self.toRepr())
637 raise TriaxialError(eps=eps, sideOf=s, x=v.x, y=v.y, z=v.z, txt=t)
638 return s
640 def toEllipsoid(self, **name):
641 '''Convert this triaxial to a I{biaxial} L{Ellipsoid}, provided 2 axes match.
643 @kwarg name: Optional C{B{name}=NN} (C{str}).
645 @return: An L{Ellipsoid} with north along this C{Z} axis if C{a == b},
646 this C{Y} axis if C{a == c} or this C{X} axis if C{b == c}.
648 @raise TriaxialError: This C{a != b}, C{b != c} and C{c != a}.
650 @see: Method L{Ellipsoid.toTriaxial}.
651 '''
652 a, b, c = self._abc3
653 if a == b:
654 b = c # N = c-Z
655 elif b == c: # N = a-X
656 a, b = b, a
657 elif a != c: # N = b-Y
658 t = _SPACE_(_a_, _NOTEQUAL_, _b_, _NOTEQUAL_, _c_)
659 raise TriaxialError(a=a, b=b, c=c, txt=t)
660 return _MODS.ellipsoids.Ellipsoid(a, b=b, name=self._name__(name))
662 toBiaxial = toEllipsoid
664 def toStr(self, prec=9, **name): # PYCHOK signature
665 '''Return this C{Triaxial} as a string.
667 @kwarg prec: Precision, number of decimal digits (0..9).
668 @kwarg name: Optional name (C{str}), to override or C{None}
669 to exclude this triaxial's name.
671 @return: This C{Triaxial}'s attributes (C{str}).
672 '''
673 T = _UnOrderedTriaxialBase
674 m = _MODS.triaxials
675 C = m.Triaxial3B
676 if isinstance(self, C):
677 t = T.b, C.e2, C.k2, C.kp2
678 else:
679 t = T.a, # props
680 C = m.ConformalSphere
681 t += (C.ab, C.bc) if isinstance(self, C) else (T.b, T.c)
682 C = _Triaxial3Base
683 t += (C.k2, C.kp2) if isinstance(self, C) else \
684 (T.e2ab, T.e2bc, T.e2ac)
685 for C in (m.Conformal, m.Conformal3):
686 if isinstance(self, C):
687 t += C.xyQ2,
688 break
689 t += T.volume, T.area, T.R2
690 return self._instr(prec=prec, props=t, **name)
692 @Property_RO
693 def unOrdered(self):
694 '''Is this triaxial I{un-ordered} and I{not spherical} (C{bool})?
695 '''
696 return not (self.isOrdered or bool(self.isSpherical))
698 @Property_RO
699 def volume(self):
700 '''Get the volume (C{meter**3}), M{4 / 3 * PI * a * b * c}.
701 '''
702 a, b, c = self._abc3
703 return Meter3(volume=a * b * c * PI_3 * _4_0)
706class _OrderedTriaxialBase(_UnOrderedTriaxialBase):
707 '''(INTERNAL) Base class for all I{ordered} triaxial classes.
708 '''
709 _unordered = False
711 def __init__(self, a_triaxial, b=None, c=None, **name):
712 '''New I{ordered} L{Triaxial}, L{Triaxial3}, L{Conformal} or L{Conformal3}.
714 @arg a_triaxial: Largest semi-axis (C{scalar}, conventionally in C{meter})
715 or an other L{Triaxial} or L{Triaxial_} instance.
716 @kwarg b: Middle semi-axis (C{meter}, same units as B{C{a}}), required
717 if C{B{a_triaxial} is scalar}, ignored otherwise.
718 @kwarg c: Smallest semi-axis (C{meter}, like B{C{b}}).
719 @kwarg name: Optional C{B{name}=NN} (C{str}).
721 @note: The semi-axes must be ordered as C{B{a} >= B{b} >= B{c} > 0} and
722 must be ellipsoidal, C{B{a} > B{c}}.
724 @raise TriaxialError: Semi-axes unordered, spherical or invalid.
725 '''
726 _UnOrderedTriaxialBase.__init__(self, a_triaxial, b=b, c=c, **name)
728 @Property_RO
729 def _a2b2_a2c2(self):
730 '''@see: Methods C{.forwardBetaOmega} and property C{._k2_kp2E}.
731 '''
732 s = self._a2c2
733 if s:
734 s = self._a2b2 / s
735 return s or _0_0
737 @Property_RO
738 def area(self):
739 '''Get the surface area (C{meter} I{squared}).
741 @see: U{Surface area<https://WikiPedia.org/wiki/Ellipsoid#Surface_area>}.
742 '''
743 a, b, c = self._abc3
744 if a != b and self.e2ac > 0:
745 a = _UnOrderedTriaxialBase(self).area
746 else: # a == b > c
747 a = _MODS.ellipsoids.Ellipsoid(a, b=c).areax
748 return a
750# @Property_RO
751# def area2k1J(self):
752# '''Get the surface area using elliptic integrals (C{meter} I{squared}).
753# '''
754# a, b, c = self._abc3
755# if a != b and self.e2ac > 0:
756# k2, kp2 = t = self._k2_kp2E
757# if min(t) < 0 or max(t) > _1_0 or self.isFlat:
758# a = self.areaKT()
759# else: # k2, kp2 swapped!
760# # from pygeodesy import asin1, fdot_, PI2
761# aE = _MODS.elliptic.Elliptic(kp2, _0_0, k2, _1_0)
762# s = sqrt(self.e2ac) # sin(phi)**2 == 1 - (c/a)**2
763# t = self._c2_a2 / s # cos(phi)**2 == (c/a)**2
764# r = asin1(s) # phi = atan2(c/a, s) = asin(s)
765# c = fdot_(aE.fE(r), s, c / a, c / b,
766# aE.fF(r), t)
767# a = Meter2(area2k1J=a * b * c * PI2)
768# else: # a == b > c
769# a = _MODS.ellipsoids.Ellipsoid(a, b=c).areax
770# return a
772 @Property_RO
773 def _k2_kp2E(self):
774 '''(INTERNAL) Get elliptic C{k2} and C{kp2} for C{._xE}, C{._yE} and C{.areaE}.
775 '''
776 # k2 = a2b2 / a2c2 * c2_b2
777 # kp2 = b2c2 / a2c2 * a2_b2
778 # b2 = b**2
779 # xE = Elliptic(k2, -a2b2 / b2, kp2, a2_b2)
780 # yE = Elliptic(kp2, +b2c2 / b2, k2, c2_b2)
781 # aE = Elliptic(kp2, 0, k2, 1)
782 k2 = (self._c2_b2 * self._a2b2_a2c2) or _0_0
783 kp2 = (self._a2_b2 * self._b2c2 / self._a2c2) if k2 else _1_0
784 return k2, kp2
786 def _radialTo3(self, sbeta, cbeta, somega, comega):
787 '''(INTERNAL) Convert I{ellipsoidal} lat- C{beta} and longitude
788 C{omega} to a cartesian I{on this triaxial's surface}, also
789 I{ordered} helper for C{.height4 with normal=False}.
790 '''
791 sa, ca = self._norm2(sbeta, cbeta)
792 sb, cb = self._norm2(somega, comega)
794 b2_a2 = self._b2_a2 # == (b/a)**2
795 c2_a2 = -self._c2_a2 # == -(c/a)**2
796 a2c2_a2 = self. e2ac # (a**2 - c**2) / a**2 == 1 - (c/a)**2
798 x2 = _Fsumf_(_1_0, -b2_a2 * sa**2, c2_a2 * ca**2).fover(a2c2_a2)
799 z2 = _Fsumf_(c2_a2, sb**2, b2_a2 * cb**2).fover(a2c2_a2)
801 x, y, z = self._abc3
802 x *= cb * _sqrt0(x2)
803 y *= ca * sb
804 z *= sa * _sqrt0(z2)
805 return x, y, z
808class _Triaxial3Base(_OrderedTriaxialBase):
809 '''(INTERNAL) Base class for I{unordered} triaxialC{3} classes.
810 '''
811 _e2_k2_kp2 = None
812 _Lon0 = None
814 @Property_RO
815 def e2(self):
816 '''Get the I{squared eccentricity} (C{scalar}), M{(a**2 - c**2) / b**2}.
817 '''
818 if self._e2_k2_kp2:
819 e2, _, _ = self._e2_k2_kp2
820 else:
821 e2 = self._a2c2 / self.b2
822 return Float(e2=e2)
824 def _init_abc3_e2_k2_kp2(self, b, e2, k2, kp2, **name):
825 '''(INTERNAL) C{Triaxial3B.__init__}.
826 '''
827 if name:
828 self.name = name
829 s = k2 + kp2
830 if s > 0 and s != _1_0:
831 k2 = k2 / s # /= chokes PyChecker
832 kp2 = kp2 / s
833 if min(e2, k2, kp2) < 0 or not s > 0:
834 raise TriaxialError(e2=e2, k2=k2, kp2=kp2)
835 if e2:
836 a = Radius_(a=_sqrt0(_1_0 + e2 * kp2) * b) if kp2 else b
837 c = Radius_(c=_sqrt0(_1_0 - e2 * k2) * b) if k2 else b
838 else: # spherical
839 a = c = b
840 if not (_isfinite(b) and a >= b >= c > 0):
841 raise TriaxialError(b=b, a=a, c=c, e2=e2,
842 k2=k2, kp2=kp2, txt=_not_ordered_)
843 self._abc3 = a, b, c
844 self._e2_k2_kp2 = e2, k2, kp2
846 @property_RO
847 def isBiaxial(self):
848 '''Is this triaxial I{biaxial} (C{bool}), C{a} == C{b} or C{b} == C{c}?
849 '''
850 return self.isOblate or self.isProlate
852 @property_RO
853 def isOblate(self):
854 '''Is this triaxial I{oblate} (C{bool}), C{a} == C{b}?
855 '''
856 return bool(self.kp2 == 0)
858 @property_RO
859 def isProlate(self):
860 '''Is this triaxial I{prolate} (C{bool}), C{b} == C{c}?
861 '''
862 return bool(self.k2 == 0)
864 @Property_RO
865 def _k_kp(self):
866 '''(INTERNAL) Get the oblate C{k} and prolate C{kp} parameters.
867 '''
868 return map1(_sqrt0, *self._k2_kp2)
870 @Property_RO
871 def k2(self):
872 '''(INTERNAL) Get the oblate C{k2} parameter I{squared}.
873 '''
874 k2, _ = self._k2_kp2
875 return k2
877 @Property_RO
878 def _k2_kp2(self):
879 '''(INTERNAL) Get the oblate C{k2} and prolate C{kp2} parameters I{squared}.
880 '''
881 if self._e2_k2_kp2:
882 _, k2, kp2 = self._e2_k2_kp2
883 else:
884 s = self._a2c2
885 k2 = (self._b2c2 / s) if s else _1_0
886 kp2 = (self._a2b2 / s) if s else _0_0
887 return k2, kp2
889 @Property_RO
890 def kp2(self):
891 '''(INTERNAL) Get the prolate C{kp2} parameter I{squared}.
892 '''
893 _, kp2 = self._k2_kp2
894 return kp2
896 @Property_RO
897 def _lcc23(self):
898 return self._a2c2, self._b2c2, _0_0
900 @property_doc_(" longitude of the I{earth}'s major semi-axis C{a}, (L{Ang}), Karney's C{Triaxial_Earth_lon0}.")
901 def Lon0(self):
902 if self._Lon0 is None:
903 WGS84_3 = self.name.startswith('WGS84_3')
904 self.Lon0 = -(1493 / 100) if WGS84_3 else 0
905 return self._Lon0
907 @Lon0.setter # PYCHOK setter!
908 def Lon0(self, lon0):
909 m = _MODS.angles
910 d = lon0.degrees if m.isAng(lon0) else Degrees(lon0)
911 n = _Triaxial3Base.Lon0.name
912 self._Lon0 = m.Ang.fromDegrees(d, name=n)
914 @Property_RO
915 def _xE(self):
916 '''(INTERNAL) Get the x-elliptic function.
917 '''
918 return self._xyE(self.e2, self.k2, self.kp2)
920 def _xyE(self, e2, k2, kp2):
921 '''(INTERNAL) Helper for C{._xE} and C{._yE}.
922 '''
923 if e2:
924 a2 = -kp2 * e2
925 ap2 = _1_0 - a2
926 kp2 *= _1_0 - k2 * e2
927 k2 *= ap2
928 else:
929 a2, ap2 = _0_0, _1_0
930 return _MODS.elliptic.Elliptic(kp2, a2, k2, ap2)
932 @Property_RO
933 def _yE(self):
934 '''(INTERNAL) Get the y-elliptic function.
935 '''
936 return self._xyE(-self.e2, self.kp2, self.k2)
939class TriaxialError(_ValueError):
940 '''Raised for any triaxial issue.
941 '''
942 pass # ...
945class _TriaxialsBase(_NamedEnum):
946 '''(INTERNAL) C{Triaxial*} registry, I{must} be a sub-class
947 to accommodate the L{_LazyNamedEnumItem} properties.
948 '''
949 _assert_kwds = {} # like propertyROnce
950 _Triaxial = None # must be overloaded
952 def _Lazy(self, *abc, **name):
953 '''(INTERNAL) Instantiate the C{self._Triaxial}.
954 '''
955 return self._Triaxial(*abc, **name)
957 def _assert(self): # PYCHOK signature
958 kwds = _TriaxialsBase._assert_kwds
959 if not kwds:
960 _lazy = _MODS.named._lazyNamedEnumItem
961 EWGS84 = _MODS.ellipsoids._EWGS84
962 abc84_35 = map1(m2km, EWGS84.a + 35, EWGS84.a - 35, EWGS84.b)
963 # <https://ArxIV.org/pdf/1909.06452.pdf> Table 1 Semi-axes in Km
964 # <https://www.JPS.NASA.gov/education/images/pdf/ss-moons.pdf>
965 # <https://link.Springer.com/article/10.1007/s00190-022-01650-9>
966 # <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Constants.html>
967 for n, abc in dict( # a (Km) b (Km) c (Km) planet
968 Amalthea= (125.0, 73.0, 64.0), # Jupiter
969 Ariel= (581.1, 577.9, 577.7), # Uranus
970 Earth= (6378.173435, 6378.1039, 6356.7544),
971 Enceladus=(256.6, 251.4, 248.3), # Saturn
972 Europa= (1564.13, 1561.23, 1560.93), # Jupiter
973 Io= (1829.4, 1819.3, 1815.7), # Jupiter
974 Mars= (3394.6, 3393.3, 3376.3),
975 Mimas= (207.4, 196.8, 190.6), # Saturn
976 Miranda= (240.4, 234.2, 232.9), # Uranus
977 Moon= (1735.55, 1735.324, 1734.898), # Earth
978 Tethys= (535.6, 528.2, 525.8), # Saturn
979 WGS84_3= (6378.17136, 6378.10161, 6356.75184), # C++
980 WGS84_3r=(6378.172, 6378.102, 6356.752), # C++, rounded
981 WGS84_35=abc84_35).items():
982 kwds[n] = _lazy(n, *map(km2m, abc))
983 _NamedEnum._assert(self, **kwds)
986def _getitems(items, *indices):
987 '''(INTERNAL) Get the C{items} at the given I{indices}.
989 @return: C{Type(items[i] for i in indices)} with
990 C{Type = type(items)}, any C{type} having
991 the special method C{__getitem__}.
992 '''
993 return type(items)(map(items.__getitem__, indices))
996def _hypot2_1(x, y, z=0):
997 '''(INTERNAL) Compute M{x**2 + y**2 + z**2 - 1} with C{max(fabs(x), fabs(y),
998 fabs(z))} rarely greater than 1.0.
999 '''
1000 return fsumf_(_N_1_0, x*x, y*y, z*z)
1003def _otherV3d_(x_xyz, y, z, **name):
1004 '''(INTERNAL) Get a Vector3d from C{x_xyz}, C{y} and C{z}.
1005 '''
1006 return _otherV3d(x_xyz=x_xyz, **name) if y is z is None else \
1007 Vector3d(x_xyz, y, z, **name)
1010def _over0(p, q):
1011 '''(INTERNAL) Return C{p / q} or C{0}.
1012 '''
1013 return (p / q) if q > fabs(p) else _0_0
1016def _over02(p, q):
1017 '''(INTERNAL) Return C{(p / q)**2} or C{0}.
1018 '''
1019 return (p / q)**2 if p and q else _0_0
1022def _sqrt0(x):
1023 '''(INTERNAL) C{sqrt0} with C{TriaxialError}.
1024 '''
1025 return sqrt0(x, Error=TriaxialError)
1028__all__ += _ALL_DOCS(_OrderedTriaxialBase, _Triaxial3Base, _UnOrderedTriaxialBase)
1030# **) MIT License
1031#
1032# Copyright (C) 2025-2026 -- mrJean1 at Gmail -- All Rights Reserved.
1033#
1034# Permission is hereby granted, free of charge, to any person obtaining a
1035# copy of this software and associated documentation files (the "Software"),
1036# to deal in the Software without restriction, including without limitation
1037# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1038# and/or sell copies of the Software, and to permit persons to whom the
1039# Software is furnished to do so, subject to the following conditions:
1040#
1041# The above copyright notice and this permission notice shall be included
1042# in all copies or substantial portions of the Software.
1043#
1044# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1045# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1046# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1047# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1048# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1049# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1050# OTHER DEALINGS IN THE SOFTWARE.