Coverage for pygeodesy/geodsolve.py: 98%

82 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-09-09 13:03 -0400

1 

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

3 

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}. 

7 

8Set env variable C{PYGEODESY_GEODSOLVE} to the (fully qualified) path 

9of the C{GeodSolve} executable. 

10''' 

11 

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.namedTuples import Destination3Tuple, Distance3Tuple 

20from pygeodesy.props import Property, Property_RO, property_RO 

21from pygeodesy.solveBase import _SolveGDictBase, _SolveGDictLineBase 

22from pygeodesy.utily import _unrollon, _Wrap, wrap360 

23 

24__all__ = _ALL_LAZY.geodsolve 

25__version__ = '25.09.03' 

26 

27 

28class _GeodesicSolveBase(_SolveGDictBase): 

29 '''(INTERNAL) Base class for L{GeodesicSolve} and L{GeodesicLineSolve}. 

30 ''' 

31 _Error = GeodesicError 

32 _Names_Direct = \ 

33 _Names_Inverse = GeodSolve12Tuple._Names_ 

34 _Xable_name = _Xables.GeodSolve.__name__ # typename 

35 _Xable_path = _Xables.GeodSolve() 

36 

37 @Property_RO 

38 def _b_option(self): 

39 return ('-b',) if self.reverse2 else () 

40 

41 @Property_RO 

42 def _cmdBasic(self): 

43 '''(INTERNAL) Get the basic C{GeodSolve} cmd (C{tuple}). 

44 ''' 

45 return (self.GeodSolve, '-f') + (self._b_option + 

46 self._e_option + 

47 self._E_option + 

48 self._p_option + 

49 self._u_option) 

50 

51 @Property 

52 def GeodSolve(self): 

53 '''Get the U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} 

54 executable (C{filename}). 

55 ''' 

56 return self._Xable_path 

57 

58 @GeodSolve.setter # PYCHOK setter! 

59 def GeodSolve(self, path): 

60 '''Set the U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} 

61 executable (C{filename}), the (fully qualified) path to the C{GeodSolve} executable. 

62 

63 @raise GeodesicError: Invalid B{C{path}}, B{C{path}} doesn't exist or 

64 isn't the C{GeodSolve} executable. 

65 ''' 

66 self._setXable(path) 

67 

68 def toStr(self, **prec_sep): # PYCHOK signature 

69 '''Return this C{GeodesicSolve} as string. 

70 

71 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=", "} 

72 for the C{float} C{prec}ision, number of decimal digits 

73 (0..9) and the C{sep}arator string to join. Trailing 

74 zero decimals are stripped for B{C{prec}} values of 1 

75 and above, but kept for negative B{C{prec}} values. 

76 

77 @return: GeodesicSolve items (C{str}). 

78 ''' 

79 return _SolveGDictBase._toStr(self, GeodSolve=self.GeodSolve, **prec_sep) 

80 

81 @Property_RO 

82 def _u_option(self): 

83 return ('-u',) if self.unroll else () 

84 

85 

86class GeodesicSolve(_GeodesicSolveBase): 

87 '''Wrapper to invoke I{Karney}'s U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} 

88 as an C{Exact} version of I{Karney}'s Python class U{Geodesic<https://GeographicLib.SourceForge.io/C++/doc/ 

89 python/code.html#geographiclib.geodesic.Geodesic>}. 

90 

91 @note: Use property C{GeodSolve} or env variable C{PYGEODESY_GEODSOLVE} to specify the (fully 

92 qualified) path to the C{GeodSolve} executable. 

93 

94 @note: This C{geodesic} is intended I{for testing purposes only}, it invokes the C{GeodSolve} 

95 executable for I{every} method call. 

96 ''' 

97 

98 def Area(self, polyline=False, **name): 

99 '''Set up a L{GeodesicAreaExact} to compute area and perimeter 

100 of a polygon. 

101 

102 @kwarg polyline: If C{True}, compute the perimeter only, otherwise 

103 perimeter and area (C{bool}). 

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

105 

106 @return: A L{GeodesicAreaExact} instance. 

107 

108 @note: The B{C{debug}} setting is passed as C{verbose} 

109 to the returned L{GeodesicAreaExact} instance. 

