Coverage for pygeodesy/rhumb/solve.py: 92%

96 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-03-25 15:01 -0400

1 

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

3 

4u'''Wrapper to invoke I{Karney}'s U{RhumbSolve 

5<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>} utility 

6as an (exact) rhumb or rhumb line from I{either GeographicLib 2.0 or 2.2+}. 

7 

8@note: Set env variable C{PYGEODESY_RHUMBSOLVE} to the (fully qualified) 

9 path of the C{RhumbSolve} executable. 

10''' 

11from pygeodesy.basics import _xinstanceof 

12from pygeodesy.constants import _180_0, _N_180_0, _over, _0_0 # PYCHOK used! 

13from pygeodesy.errors import RhumbError # PYCHOK used! 

14from pygeodesy.interns import NN, _a12_, _azi12_, _DMAIN_, _lat2_, \ 

15 _lon2_, _s12_, _S12_, _UNDER_ 

16from pygeodesy.karney import Caps, GDict, _norm180, Rhumb8Tuple, \ 

17 _sincos2d, _Xables 

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 

21from pygeodesy.solveBase import _SolveGDictBase, _SolveGDictLineBase 

22from pygeodesy.utily import _unrollon, _Wrap, wrap360 

23 

24__all__ = _ALL_LAZY.rhumb_solve 

25__version__ = '25.12.06' 

26 

27 

28class _RhumbSolveBase(_SolveGDictBase): 

29 '''(INTERNAL) Base class for L{RhumbSolve} and L{RhumbLineSolve}. 

30 ''' 

31 _Error = RhumbError 

32 _Names_Direct = \ 

33 _Names_Distance = _lat2_, _lon2_, _S12_ 

34 _Names_Inverse = _azi12_, _s12_, _S12_ 

35 _Xable_name = _Xables.RhumbSolve.__name__ # typename 

36 _Xable_path = _Xables.RhumbSolve() 

37 

38 @Property_RO 

39 def _cmdBasic(self): 

40 '''(INTERNAL) Get the basic C{RhumbSolve} cmd (C{tuple}). 

41 ''' 

42 return (self.RhumbSolve,) + (self._e_option + 

43 self._p_option + 

44 self._s_option) 

45 

46 @Property 

47 def RhumbSolve(self): 

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

49 executable (C{filename}). 

50 ''' 

51 return self._Xable_path 

52 

53 @RhumbSolve.setter # PYCHOK setter! 

54 def RhumbSolve(self, path): 

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

56 executable (C{filename}), the (fully qualified) path to the C{RhumbSolve} executable. 

57 

58 @raise RhumbError: Invalid B{C{path}}, B{C{path}} doesn't exist or isn't 

59 the C{RhumbSolve} executable. 

60 ''' 

61 self._setXable(path) 

62 

63 @Property_RO 

64 def _s_option(self): # == not -E for GeodSolve 

65 return () if self.Exact else ('-s',) 

66 

67# @Property_RO 

68# def _u_option(self): 

69# return '-u' if self.unroll else () 

70 

71 

72class RhumbSolve(_RhumbSolveBase): 

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

74 like a class, similar to L{pygeodesy.Rhumb} and L{pygeodesy.RhumbAux}. 

75 

76 @note: Use property C{RhumbSolve} or env variable C{PYGEODESY_RHUMBSOLVE} to specify the (fully 

77 qualified) path to the C{RhumbSolve} executable. 

78 

79 @note: This C{rhumb} is intended I{for testing purposes only}, it invokes the C{RhumbSolve} 

80 executable for I{every} method call. 

81 ''' 

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

83# '''Set up a L{RhumbArea} to compute area and 

84# perimeter of a polygon. 

85# 

86# @kwarg polyline: If C{True}, compute the perimeter only, 

87# otherwise perimeter and area (C{bool}). 

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

89# 

90# @return: A L{RhumbArea} instance. 

91# 

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

93# to the returned L{RhumbAreaExact} instance. 

94# ''' 

95# rA = _MODS.rhumbs.rhumb*.RhumbArea(self, polyline=polyline, 

