1

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

3

4 u'''Triaxal ellipsoid classes I{ordered} L{Triaxial} and I{unordered} L{Triaxial_} and Jacobi

5 conformal projections L{JacobiConformal} and L{JacobiConformalSpherical}, transcoded from

6 I{Charles Karney}'s C++ class U{JacobiConformal<https://GeographicLib.SourceForge.io/C++/doc/

7 classGeographicLib_1_1JacobiConformal.html#details>} to pure Python and miscellaneous classes

8 L{BetaOmega2Tuple}, L{BetaOmega3Tuple}, L{Jacobi2Tuple} and L{TriaxialError}.

9

10 Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2008-2024). For more information,

11 see the U{GeographicLib<https://GeographicLib.SourceForge.io>} documentation.

12

13@see: U{Geodesics on a triaxial ellipsoid<https://WikiPedia.org/wiki/Geodesics_on_an_ellipsoid#

14 Geodesics_on_a_triaxial_ellipsoid>} and U{Triaxial coordinate systems and their geometrical

15 interpretation<https://www.Topo.Auth.GR/wp-content/uploads/sites/111/2021/12/09_Panou.pdf>}.

16

17@var Triaxials.Amalthea: Triaxial(name='Amalthea', a=125000, b=73000, c=64000, e2ab=0.658944, e2bc=0.231375493, e2ac=0.737856, volume=2446253479595252, area=93239507787.490371704, area_p=93212299402.670425415)

18@var Triaxials.Ariel: Triaxial(name='Ariel', a=581100, b=577900, c=577700, e2ab=0.01098327, e2bc=0.000692042, e2ac=0.011667711, volume=812633172614203904, area=4211301462766.580078125, area_p=4211301574065.829589844)

19@var Triaxials.Earth: Triaxial(name='Earth', a=6378173.435, b=6378103.9, c=6356754.399999999, e2ab=0.000021804, e2bc=0.006683418, e2ac=0.006705077, volume=1083208241574987694080, area=510065911057441.0625, area_p=510065915922713.6875)

20@var Triaxials.Enceladus: Triaxial(name='Enceladus', a=256600, b=251400, c=248300, e2ab=0.040119337, e2bc=0.024509841, e2ac=0.06364586, volume=67094551514082248, area=798618496278.596679688, area_p=798619018175.109985352)

21@var Triaxials.Europa: Triaxial(name='Europa', a=1564130, b=1561230, c=1560930, e2ab=0.003704694, e2bc=0.000384275, e2ac=0.004087546, volume=15966575194402123776, area=30663773697323.51953125, area_p=30663773794562.45703125)

22@var Triaxials.Io: Triaxial(name='Io', a=1829400, b=1819300, c=1815700, e2ab=0.011011391, e2bc=0.003953651, e2ac=0.014921506, volume=25313121117889765376, area=41691875849096.7421875, area_p=41691877397441.2109375)

23@var Triaxials.Mars: Triaxial(name='Mars', a=3394600, b=3393300, c=3376300, e2ab=0.000765776, e2bc=0.009994646, e2ac=0.010752768, volume=162907283585817247744, area=144249140795107.4375, area_p=144249144150662.15625)

24@var Triaxials.Mimas: Triaxial(name='Mimas', a=207400, b=196800, c=190600, e2ab=0.09960581, e2bc=0.062015624, e2ac=0.155444317, volume=32587072869017956, area=493855762247.691833496, area_p=493857714107.9375)

25@var Triaxials.Miranda: Triaxial(name='Miranda', a=240400, b=234200, c=232900, e2ab=0.050915557, e2bc=0.011070811, e2ac=0.061422691, volume=54926187094835456, area=698880863325.757080078, area_p=698881306767.950317383)

26@var Triaxials.Moon: Triaxial(name='Moon', a=1735550, b=1735324, c=1734898, e2ab=0.000260419, e2bc=0.000490914, e2ac=0.000751206, volume=21886698675223740416, area=37838824729886.09375, area_p=37838824733332.21875)

27@var Triaxials.Tethys: Triaxial(name='Tethys', a=535600, b=528200, c=525800, e2ab=0.027441672, e2bc=0.009066821, e2ac=0.036259685, volume=623086233855821440, area=3528073490771.394042969, area_p=3528074261832.738769531)

28@var Triaxials.WGS84_35: Triaxial(name='WGS84_35', a=6378172, b=6378102, c=6356752.314245179, e2ab=0.00002195, e2bc=0.006683478, e2ac=0.006705281, volume=1083207319768789942272, area=510065621722018.125, area_p=510065626587483.3125)

29'''

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

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

32

33 from pygeodesy.basics import _isin, isLatLon, isscalar

34 from pygeodesy.constants import EPS, EPS0, EPS02, EPS4, INT0, PI2, PI_3, PI4, \

35 _EPS2e4, _SQRT2_2, float0_, isfinite, isnear1, _over, \

36 _0_0, _0_5, _1_0, _N_1_0, _64_0, _4_0 # PYCHOK used!

37 from pygeodesy.datums import Datum, _spherical_datum, _WGS84, Ellipsoid, _EWGS84, Fmt

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

39# from pygeodesy.elliptic import Elliptic # _MODS

40 from pygeodesy.errors import _AssertionError, _ValueError

41 from pygeodesy.fmath import Fdot, fdot, fmean_, hypot, hypot_, norm2, sqrt0

42 from pygeodesy.fsums import _Fsumf_, fsumf_, fsum1f_

43 from pygeodesy.interns import NN, _a_, _b_, _beta_, _c_, _distant_, _DMAIN_, \

44 _finite_, _height_, _inside_, _near_, _negative_, \

45 _not_, _NOTEQUAL_, _null_, _opposite_, _outside_, \

46 _SPACE_, _spherical_, _too_, _x_, _y_

47# from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .vector3d

48 from pygeodesy.named import _lazyNamedEnumItem as _lazy, _name__, _NamedEnum, \

49 _NamedEnumItem, _Pass

50 from pygeodesy.namedTuples import LatLon3Tuple, _NamedTupleTo, Vector3Tuple, \

51 Vector4Tuple

52 from pygeodesy.props import Property_RO, property_ROver

53# from pygeodesy.streprs import Fmt # from .datums

54 from pygeodesy.units import Degrees, Float, Height_, Meter, Meter2, Meter3, \

55 Radians, Radius, Scalar_

56 from pygeodesy.utily import asin1, atan2, atan2d, km2m, m2km, SinCos2, sincos2d_

57 from pygeodesy.vector3d import _otherV3d, Vector3d, _ALL_LAZY, _MODS

58

59 from math import fabs, sqrt

60

61__all__ = _ALL_LAZY.triaxials

62__version__ = '25.04.14'

63

64_not_ordered_ = _not_('ordered')

65_omega_ = 'omega'

66_TRIPS = 359 # Eberly 1074?

67

68

69 class _NamedTupleToX(_NamedTupleTo): # in .testNamedTuples

70 '''(INTERNAL) Base class for L{BetaOmega2Tuple},

71 L{BetaOmega3Tuple} and L{Jacobi2Tuple}.

72 '''

73 def _toDegrees(self, name, **toDMS_kwds):

74 '''(INTERNAL) Convert C{self[0:2]} to L{Degrees} or C{toDMS}.

75 '''

76 return self._toX3U(_NamedTupleTo._Degrees3, Degrees, name, *self, **toDMS_kwds)

77

78 def _toRadians(self, name):

79 '''(INTERNAL) Convert C{self[0:2]} to L{Radians}.

80 '''

81 return self._toX3U(_NamedTupleTo._Radians3, Radians, name, *self)

82

83 def _toX3U(self, _X3, U, name, a, b, *c, **kwds):

84 a, b, s = _X3(self, a, b, **kwds)

85 if s is None or name:

86 n = self._name__(name)

87 s = self.classof(a, b, *c, name=n).reUnit(U, U).toUnits()

88 return s

89

90

91 class BetaOmega2Tuple(_NamedTupleToX):

92 '''2-Tuple C{(beta, omega)} with I{ellipsoidal} lat- and

93 longitude C{beta} and C{omega} both in L{Radians} (or

94 L{Degrees}).

95 '''

96 _Names_ = (_beta_, _omega_)

97 _Units_ = (_Pass, _Pass)

98

99 def toDegrees(self, name=NN, **toDMS_kwds):

100 '''Convert this L{BetaOmega2Tuple} to L{Degrees} or C{toDMS}.

101

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

103

104 @return: L{BetaOmega2Tuple}C{(beta, omega)} with C{beta} and

105 C{omega} both in L{Degrees} or as L{toDMS} strings

106 provided some B{C{toDMS_kwds}} keyword arguments are

107 specified.

108 '''

109 return self._toDegrees(name, **toDMS_kwds)

110

111 def toRadians(self, **name):

112 '''Convert this L{BetaOmega2Tuple} to L{Radians}.

113

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

115

116 @return: L{BetaOmega2Tuple}C{(beta, omega)} with C{beta} and C{omega}

117 both in L{Radians}.

118 '''

119 return self._toRadians(name)

120

121

122 class BetaOmega3Tuple(_NamedTupleToX):

123 '''3-Tuple C{(beta, omega, height)} with I{ellipsoidal} lat- and

124 longitude C{beta} and C{omega} both in L{Radians} (or L{Degrees})

125 and the C{height}, rather the (signed) I{distance} to the triaxial's

126 surface (measured along the radial line to the triaxial's center)

127 in C{meter}, conventionally.

128 '''