110 ''' 

111 gaX = _MODS.geodesicx.GeodesicAreaExact(self, polyline=polyline, **name) 

112 if self.verbose or self.debug: # PYCHOK no cover 

113 gaX.verbose = True 

114 return gaX 

115 

116 Polygon = Area # for C{geographiclib} compatibility 

117 

118 def Direct3(self, lat1, lon1, azi1, s12): # PYCHOK outmask 

119 '''Return the destination lat, lon and reverse azimuth (final bearing) 

120 in C{degrees}. 

121 

122 @return: L{Destination3Tuple}C{(lat, lon, final)}. 

123 ''' 

124 r = self._GDictDirect(lat1, lon1, azi1, False, s12, floats=False) 

125 return Destination3Tuple(float(r.lat2), float(r.lon2), wrap360(r.azi2), 

126 iteration=r._iteration) 

127 

128 def _DirectLine(self, ll1, azi12, **caps_name): # PYCHOK no cover 

129 '''(INTERNAL) Short-cut version. 

130 ''' 

131 return self.DirectLine(ll1.lat, ll1.lon, azi12, **caps_name) 

132 

133 def DirectLine(self, lat1, lon1, azi1, caps=Caps.ALL, **name): 

134 '''Set up a L{GeodesicLineSolve} to compute several points 

135 on a single geodesic. 

136 

137 @arg lat1: Latitude of the first point (C{degrees}). 

138 @arg lon1: Longitude of the first point (C{degrees}). 

139 @arg azi1: Azimuth at the first point (compass C{degrees}). 

140 @kwarg caps: Desired capabilities for the L{GeodesicLineSolve} instance. 

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

142 

143 @return: A L{GeodesicLineSolve} instance. 

144 

145 @note: If the point is at a pole, the azimuth is defined by keeping 

146 B{C{lon1}} fixed, writing C{B{lat1} = ±(90 − ε)}, and taking 

147 the limit C{ε → 0+}. 

148 

149 @see: C++ U{GeodesicExact.Line 

150 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} 

