Coverage for pygeodesy/geodsolve.py: 98%
83 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
2# -*- coding: utf-8 -*-
4u'''Wrapper to invoke I{Karney}'s U{GeodSolve
5<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} utility
6as an (exact) geodesic, but intended I{for testing purposes only}.
8Set env variable C{PYGEODESY_GEODSOLVE} to the (fully qualified) path
9of the C{GeodSolve} executable.
10'''
12from pygeodesy.basics import _xinstanceof # typename
13# from pygeodesy.constants import NAN, _0_0 # from .karney
14# from pygeodesy.geodesicx import GeodesicAreaExact # _MODS
15from pygeodesy.interns import _DMAIN_, NN, _UNDER_
16from pygeodesy.karney import Caps, GeodesicError, GeodSolve12Tuple, \
17 _sincos2d, _Xables, _0_0, NAN
18from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
19from pygeodesy.named import _name1__
20from pygeodesy.namedTuples import Destination3Tuple, Distance3Tuple
21from pygeodesy.props import Property, Property_RO, property_RO
22from pygeodesy.solveBase import _SolveGDictBase, _SolveGDictLineBase
23from pygeodesy.utily import _unrollon, _Wrap, wrap360
25__all__ = _ALL_LAZY.geodsolve
26__version__ = '25.04.14'
29class _GeodesicSolveBase(_SolveGDictBase):
30 '''(INTERNAL) Base class for L{GeodesicSolve} and L{GeodesicLineSolve}.
31 '''
32 _Error = GeodesicError
33 _Names_Direct = \
34 _Names_Inverse = GeodSolve12Tuple._Names_
35 _Xable_name = _Xables.GeodSolve.__name__ # typename
36 _Xable_path = _Xables.GeodSolve()
38 @Property_RO
39 def _b_option(self):
40 return ('-b',) if self.reverse2 else ()
42 @Property_RO
43 def _cmdBasic(self):
44 '''(INTERNAL) Get the basic C{GeodSolve} cmd (C{tuple}).
45 '''
46 return (self.GeodSolve, '-f') + (self._b_option +
47 self._e_option +
48 self._E_option +
49 self._p_option +
50 self._u_option)
52 @Property
53 def GeodSolve(self):
54 '''Get the U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>}
55 executable (C{filename}).
56 '''
57 return self._Xable_path
59 @GeodSolve.setter # PYCHOK setter!
60 def GeodSolve(self, path):
61 '''Set the U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>}
62 executable (C{filename}), the (fully qualified) path to the C{GeodSolve} executable.
64 @raise GeodesicError: Invalid B{C{path}}, B{C{path}} doesn't exist or
65 isn't the C{GeodSolve} executable.
66 '''
67 self._setXable(path)
69 def toStr(self, **prec_sep): # PYCHOK signature
70 '''Return this C{GeodesicSolve} as string.
72 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=", "}
73 for the C{float} C{prec}ision, number of decimal digits
74 (0..9) and the C{sep}arator string to join. Trailing
75 zero decimals are stripped for B{C{prec}} values of 1
76 and above, but kept for negative B{C{prec}} values.
78 @return: GeodesicSolve items (C{str}).
79 '''
80 return _SolveGDictBase._toStr(self, GeodSolve=self.GeodSolve, **prec_sep)
82 @Property_RO
83 def _u_option(self):
84 return ('-u',) if self.unroll else ()
87class GeodesicSolve(_GeodesicSolveBase):
88 '''Wrapper to invoke I{Karney}'s U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>}
89 as an C{Exact} version of I{Karney}'s Python class U{Geodesic<https://GeographicLib.SourceForge.io/C++/doc/
90 python/code.html#geographiclib.geodesic.Geodesic>}.
92 @note: Use property C{GeodSolve} or env variable C{PYGEODESY_GEODSOLVE} to specify the (fully
93 qualified) path to the C{GeodSolve} executable.
95 @note: This C{geodesic} is intended I{for testing purposes only}, it invokes the C{GeodSolve}
96 executable for I{every} method call.
97 '''
99 def Area(self, polyline=False, **name):
100 '''Set up a L{GeodesicAreaExact} to compute area and perimeter
101 of a polygon.
103 @kwarg polyline: If C{True}, compute the perimeter only, otherwise
104 perimeter and area (C{bool}).
105 @kwarg name: Optional C{B{name}=NN} (C{str}).
107 @return: A L{GeodesicAreaExact} instance.
109 @note: The B{C{debug}} setting is passed as C{verbose}
110 to the returned L{GeodesicAreaExact} instance.
111 '''
112 gaX = _MODS.geodesicx.GeodesicAreaExact(self, polyline=polyline, **name)
113 if self.verbose or self.debug: # PYCHOK no cover
114 gaX.verbose = True
115 return gaX
117 Polygon = Area # for C{geographiclib} compatibility
119 def Direct3(self, lat1, lon1, azi1, s12): # PYCHOK outmask
120 '''Return the destination lat, lon and reverse azimuth (final bearing)
121 in C{degrees}.
123 @return: L{Destination3Tuple}C{(lat, lon, final)}.
124 '''
125 r = self._GDictDirect(lat1, lon1, azi1, False, s12, floats=False)
126 return Destination3Tuple(float(r.lat2), float(r.lon2), wrap360(r.azi2),
127 iteration=r._iteration)
129 def _DirectLine(self, ll1, azi12, **caps_name): # PYCHOK no cover
130 '''(INTERNAL) Short-cut version.
131 '''
132 return self.DirectLine(ll1.lat, ll1.lon, azi12, **caps_name)
134 def DirectLine(self, lat1, lon1, azi1, **caps_name):
135 '''Set up a L{GeodesicLineSolve} to compute several points
136 on a single geodesic.
138 @arg lat1: Latitude of the first point (C{degrees}).
139 @arg lon1: Longitude of the first point (C{degrees}).
140 @arg azi1: Azimuth at the first point (compass C{degrees}).
141 @kwarg caps_name: Optional C{B{name}=NN} (C{str}) and keyword
142 argument C{B{caps}=Caps.ALL}, bit-or'ed combination
143 of L{Caps<pygeodesy.karney.Caps>} values specifying
144 the capabilities the L{GeodesicLineSolve} instance
145 should possess.
147 @return: A L{GeodesicLineSolve} instance.
149 @note: If the point is at a pole, the azimuth is defined by keeping
150 B{C{lon1}} fixed, writing C{B{lat1} = ±(90 − ε)}, and taking
151 the limit C{ε → 0+}.
153 @see: C++ U{GeodesicExact.Line
154 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>}
155 and Python U{Geodesic.Line<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
156 '''
157 return GeodesicLineSolve(self, lat1, lon1, azi1, **_name1__(caps_name, _or_nameof=self))
159 Line = DirectLine
161 def _Inverse(self, ll1, ll2, wrap, **outmask): # PYCHOK no cover
162 '''(INTERNAL) Short-cut version, see .ellipsoidalBaseDI.intersecant2.
163 '''
164 if wrap:
165 ll2 = _unrollon(ll1, _Wrap.point(ll2))
166 return self.Inverse(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **outmask)
168 def Inverse3(self, lat1, lon1, lat2, lon2): # PYCHOK outmask
169 '''Return the distance in C{meter} and the forward and
170 reverse azimuths (initial and final bearing) in C{degrees}.
172 @return: L{Distance3Tuple}C{(distance, initial, final)}.
173 '''
174 r = self._GDictInverse(lat1, lon1, lat2, lon2, floats=False)
175 return Distance3Tuple(float(r.s12), wrap360(r.azi1), wrap360(r.azi2),
176 iteration=r._iteration)
178 def _InverseLine(self, ll1, ll2, wrap, **caps_name): # PYCHOK no cover
179 '''(INTERNAL) Short-cut version.
180 '''
181 if wrap:
182 ll2 = _unrollon(ll1, _Wrap.point(ll2))
183 return self.InverseLine(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **caps_name)
185 def InverseLine(self, lat1, lon1, lat2, lon2, **caps_name): # PYCHOK no cover
186 '''Set up a L{GeodesicLineSolve} to compute several points
187 on a single geodesic.
189 @arg lat1: Latitude of the first point (C{degrees}).
190 @arg lon1: Longitude of the first point (C{degrees}).
191 @arg lat2: Latitude of the second point (C{degrees}).
192 @arg lon2: Longitude of the second point (C{degrees}).
193 @kwarg caps_name: Optional C{B{name}=NN} (C{str}) and keyword
194 argument C{B{caps}=Caps.ALL}, bit-or'ed combination
195 of L{Caps<pygeodesy.karney.Caps>} values specifying
196 the capabilities the L{GeodesicLineSolve} instance
197 should possess.
199 @return: A L{GeodesicLineSolve} instance.
201 @note: Both B{C{lat1}} and B{C{lat2}} should in the range C{[-90, +90]}.
203 @see: C++ U{GeodesicExact.InverseLine
204 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and
205 Python U{Geodesic.InverseLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
206 '''
207 r = self.Inverse(lat1, lon1, lat2, lon2)
208 gl = GeodesicLineSolve(self, lat1, lon1, r.azi1, **_name1__(caps_name, _or_nameof=self))
209 gl._a13 = r.a12 # gl.SetArc(r.a12)
210 gl._s13 = r.s12 # gl.SetDistance(r.s12)
211 return gl
214class GeodesicLineSolve(_GeodesicSolveBase, _SolveGDictLineBase):
215 '''Wrapper to invoke I{Karney}'s U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>}
216 as an C{Exact} version of I{Karney}'s Python class U{GeodesicLine<https://GeographicLib.SourceForge.io/C++/doc/
217 python/code.html#geographiclib.geodesicline.GeodesicLine>}.
219 @note: Use property C{GeodSolve} or env variable C{PYGEODESY_GEODSOLVE} to specify the (fully
220 qualified) path to the C{GeodSolve} executable.
222 @note: This C{geodesic} is intended I{for testing purposes only}, it invokes the C{GeodSolve}
223 executable for I{every} method call.
224 '''
225 _a13 = \
226 _s13 = NAN # see GeodesicSolve._InverseLine
228 def __init__(self, geodesic, lat1, lon1, azi1, caps=Caps.ALL, **name):
229 '''New L{GeodesicLineSolve} instance, allowing points to be found along
230 a geodesic starting at C{(B{lat1}, B{lon1})} with azimuth B{C{azi1}}.
232 @arg geodesic: The geodesic to use (L{GeodesicSolve}).
233 @arg lat1: Latitude of the first point (C{degrees}).
234 @arg lon1: Longitude of the first point (C{degrees}).
235 @arg azi1: Azimuth at the first points (compass C{degrees}).
236 @kwarg caps: Bit-or'ed combination of L{Caps<pygeodesy.karney.Caps>} values
237 specifying the capabilities the L{GeodesicLineSolve} instance
238 should possess, C{B{caps}=Caps.ALL} always. Include C{Caps.LINE_OFF}
239 if updates to the B{C{geodesic}} should I{not be reflected} in this
240 L{GeodesicLineSolve} instance.
241 @kwarg name: Optional C{B{name}=NN} (C{str}).
243 @raise GeodesicError: Invalid path for the C{GeodSolve} executable or isn't the
244 C{GeodSolve} executable, see property C{geodesic.GeodSolve}.
246 @raise TypeError: Invalid B{C{geodesic}}.
247 '''
248 _xinstanceof(GeodesicSolve, geodesic=geodesic)
249 if (caps & Caps.LINE_OFF): # copy to avoid updates
250 geodesic = geodesic.copy(deep=False, name=_UNDER_(NN, geodesic.name)) # NOT _under!
251 _SolveGDictLineBase.__init__(self, geodesic, lat1, lon1, caps, azi1=azi1, **name)
252 try:
253 self.GeodSolve = geodesic.GeodSolve # geodesic or copy of geodesic
254 except GeodesicError:
255 pass
257 @Property_RO
258 def a13(self):
259 '''Get the arc length to reference point 3 (C{degrees}).
261 @see: Methods L{Arc} and L{SetArc}.
262 '''
263 return self._a13
265 def Arc(self): # PYCHOK no cover
266 '''Return the arc length to reference point 3 (C{degrees} or C{NAN}).
268 @see: Method L{SetArc} and property L{a13}.
269 '''
270 return self.a13
272 def ArcPosition(self, a12, outmask=Caps.STANDARD): # PYCHOK no cover
273 '''Find the position on the line given B{C{a12}}.
275 @arg a12: Spherical arc length from the first point to the
276 second point (C{degrees}).
278 @return: A C{GDict} with 12 items C{lat1, lon1, azi1, lat2, lon2,
279 azi2, m12, a12, s12, M12, M21, S12}.
280 '''
281 return self._GDictInvoke(self._cmdArc, self._Names_Direct, a12)._unCaps(outmask)
283 @Property_RO
284 def azi1(self):
285 '''Get the azimuth at the first point (compass C{degrees}).
286 '''
287 return self._lla1.azi1
289 azi12 = azi1 # like RhumbLineSolve
291 @Property_RO
292 def azi1_sincos2(self):
293 '''Get the sine and cosine of the first point's azimuth (2-tuple C{(sin, cos)}).
294 '''
295 return _sincos2d(self.azi1)
297 azi12_sincos2 = azi1_sincos2
299 @Property_RO
300 def _cmdArc(self):
301 '''(INTERNAL) Get the C{GeodSolve} I{-a -L} cmd (C{tuple}).
302 '''
303 return self._cmdDistance + ('-a',)
305 def Distance(self):
306 '''Return the distance to reference point 3 (C{meter} or C{NAN}).
307 '''
308 return self.s13
310 @property_RO
311 def geodesic(self):
312 '''Get the geodesic (L{GeodesicSolve}).
313 '''
314 return self._solve # see .solveBase._SolveLineBase
316 def Intersecant2(self, lat0, lon0, radius, **kwds): # PYCHOK no cover
317 '''B{Not implemented}, throws a C{NotImplementedError} always.'''
318 self._notImplemented(lat0, lon0, radius, **kwds)
320 def PlumbTo(self, lat0, lon0, **kwds): # PYCHOK no cover
321 '''B{Not implemented}, throws a C{NotImplementedError} always.'''
322 self._notImplemented(lat0, lon0, **kwds)
324 def Position(self, s12, outmask=Caps.STANDARD):
325 '''Find the position on the line given B{C{s12}}.
327 @arg s12: Distance from the first point to the second (C{meter}).
329 @return: A C{GDict} with 12 items C{lat1, lon1, azi1, lat2, lon2,
330 azi2, m12, a12, s12, M12, M21, S12}, possibly C{a12=NAN}.
331 '''
332 return self._GDictInvoke(self._cmdDistance, self._Names_Direct, s12)._unCaps(outmask)
334 @Property_RO
335 def s13(self):
336 '''Get the distance to reference point 3 (C{meter} or C{NAN}).
338 @see: Methods L{Distance} and L{SetDistance}.
339 '''
340 return self._s13
342 def SetArc(self, a13): # PYCHOK no cover
343 '''Set reference point 3 in terms relative to the first point.
345 @arg a13: Spherical arc length from the first to the reference
346 point (C{degrees}).
348 @return: The distance C{s13} (C{meter}) between the first and
349 the reference point or C{NAN}.
350 '''
351 if self._a13 != a13:
352 self._a13 = a13
353 self._s13 = self.ArcPosition(a13, outmask=Caps.DISTANCE).s12 # if a13 else _0_0
354# _update_all(self)
355 return self._s13
357 def SetDistance(self, s13): # PYCHOK no cover
358 '''Set reference point 3 in terms relative to the first point.
360 @arg s13: Distance from the first to the reference point (C{meter}).
362 @return: The arc length C{a13} (C{degrees}) between the first and
363 the reference point or C{NAN}.
364 '''
365 if self._s13 != s13:
366 self._s13 = s13
367 self._a13 = self.Position(s13, outmask=Caps.DISTANCE).a12 if s13 else _0_0
368# _update_all(self)
369 return self._a13 # NAN for GeodesicLineExact without Cap.DISTANCE_IN
371 def toStr(self, **prec_sep): # PYCHOK signature
372 '''Return this C{GeodesicLineSolve} as string.
374 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=", "}
375 for the C{float} C{prec}ision, number of decimal digits
376 (0..9) and the C{sep}arator string to join. Trailing
377 zero decimals are stripped for B{C{prec}} values of 1
378 and above, but kept for negative B{C{prec}} values.
380 @return: GeodesicLineSolve items (C{str}).
381 '''
382 return _SolveGDictLineBase._toStr(self, azi1=self.azi1, geodesic=self._solve,
383 GeodSolve=self.GeodSolve, **prec_sep)
386__all__ += _ALL_DOCS(_GeodesicSolveBase)
388if __name__ == _DMAIN_:
390 def _main():
391 from pygeodesy import printf
392 from sys import argv
394 gS = GeodesicSolve(name='Test')
395 gS.verbose = '--verbose' in argv # or '-v' in argv
397 if not _Xables.X_OK(gS.GeodSolve): # not set
398 gS.GeodSolve = _Xables.GeodSolve(_Xables.bin_)
399 printf('version: %s', gS.version)
401 r = gS.Direct(40.6, -73.8, 51, 5.5e6)
402 printf('Direct: %r', r, nl=1)
403 printf('Direct3: %r', gS.Direct3(40.6, -73.8, 51, 5.5e6))
405 printf('Inverse: %r', gS.Inverse( 40.6, -73.8, 51.6, -0.5), nl=1)
406 printf('Inverse1: %r', gS.Inverse1(40.6, -73.8, 51.6, -0.5))
407 printf('Inverse3: %r', gS.Inverse3(40.6, -73.8, 51.6, -0.5))
409 glS = GeodesicLineSolve(gS, 40.6, -73.8, 51, name='LineTest')
410 p = glS.Position(5.5e6)
411 printf('Position: %5s %r', p == r, p, nl=1)
412 p = glS.ArcPosition(49.475527)
413 printf('ArcPosition: %5s %r', p == r, p)
415 _main()
417# % python3 -m pygeodesy.geodsolve
419# version: /opt/local/bin/GeodSolve: GeographicLib version 2.2
421# Direct: GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, M12=0.650911, M21=0.651229, s12=5500000.0, S12=39735075134877.09375)
422# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397)
424# Inverse: GDict(a12=49.94131, azi1=51.198883, azi2=107.821777, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, m12=4877684.602706, M12=0.64473, M21=0.645046, s12=5551759.400319, S12=40041368848742.53125)
425# Inverse1: 49.94131021789904
426# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777)
428# Position: True GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, M12=0.650911, M21=0.651229, s12=5500000.0, S12=39735075134877.09375)
429# ArcPosition: False GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141174, m12=4844148.669561, M12=0.650911, M21=0.651229, s12=5499999.948497, S12=39735074737272.734375)
432# % python3 -m pygeodesy.geodsolve
434# version: /opt/local/bin/GeodSolve: GeographicLib version 2.3
436# Direct: GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, M12=0.650911, M21=0.651229, s12=5500000.0, S12=39735075134877.078125)
437# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397)
439# Inverse: GDict(a12=49.94131, azi1=51.198883, azi2=107.821777, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, m12=4877684.602706, M12=0.64473, M21=0.645046, s12=5551759.400319, S12=40041368848742.53125)
440# Inverse1: 49.94131021789904
441# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777)
443# Position: False GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, s12=5500000.0)
444# ArcPosition: False GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141174, s12=5499999.948497)
447# % python3 -m pygeodesy.geodsolve --verbose
449# GeodesicSolve 'Test' 1: /opt/local/bin/GeodSolve --version (invoke)
450# GeodesicSolve 'Test' 1: /opt/local/bin/GeodSolve: GeographicLib version 2.2 (0)
451# version: /opt/local/bin/GeodSolve: GeographicLib version 2.2
452# GeodesicSolve 'Test' 2: /opt/local/bin/GeodSolve -f -E -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct)
453# GeodesicSolve 'Test' 2: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.0, lat2=51.884564505606761, lon2=-1.141172861200829, azi2=107.189397162605886, s12=5500000.0, a12=49.475527463251467, m12=4844148.703101486, M12=0.65091056699808603, M21=0.65122865892196558, S12=39735075134877.094 (0)
455# Direct: GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, M12=0.650911, M21=0.651229, s12=5500000.0, S12=39735075134877.09375)
456# GeodesicSolve 'Test' 3: /opt/local/bin/GeodSolve -f -E -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct3)
457# GeodesicSolve 'Test' 3: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.0, lat2=51.884564505606761, lon2=-1.141172861200829, azi2=107.189397162605886, s12=5500000.0, a12=49.475527463251467, m12=4844148.703101486, M12=0.65091056699808603, M21=0.65122865892196558, S12=39735075134877.094 (0)
458# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397)
459# GeodesicSolve 'Test' 4: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse)
460# GeodesicSolve 'Test' 4: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.198882845579824, lat2=51.600000000000001, lon2=-0.5, azi2=107.821776735514248, s12=5551759.4003186841, a12=49.941310217899037, m12=4877684.6027061976, M12=0.64472969205948238, M21=0.64504567852134398, S12=40041368848742.531 (0)
462# Inverse: GDict(a12=49.94131, azi1=51.198883, azi2=107.821777, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, m12=4877684.602706, M12=0.64473, M21=0.645046, s12=5551759.400319, S12=40041368848742.53125)
463# GeodesicSolve 'Test' 5: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse1)
464# GeodesicSolve 'Test' 5: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.198882845579824, lat2=51.600000000000001, lon2=-0.5, azi2=107.821776735514248, s12=5551759.4003186841, a12=49.941310217899037, m12=4877684.6027061976, M12=0.64472969205948238, M21=0.64504567852134398, S12=40041368848742.531 (0)
465# Inverse1: 49.94131021789904
466# GeodesicSolve 'Test' 6: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse3)
467# GeodesicSolve 'Test' 6: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.198882845579824, lat2=51.600000000000001, lon2=-0.5, azi2=107.821776735514248, s12=5551759.4003186841, a12=49.941310217899037, m12=4877684.6027061976, M12=0.64472969205948238, M21=0.64504567852134398, S12=40041368848742.531 (0)
468# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777)
470# Position: True GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, M12=0.650911, M21=0.651229, s12=5500000.0, S12=39735075134877.09375)
471# ArcPosition: False GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141174, m12=4844148.669561, M12=0.650911, M21=0.651229, s12=5499999.948497, S12=39735074737272.734375)
474# % python3 -m pygeodesy.geodsolve --verbose
476# GeodesicSolve 'Test'@1: /opt/local/bin/GeodSolve --version (invoke)
477# GeodesicSolve 'Test'@1: '/opt/local/bin/GeodSolve: GeographicLib version 2.3' (0, stdout/-err)
478# GeodesicSolve 'Test'@1: /opt/local/bin/GeodSolve: GeographicLib version 2.3 (0)
479# version: /opt/local/bin/GeodSolve: GeographicLib version 2.3
480# GeodesicSolve 'Test'@2: /opt/local/bin/GeodSolve -f -E -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct)
481# GeodesicSolve 'Test'@2: '40.600000000000001 -73.799999999999997 51.000000000000000 51.884564505606761 -1.141172861200843 107.189397162605871 5500000.0000000000 49.475527463251460 4844148.7031014860 0.65091056699808614 0.65122865892196569 39735075134877.078' (0, stdout/-err)
482# GeodesicSolve 'Test'@2: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.0, lat2=51.884564505606761, lon2=-1.141172861200843, azi2=107.189397162605871, s12=5500000.0, a12=49.47552746325146, m12=4844148.703101486, M12=0.65091056699808614, M21=0.65122865892196569, S12=39735075134877.078 (0)
484# Direct: GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, M12=0.650911, M21=0.651229, s12=5500000.0, S12=39735075134877.078125)
485# GeodesicSolve 'Test'@3: /opt/local/bin/GeodSolve -f -E -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct3)
486# GeodesicSolve 'Test'@3: '40.600000000000001 -73.799999999999997 51.000000000000000 51.884564505606761 -1.141172861200843 107.189397162605871 5500000.0000000000 49.475527463251460 4844148.7031014860 0.65091056699808614 0.65122865892196569 39735075134877.078' (0, stdout/-err)
487# GeodesicSolve 'Test'@3: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.0, lat2=51.884564505606761, lon2=-1.141172861200843, azi2=107.189397162605871, s12=5500000.0, a12=49.47552746325146, m12=4844148.703101486, M12=0.65091056699808614, M21=0.65122865892196569, S12=39735075134877.078 (0)
488# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397)
489# GeodesicSolve 'Test'@4: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse)
490# GeodesicSolve 'Test'@4: '40.600000000000001 -73.799999999999997 51.198882845579824 51.600000000000001 -0.500000000000000 107.821776735514248 5551759.4003186813 49.941310217899037 4877684.6027061967 0.64472969205948238 0.64504567852134398 40041368848742.531' (0, stdout/-err)
491# GeodesicSolve 'Test'@4: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.198882845579824, lat2=51.600000000000001, lon2=-0.5, azi2=107.821776735514248, s12=5551759.4003186813, a12=49.941310217899037, m12=4877684.6027061967, M12=0.64472969205948238, M21=0.64504567852134398, S12=40041368848742.531 (0)
493# Inverse: GDict(a12=49.94131, azi1=51.198883, azi2=107.821777, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, m12=4877684.602706, M12=0.64473, M21=0.645046, s12=5551759.400319, S12=40041368848742.53125)
494# GeodesicSolve 'Test'@5: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse1)
495# GeodesicSolve 'Test'@5: '40.600000000000001 -73.799999999999997 51.198882845579824 51.600000000000001 -0.500000000000000 107.821776735514248 5551759.4003186813 49.941310217899037 4877684.6027061967 0.64472969205948238 0.64504567852134398 40041368848742.531' (0, stdout/-err)
496# GeodesicSolve 'Test'@5: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.198882845579824, lat2=51.600000000000001, lon2=-0.5, azi2=107.821776735514248, s12=5551759.4003186813, a12=49.941310217899037, m12=4877684.6027061967, M12=0.64472969205948238, M21=0.64504567852134398, S12=40041368848742.531 (0)
497# Inverse1: 49.94131021789904
498# GeodesicSolve 'Test'@6: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse3)
499# GeodesicSolve 'Test'@6: '40.600000000000001 -73.799999999999997 51.198882845579824 51.600000000000001 -0.500000000000000 107.821776735514248 5551759.4003186813 49.941310217899037 4877684.6027061967 0.64472969205948238 0.64504567852134398 40041368848742.531' (0, stdout/-err)
500# GeodesicSolve 'Test'@6: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.198882845579824, lat2=51.600000000000001, lon2=-0.5, azi2=107.821776735514248, s12=5551759.4003186813, a12=49.941310217899037, m12=4877684.6027061967, M12=0.64472969205948238, M21=0.64504567852134398, S12=40041368848742.531 (0)
501# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777)
503# Position: False GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, s12=5500000.0)
504# ArcPosition: False GDict(a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141174, s12=5499999.948497)
506# **) MIT License
507#
508# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
509#
510# Permission is hereby granted, free of charge, to any person obtaining a
511# copy of this software and associated documentation files (the "Software"),
512# to deal in the Software without restriction, including without limitation
513# the rights to use, copy, modify, merge, publish, distribute, sublicense,
514# and/or sell copies of the Software, and to permit persons to whom the
515# Software is furnished to do so, subject to the following conditions:
516#
517# The above copyright notice and this permission notice shall be included
518# in all copies or substantial portions of the Software.
519#
520# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
521# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
522# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
523# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
524# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
525# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
526# OTHER DEALINGS IN THE SOFTWARE.