129 _Names_ = BetaOmega2Tuple._Names_ + (_height_,)

130 _Units_ = BetaOmega2Tuple._Units_ + ( Meter,)

131

132 def toDegrees(self, name=NN, **toDMS_kwds):

133 '''Convert this L{BetaOmega3Tuple} to L{Degrees} or C{toDMS}.

134

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

136

137 @return: L{BetaOmega3Tuple}C{(beta, omega, height)} with

138 C{beta} and C{omega} both in L{Degrees} or as

139 L{toDMS} strings provided some B{C{toDMS_kwds}}

140 keyword arguments are specified.

141 '''

142 return self._toDegrees(name, **toDMS_kwds)

143

144 def toRadians(self, **name):

145 '''Convert this L{BetaOmega3Tuple} to L{Radians}.

146

147 @kwarg name: Optional C{B{name}=NN} (C{str}), overriding this name.

148

149 @return: L{BetaOmega3Tuple}C{(beta, omega, height)} with C{beta}

150 and C{omega} both in L{Radians}.

151 '''

152 return self._toRadians(name)

153

154 def to2Tuple(self, **name):

155 '''Reduce this L{BetaOmega3Tuple} to a L{BetaOmega2Tuple}.

156

157 @kwarg name: Optional C{B{name}=NN} (C{str}), overriding this name.

158 '''

159 return BetaOmega2Tuple(*self[:2], name=self._name__(name))

160

161

162 class Jacobi2Tuple(_NamedTupleToX):

163 '''2-Tuple C{(x, y)} with a Jacobi Conformal C{x} and C{y}

164 projection, both in L{Radians} (or L{Degrees}).

165 '''

166 _Names_ = (_x_, _y_)

167 _Units_ = (_Pass, _Pass)

168

169 def toDegrees(self, name=NN, **toDMS_kwds):

170 '''Convert this L{Jacobi2Tuple} to L{Degrees} or C{toDMS}.

171

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

173

174 @return: L{Jacobi2Tuple}C{(x, y)} with C{x} and C{y} both

175 in L{Degrees} or as L{toDMS} strings provided some

176 B{C{toDMS_kwds}} keyword arguments are specified.

177 '''

178 return self._toDegrees(name, **toDMS_kwds)

179

180 def toRadians(self, **name):

181 '''Convert this L{Jacobi2Tuple} to L{Radians}.

182

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

184

185 @return: L{Jacobi2Tuple}C{(x, y)} with C{x} and C{y} both in L{Radians}.

186 '''

187 return self._toRadians(name)

188

189

190 class Triaxial_(_NamedEnumItem):

191 '''I{Unordered} triaxial ellipsoid and base class.

192

193 Triaxial ellipsoids with right-handed semi-axes C{a}, C{b} and C{c}, oriented

194 such that the large principal ellipse C{ab} is the equator I{Z}=0, I{beta}=0,

195 while the small principal ellipse C{ac} is the prime meridian, plane I{Y}=0,

196 I{omega}=0.

197

198 The four umbilic points, C{abs}(I{omega}) = C{abs}(I{beta}) = C{PI/2}, lie on

199 the middle principal ellipse C{bc} in plane I{X}=0, I{omega}=C{PI/2}.

200

201 @note: I{Geodetic} C{lat}- and C{lon}gitudes are in C{degrees}, I{geodetic}

202 C{phi} and C{lam}bda are in C{radians}, but I{ellipsoidal} lat- and

203 longitude C{beta} and C{omega} are in L{Radians} by default (or in

204 L{Degrees} if converted).

205 '''

206 _ijk = _kji = None

207 _unordered = True

208

209 def __init__(self, a_triaxial, b=None, c=None, **name):

210 '''New I{unordered} L{Triaxial_}.

211

212 @arg a_triaxial: Large, C{X} semi-axis (C{scalar}, conventionally in

213 C{meter}) or an other L{Triaxial} or L{Triaxial_} instance.

214 @kwarg b: Middle, C{Y} semi-axis (C{meter}, same units as B{C{a}}), required

215 if C{B{a_triaxial} is scalar}, ignored otherwise.

216 @kwarg c: Small, C{Z} semi-axis (C{meter}, like B{C{b}}).

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

218

219 @raise TriaxialError: Invalid semi-axis or -axes.

220 '''

221 try:

222 try:

223 a = a_triaxial

224 t = a._abc3

225 except AttributeError:

226 t = Radius(a=a), Radius(b=b), Radius(c=c)

227 except (TypeError, ValueError) as x:

228 raise TriaxialError(a=a, b=b, c=c, cause=x)

229 if name:

230 self.name = name

231

232 a, b, c = self._abc3 = t

233 if self._unordered: # == not isinstance(self, Triaxial)

234 s, _, t = sorted(t)

235 if not (isfinite(t) and s > 0):

236 raise TriaxialError(a=a, b=b, c=c) # txt=_invalid_

237 elif not (isfinite(a) and a >= b >= c > 0):

238 raise TriaxialError(a=a, b=b, c=c, txt=_not_ordered_)

239 elif not (a > c and self._a2c2 > 0 and self.e2ac > 0):

240 raise TriaxialError(a=a, c=c, e2ac=self.e2ac, txt=_spherical_)

241

242 def __str__(self):

243 return self.toStr()

244

245 @Property_RO

246 def a(self):

247 '''Get the C{largest, x} semi-axis (C{meter}, conventionally).

248 '''

249 a, _, _ = self._abc3

250 return a

251

252 @Property_RO

253 def _a2b2(self):

254 '''(INTERNAL) Get C{a**2 - b**2} == E_sub_e**2.

255 '''

256 a, b, _ = self._abc3

257 return ((a - b) * (a + b)) if a != b else _0_0

258

259 @Property_RO

260 def _a2_b2(self):

261 '''(INTERNAL) Get C{(a / b)**2}.

262 '''

263 a, b, _ = self._abc3

264 return (a / b)**2 if a != b else _1_0

265

266 @Property_RO

267 def _a2c2(self):

268 '''(INTERNAL) Get C{a**2 - c**2} == E_sub_x**2.

269 '''

270 a, _, c = self._abc3

271 return ((a - c) * (a + c)) if a != c else _0_0

272

273 @Property_RO

274 def area(self):

275 '''Get the surface area (C{meter} I{squared}).

276 '''

277 c, b, a = sorted(self._abc3)

278 if a > c:

279 a = Triaxial(a, b, c).area if a > b else \

280 Ellipsoid(a, b=c).areax # a == b

281 else: # a == c == b

282 a = Meter2(area=a**2 * PI4)

283 return a

284

285 def area_p(self, p=1.6075):

286 '''I{Approximate} the surface area (C{meter} I{squared}).

287

288 @kwarg p: Exponent (C{scalar} > 0), 1.6 for near-spherical or 1.5849625007

289 for "near-flat" triaxials.

290

291 @see: U{Surface area<https://WikiPedia.org/wiki/Ellipsoid#Approximate_formula>}.

292 '''

293 a, b, c = self._abc3

294 if a == b == c:

295 a *= a

296 else:

297 _p = pow

298 a = _p(fmean_(_p(a * b, p), _p(a * c, p), _p(b * c, p)), _1_0 / p)

299 return Meter2(area_p=a * PI4)

300

301 @Property_RO

302 def b(self):

303 '''Get the C{middle, y} semi-axis (C{meter}, same units as B{C{a}}).

304 '''

305 _, b, _ = self._abc3

306 return b

307

308 @Property_RO

309 def _b2c2(self):

310 '''(INTERNAL) Get C{b**2 - c**2} == E_sub_y**2.

311 '''

312 _, b, c = self._abc3

313 return ((b - c) * (b + c)) if b != c else _0_0

314

315 @Property_RO

316 def c(self):

317 '''Get the C{smallest, z} semi-axis (C{meter}, same units as B{C{a}}).

318 '''

319 _, _, c = self._abc3

320 return c

321

322 @Property_RO

323 def _c2_b2(self):

324 '''(INTERNAL) Get C{(c / b)**2}.

325 '''

326 _, b, c = self._abc3

327 return (c / b)**2 if b != c else _1_0

328

329 @Property_RO

330 def e2ab(self):

331 '''Get the C{ab} ellipse' I{(1st) eccentricity squared} (C{scalar}), M{1 - (b/a)**2}.

332 '''

333 return Float(e2ab=(_1_0 - self._1e2ab) or _0_0)

334

335 @Property_RO

336 def _1e2ab(self):

337 '''(INTERNAL) Get C{1 - e2ab} == C{(b/a)**2}.

338 '''

339 a, b, _ = self._abc3

340 return (b / a)**2 if a != b else _1_0

341

342 @Property_RO

343 def e2ac(self):

344 '''Get the C{ac} ellipse' I{(1st) eccentricity squared} (C{scalar}), M{1 - (c/a)**2}.

345 '''

346 return Float(e2ac=(_1_0 - self._1e2ac) or _0_0)

347

348 @Property_RO

349 def _1e2ac(self):

350 '''(INTERNAL) Get C{1 - e2ac} == C{(c/a)**2}.

351 '''

352 a, _, c = self._abc3

353 return (c / a)**2 if a != c else _1_0

354

355 @Property_RO

356 def e2bc(self):

357 '''Get the C{bc} ellipse' I{(1st) eccentricity squared} (C{scalar}), M{1 - (c/b)**2}.

358 '''