151 and Python U{Geodesic.Line<https://GeographicLib.SourceForge.io/Python/doc/code.html>}. 

152 ''' 

153 return GeodesicLineSolve(self, lat1, lon1, azi1, caps=caps, **name) 

154 

155 Line = ArcDirectLine = DirectLine 

156 

157 def _Inverse(self, ll1, ll2, wrap, **outmask): # PYCHOK no cover 

158 '''(INTERNAL) Short-cut version, see .ellipsoidalBaseDI.intersecant2. 

159 ''' 

160 if wrap: 

161 ll2 = _unrollon(ll1, _Wrap.point(ll2)) 

162 return self.Inverse(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **outmask) 

163 

164 def Inverse3(self, lat1, lon1, lat2, lon2): # PYCHOK outmask 

165 '''Return the distance in C{meter} and the forward and 

166 reverse azimuths (initial and final bearing) in C{degrees}. 

167 

168 @return: L{Distance3Tuple}C{(distance, initial, final)}. 

169 ''' 

170 r = self._GDictInverse(lat1, lon1, lat2, lon2, floats=False) 

171 return Distance3Tuple(float(r.s12), wrap360(r.azi1), wrap360(r.azi2), 

172 iteration=r._iteration) 

173 

174 def _InverseLine(self, ll1, ll2, wrap, **caps_name): # PYCHOK no cover 

175 '''(INTERNAL) Short-cut version. 

176 ''' 

177 if wrap: 

178 ll2 = _unrollon(ll1, _Wrap.point(ll2)) 

179 return self.InverseLine(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **caps_name) 

180 

181 def InverseLine(self, lat1, lon1, lat2, lon2, caps=Caps.ALL, **name): # PYCHOK no cover 

182 '''Set up a L{GeodesicLineSolve} to compute several points 

183 on a single geodesic. 

184 

185 @arg lat1: Latitude of the first point (C{degrees}). 

186 @arg lon1: Longitude of the first point (C{degrees}). 

187 @arg lat2: Latitude of the second point (C{degrees}). 

188 @arg lon2: Longitude of the second point (C{degrees}). 

189 @kwarg caps: Desired capabilities for the L{GeodesicLineSolve} instance. 

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

191 

192 @return: A L{GeodesicLineSolve} instance. 

193 

194 @note: Both B{C{lat1}} and B{C{lat2}} should in the range C{[-90, +90]}. 

195 

196 @see: C++ U{GeodesicExact.InverseLine 

197 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and 

198 Python U{Geodesic.InverseLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}. 

199 ''' 

200 r = self.Inverse(lat1, lon1, lat2, lon2) 

201 gl = GeodesicLineSolve(self, lat1, lon1, r.azi1, caps=caps, **name) 

202 gl._a13 = r.a12 # gl.SetArc(r.a12) 

203 gl._s13 = r.s12 # gl.SetDistance(r.s12) 

204 return gl 

205 

206 

207class GeodesicLineSolve(_GeodesicSolveBase, _SolveGDictLineBase): 

208 '''Wrapper to invoke I{Karney}'s U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} 

209 as an C{Exact} version of I{Karney}'s Python class U{GeodesicLine<https://GeographicLib.SourceForge.io/C++/doc/ 

210 python/code.html#geographiclib.geodesicline.GeodesicLine>}. 

211 

212 @note: Use property C{GeodSolve} or env variable C{PYGEODESY_GEODSOLVE} to specify the (fully 

213 qualified) path to the C{GeodSolve} executable. 

214 

215 @note: This C{geodesic} is intended I{for testing purposes only}, it invokes the C{GeodSolve} 

216 executable for I{every} method call. 

217 ''' 

218 _a13 = \ 

219 _s13 = NAN # see GeodesicSolve._InverseLine 

220 

221 def __init__(self, geodesic, lat1, lon1, azi1, caps=Caps.ALL, **name): 

222 '''New L{GeodesicLineSolve} instance, allowing points to be found along 

223 a geodesic starting at C{(B{lat1}, B{lon1})} with azimuth B{C{azi1}}. 

224 

225 @arg geodesic: The geodesic to use (L{GeodesicSolve}). 

226 @arg lat1: Latitude of the first point (C{degrees}). 

227 @arg lon1: Longitude of the first point (C{degrees}). 

228 @arg azi1: Azimuth at the first points (compass C{degrees}). 

229 @kwarg caps: Bit-or'ed combination of L{Caps<pygeodesy.karney.Caps>} values 

230 specifying the capabilities the L{GeodesicLineSolve} instance 

231 should possess, C{B{caps}=Caps.ALL} always. Include C{Caps.LINE_OFF} 

232 if updates to the B{C{geodesic}} should I{not be reflected} in this 

233 L{GeodesicLineSolve} instance. 

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

235 

236 @raise GeodesicError: Invalid path for the C{GeodSolve} executable or isn't the 

237 C{GeodSolve} executable, see property C{geodesic.GeodSolve}. 

238 

239 @raise TypeError: Invalid B{C{geodesic}}. 

240 ''' 

241 _xinstanceof(GeodesicSolve, geodesic=geodesic) 

242 if (caps & Caps.LINE_OFF): # copy to avoid updates 

243 geodesic = geodesic.copy(deep=False, name=_UNDER_(NN, geodesic.name)) # NOT _under! 

244 _SolveGDictLineBase.__init__(self, geodesic, lat1, lon1, caps, azi1=azi1, **name) 

245 try: 

246 self.GeodSolve = geodesic.GeodSolve # geodesic or copy of geodesic 

247 except GeodesicError: 

248 pass 

249 

250 @Property_RO 

251 def a13(self): 

252 '''Get the arc length to reference point 3 (C{degrees}). 

253 

254 @see: Methods L{Arc} and L{SetArc}. 

255 ''' 

256 return self._a13 

257 

258 def Arc(self): # PYCHOK no cover 

259 '''Return the arc length to reference point 3 (C{degrees} or C{NAN}). 

260 

261 @see: Method L{SetArc} and property L{a13}. 

262 ''' 

263 return self.a13 

264 

265 def ArcPosition(self, a12, outmask=Caps.STANDARD): # PYCHOK no cover 

266 '''Find the position on the line given B{C{a12}}. 

267 

268 @arg a12: Spherical arc length from the first point to the 

269 second point (C{degrees}). 

270 

271 @return: A C{GDict} with 12 items C{lat1, lon1, azi1, lat2, lon2, 

272 azi2, m12, a12, s12, M12, M21, S12}. 

273 ''' 

274 return self._GDictInvoke(self._cmdArc, self._Names_Direct, a12)._unCaps(outmask) 

275 

276 @Property_RO 

277 def azi1(self): 

278 '''Get the azimuth at the first point (compass C{degrees}). 

279 ''' 

280 return self._lla1.azi1 

281 

282 azi12 = azi1 # like RhumbLineSolve 

283 

284 @Property_RO 

285 def azi1_sincos2(self): 

286 '''Get the sine and cosine of the first point's azimuth (2-tuple C{(sin, cos)}). 

287 ''' 

288 return _sincos2d(self.azi1) 

289 

290 azi12_sincos2 = azi1_sincos2 

291 

292 @Property_RO 

293 def _cmdArc(self): 

294 '''(INTERNAL) Get the C{GeodSolve} I{-a -L} cmd (C{tuple}). 

295 ''' 

296 return self._cmdDistance + ('-a',) 

297 

298 def Distance(self): 

299 '''Return the distance to reference point 3 (C{meter} or C{NAN}). 

300 ''' 

301 return self.s13 

302 

303 @property_RO 

304 def geodesic(self): 

305 '''Get the geodesic (L{GeodesicSolve}). 

306 ''' 

307 return self._solve # see .solveBase._SolveLineBase 

308 

309 def Intersecant2(self, lat0, lon0, radius, **kwds): # PYCHOK no cover 

310 '''B{Not implemented}, throws a C{NotImplementedError} always.''' 

311 self._notImplemented(lat0, lon0, radius, **kwds) 

312 

313 def PlumbTo(self, lat0, lon0, **kwds): # PYCHOK no cover 

314 '''B{Not implemented}, throws a C{NotImplementedError} always.''' 

315 self._notImplemented(lat0, lon0, **kwds) 

316 

317 def Position(self, s12, outmask=Caps.STANDARD): 

318 '''Find the position on the line given B{C{s12}}. 

319 

320 @arg s12: Distance from the first point to the second (C{meter}). 

321 

322 @return: A C{GDict} with 12 items C{lat1, lon1, azi1, lat2, lon2, 

323 azi2, m12, a12, s12, M12, M21, S12}, possibly C{a12=NAN}. 

324 ''' 

325 return self._GDictInvoke(self._cmdDistance, self._Names_Direct, s12)._unCaps(outmask) 

326 

327 @Property_RO 

328 def s13(self): 

329 '''Get the distance to reference point 3 (C{meter} or C{NAN}). 

330 

331 @see: Methods L{Distance} and L{SetDistance}. 

332 ''' 

333 return self._s13 

334 

335 def SetArc(self, a13): # PYCHOK no cover 

336 '''Set reference point 3 in terms relative to the first point. 

337 

338 @arg a13: Spherical arc length from the first to the reference 

339 point (C{degrees}). 

340 

341 @return: The distance C{s13} (C{meter}) between the first and 

342 the reference point or C{NAN}. 

343 ''' 

344 if self._a13 != a13: 

345 self._a13 = a13 

346 self._s13 = self.ArcPosition(a13, outmask=Caps.DISTANCE).s12 # if a13 else _0_0 

347# _update_all(self) 

348 return self._s13 

349 

350 def SetDistance(self, s13): # PYCHOK no cover 

351 '''Set reference point 3 in terms relative to the first point. 

352 

353 @arg s13: Distance from the first to the reference point (C{meter}). 

354 

355 @return: The arc length C{a13} (C{degrees}) between the first and 

356 the reference point or C{NAN}. 

357 ''' 

358 if self._s13 != s13: 

359 self._s13 = s13 

360 self._a13 = self.Position(s13, outmask=Caps.DISTANCE).a12 if s13 else _0_0 

361# _update_all(self) 

362 return self._a13 # NAN for GeodesicLineExact without Cap.DISTANCE_IN 

363 

364 def toStr(self, **prec_sep): # PYCHOK signature 

365 '''Return this C{GeodesicLineSolve} as string. 

366 

367 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=", "} 

368 for the C{float} C{prec}ision, number of decimal digits 

369 (0..9) and the C{sep}arator string to join. Trailing 

370 zero decimals are stripped for B{C{prec}} values of 1 

371 and above, but kept for negative B{C{prec}} values. 

372 

373 @return: GeodesicLineSolve items (C{str}). 

374 ''' 

375 return _SolveGDictLineBase._toStr(self, azi1=self.azi1, geodesic=self._solve, 

376 GeodSolve=self.GeodSolve, **prec_sep) 

377 

378 

379__all__ += _ALL_DOCS(_GeodesicSolveBase) 

380 

381if __name__ == _DMAIN_: 

382 

383 def _main(): 

384 from pygeodesy import printf 

385 from sys import argv 

386 

387 gS = GeodesicSolve(name='Test') 

388 gS.verbose = '--verbose' in argv # or '-v' in argv 

389 

390 if not _Xables.X_OK(gS.GeodSolve): # not set 

391 gS.GeodSolve = _Xables.GeodSolve(_Xables.bin_) 

392 printf('version: %s', gS.version) 

393 

394 r = gS.Direct(40.6, -73.8, 51, 5.5e6) 

395 printf('Direct: %r', r, nl=1) 

396 printf('Direct3: %r', gS.Direct3(40.6, -73.8, 51, 5.5e6)) 

397 

398 printf('Inverse: %r', gS.Inverse( 40.6, -73.8, 51.6, -0.5), nl=1) 

399 printf('Inverse1: %r', gS.Inverse1(40.6, -73.8, 51.6, -0.5)) 

400 printf('Inverse3: %r', gS.Inverse3(40.6, -73.8, 51.6, -0.5)) 

401 

402 glS = GeodesicLineSolve(gS, 40.6, -73.8, 51, name='LineTest') 

403 p = glS.Position(5.5e6) 

404 printf('Position: %5s %r', p == r, p, nl=1) 

405 p = glS.ArcPosition(49.475527) 

406 printf('ArcPosition: %5s %r', p == r, p) 

407 

408 _main() 

409 

410# % python3 -m pygeodesy.geodsolve 

411 

412# version: /opt/local/bin/GeodSolve: GeographicLib version 2.2 

413 

414# 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) 

415# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397) 

416 

417# 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) 

418# Inverse1: 49.94131021789904 

419# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777) 

420 

421# 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) 

422# 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) 

423 

424 

425# % python3 -m pygeodesy.geodsolve 

426 

427# version: /opt/local/bin/GeodSolve: GeographicLib version 2.3 

428 

429# 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) 

430# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397) 

431 

432# 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) 

433# Inverse1: 49.94131021789904 

434# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777) 

435 

436# 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) 

437# 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) 

438 

439 

440# % python3 -m pygeodesy.geodsolve --verbose 

441 

442# GeodesicSolve 'Test' 1: /opt/local/bin/GeodSolve --version (invoke) 

443# GeodesicSolve 'Test' 1: /opt/local/bin/GeodSolve: GeographicLib version 2.2 (0) 

444# version: /opt/local/bin/GeodSolve: GeographicLib version 2.2 

445# GeodesicSolve 'Test' 2: /opt/local/bin/GeodSolve -f -E -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct) 

446# 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) 

447 

448# 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) 

449# GeodesicSolve 'Test' 3: /opt/local/bin/GeodSolve -f -E -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct3) 

450# 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) 

451# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397) 

452# GeodesicSolve 'Test' 4: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse) 

453# 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) 

454 

455# 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) 

456# GeodesicSolve 'Test' 5: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse1) 

457# 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) 

458# Inverse1: 49.94131021789904 

459# GeodesicSolve 'Test' 6: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse3) 

460# 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) 

461# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777) 

462 

463# 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) 

464# 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) 

465 

466 

467# % python3 -m pygeodesy.geodsolve --verbose 

468 

469# GeodesicSolve 'Test'@1: /opt/local/bin/GeodSolve --version (invoke) 

470# GeodesicSolve 'Test'@1: '/opt/local/bin/GeodSolve: GeographicLib version 2.3' (0, stdout/-err) 

471# GeodesicSolve 'Test'@1: /opt/local/bin/GeodSolve: GeographicLib version 2.3 (0) 

472# version: /opt/local/bin/GeodSolve: GeographicLib version 2.3 

473# GeodesicSolve 'Test'@2: /opt/local/bin/GeodSolve -f -E -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct) 

474# 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) 

475# 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) 

476 

477# 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) 

478# GeodesicSolve 'Test'@3: /opt/local/bin/GeodSolve -f -E -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct3) 

479# 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) 

480# 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) 

481# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397) 

482# GeodesicSolve 'Test'@4: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse) 

483# 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) 

484# 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) 

485 

486# 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) 

487# GeodesicSolve 'Test'@5: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse1) 

488# 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) 

489# 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) 

490# Inverse1: 49.94131021789904 

491# GeodesicSolve 'Test'@6: /opt/local/bin/GeodSolve -f -E -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse3) 

492# 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) 

493# 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) 

494# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777) 

495 

496# 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) 

497# 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) 

498 

499# **) MIT License 

500# 

501# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

502# 

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

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

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

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

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

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

509# 

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

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

512# 

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

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

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

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

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

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

519# OTHER DEALINGS IN THE SOFTWARE.