96# name=self._name__(name)) 

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

98# rA.verbose = True 

99# return rA 

100 

101# Polygon = Area # for C{geographiclib} compatibility 

102 

103 def _azimuth_reverse(self, azimuth): 

104 '''(INTERNAL) Reverse final azimuth C{azimuth}. 

105 ''' 

106 z = _norm180(float(azimuth)) 

107 if self.reverse2: # like .utils.atan2d 

108 z += _180_0 if z < 0 else _N_180_0 

109 return z 

110 

111 def _Direct(self, ll1, azi12, s12, **outmask): 

112 '''(INTERNAL) Short-cut version, see .latlonBase. 

113 ''' 

114 return self.Direct(ll1.lat, ll1.lon, azi12, s12, **outmask) 

115 

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

117 '''Return the destination lat, lon and reverse azimuth 

118 (final bearing) in C{degrees}. 

119 

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

121 ''' 

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

123 z = self._azimuth_reverse(r.azi12) 

124 return Destination3Tuple(float(r.lat2), float(r.lon2), wrap360(z), 

125 iteration=r._iteration) 

126 

127 def _DirectLine(self, ll1, azi12, **name_caps): 

128 '''(INTERNAL) Short-cut version, see .latlonBase. 

129 ''' 

130 return self.DirectLine(ll1.lat, ll1.lon, azi12, **name_caps) 

131 

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

133 '''Set up a L{RhumbLineSolve} in terms of the I{direct} rhumb 

134 problem to compute several points on a single rhumb line. 

135 

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

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

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

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

140 values specifying the capabilities the L{RhumbLineSolve} 

141 instance should possess, always C{Caps.ALL}. 

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

143 

144 @return: A L{RhumbLineSolve} instance. 

145 

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

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

148 the limit C{ε → 0+}. 

149 

150 @see: C++ U{RhumbExact.Line 

151 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1RhumbExact.html>} 

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

153 ''' 

154 return RhumbLineSolve(self, lat1, lon1, azi1, caps=caps, name=self._name__(name)) 

155 

156 def _GDictDirect(self, lat, lon, azi1, arcmode, s12_a12, *unused, **floats): # PYCHOK signature 

157 '''(INTERNAL) Get C{_GenDirect}-like result as an 8-item C{GDict}. 

158 ''' 

159 d = _RhumbSolveBase._GDictDirect(self, lat, lon, azi1, arcmode, s12_a12, **floats) 

160 r = GDict(lat1=lat, lon1=lon, azi12=azi1, s12=s12_a12) # a12=_over(s12_a12, self._mpd) 

161 r.update(d) 

162 return r 

163 

164 def _GDictInverse(self, lat1, lon1, lat2, lon2, *unused, **floats): # PYCHOK signature 

165 '''(INTERNAL) Get C{_GenInverse}-like result as an 8-item C{GDict}, but I{without} C{_SALP_CALPs_}. 

166 ''' 

167 i = _RhumbSolveBase._GDictInverse(self, lat1, lon1, lat2, lon2, **floats) 

168 a = _over(float(i.s12), self._mpd) # for .Inverse1 

169 r = GDict(lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2, a12=a) 

170 r.update(i) 

171 return r 

172 

173 def _Inverse(self, ll1, ll2, wrap, **unused): 

174 '''(INTERNAL) Short-cut version, see .latlonBase. 

175 ''' 

176 if wrap: # PYCHOK no cover 

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

178 return self._GDictInverse(ll1.lat, ll1.lon, ll2.lat, ll2.lon) 

179 

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

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

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

183 

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

185 ''' 

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

187 z = self._azimuth_reverse(r.azi12) 

188 return Distance3Tuple(float(r.s12), wrap360(r.azi12), wrap360(z), 

189 iteration=r._iteration) 

190 

191 def _InverseLine(self, ll1, ll2, wrap, **name_caps): 

192 '''(INTERNAL) Short-cut version, see .latlonBase. 