359 return Float(e2bc=(_1_0 - self._1e2bc) or _0_0)

360

361 _1e2bc = _c2_b2 # C{1 - e2bc} == C{(c/b)**2}

362

363 @property_ROver

364 def _Elliptic(self):

365 '''(INTERNAL) Get class L{Elliptic}, I{once}.

366 '''

367 return _MODS.elliptic.Elliptic # overwrite property_ROver

368

369 def hartzell4(self, pov, los=False, **name):

370 '''Compute the intersection of this triaxial's surface with a Line-Of-Sight

371 from a Point-Of-View in space.

372

373 @see: Function L{hartzell4<triaxials.hartzell4>} for further details.

374 '''

375 return hartzell4(pov, los=los, tri_biax=self, **name)

376

377 def height4(self, x_xyz, y=None, z=None, normal=True, eps=EPS, **name):

378 '''Compute the projection on and the height above or below this triaxial's surface.

379

380 @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian}, L{Ecef9Tuple},

381 L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).

382 @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}, ignored

383 otherwise.

384 @kwarg z: Z component (C{scalar}), like B{C{y}}.

385 @kwarg normal: If C{True}, the projection is the I{perpendicular, plumb} to the

386 triaxial's surface, otherwise the C{radial} line to the center of

387 this triaxial (C{bool}).

388 @kwarg eps: Tolerance for root finding and validation (C{scalar}), use a negative

389 value to skip validation.

390 @kwarg name: Optional C{B{name}="heigh4"} (C{str}).

391

392 @return: L{Vector4Tuple}C{(x, y, z, h)} with the cartesian coordinates C{x}, C{y}

393 and C{z} of the projection on or the intersection with and with the height

394 C{h} above or below the triaxial's surface in C{meter}, conventionally.

395

396 @raise TriaxialError: Non-cartesian B{C{xyz}}, invalid B{C{eps}}, no convergence in

397 root finding or validation failed.

398

399 @see: Methods L{Triaxial.normal3d} and L{Ellipsoid.height4}, I{Eberly}'s U{Distance from a Point to ...

400 <https://www.GeometricTools.com/Documentation/DistancePointEllipseEllipsoid.pdf>} and I{Bektas}'

401 U{Shortest Distance from a Point to Triaxial Ellipsoid<https://www.ResearchGate.net/publication/

402 272149005_SHORTEST_DISTANCE_FROM_A_POINT_TO_TRIAXIAL_ELLIPSOID>}.

403 '''

404 v, r = _otherV3d_(x_xyz, y, z), self.isSpherical

405

406 i, h = None, v.length

407 if h < EPS0: # EPS

408 x = y = z = _0_0

409 h -= min(self._abc3) # nearest

410 elif r: # .isSpherical

411 x, y, z = v.times(r / h).xyz3

412 h -= r

413 else:

414 x, y, z = v.xyz3

415 try:

416 if normal: # plumb to surface

417 x, y, z, h, i = _plumbTo5(x, y, z, self, eps=eps)

418 else: # radial to center

419 x, y, z = self._radialTo3(z, hypot(x, y), y, x)

420 h = v.minus_(x, y, z).length

421 except Exception as e:

422 raise TriaxialError(x=x, y=y, z=z, cause=e)

423 if h > 0 and self.sideOf(v, eps=EPS0) < 0:

424 h = -h # inside

425 n = _name__(name, name__=self.height4) # typename

426 return Vector4Tuple(x, y, z, h, iteration=i, name=n)

427

428 @Property_RO

429 def isOrdered(self):

430 '''Is this triaxial I{ordered} and I{not spherical} (C{bool})?

431 '''

432 a, b, c = self._abc3

433 return bool(a >= b > c) # b > c!

434

435 @Property_RO

436 def isSpherical(self):

437 '''Is this triaxial I{spherical} (C{Radius} or INT0)?

438 '''

439 a, b, c = self._abc3

440 return a if a == b == c else INT0

441

442 def _norm2(self, s, c, *a):

443 '''(INTERNAL) Normalize C{s} and C{c} iff not already.

444 '''

445 if fabs(_hypot2_1(s, c)) > EPS02:

446 s, c = norm2(s, c)

447 if a:

448 s, c = norm2(s * self.b, c * a[0])

449 return float0_(s, c)

450

451 def normal3d(self, x_xyz, y=None, z=None, length=_1_0):

452 '''Get a 3-D vector at a cartesian I{on and perpendicular to} this triaxial's surface.

453

454 @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},

455 L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).

456 @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}, ignored

457 otherwise.

458 @kwarg z: Z component (C{scalar}), like B{C{y}}.

459 @kwarg length: Optional, signed length and in-/outward direction (C{scalar}).

460

461 @return: A C{Vector3d(x_, y_, z_)} normalized to B{C{length}}, pointing in- or

462 outward for neg- respectively positive B{C{length}}.

463

464 @raise TriaxialError: Zero length cartesian or vector.

465

466 @note: Cartesian C{(B{x}, B{y}, B{z})} I{must be on} this triaxial's surface, use

467 method L{Triaxial.sideOf} to validate.

468

469 @see: Methods L{Triaxial.height4} and L{Triaxial.sideOf}.

470 '''

471 # n = 2 * (x / a2, y / b2, z / c2)

472 # == 2 * (x, y * a2 / b2, z * a2 / c2) / a2 # iff ordered

473 # == 2 * (x, y / _1e2ab, z / _1e2ac) / a2

474 # == unit(x, y / _1e2ab, z / _1e2ac).times(length)

475 x, y, z = _otherV3d_(x_xyz, y, z).xyz3

476 n = Vector3d(x, y / self._1e2ab,

477 z / self._1e2ac, name__=self.normal3d)

478 u = n.length

479 if u < EPS0:

480 raise TriaxialError(x=x_xyz, y=y, z=z, txt=_null_)

481 return n.times(length / u)

482

483 def _order3(self, *abc, **reverse): # reverse=False

484 '''(INTERNAL) Un-/Order C{a}, C{b} and C{c}.

485

486 @return: 3-Tuple C{(a, b, c)} ordered by or un-ordered

487 (reverse-ordered) C{ijk} if C{B{reverse}=True}.

488 '''

489 ijk = self._order_ijk(**reverse)

490 return _getitems(abc, *ijk) if ijk else abc

491

492 def _order3d(self, v, **reverse): # reverse=False

493 '''(INTERNAL) Un-/Order a C{Vector3d}.

494

495 @return: Vector3d(x, y, z) un-/ordered.

496 '''

497 ijk = self._order_ijk(**reverse)

498 return v.classof(*_getitems(v.xyz3, *ijk)) if ijk else v

499

500 @Property_RO

501 def _ordered4(self):

502 '''(INTERNAL) Helper for C{_hartzell3} and C{_plumbTo5}.

503 '''

504 def _order2(reverse, a, b, c):

505 '''(INTERNAL) Un-Order C{a}, C{b} and C{c}.

506

507 @return: 2-Tuple C{((a, b, c), ijk)} with C{a} >= C{b} >= C{c}

508 and C{ijk} a 3-tuple with the initial indices.

509 '''

510 i, j, k = range(3)

511 if a < b:

512 a, b, i, j = b, a, j, i

513 if a < c:

514 a, c, i, k = c, a, k, i

515 if b < c:

516 b, c, j, k = c, b, k, j

517 # reverse (k, j, i) since (a, b, c) is reversed-sorted

518 ijk = (k, j, i) if reverse else (None if i < j < k else (i, j, k))

519 return (a, b, c), ijk

520

521 abc, T = self._abc3, self

522 if not self.isOrdered:

523 abc, ijk = _order2(False, *abc)

524 if ijk:

525 _, kji = _order2(True, *ijk)

526 T = Triaxial_(*abc)

527 T._ijk, T._kji = ijk, kji

528 return abc + (T,)

529

530 def _order_ijk(self, reverse=False):

531 '''(INTERNAL) Get the un-/order indices.

532 '''

533 return self._kji if reverse else self._ijk

534

535 def _radialTo3(self, sbeta, cbeta, somega, comega):

536 '''(INTERNAL) I{Unordered} helper for C{.height4}.

537 '''

538 def _rphi(a, b, sphi, cphi):

539 # <https://WikiPedia.org/wiki/Ellipse#Polar_form_relative_to_focus>

540 # polar form: radius(phi) = a * b / hypot(a * sphi, b * cphi)

541 return (b / hypot(sphi, b / a * cphi)) if a > b else (

542 (a / hypot(cphi, a / b * sphi)) if a < b else a)

543

544 sa, ca = self._norm2(sbeta, cbeta)

545 sb, cb = self._norm2(somega, comega)

546

547 a, b, c = self._abc3

548 if a != b:

549 a = _rphi(a, b, sb, cb)

550 if a != c:

551 c = _rphi(a, c, sa, ca)

552 t = c * ca

553 return (t * cb), (t * sb), (c * sa)

554

555 def sideOf(self, x_xyz, y=None, z=None, eps=EPS4):

556 '''Is a cartesian on, above or below the surface of this triaxial?

557

558 @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},

559 L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).

560 @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar},

561 ignored otherwise.

562 @kwarg z: Z component (C{scalar}), like B{C{y}}.

563 @kwarg eps: On-surface tolerance (C{scalar}, distance I{squared}).

564

565 @return: C{INT0} if C{(B{x}, B{y}, B{z})} is near this triaxial's surface

566 within tolerance B{C{eps}}, otherwise the signed, radial distance

567 I{squared} (C{float}), negative for in- or positive for outside

568 this triaxial.

569

570 @see: Methods L{Triaxial.height4} and L{Triaxial.normal3d}.

571 '''

572 v = _otherV3d_(x_xyz, y, z)

573 s = fsumf_(_N_1_0, *map(_over02, v.xyz3, self._abc3))

574 return INT0 if fabs(s) < eps else s

575

576 def toEllipsoid(self, **name):

577 '''Convert this triaxial to an L{Ellipsoid}, provided 2 axes match.

578

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

580

581 @return: An L{Ellipsoid} with north along this C{Z} axis if C{a == b},

582 this C{Y} axis if C{a == c} or this C{X} axis if C{b == c}.

583

584 @raise TriaxialError: This C{a != b}, C{b != c} and C{c != a}.

585

586 @see: Method L{Ellipsoid.toTriaxial}.

587 '''

588 a, b, c = self._abc3

589 if a == b:

590 b = c # N = c-Z

591 elif b == c: # N = a-X

592 a, b = b, a

593 elif a != c: # N = b-Y

594 t = _SPACE_(_a_, _NOTEQUAL_, _b_, _NOTEQUAL_, _c_)

595 raise TriaxialError(a=a, b=b, c=c, txt=t)

596 return Ellipsoid(a, b=b, name=self._name__(name))

597

598 def toStr(self, prec=9, **name): # PYCHOK signature

599 '''Return this C{Triaxial} as a string.

600

601 @kwarg prec: Precision, number of decimal digits (0..9).

602 @kwarg name: Optional name (C{str}), to override or C{None}

603 to exclude this triaxial's name.

604

605 @return: This C{Triaxial}'s attributes (C{str}).

606 '''

607 T = Triaxial_

608 t = T.a,

609 J = JacobiConformalSpherical

610 t += (J.ab, J.bc) if isinstance(self, J) else (T.b, T.c)

611 t += T.e2ab, T.e2bc, T.e2ac

612 J = JacobiConformal

613 if isinstance(self, J):

614 t += J.xyQ2,

615 t += T.volume, T.area

616 return self._instr(area_p=self.area_p(), prec=prec, props=t, **name)

617

618 @Property_RO

619 def unOrdered(self):

620 '''Is this triaxial I{un-ordered} and I{not spherical} (C{bool})?

621 '''

622 return not (self.isOrdered or bool(self.isSpherical))

623

624 @Property_RO

625 def volume(self):

626 '''Get the volume (C{meter**3}), M{4 / 3 * PI * a * b * c}.

627 '''

628 a, b, c = self._abc3

629 return Meter3(volume=a * b * c * PI_3 * _4_0)

630

631

632 class Triaxial(Triaxial_):

633 '''I{Ordered} triaxial ellipsoid.

634

635 @see: L{Triaxial_} for more information.

636 '''

637 _unordered = False

638

639 def __init__(self, a_triaxial, b=None, c=None, **name):

640 '''New I{ordered} L{Triaxial}.

641

642 @arg a_triaxial: Largest semi-axis (C{scalar}, conventionally in C{meter})

643 or an other L{Triaxial} or L{Triaxial_} instance.

644 @kwarg b: Middle semi-axis (C{meter}, same units as B{C{a}}), required

645 if C{B{a_triaxial} is scalar}, ignored otherwise.

646 @kwarg c: Smallest semi-axis (C{meter}, like B{C{b}}).

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

648

649 @note: The semi-axes must be ordered as C{B{a} >= B{b} >= B{c} > 0} and

650 must be ellipsoidal, C{B{a} > B{c}}.

651

652 @raise TriaxialError: Semi-axes unordered, spherical or invalid.

653 '''

654 Triaxial_.__init__(self, a_triaxial, b=b, c=c, **name)

655

656 @Property_RO

657 def _a2b2_a2c2(self):

658 '''@see: Methods C{.forwardBetaOmega} and C{._k2_kp2}.

659 '''

660 return self._a2b2 / self._a2c2

661

662 @Property_RO

663 def area(self):

664 '''Get the surface area (C{meter} I{squared}).

665

666 @see: U{Surface area<https://WikiPedia.org/wiki/Ellipsoid#Surface_area>}.

667 '''

668 a, b, c = self._abc3

669 if a != b:

670 kp2, k2 = self._k2_kp2 # swapped!

671 aE = self._Elliptic(k2, _0_0, kp2, _1_0)

672 c2 = self._1e2ac # cos(phi)**2 = (c/a)**2

673 s = sqrt(self.e2ac) # sin(phi)**2 = 1 - c2

674 r = asin1(s) # phi = atan2(sqrt(c2), s)

675 b *= fsum1f_(aE.fE(r) * s, (c / a) * (c / b),

676 aE.fF(r) * c2 / s)

677 a = Meter2(area=a * b * PI2)

678 else: # a == b > c

679 a = Ellipsoid(a, b=c).areax

680 return a

681

682 def forwardBetaOmega(self, beta, omega, height=0, **name):

683 '''Convert I{ellipsoidal} lat- C{beta}, longitude C{omega} and C{height}

684 to cartesian.

685

686 @arg beta: Ellipsoidal latitude (C{radians} or L{Degrees}).

687 @arg omega: Ellipsoidal longitude (C{radians} or L{Degrees}).

688 @arg height: Height above or below the ellipsoid's surface (C{meter}, same

689 units as this triaxial's C{a}, C{b} and C{c} semi-axes).

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

691

692 @return: A L{Vector3Tuple}C{(x, y, z)}.

693

694 @see: Method L{Triaxial.reverseBetaOmega} and U{expressions (23-25)<https://

695 www.Topo.Auth.GR/wp-content/uploads/sites/111/2021/12/09_Panou.pdf>}.

696 '''

697 if height:

698 z = self._Height(height) + self.c

699 if z > 0:

700 z2 = z**2

701 x = z * _sqrt0(_1_0 + self._a2c2 / z2)

702 y = z * _sqrt0(_1_0 + self._b2c2 / z2)

703 else:

704 x = y = z = _0_0

705 else:

706 x, y, z = self._abc3

707 if z: # and x and y:

708 sa, ca = SinCos2(beta)

709 sb, cb = SinCos2(omega)

710

711 r = self._a2b2_a2c2

712 x *= cb * _sqrt0(ca**2 + sa**2 * r)

713 y *= ca * sb

714 z *= sa * _sqrt0(_1_0 - cb**2 * r)

715 return Vector3Tuple(x, y, z, **name)

716

717 def forwardBetaOmega_(self, sbeta, cbeta, somega, comega, **name):

718 '''Convert I{ellipsoidal} lat- and longitude C{beta} and C{omega}

719 to cartesian coordinates I{on the triaxial's surface}.

720

721 @arg sbeta: Ellipsoidal latitude C{sin(beta)} (C{scalar}).

722 @arg cbeta: Ellipsoidal latitude C{cos(beta)} (C{scalar}).

723 @arg somega: Ellipsoidal longitude C{sin(omega)} (C{scalar}).

724 @arg comega: Ellipsoidal longitude C{cos(omega)} (C{scalar}).

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

726

727 @return: A L{Vector3Tuple}C{(x, y, z)} on the surface.

728

729 @raise TriaxialError: This triaxial is near-spherical.

730

731 @see: Method L{Triaxial.reverseBetaOmega}, U{Triaxial ellipsoid coordinate

732 system<https://WikiPedia.org/wiki/Geodesics_on_an_ellipsoid#

733 Triaxial_ellipsoid_coordinate_system>} and U{expressions (23-25)<https://

734 www.Topo.Auth.GR/wp-content/uploads/sites/111/2021/12/09_Panou.pdf>}.

735 '''

736 t = self._radialTo3(sbeta, cbeta, somega, comega)

737 return Vector3Tuple(*t, **name)

738

739 def forwardCartesian(self, x_xyz, y=None, z=None, **normal_eps_name):

740 '''Project a cartesian on this triaxial.

741

742 @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},

743 L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).

744 @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar},

745 ignored otherwise.

746 @kwarg z: Z component (C{scalar}), like B{C{y}}.

747 @kwarg normal_eps_name: Optional keyword arguments C{B{normal}=True},

748 C{B{eps}=EPS} and overriding C{B{name}="height4"} (C{str}),

749 see method L{Triaxial.height4}.

750

751 @see: Method L{Triaxial.height4} for further information and method

752 L{Triaxial.reverseCartesian} to reverse the projection.

753 '''

754 return self.height4(x_xyz, y, z, **normal_eps_name)

755

756 def forwardLatLon(self, lat, lon, height=0, **name):

757 '''Convert I{geodetic} lat-, longitude and height to cartesian.

758

759 @arg lat: Geodetic latitude (C{degrees}).

760 @arg lon: Geodetic longitude (C{degrees}).

761 @arg height: Height above the ellipsoid (C{meter}, same units

762 as this triaxial's C{a}, C{b} and C{c} axes).

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

764

765 @return: A L{Vector3Tuple}C{(x, y, z)}.

766

767 @see: Method L{Triaxial.reverseLatLon} and U{expressions (9-11)<https://

768 www.Topo.Auth.GR/wp-content/uploads/sites/111/2021/12/09_Panou.pdf>}.

769 '''

770 return self._forwardLatLon3(height, name, *sincos2d_(lat, lon))