193 ''' 

194 if wrap: # PYCHOK no cover 

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

196 return self.InverseLine(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **name_caps) 

197 

198 def InverseLine(self, lat1, lon1, lat2, lon2, caps=Caps.STANDARD, **name): 

199 '''Define a L{RhumbLineSolve} in terms of the I{inverse} 

200 rhumb problem. 

201 

202 @arg lat1: Latitude of the first point (C{degrees90}). 

203 @arg lon1: Longitude of the first point (C{degrees180}). 

204 @arg lat2: Latitude of the second point (C{degrees90}). 

205 @arg lon2: Longitude of the second point (C{degrees180}). 

206 @kwarg caps: Optional C{caps}, see L{RhumbLine} C{B{caps}}. 

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

208 

209 @return: A L{RhumbLineSolve} instance and invoke its method 

210 L{RhumbLine.Position} to compute each point. 

211 

212 @note: Updates to this rhumb are reflected in the returned 

213 rhumb line. 

214 ''' 

215 r = self.Inverse(lat1, lon1, lat2, lon2) # outmask=Caps.AZIMUTH 

216 return RhumbLineSolve(self, lat1, lon1, r.azi12, caps=caps, 

217 name=self._name__(name)) 

218 

219 Line = DirectLine 

220 

221 def toStr(self, **prec_sep_other): # PYCHOK signature 

222 '''Return this C{RhumbSolve} as string. 

223 

224 @kwarg prec_sep: See L{toStr<pygeodesy.solveBase._SolveBase.toStr>}. 

225 

226 @return: RhumbSolve items (C{str}). 

227 ''' 

228 return _RhumbSolveBase.toStr(self, RhumbSolve=self.RhumbSolve, **prec_sep_other) 

229 

230 

231class RhumbLineSolve(_RhumbSolveBase, _SolveGDictLineBase): 

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

233 like a class, similar to L{pygeodesy.RhumbLine} and L{pygeodesy.RhumbLineAux}. 

234 

235 @note: Use property C{RhumbSolve} or env variable C{PYGEODESY_RHUMBSOLVE} to specify the (fully 

236 qualified) path to the C{RhumbSolve} executable. 

237 

238 @note: This C{rhumb line} is intended I{for testing purposes only}, it invokes the C{RhumbSolve} 

239 executable for I{every} method call. 

240 ''' 

241 def __init__(self, rhumb, lat1, lon1, azi12, caps=Caps.STANDARD, **name): 

242 '''New L{RhumbLineSolve} instance, allowing points to be found along 

243 a rhumb starting at C{(B{lat1}, B{lon1})} with azimuth B{C{azi12}}. 

244 

245 @arg rhumb: The rhumb to use (L{RhumbSolve}). 

246 @arg lat1: Latitude of the first point (C{degrees90}). 

247 @arg lon1: Longitude of the first point (C{degrees180}). 

248 @arg azi12: Azimuth of the rhumb line (compass C{degrees180}). 

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

250 values specifying the capabilities the L{RhumbLineSolve} 

251 instance should possess, always C{Caps.ALL}. Include 

252 C{Caps.LINE_OFF} if updates to the B{C{rhumb}} should 

253 I{not be reflected} in this L{RhumbLineSolve} instance. 

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

255 

256 @kwarg name: Optional name (C{str}). 

257 

258 @raise RhumbError: Invalid path for C{RhumbSolve} executable or isn't the 

259 C{RhumbSolve} executable, see property C{B{rhumb}.RhumbSolve}. 

260 

261 @raise TypeError: Invalid B{C{rhumb}}. 

262 ''' 

263 _xinstanceof(RhumbSolve, rhumb=rhumb) 

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

265 rhumb = rhumb.copy(deep=False, name=NN(_UNDER_, rhumb.name)) 

266 _SolveGDictLineBase.__init__(self, rhumb, lat1, lon1, caps, azi12=azi12, **name) 

267 try: 

268 self.RhumbSolve = rhumb.RhumbSolve # rhumb or copy of rhumb 

269 except RhumbError: 

270 pass 

271 

272# def ArcPosition(self, a12, *unused): 

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

274# 

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

276# second point (C{degrees}). 

277# 

278# @return: A C{dict} with 8 items C{lat1, lon1, lat2, lon2, 