771

772 def forwardLatLon_(self, slat, clat, slon, clon, height=0, **name):

773 '''Convert I{geodetic} lat-, longitude and height to cartesian.

774

775 @arg slat: Geodetic latitude C{sin(lat)} (C{scalar}).

776 @arg clat: Geodetic latitude C{cos(lat)} (C{scalar}).

777 @arg slon: Geodetic longitude C{sin(lon)} (C{scalar}).

778 @arg clon: Geodetic longitude C{cos(lon)} (C{scalar}).

779 @arg height: Height above the ellipsoid (C{meter}, same units

780 as this triaxial's axes C{a}, C{b} and C{c}).

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

782

783 @return: A L{Vector3Tuple}C{(x, y, z)}.

784

785 @see: Method L{Triaxial.reverseLatLon} and U{expressions (9-11)<https://

786 www.Topo.Auth.GR/wp-content/uploads/sites/111/2021/12/09_Panou.pdf>}.

787 '''

788 sa, ca = self._norm2(slat, clat)

789 sb, cb = self._norm2(slon, clon)

790 return self._forwardLatLon3(height, name, sa, ca, sb, cb)

791

792 def _forwardLatLon3(self, height, name, sa, ca, sb, cb): # name always **name

793 '''(INTERNAL) Helper for C{.forwardLatLon} and C{.forwardLatLon_}.

794 '''

795 ca_x_sb = ca * sb

796 h = self._Height(height)

797 # 1 - (1 - (c/a)**2) * sa**2 - (1 - (b/a)**2) * ca**2 * sb**2

798 t = fsumf_(_1_0, -self.e2ac * sa**2, -self.e2ab * ca_x_sb**2)

799 n = self.a / _sqrt0(t) # prime vertical

800 x = (h + n) * ca * cb

801 y = (h + n * self._1e2ab) * ca_x_sb

802 z = (h + n * self._1e2ac) * sa

803 return Vector3Tuple(x, y, z, **name)

804

805 def _Height(self, height):

806 '''(INTERNAL) Validate a C{height}.

807 '''

808 return Height_(height=height, low=-self.c, Error=TriaxialError)

809

810 @Property_RO

811 def _k2_kp2(self):

812 '''(INTERNAL) Get C{k2} and C{kp2} for C{._xE}, C{._yE} and C{.area}.

813 '''

814 # k2 = a2b2 / a2c2 * c2_b2

815 # kp2 = b2c2 / a2c2 * a2_b2

816 # b2 = b**2

817 # xE = Elliptic(k2, -a2b2 / b2, kp2, a2_b2)

818 # yE = Elliptic(kp2, +b2c2 / b2, k2, c2_b2)

819 # aE = Elliptic(kp2, 0, k2, 1)

820 return (self._a2b2_a2c2 * self._c2_b2,

821 self._b2c2 / self._a2c2 * self._a2_b2)

822

823 def _radialTo3(self, sbeta, cbeta, somega, comega):

824 '''(INTERNAL) Convert I{ellipsoidal} lat- and longitude C{beta} and

825 C{omega} to cartesian coordinates I{on the triaxial's surface},

826 also I{ordered} helper for C{.height4}.

827 '''

828 sa, ca = self._norm2(sbeta, cbeta)

829 sb, cb = self._norm2(somega, comega)

830

831 b2_a2 = self._1e2ab # == (b/a)**2

832 c2_a2 = -self._1e2ac # == -(c/a)**2

833 a2c2_a2 = self. e2ac # (a**2 - c**2) / a**2 == 1 - (c/a)**2

834

835 x2 = _Fsumf_(_1_0, -b2_a2 * sa**2, c2_a2 * ca**2).fover(a2c2_a2)

836 z2 = _Fsumf_(c2_a2, sb**2, b2_a2 * cb**2).fover(a2c2_a2)

837

838 x, y, z = self._abc3

839 x *= cb * _sqrt0(x2)

840 y *= ca * sb

841 z *= sa * _sqrt0(z2)

842 return x, y, z

843

844 def reverseBetaOmega(self, x_xyz, y=None, z=None, **name):

845 '''Convert cartesian to I{ellipsoidal} lat- and longitude, C{beta}, C{omega}

846 and height.

847

848 @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},

849 L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).

850 @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar},

851 ignored otherwise.

852 @kwarg z: Z component (C{scalar}), like B{C{y}}.

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

854

855 @return: A L{BetaOmega3Tuple}C{(beta, omega, height)} with C{beta} and

856 C{omega} in L{Radians} and (radial) C{height} in C{meter}, same

857 units as this triaxial's axes.

858

859 @see: Methods L{Triaxial.forwardBetaOmega} and L{Triaxial.forwardBetaOmega_}

860 and U{expressions (21-22)<https://www.Topo.Auth.GR/wp-content/uploads/

861 sites/111/2021/12/09_Panou.pdf>}.

862 '''

863 v = _otherV3d_(x_xyz, y, z)

864 a, b, h = self._reverseLatLon3(v, atan2, v, self.forwardBetaOmega_)

865 return BetaOmega3Tuple(Radians(beta=a), Radians(omega=b), h, **name)

866

867 def reverseCartesian(self, x_xyz, y=None, z=None, h=0, normal=True, eps=_EPS2e4, **name):

868 '''Unproject" a cartesian I{on} this triaxial's surface to a cartesion I{off}

869 this triaxial's surface.

870

871 @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},

872 L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).

873 @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar},

874 ignored otherwise.

875 @kwarg z: Z component (C{scalar}), like B{C{y}}.

876 @arg h: Height above or below this triaxial's surface (C{meter}, same units

877 as this triaxial's axes).

878 @kwarg normal: If C{True}, the height is C{normal} to the surface, otherwise

879 C{radially} to the center of this triaxial (C{bool}).

880 @kwarg eps: Tolerance for on-surface test (C{scalar}), see method L{Triaxial.sideOf}.

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

882

883 @return: A L{Vector3Tuple}C{(x, y, z)}.

884

885 @raise TrialError: Cartesian C{(x, y, z)} not on this triaxial's surface.

886

887 @see: Methods L{Triaxial.forwardCartesian} and L{Triaxial.height4}.

888 '''

889 v = _otherV3d_(x_xyz, y, z, **name)

890 s = self.sideOf(v.xyz, eps=eps)

891 if s: # PYCHOK no cover

892 t = _SPACE_((_inside_ if s < 0 else _outside_), self.toRepr())

893 raise TriaxialError(eps=eps, sideOf=s, x=v.x, y=v.y, z=v.z, txt=t)

894

895 if h:

896 if normal:

897 v = v.plus(self.normal3d(*v.xyz, length=h))

898 elif v.length > EPS0:

899 v = v.times(_1_0 + (h / v.length))

900 return v.xyz # Vector3Tuple

901

902 def reverseLatLon(self, x_xyz, y=None, z=None, **name):

903 '''Convert cartesian to I{geodetic} lat-, longitude and height.

904

905 @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},

906 L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).

907 @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar},

908 ignored otherwise.

909 @kwarg z: Z component (C{scalar}), like B{C{y}}.

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

911

912 @return: A L{LatLon3Tuple}C{(lat, lon, height)} with C{lat} and C{lon}

913 in C{degrees} and (radial) C{height} in C{meter}, same units

914 as this triaxial's axes.

915

916 @see: Methods L{Triaxial.forwardLatLon} and L{Triaxial.forwardLatLon_}

917 and U{expressions (4-5)<https://www.Topo.Auth.GR/wp-content/uploads/

918 sites/111/2021/12/09_Panou.pdf>}.

919 '''

920 v = _otherV3d_(x_xyz, y, z)

921 s = v.times_(self._1e2ac, # == 1 - e_sub_x**2

922 self._1e2bc, # == 1 - e_sub_y**2

923 _1_0)

924 a, b, h = self._reverseLatLon3(s, atan2d, v, self.forwardLatLon_)

925 return LatLon3Tuple(Degrees(lat=a), Degrees(lon=b), h, **name)

926

927 def _reverseLatLon3(self, s, atan2_, v, forward_):

928 '''(INTERNAL) Helper for C{.reverseBetOmg} and C{.reverseLatLon}.

929 '''

930 x, y, z = s.xyz3

931 d = hypot( x, y)

932 a = atan2_(z, d)

933 b = atan2_(y, x)

934 h = v.minus_(*forward_(z, d, y, x)).length

935 return a, b, h

936

937

938 class JacobiConformal(Triaxial):

939 '''This is a conformal projection of a triaxial ellipsoid to a plane in which the

940 C{X} and C{Y} grid lines are straight.

941

942 Ellipsoidal coordinates I{beta} and I{omega} are converted to Jacobi Conformal

943 I{y} respectively I{x} separately. Jacobi's coordinates have been multiplied

944 by C{sqrt(B{a}**2 - B{c}**2) / (2 * B{b})} so that the customary results are

945 returned in the case of an ellipsoid of revolution.

946

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

948 licensed under the MIT/X11 License.

949

950 @note: This constructor can I{not be used to specify a sphere}, see alternate

951 L{JacobiConformalSpherical}.

952

953 @see: L{Triaxial}, C++ class U{JacobiConformal<https://GeographicLib.SourceForge.io/

954 C++/doc/classGeographicLib_1_1JacobiConformal.html#details>}, U{Jacobi's conformal

955 projection<https://GeographicLib.SourceForge.io/C++/doc/jacobi.html>} and Jacobi,

956 C. G. J. I{U{Vorlesungen über Dynamik<https://Books.Google.com/books?

957 id=ryEOAAAAQAAJ&pg=PA212>}}, page 212ff.

958 '''

959

960 @Property_RO

961 def _xE(self):

962 '''(INTERNAL) Get the x-elliptic function.

963 '''

964 k2, kp2 = self._k2_kp2

965 # -a2b2 / b2 == (b2 - a2) / b2 == 1 - a2 / b2 == 1 - a2_b2

966 return self._Elliptic(k2, _1_0 - self._a2_b2, kp2, self._a2_b2)

967

968 def xR(self, omega):

969 '''Compute a Jacobi Conformal C{x} projection.

970

971 @arg omega: Ellipsoidal longitude (C{radians} or L{Degrees}).

972

973 @return: The C{x} projection (L{Radians}).

974 '''

975 return self.xR_(*SinCos2(omega))

976

977 def xR_(self, somega, comega):

978 '''Compute a Jacobi Conformal C{x} projection.

979

980 @arg somega: Ellipsoidal longitude C{sin(omega)} (C{scalar}).

981 @arg comega: Ellipsoidal longitude C{cos(omega)} (C{scalar}).

982

983 @return: The C{x} projection (L{Radians}).

984 '''

985 s, c = self._norm2(somega, comega, self.a)

986 return Radians(x=self._xE.fPi(s, c) * self._a2_b2)

987

988 @Property_RO

989 def xyQ2(self):

990 '''Get the Jacobi Conformal quadrant size (L{Jacobi2Tuple}C{(x, y)}).

991 '''

992 return Jacobi2Tuple(Radians(x=self._a2_b2 * self._xE.cPi),

993 Radians(y=self._c2_b2 * self._yE.cPi),

994 name=JacobiConformal.xyQ2.name)

995

996 def xyR2(self, beta, omega, **name):

997 '''Compute a Jacobi Conformal C{x} and C{y} projection.

998

999 @arg beta: Ellipsoidal latitude (C{radians} or L{Degrees}).

1000 @arg omega: Ellipsoidal longitude (C{radians} or L{Degrees}).

1001 @kwarg name: Optional name (C{str}), overriding C{B{name}="xyR2"}.

1002

1003 @return: A L{Jacobi2Tuple}C{(x, y)}.

1004 '''

1005 return self.xyR2_(*(SinCos2(beta) + SinCos2(omega)),

1006 name=_name__(name, name__=self.xyR2))

1007

1008 def xyR2_(self, sbeta, cbeta, somega, comega, **name):

1009 '''Compute a Jacobi Conformal C{x} and C{y} projection.

1010

1011 @arg sbeta: Ellipsoidal latitude C{sin(beta)} (C{scalar}).

1012 @arg cbeta: Ellipsoidal latitude C{cos(beta)} (C{scalar}).

1013 @arg somega: Ellipsoidal longitude C{sin(omega)} (C{scalar}).

1014 @arg comega: Ellipsoidal longitude C{cos(omega)} (C{scalar}).

1015 @kwarg name: Optional name (C{str}), overriding C{B{name}="xyR2_"}.

1016

1017 @return: A L{Jacobi2Tuple}C{(x, y)}.

1018 '''

1019 return Jacobi2Tuple(self.xR_(somega, comega),

1020 self.yR_(sbeta, cbeta),

1021 name=_name__(name, name__=self.xyR2_))

1022

1023 @Property_RO

1024 def _yE(self):

1025 '''(INTERNAL) Get the x-elliptic function.

1026 '''

1027 kp2, k2 = self._k2_kp2 # swapped!

1028 # b2c2 / b2 == (b2 - c2) / b2 == 1 - c2 / b2 == e2bc

1029 return self._Elliptic(k2, self.e2bc, kp2, self._c2_b2)

1030

1031 def yR(self, beta):

1032 '''Compute a Jacobi Conformal C{y} projection.

1033

1034 @arg beta: Ellipsoidal latitude (C{radians} or L{Degrees}).

1035

1036 @return: The C{y} projection (L{Radians}).

1037 '''

1038 return self.yR_(*SinCos2(beta))

1039

1040 def yR_(self, sbeta, cbeta):

1041 '''Compute a Jacobi Conformal C{y} projection.

1042

1043 @arg sbeta: Ellipsoidal latitude C{sin(beta)} (C{scalar}).

1044 @arg cbeta: Ellipsoidal latitude C{cos(beta)} (C{scalar}).

1045

1046 @return: The C{y} projection (L{Radians}).

1047 '''

1048 s, c = self._norm2(sbeta, cbeta, self.c)

1049 return Radians(y=self._yE.fPi(s, c) * self._c2_b2)

1050

1051

1052 class JacobiConformalSpherical(JacobiConformal):

1053 '''An alternate, I{spherical} L{JacobiConformal} projection.

1054

1055 @see: L{JacobiConformal} for other and more details.

1056 '''

1057 _ab = _bc = 0

1058

1059 def __init__(self, radius_triaxial, ab=0, bc=0, **name):

1060 '''New L{JacobiConformalSpherical}.

1061

1062 @arg radius_triaxial: Radius (C{scalar}, conventionally in

1063 C{meter}) or an other L{JacobiConformalSpherical},

1064 L{JacobiConformal} or ordered L{Triaxial}.

1065 @kwarg ab: Relative magnitude of C{B{a} - B{b}} (C{meter},

1066 same units as C{scalar B{radius}}.

1067 @kwarg bc: Relative magnitude of C{B{b} - B{c}} (C{meter},

1068 same units as C{scalar B{radius}}.

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

1070

1071 @raise TriaxialError: Invalid B{C{radius_triaxial}}, negative

1072 B{C{ab}}, negative B{C{bc}} or C{(B{ab}

1073 + B{bc})} not positive.

1074

1075 @note: If B{C{radius_triaxial}} is a L{JacobiConformalSpherical}

1076 and if B{C{ab}} and B{C{bc}} are both zero or C{None},

1077 the B{C{radius_triaxial}}'s C{ab}, C{bc}, C{a}, C{b}

1078 and C{c} are copied.

1079 '''

1080 try:

1081 r = radius_triaxial

1082 if isinstance(r, Triaxial): # ordered only

1083 t = r._abc3

1084 j = isinstance(r, JacobiConformalSpherical) and not bool(ab or bc)

1085 else:

1086 t = (Radius(radius=r),) * 3

1087 j = False

1088 self._ab = r.ab if j else Scalar_(ab=ab) # low=0

1089 self._bc = r.bc if j else Scalar_(bc=bc) # low=0

1090 if (self.ab + self.bc) <= 0:

1091 raise ValueError(_negative_)

1092 a, _, c = self._abc3 = t

1093 if not (a >= c and isfinite(self._a2b2)

1094 and isfinite(self._a2c2)):

1095 raise ValueError(_not_(_finite_))

1096 except (TypeError, ValueError) as x:

1097 raise TriaxialError(radius_triaxial=r, ab=ab, bc=bc, cause=x)

1098 if name:

1099 self.name = name

1100

1101 @Property_RO

1102 def ab(self):

1103 '''Get relative magnitude C{a - b} (C{meter}, same units as B{C{a}}).

1104 '''

1105 return self._ab

1106

1107 @Property_RO

1108 def _a2b2(self):

1109 '''(INTERNAL) Get C{a**2 - b**2} == ab * (a + b).

1110 '''

1111 a, b, _ = self._abc3

1112 return self.ab * (a + b)

1113

1114 @Property_RO

1115 def _a2c2(self):

1116 '''(INTERNAL) Get C{a**2 - c**2} == a2b2 + b2c2.

1117 '''

1118 return self._a2b2 + self._b2c2

1119

1120 @Property_RO

1121 def bc(self):

1122 '''Get relative magnitude C{b - c} (C{meter}, same units as B{C{a}}).

1123 '''

1124 return self._bc

1125

1126 @Property_RO

1127 def _b2c2(self):

1128 '''(INTERNAL) Get C{b**2 - c**2} == bc * (b + c).

1129 '''

1130 _, b, c = self._abc3

1131 return self.bc * (b + c)

1132

1133 @Property_RO

1134 def radius(self):

1135 '''Get radius (C{meter}, conventionally).

1136 '''

1137 return self.a

1138

1139

1140 class TriaxialError(_ValueError):

1141 '''Raised for L{Triaxial} issues.

1142 '''

1143 pass # ...

1144

1145

1146 class Triaxials(_NamedEnum):

1147 '''(INTERNAL) L{Triaxial} registry, I{must} be a sub-class

1148 to accommodate the L{_LazyNamedEnumItem} properties.

1149 '''

1150 def _Lazy(self, *abc, **name):

1151 '''(INTERNAL) Instantiate the C{Triaxial}.

1152 '''

1153 a, b, c = map(km2m, abc)

1154 return Triaxial(a, b, c, **name)

1155

1156 Triaxials = Triaxials(Triaxial, Triaxial_) # PYCHOK singleton

1157'''Some pre-defined L{Triaxial}s, all I{lazily} instantiated.'''

1158# <https://ArxIV.org/pdf/1909.06452.pdf> Table 1 Semi-axes in Km

1159# <https://www.JPS.NASA.gov/education/images/pdf/ss-moons.pdf>

1160# <https://link.Springer.com/article/10.1007/s00190-022-01650-9>