279# azi12, a12, s12, S12}. 

280# ''' 

281# s = a12 * self._mpd 

282# a = self._GDictInvoke(self._cmdArc, self._Names_Distance, s) 

283# r = GDict(a12=a12, s12=s, **self._lla1) 

284# r.updated(a) 

285# return r 

286 

287 @Property_RO 

288 def azi12(self): 

289 '''Get this rhumb line's azimuth (compass C{degrees}). 

290 ''' 

291 return self._lla1.azi12 

292 

293 azi1 = azi12 # like GeodesicLineSolve 

294 

295 @Property_RO 

296 def azi12_sincos2(self): # PYCHOK no cover 

297 '''Get the sine and cosine of this rhumb line's azimuth (2-tuple C{(sin, cos)}). 

298 ''' 

299 return _sincos2d(self.azi12) 

300 

301 azi1_sincos2 = azi12_sincos2 

302 

303# @Property_RO 

304# def _cmdArc(self): 

305# '''(INTERNAL) Get the C{RhumbSolve} I{-a -L} cmd (C{tuple}). 

306# ''' 

307# return self._cmdDistance + ('-a',) 

308 

309 def Position(self, s12, **unused): 

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

311 

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

313 

314 @return: A L{GDict} with 7 items C{lat1, lon1, lat2, lon2, 

315 azi12, s12, S12}. 

316 ''' 

317 d = self._GDictInvoke(self._cmdDistance, self._Names_Distance, s12) 

318 r = GDict(s12=s12, **self._lla1) # a12=_over(s12, self._mpd) 

319 r.update(d) 

320 return r 

321 

322 def toStr(self, **prec_sep_other): # PYCHOK signature 

323 '''Return this C{RhumbLineSolve} as string. 

324 

325 @kwarg prec_sep: See L{toStr<pygeodesy.solveBase._SolveBase.toStr>}. 

326 

327 @return: RhumbLineSolve items (C{str}). 

328 ''' 

329 return _SolveGDictLineBase.toStr(self, rhumb=self._solve, **prec_sep_other) 

330 

331 

332class RhumbSolve7Tuple(Rhumb8Tuple): 

333 '''7-Tuple C{(lat1, lon1, lat2, lon2, azi12, s12, S12)} with lat- C{lat1}, 

334 C{lat2} and longitudes C{lon1}, C{lon2} of both points, the azimuth of 

335 the rhumb line C{azi12}, the distance C{s12} and the area C{S12} under 

336 the rhumb line between both points. 