1161_abc84_35 = (_EWGS84.a + 35), (_EWGS84.a - 35), _EWGS84.b

1162 Triaxials._assert( # a (Km) b (Km) c (Km) planet

1163 Amalthea = _lazy('Amalthea', 125.0, 73.0, _64_0), # Jupiter

1164 Ariel = _lazy('Ariel', 581.1, 577.9, 577.7), # Uranus

1165 Earth = _lazy('Earth', 6378.173435, 6378.1039, 6356.7544),

1166 Enceladus = _lazy('Enceladus', 256.6, 251.4, 248.3), # Saturn

1167 Europa = _lazy('Europa', 1564.13, 1561.23, 1560.93), # Jupiter

1168 Io = _lazy('Io', 1829.4, 1819.3, 1815.7), # Jupiter

1169 Mars = _lazy('Mars', 3394.6, 3393.3, 3376.3),

1170 Mimas = _lazy('Mimas', 207.4, 196.8, 190.6), # Saturn

1171 Miranda = _lazy('Miranda', 240.4, 234.2, 232.9), # Uranus

1172 Moon = _lazy('Moon', 1735.55, 1735.324, 1734.898), # Earth

1173 Tethys = _lazy('Tethys', 535.6, 528.2, 525.8), # Saturn

1174 WGS84_35 = _lazy('WGS84_35', *map(m2km, _abc84_35)))

1175 del _abc84_35, _EWGS84

1176

1177

1178 def _getitems(items, *indices):

1179 '''(INTERNAL) Get the C{items} at the given I{indices}.

1180

1181 @return: C{Type(items[i] for i in indices)} with

1182 C{Type = type(items)}, any C{type} having

1183 the special method C{__getitem__}.

1184 '''

1185 return type(items)(map(items.__getitem__, indices))

1186

1187

1188 def _hartzell3(pov, los, Tun): # in .Ellipsoid.hartzell4, .formy.hartzell

1189 '''(INTERNAL) Hartzell's "Satellite Line-of-Sight Intersection ...",

1190 formula from a Point-Of-View to an I{un-/ordered} Triaxial.

1191 '''

1192 def _toUvwV3d(los, pov):

1193 try: # pov must be LatLon or Cartesian if los is a Los

1194 v = los.toUvw(pov)

1195 except (AttributeError, TypeError):

1196 v = _otherV3d(los=los)

1197 return v

1198

1199 p3 = _otherV3d(pov=pov.toCartesian() if isLatLon(pov) else pov)

1200 if los is True: # normal

1201 a, b, c, d, i = _plumbTo5(p3.x, p3.y, p3.z, Tun)

1202 return type(p3)(a, b, c), d, i

1203

1204 u3 = p3.negate() if los is False or los is None else _toUvwV3d(los, pov)

1205

1206 a, b, c, T = Tun._ordered4

1207

1208 a2 = a**2 # largest, factored out

1209 b2, p2 = (b**2, T._1e2ab) if b != a else (a2, _1_0)

1210 c2, q2 = (c**2, T._1e2ac) if c != a else (a2, _1_0)

1211

1212 p3 = T._order3d(p3)

1213 u3 = T._order3d(u3).unit() # unit vector, opposing signs

1214

1215 x2, y2, z2 = p3.x2y2z2 # p3.times_(p3).xyz3

1216 ux, vy, wz = u3.times_(p3).xyz3

1217 u2, v2, w2 = u3.x2y2z2 # u3.times_(u3).xyz3

1218

1219 t = (p2 * c2), c2, b2

1220 m = fdot(t, u2, v2, w2) # a2 factored out

1221 if m < EPS0: # zero or near-null LOS vector

1222 raise _ValueError(_near_(_null_))

1223

1224 r = fsumf_(b2 * w2, c2 * v2, -v2 * z2, vy * wz * 2,

1225 -w2 * y2, -u2 * y2 * q2, -u2 * z2 * p2, ux * wz * 2 * p2,

1226 -w2 * x2 * p2, b2 * u2 * q2, -v2 * x2 * q2, ux * vy * 2 * q2)

1227 if r > 0: # a2 factored out

1228 r = sqrt(r) * b * c # == a * a * b * c / a2

1229 elif r < 0: # LOS pointing away from or missing the triaxial

1230 raise _ValueError(_opposite_ if max(ux, vy, wz) > 0 else _outside_)

1231

1232 d = Fdot(t, ux, vy, wz).fadd_(r).fover(m) # -r for antipode, a2 factored out

1233 if d > 0: # POV inside or LOS outside or missing the triaxial

1234 s = fsumf_(_N_1_0, _over(x2, a2), _over(y2, b2), _over(z2, c2)) # like _sideOf

1235 raise _ValueError(_outside_ if s > 0 else _inside_)

1236 elif fsum1f_(x2, y2, z2, -d*d) < 0: # d past triaxial's center

1237 raise _ValueError(_too_(_distant_))

1238

1239 v = p3.minus(u3.times(d)) # cartesian type(pov) or Vector3d

1240 h = p3.minus(v).length # distance to pov == -d

1241 return T._order3d(v, reverse=True), h, None

1242

1243

1244 def hartzell4(pov, los=False, tri_biax=_WGS84, **name):

1245 '''Compute the intersection of a tri-/biaxial ellipsoid and a Line-Of-Sight from

1246 a Point-Of-View outside.

1247

1248 @arg pov: Point-Of-View outside the tri-/biaxial (C{Cartesian}, L{Ecef9Tuple},

1249 C{LatLon} or L{Vector3d}).

1250 @kwarg los: Line-Of-Sight, I{direction} to the tri-/biaxial (L{Los}, L{Vector3d})

1251 or C{True} for the I{normal, perpendicular, plumb} to the surface of

1252 the tri-/biaxial or C{False} or C{None} to point to its center.

1253 @kwarg tri_biax: A triaxial (L{Triaxial}, L{Triaxial_}, L{JacobiConformal} or

1254 L{JacobiConformalSpherical}) or biaxial ellipsoid (L{Datum},

1255 L{Ellipsoid}, L{Ellipsoid2}, L{a_f2Tuple}) or spherical earth

1256 radius (C{scalar}, conventionally in C{meter}).

1257 @kwarg name: Optional name (C{str}), overriding C{B{name}="hartzell4"}.

1258

1259 @return: L{Vector4Tuple}C{(x, y, z, h)} on the tri-/biaxial's surface, with C{h}

1260 the distance from B{C{pov}} to C{(x, y, z)} I{along the} B{C{los}}, all

1261 in C{meter}, conventionally.

1262

1263 @raise TriaxialError: Invalid B{C{pov}} or B{C{pov}} inside the tri-/biaxial or

1264 invalid B{C{los}} or B{C{los}} points outside or away from

1265 the tri-/biaxial.

1266

1267 @raise TypeError: Invalid B{C{tri_biax}}, C{ellipsoid} or C{datum}.

1268

1269 @see: Class L{pygeodesy3.Los}, functions L{pygeodesy.tyr3d} and L{pygeodesy.hartzell}

1270 and U{lookAtSpheroid<https://PyPI.org/project/pymap3d>} and U{"Satellite

1271 Line-of-Sight Intersection with Earth"<https://StephenHartzell.Medium.com/

1272 satellite-line-of-sight-intersection-with-earth-d786b4a6a9b6>}.

1273 '''

1274 if isinstance(tri_biax, Triaxial_):

1275 T = tri_biax

1276 else:

1277 D = tri_biax if isinstance(tri_biax, Datum) else \

1278 _spherical_datum(tri_biax, name__=hartzell4) # typename

1279 T = D.ellipsoid._triaxial

1280 try:

1281 v, h, i = _hartzell3(pov, los, T)

1282 except Exception as x:

1283 raise TriaxialError(pov=pov, los=los, tri_biax=tri_biax, cause=x)

1284 n = _name__(name, name__=hartzell4) # typename

1285 return Vector4Tuple(v.x, v.y, v.z, h, iteration=i, name=n)

1286

1287

1288 def _hypot2_1(x, y, z=0):

1289 '''(INTERNAL) Compute M{x**2 + y**2 + z**2 - 1} with C{max(fabs(x), fabs(y),

1290 fabs(z))} rarely greater than 1.0.

1291 '''

1292 return fsumf_(_N_1_0, x*x, y*y, z*z)

1293

1294

1295 def _otherV3d_(x_xyz, y, z, **name):

1296 '''(INTERNAL) Get a Vector3d from C{x_xyz}, C{y} and C{z}.

1297 '''

1298 return Vector3d(x_xyz, y, z, **name) if isscalar(x_xyz) else \

1299 _otherV3d(x_xyz=x_xyz, **name)

1300

1301

1302 def _over0(p, q):

1303 '''(INTERNAL) Return C{p / q} or C{0}.

1304 '''

1305 return (p / q) if q > fabs(p) else _0_0

1306

1307

1308 def _over02(p, q):

1309 '''(INTERNAL) Return C{(p / q)**2} or C{0}.

1310 '''

1311 return (p / q)**2 if p and q else _0_0

1312

1313

1314 def _plumbTo3(px, py, E, eps=EPS): # in .ellipsoids.Ellipsoid.height4

1315 '''(INTERNAL) Nearest point on a 2-D ellipse in 1st quadrant.

1316 '''

1317 a, b = E.a, E.b

1318 if min(px, py, a, b) < EPS0:

1319 raise _AssertionError(px=px, py=py, a=a, b=b, E=E)

1320

1321 a2 = a - b * E.b_a

1322 b2 = b - a * E.a_b

1323 tx = ty = _SQRT2_2

1324 for i in range(16): # max 5

1325 ex = tx**3 * a2

1326 ey = ty**3 * b2

1327

1328 qx = px - ex

1329 qy = py - ey

1330 q = hypot(qx, qy)

1331 if q < EPS0: # PYCHOK no cover

1332 break

1333 r = hypot(ex - tx * a,

1334 ey - ty * b) / q

1335

1336 sx, tx = tx, min(_1_0, max(0, (ex + qx * r) / a))

1337 sy, ty = ty, min(_1_0, max(0, (ey + qy * r) / b))

1338 t = hypot(ty, tx)

1339 if t < EPS0: # PYCHOK no cover

1340 break

1341 tx = tx / t # /= chokes PyChecker

1342 ty = ty / t

1343 if fabs(sx - tx) < eps and \

1344 fabs(sy - ty) < eps:

1345 break

1346

1347 tx *= a / px

1348 ty *= b / py

1349 return tx, ty, i # x and y as fractions

1350

1351

1352 def _plumbTo4(x, y, a, b, eps=EPS):

1353 '''(INTERNAL) Nearest point on and distance to a 2-D ellipse, I{unordered}.

1354

1355 @see: Function C{_plumbTo3} and I{Eberly}'s U{Distance from a Point to ... an Ellipse ...

1356 <https://www.GeometricTools.com/Documentation/DistancePointEllipseEllipsoid.pdf>}.

1357 '''

1358 if b > a:

1359 b, a, d, i = _plumbTo4(y, x, b, a, eps=eps)

1360 return a, b, d, i

1361

1362 if not (b > 0 and isfinite(a)):

1363 raise _ValueError(a=a, b=b)

1364

1365 i = None

1366 if y:

1367 if x:

1368 u = fabs(x / a)

1369 w = fabs(y / b)

1370 g = _hypot2_1(u, w)

1371 if fabs(g) > EPS02:

1372 r = (b / a)**2

1373 t, i = _rootNd(_1_0 / r, 0, u, 0, w, g) # eps

1374 a = _over(x, t * r + _1_0)

1375 b = _over(y, t + _1_0)

1376 d = hypot(x - a, y - b)

1377 else: # on the ellipse

1378 a, b, d = x, y, _0_0

1379 else: # x == 0

1380 if y < 0:

1381 b = -b

1382 a = x # signed-0

1383 d = fabs(y - b)

1384

1385 elif x: # y == 0

1386 d, r = None, _over0(a * x, (a + b) * (a - b))

1387 if r:

1388 a *= r

1389 r = _1_0 - r**2

1390 if r > EPS02:

1391 b *= sqrt(r)

1392 d = hypot(x - a, y - b)

1393 elif x < 0:

1394 a = -a

1395 if d is None:

1396 b = y # signed-0

1397 d = fabs(x - a)

1398

1399 else: # x == y == 0

1400 a = x # signed-0

1401 d = b

1402

1403 return a, b, d, i

1404

1405

1406 def _plumbTo5(x, y, z, Tun, eps=EPS): # in .testTriaxials

1407 '''(INTERNAL) Nearest point on and distance to an I{un-/ordered} triaxial.

1408

1409 @see: I{Eberly}'s U{Distance from a Point to ... an Ellipsoid ...<https://

1410 www.GeometricTools.com/Documentation/DistancePointEllipseEllipsoid.pdf>}.

1411 '''

1412 a, b, c, T = Tun._ordered4

1413 if Tun is not T: # T is ordered, Tun isn't

1414 t = T._order3(x, y, z) + (T,)

1415 a, b, c, d, i = _plumbTo5(*t, eps=eps)

1416 return T._order3(a, b, c, reverse=True) + (d, i)

1417

1418 if not (c > 0 and isfinite(a)):

1419 raise _ValueError(a=a, b=b, c=c)

1420

1421 if eps > 0:

1422 val = max(eps * 1e8, EPS)

1423 else: # no validation

1424 val, eps = 0, max(EPS0, -eps)

1425

1426 i = None

1427 if z:

1428 if y:

1429 if x:

1430 u = fabs(x / a)

1431 v = fabs(y / b)

1432 w = fabs(z / c)

1433 g = _hypot2_1(u, v, w)

1434 if fabs(g) > EPS02:

1435 r = T._1e2ac # (c / a)**2

1436 s = T._1e2bc # (c / b)**2

1437 t, i = _rootNd(_1_0 / r, _1_0 / s, u, v, w, g) # eps

1438 a = _over(x, t * r + _1_0)

1439 b = _over(y, t * s + _1_0)

1440 c = _over(z, t + _1_0)

1441 d = hypot_(x - a, y - b, z - c)

1442 else: # on the ellipsoid

1443 a, b, c, d = x, y, z, _0_0

1444 else: # x == 0

1445 a = x # 0

1446 b, c, d, i = _plumbTo4(y, z, b, c, eps=eps)

1447 elif x: # y == 0

1448 b = y # 0

1449 a, c, d, i = _plumbTo4(x, z, a, c, eps=eps)

1450 else: # x == y == 0

1451 if z < 0:

1452 c = -c

1453 a, b, d = x, y, fabs(z - c)

1454

1455 else: # z == 0

1456 u = _over0(a * x, T._a2c2) # (a + c) * (a - c)

1457 v = _over0(b * y, T._b2c2) # (b + c) * (b - c)

1458 s = _hypot2_1(u, v)

1459 if u and v and s < 0:

1460 a *= u

1461 b *= v

1462 c *= sqrt(-s)

1463 d = hypot_(x - a, y - b, c)

1464 else:

1465 c = z # signed-0

1466 a, b, d, i = _plumbTo4(x, y, a, b, eps=eps)

1467

1468 if val > 0:

1469 _validate(a, b, c, d, T, x, y, z, val)

1470 return a, b, c, d, i

1471

1472

1473 def _rootNd(r, s, u, v, w, g, eps=EPS0):

1474 '''(INTERNAL) Robust 2-D or 3-D root finder: 2-D if C{s == v == 0} else 3-D root.

1475

1476 @see: I{Eberly}'s U{Robust Root Finders ... and Listing 4<https://

1477 www.GeometricTools.com/Documentation/DistancePointEllipseEllipsoid.pdf>}.

1478 '''

1479 u *= r

1480 v *= s # 0 for 2-D root

1481 t0 = w - _1_0

1482 t1 = _0_0 if g < 0 else (hypot_(u, w, v) - _1_0)

1483 # assert t0 <= t1

1484 for i in range(1, _TRIPS): # 48..58

1485 t = (t1 + t0) * _0_5

1486 e = t1 - t0

1487 if eps > e > -eps or _isin(t, t0, t1):

1488 break

1489 g = fsumf_(_N_1_0, # ~= _hypot2_1

1490 _over02(u, t + r),

1491 _over02(w, t + _1_0), (

1492 _over02(v, t + s) if v else _0_0))

1493 if g > 0:

1494 t0 = t

1495 elif g < 0:

1496 t1 = t

1497 else:

1498 break

1499 else: # PYCHOK no cover

1500 t = Fmt.no_convergence(e, eps)

1501 raise _ValueError(t, txt__=_rootNd)

1502 return t, i

1503

1504

1505 def _sqrt0(x):

1506 '''(INTERNAL) C{sqrt0} with C{TriaxialError}.

1507 '''

1508 return sqrt0(x, Error=TriaxialError)

1509

1510

1511 def _validate(a, b, c, d, T, x, y, z, val):

1512 '''(INTERNAL) Validate an C{_plumTo5} result.

1513 '''

1514 e = T.sideOf(a, b, c, eps=val)

1515 if e: # not near the ellipsoid's surface

1516 raise _ValueError(a=a, b=b, c=c, d=d,

1517 sideOf=e, eps=val)

1518 if d: # angle of delta and normal vector

1519 m = Vector3d(x, y, z).minus_(a, b, c)

1520 if m.euclid > val:

1521 m = m.unit()

1522 n = T.normal3d(a, b, c)

1523 e = n.dot(m) # n.negate().dot(m)

1524 if not isnear1(fabs(e), eps1=val):

1525 raise _ValueError(n=n, m=m,

1526 dot=e, eps=val)

1527

1528

1529 if __name__ == _DMAIN_:

1530

1531 from pygeodesy import printf

1532 from pygeodesy.interns import _COMMA_, _NL_, _NLATvar_

1533

1534 t = Triaxial_(6378388.0, 6378318.0, 6356911.9461)

1535 t = t.height4(3909863.9271, 3909778.123, 3170932.5016)

1536 printf('# Bektas: %r', t)

1537

1538 # __doc__ of this file, force all into registery

1539 t = [NN] + Triaxials.toRepr(all=True, asorted=True).split(_NL_)

1540 printf(_NLATvar_.join(i.strip(_COMMA_) for i in t))

1541

1542# % python3 -m pygeodesy.triaxials

1543#

1544# Bektas: height4(x=3909251.554667, y=3909165.750567, z=3170432.501602, h=999.999996)

1545

1546# **) MIT License

1547#

1548# Copyright (C) 2022-2025 -- mrJean1 at Gmail -- All Rights Reserved.

1549#

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

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

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

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

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

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

1556#

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

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

1559#

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

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

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

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

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

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

1566# OTHER DEALINGS IN THE SOFTWARE.