337 ''' 

338 assert Rhumb8Tuple._Names_.index(_a12_) == 7 

339 _Names_ = Rhumb8Tuple._Names_[:7] # drop a12 

340 _Units_ = Rhumb8Tuple._Units_[:7] 

341 

342 

343__all__ += _ALL_DOCS(_RhumbSolveBase) 

344 

345if __name__ == _DMAIN_: 

346 

347 from pygeodesy import printf 

348 from sys import argv 

349 

350 def rhumb_intercept(rS, lat1, lon1, lat2, lon2, azi2, s23): 

351 # using RhumbSolve and GeodesicExact for I{Karney}'s C++ U{rhumb-intercept.cpp 

352 # <https://SourceForge.net/p/geographiclib/discussion/1026620/thread/2ddc295e/> 

353 from pygeodesy.constants import EPS4 as _TOL 

354 from pygeodesy.karney import _diff182 

355 

356 E = rS.ellipsoid 

357 gX = E.geodesicx # == GeodesicExact(E) 

358 m = gX.STANDARD | gX.REDUCEDLENGTH | gX.GEODESICSCALE 

359 

360 rlS = rS.Line(lat2, lon2, azi2) 

361 sa, _ = rlS.azi12_sincos2 # aka _salp, _calp 

362 for i in range(1, 16): 

363 p = rlS.Position(s23) # outmask=gX.LATITUDE_LONGITUDE 

364 r = gX.Inverse(lat1, lon1, p.lat2, p.lon2, outmask=m) 

365 d, _ = _diff182(azi2, r.azi2, K_2_0=True) 

366 s, c = _sincos2d(d) 

367 printf('%2d %.3f %.8f, %.8f, %.8e', 

368 i, s23, r.lat2, r.lon2, c) 

369 s2, c2 = _sincos2d(r.lat2) 

370 c2 *= E.rocTransverse(r.lat2) 

371 if c2 and r.m12: 

372 s *= (s2 * sa) / c2 - s * r.M21 / r.m12 

373 t = (c / s) if s else _0_0 

374 if abs(t) < _TOL: 

375 break 

376 s23 += t 

377 else: 

378 break 

379 

380 rS = RhumbSolve(name='Test') 

381 rS.verbose = v = '--verbose' in argv # or '-v' in argv 

382 

383 if not _Xables.X_OK(rS.RhumbSolve): # not set 

384 rS.RhumbSolve = _Xables.RhumbSolve(_Xables.bin_) 

385 printf('version: %s', rS.version) 

386 

387 if len(argv) > 6: # 60 0 30 0 45 1e6 

388 t = (14, 's23'), (7, 'lat3'), (11, 'lon3'), (13, 'cos()') 

389 printf(' '.join('%*s' % _ for _ in t)) 

390 rhumb_intercept(rS, *map(float, argv[-6:])) 

391 exit() 

392 

393 r = rS.Direct(40.6, -73.8, 51, 5.5e6) 

394 printf('Direct: %r', r) 

395 printf('Direct3: %r', rS.Direct3(40.6, -73.8, 51, 5.5e6), nt=v) 

396 

397 printf('Inverse: %r', rS.Inverse( 40.6, -73.8, 51.6, -0.5)) 

398 printf('Inverse1: %r', rS.Inverse1(40.6, -73.8, 51.6, -0.5)) 

399 printf('Inverse3: %r', rS.Inverse3(40.6, -73.8, 51.6, -0.5), nt=v) 

400 

401 printf('Inverse: %r', rS.Inverse( 40.6, -73.8, 35.8, 140.3)) 

402 printf('Inverse1: %r', rS.Inverse1(40.6, -73.8, 35.8, 140.3)) 

403 printf('Inverse3: %r', rS.Inverse3(40.6, -73.8, 35.8, 140.3), nt=v) 

404 

405 rlS = RhumbLineSolve(rS, 40.6, -73.8, 51, name='LineTest') 

406 printf('Line: %r', rlS) 

407 p = rlS.Position(5.5e6) 

408 printf('Position: %r %s', p, p == r) 

409# p = rlS.ArcPosition(49.475527) 

410# printf('ArcPosition: %r %s', p, p == r) 

411 

412 

413# % python3 -m pygeodesy.rhumb.solve 

414 

415# version: /opt/local/bin/RhumbSolve: GeographicLib version 2.7 

416# Direct: GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.132812) 

417# Direct3: Destination3Tuple(lat=71.6889, lon=0.25552, final=51.0) 

418# Inverse: GDict(a12=51.929543, azi12=77.76839, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, s12=5771083.383328, S12=37395209100030.382812) 

419# Inverse1: 51.929542507561905 

420# Inverse3: Distance3Tuple(distance=5771083.383328, initial=77.76839, final=77.76839) 

421# Inverse: GDict(a12=115.02062, azi12=-92.388888, lat1=40.6, lat2=35.8, lon1=-73.8, lon2=140.3, s12=12782581.067684, S12=-63760642939072.5) 

422# Inverse1: 115.02061966879248 

423# Inverse3: Distance3Tuple(distance=12782581.067684, initial=267.611112, final=267.611112) 

424# Line: RhumbLineSolve(invokation=1, rhumb=RhumbSolve(ellipsoid=Ellipsoid(name='WGS84', a=6378137, f=0.00335281, f_=298.25722356, b=6356752.31424518), invokation=9, RhumbSolve='/opt/local/bin/RhumbSolve', status=0), status=0) 

425# Position: GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.132812) True 

426 

427 

428# % python3 -m pygeodesy.rhumb.solve --verbose 

429 

430# RhumbSolve 'Test'@1: /opt/local/bin/RhumbSolve --version (invoke) 

431# RhumbSolve 'Test'@1: '/opt/local/bin/RhumbSolve: GeographicLib version 2.7' (0, stdout/-err) 

432# RhumbSolve 'Test'@1: /opt/local/bin/RhumbSolve: GeographicLib version 2.7 (0) 

433# version: /opt/local/bin/RhumbSolve: GeographicLib version 2.7 

434# RhumbSolve 'Test'@2: /opt/local/bin/RhumbSolve -p 10 \ 40.6 -73.8 51.0 5500000.0 (Direct) 

435# RhumbSolve 'Test'@2: '71.688899882813033 0.255519824423445 44095641862956.133' (0, stdout/-err) 

436# RhumbSolve 'Test'@2: lat2=71.688899882813033, lon2=0.255519824423445, S12=44095641862956.133 (0) 

437# Direct: GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.132812) 

438# RhumbSolve 'Test'@3: /opt/local/bin/RhumbSolve -p 10 \ 40.6 -73.8 51.0 5500000.0 (Direct3) 

439# RhumbSolve 'Test'@3: '71.688899882813033 0.255519824423445 44095641862956.133' (0, stdout/-err) 

440# RhumbSolve 'Test'@3: lat2=71.688899882813033, lon2=0.255519824423445, S12=44095641862956.133 (0) 

441# Direct3: Destination3Tuple(lat=71.6889, lon=0.25552, final=51.0) 

442# 

443# RhumbSolve 'Test'@4: /opt/local/bin/RhumbSolve -p 10 -i \ 40.6 -73.8 51.6 -0.5 (Inverse) 

444# RhumbSolve 'Test'@4: '77.768389710255676 5771083.3833280280 37395209100030.383' (0, stdout/-err) 

445# RhumbSolve 'Test'@4: azi12=77.768389710255676, s12=5771083.383328028, S12=37395209100030.383 (0) 

446# Inverse: GDict(a12=51.929543, azi12=77.76839, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, s12=5771083.383328, S12=37395209100030.382812) 

447# RhumbSolve 'Test'@5: /opt/local/bin/RhumbSolve -p 10 -i \ 40.6 -73.8 51.6 -0.5 (Inverse1) 

448# RhumbSolve 'Test'@5: '77.768389710255676 5771083.3833280280 37395209100030.383' (0, stdout/-err) 

449# RhumbSolve 'Test'@5: azi12=77.768389710255676, s12=5771083.383328028, S12=37395209100030.383 (0) 

450# Inverse1: 51.929542507561905 

451# RhumbSolve 'Test'@6: /opt/local/bin/RhumbSolve -p 10 -i \ 40.6 -73.8 51.6 -0.5 (Inverse3) 

452# RhumbSolve 'Test'@6: '77.768389710255676 5771083.3833280280 37395209100030.383' (0, stdout/-err) 

453# RhumbSolve 'Test'@6: azi12=77.768389710255676, s12=5771083.383328028, S12=37395209100030.383 (0) 

454# Inverse3: Distance3Tuple(distance=5771083.383328, initial=77.76839, final=77.76839) 

455# 

456# RhumbSolve 'Test'@7: /opt/local/bin/RhumbSolve -p 10 -i \ 40.6 -73.8 35.8 140.30000000000001 (Inverse) 

457# RhumbSolve 'Test'@7: '-92.388887981699654 12782581.0676841699 -63760642939072.500' (0, stdout/-err) 

458# RhumbSolve 'Test'@7: azi12=-92.388887981699654, s12=12782581.0676841699, S12=-63760642939072.5 (0) 

459# Inverse: GDict(a12=115.02062, azi12=-92.388888, lat1=40.6, lat2=35.8, lon1=-73.8, lon2=140.3, s12=12782581.067684, S12=-63760642939072.5) 

460# RhumbSolve 'Test'@8: /opt/local/bin/RhumbSolve -p 10 -i \ 40.6 -73.8 35.8 140.30000000000001 (Inverse1) 

461# RhumbSolve 'Test'@8: '-92.388887981699654 12782581.0676841699 -63760642939072.500' (0, stdout/-err) 

462# RhumbSolve 'Test'@8: azi12=-92.388887981699654, s12=12782581.0676841699, S12=-63760642939072.5 (0) 

463# Inverse1: 115.02061966879248 

464# RhumbSolve 'Test'@9: /opt/local/bin/RhumbSolve -p 10 -i \ 40.6 -73.8 35.8 140.30000000000001 (Inverse3) 

465# RhumbSolve 'Test'@9: '-92.388887981699654 12782581.0676841699 -63760642939072.500' (0, stdout/-err) 

466# RhumbSolve 'Test'@9: azi12=-92.388887981699654, s12=12782581.0676841699, S12=-63760642939072.5 (0) 

467# Inverse3: Distance3Tuple(distance=12782581.067684, initial=267.611112, final=267.611112) 

468# 

469# Line: RhumbLineSolve(invokation=1, rhumb=RhumbSolve(ellipsoid=Ellipsoid(name='WGS84', a=6378137, f=0.00335281, f_=298.25722356, b=6356752.31424518), invokation=9, RhumbSolve='/opt/local/bin/RhumbSolve', status=0), status=0) 

470# Position: GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.132812) True 

471 

472 

473# % python3 -m pygeodesy.rhumb.solve 60 0 30 0 45 1e6 

474 

475# version: /opt/local/bin/RhumbSolve: GeographicLib version 2.2 

476# s23 lat3 lon3 cos() 

477# 1 1000000.000 36.37559999, 7.58982303, -5.83098638e-01 

478# 2 4532573.097 58.84251798, 41.57078946, 4.05349594e-01 

479# 3 2233216.895 44.22871762, 17.86660260, -2.91432608e-01 

480# 4 3168401.173 50.17678842, 26.60741388, 3.00555188e-02 

481# 5 3082690.347 49.63189746, 25.76374255, -1.49446251e-04 

482# 6 3083112.629 49.63458216, 25.76787599, -2.59865190e-09 

483# 7 3083112.636 49.63458221, 25.76787606, 4.96052409e-16 

484# 8 3083112.636 49.63458221, 25.76787606, -4.96052409e-16 

485# 9 3083112.636 49.63458221, 25.76787606, 4.96052409e-16 

486# 10 3083112.636 49.63458221, 25.76787606, -4.96052409e-16 

487# 11 3083112.636 49.63458221, 25.76787606, 4.96052409e-16 

488# 12 3083112.636 49.63458221, 25.76787606, -4.96052409e-16 

489# 13 3083112.636 49.63458221, 25.76787606, 4.96052409e-16 

490# 14 3083112.636 49.63458221, 25.76787606, -4.96052409e-16 

491# 15 3083112.636 49.63458221, 25.76787606, 4.96052409e-16 

492 

493 

494# % python3 -m pygeodesy.rhumb.solve 60 0 30 0 45 1e6 

495 

496# version: /opt/local/bin/RhumbSolve: GeographicLib version 2.7 

497# s23 lat3 lon3 cos() 

498# 1 1000000.000 36.37559999, 7.58982303, -5.83098638e-01 

499# 2 4532573.097 58.84251798, 41.57078946, 4.05349594e-01 

500# 3 2233216.895 44.22871762, 17.86660260, -2.91432608e-01 

501# 4 3168401.173 50.17678842, 26.60741388, 3.00555188e-02 

502# 5 3082690.347 49.63189746, 25.76374255, -1.49446251e-04 

503# 6 3083112.629 49.63458216, 25.76787599, -2.59865140e-09 

504# 7 3083112.636 49.63458221, 25.76787606, -4.96052409e-16 

505# 8 3083112.636 49.63458221, 25.76787606, 0.00000000e+00 

506 

507# **) MIT License 

508# 

509# Copyright (C) 2022-2026 -- mrJean1 at Gmail -- All Rights Reserved. 

510# 

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

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

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

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

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

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

517# 

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

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

520# 

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

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

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

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

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

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

527# OTHER DEALINGS IN THE SOFTWARE.