Coverage for pygeodesy/heights.py: 95%

321 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-26 09:04 -0400

1 

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

3 

4u'''Height interpolations from C{knots}, points with known height. 

5 

6Classes L{HeightCubic}, L{HeightIDWcosineLaw}, L{HeightIDWdistanceTo}, 

7L{HeightIDWequirectangular}, L{HeightIDWeuclidean}, L{HeightIDWflatLocal}, 

8L{HeightIDWflatPolar}, L{HeightIDWhaversine}, L{HeightIDWhubeny}, 

9L{HeightIDWkarney}, L{HeightIDWthomas}, L{HeightIDWvincentys}, L{HeightLinear}, 

10L{HeightLSQBiSpline} and L{HeightSmoothBiSpline} to interpolate the height of 

11C{LatLon} locations or separate lat-/longitudes from a set of C{LatLon} points 

12with I{known heights}. 

13 

14Typical usage 

15============= 

16 

171. Get or create a set of C{LatLon} points with I{known heights}, called 

18C{knots}. The C{knots} do not need to be ordered in any particular way. 

19 

20C{>>> ...} 

21 

222. Select one of the C{Height} classes for height interpolation 

23 

24C{>>> from pygeodesy import HeightCubic as HeightXyz # or an other Height... class} 

25 

263. Instantiate a height interpolator with the C{knots} and use keyword 

27arguments to select different interpolation options 

28 

29C{>>> hinterpolator = HeightXyz(knots, **options)} 

30 

314. Get the interpolated height of C{LatLon} location(s) with 

32 

33C{>>> ll = LatLon(1, 2, ...)} 

34 

35C{>>> h = hinterpolator(ll)} 

36 

37or 

38 

39C{>>> h0, h1, h2, ... = hinterpolator(ll0, ll1, ll2, ...)} 

40 

41or a list, tuple, generator, etc. of C{LatLon}s 

42 

43C{>>> hs = hinterpolator(lls)} 

44 

455. For separate lat- and longitudes invoke the C{height} method as 

46 

47C{>>> h = hinterpolator.height(lat, lon)} 

48 

49or as 2 lists, 2 tuples, etc. 

50 

51C{>>> hs = hinterpolator.height(lats, lons)} 

52 

53or for several positionals use the C{height_} method 

54 

55C{>>> h1, h2, ... = hinterpolator.height_(lat1, lon1, lat2, lon2, ...)} 

56 

57@note: Classes L{HeightCubic} and L{HeightLinear} require package U{numpy 

58 <https://PyPI.org/project/numpy>}, classes L{HeightLSQBiSpline} and 

59 L{HeightSmoothBiSpline} require package U{scipy<https://SciPy.org>}. 

60 Classes L{HeightIDWkarney} and L{HeightIDWdistanceTo} -if used with 

61 L{ellipsoidalKarney.LatLon} points- require I{Karney}'s U{geographiclib 

62 <https://PyPI.org/project/geographiclib>} package to be installed. 

63 

64@note: Errors from C{scipy} are raised as L{SciPyError}s. Warnings issued 

65 by C{scipy} can be thrown as L{SciPyWarning} exceptions, provided 

66 Python C{warnings} are filtered accordingly, see L{SciPyWarning}. 

67 

68@see: U{SciPy<https://docs.SciPy.org/doc/scipy/reference/interpolate.html>} 

69 Interpolation. 

70''' 

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

72from __future__ import division as _; del _ # noqa: E702 ; 

73 

74from pygeodesy.basics import isscalar, len2, map1, min2, _xnumpy, _xscipy 

75from pygeodesy.constants import EPS, PI, PI_2, PI2, _0_0, _90_0, _180_0 

76from pygeodesy.datums import _ellipsoidal_datum, _WGS84 

77from pygeodesy.errors import _AssertionError, LenError, PointsError, \ 

78 _SciPyIssue, _xattr, _xkwds, _xkwds_get, \ 

79 _xkwds_item2, _xkwds_pop2 

80# from pygeodesy.fmath import fidw # _MODS 

81# from pygeodesy import formy as _formy # _MODS.into 

82# from pygeodesy.internals import _version2 # _MODS 

83from pygeodesy.interns import NN, _COMMASPACE_, _insufficient_, _NOTEQUAL_, \ 

84 _PLUS_, _scipy_, _SPACE_, _STAR_ 

85from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS, _FOR_DOCS 

86from pygeodesy.named import _name2__, _Named 

87from pygeodesy.points import _distanceTo, LatLon_, Fmt, radians, _Wrap 

88from pygeodesy.props import Property_RO, property_RO, property_ROver 

89# from pygeodesy.streprs import Fmt # from .points 

90from pygeodesy.units import _isDegrees, Float_, Int_ 

91# from pygeodesy.utily import _Wrap # from .points 

92 

93# from math import radians # from .points 

94 

95__all__ = _ALL_LAZY.heights 

96__version__ = '26.08.26' 

97 

98_error_ = 'error' 

99_formy = _MODS.into(formy=__name__) # PYLINT E1120 

100_linear_ = 'linear' 

101_llis_ = 'llis' 

102 

103 

104class HeightError(PointsError): 

105 '''Height interpolator C{Height...} or interpolation issue. 

106 ''' 

107 pass # PYLINT W0107 

108 

109 

110def _alist(ais): 

111 # return list of floats, not numpy.float64s 

112 return list(map(float, ais)) 

113 

114 

115def _ascalar(ais): # in .geoids 

116 # return single float, not numpy.float64 

117 ais = list(ais) # np.array, etc. to list 

118 if len(ais) != 1: 

119 n = Fmt.PAREN(len=repr(ais)) 

120 t = _SPACE_(len(ais), _NOTEQUAL_, 1) 

121 raise _AssertionError(n, txt=t) 

122 return float(ais[0]) # remove np.<type> 

123 

124 

125def _atuple(ais): 

126 # return tuple of floats, not numpy.float64s 

127 return tuple(map(float, ais)) 

128 

129 

130def _as_llis2(llis, m=1, Error=HeightError): # in .geoids 

131 # determine return type and convert lli C{LatLon}s to list 

132 if not isinstance(llis, tuple): # llis are *args 

133 n = Fmt.PAREN(type_=_STAR_(NN, _llis_)) 

134 raise _AssertionError(n, txt=repr(llis)) 

135 

136 n = len(llis) 

137 if n == 1: # convert single lli to 1-item list 

138 llis = llis[0] 

139 try: 

140 n, llis = len2(llis) 

141 _as = _alist # return list of interpolated heights 

142 except TypeError: # single lli 

143 n, llis = 1, [llis] 

144 _as = _ascalar # return single interpolated heights 

145 else: # of 0, 2 or more llis 

146 _as = _atuple # return tuple of interpolated heights 

147 

148 if n < m: 

149 raise _InsufficientError(m, Error=Error, llis=n) 

150 return _as, llis 

151 

152 

153def _InsufficientError(need, Error=HeightError, **name_value): # PYCHOK no cover 

154 # create an insufficient Error instance 

155 t = _COMMASPACE_(_insufficient_, str(need) + _PLUS_) 

156 return Error(txt=t, **name_value) 

157 

158 

159def _orderedup(ts, lo=EPS, hi=PI2-EPS): 

160 # clip, order and remove duplicates 

161 return sorted(set(max(lo, min(hi, t)) for t in ts)) # list 

162 

163 

164def _xyhs(wrap=False, _lat=_90_0, _lon=_180_0, height=True, **name_lls): 

165 # map (lat, lon, h) to (x, y, h) in radians, offset 

166 # x as 0 <= lon <= PI2 and y as 0 <= lat <= PI 

167 name, lls = _xkwds_item2(name_lls) 

168 _w, _r = _Wrap._latlonop(wrap), radians # PYLINT E1120 

169 try: 

170 for i, ll in enumerate(lls): 

171 y, x = _w(ll.lat, ll.lon) 

172 h = ll.height if height else 0 

173 yield (max(_0_0, _r(x + _lon)), 

174 max(_0_0, _r(y + _lat)), h) 

175 except Exception as x: 

176 i = Fmt.INDEX(name, i) # PYLINT E0601 

177 raise HeightError(i, ll, cause=x) 

178 

179 

180class _HeightNamed(_Named): # in .geoids 

181 '''(INTERNAL) Interpolator base class. 

182 ''' 

183 _datum = _WGS84 # default 

184 _Error = HeightError 

185 _kmin = 2 # min number of knots 

186 

187 _LLiC = LatLon_ # ._height class 

188 _np_sp = None # (numpy, scipy) 

189 _wrap = None # wrap knots and llis 

190 

191 def __call__(self, *llis, **wrap): # PYCHOK no cover 

192 '''Interpolate the height for one or several locations. I{Must be overloaded}. 

193 

194 @arg llis: One or more locations (each C{LatLon}), all positional. 

195 @kwarg wrap: If C{B{wrap}=True} to wrap or I{normalize} all B{C{llis}} 

196 locations (C{bool}), overriding the B{C{knots}}' setting. 

197 

198 @return: A single interpolated height (C{float}) or a list or tuple of 

199 interpolated heights (each C{float}). 

200 

201 @raise HeightError: Insufficient number of B{C{llis}} or an invalid B{C{lli}}. 

202 

203 @raise SciPyError: A C{scipy} issue. 

204 

205 @raise SciPyWarning: A C{scipy} warning as exception. 

206 ''' 

207 self._notOverloaded(callername='__call__', *llis, **wrap) 

208 

209 def _as_lls(self, lats, lons): # in .geoids 

210 LLiC, d = self._LLiC, self.datum 

211 if _isDegrees(lats) and _isDegrees(lons): 

212 llis = LLiC(lats, lons, datum=d) 

213 else: 

214 n, lats = len2(lats) 

215 m, lons = len2(lons) 

216 if n != m: # format a LenError, but raise self._Error 

217 e = LenError(type(self), lats=n, lons=m, txt=None) 

218 raise self._Error(str(e)) 

219 llis = [LLiC(*t, datum=d) for t in zip(lats, lons)] 

220 return llis 

221 

222 @property_RO 

223 def datum(self): 

224 '''Get the C{datum} setting or the default (L{Datum}). 

225 ''' 

226 return self._datum 

227 

228 def height(self, lats, lons, **wrap): # PYCHOK no cover 

229 '''I{Must be overloaded}.''' 

230 self._notOverloaded(lats, lons, **wrap) 

231 

232 def height_(self, *latlons, **wrap): 

233 '''Interpolate the height for each M{(latlons[i], latlons[i+1]) pair 

234 for i in range(0, len(latlons), B{2})}. 

235 

236 @arg latlons: Alternating lat-/longitude pairs (each C{degrees}), 

237 all positional. 

238 

239 @see: Method C{height} for further details. 

240 

241 @return: A tuple of interpolated heights (each C{float}). 

242 ''' 

243 lls = self._as_lls(latlons[0::2], latlons[1::2]) 

244 return tuple(self(lls, **wrap)) 

245 

246 @property_RO 

247 def kmin(self): 

248 '''Get the minimum number of knots (C{int}). 

249 ''' 

250 return self._kmin 

251 

252 @property_RO 

253 def wrap(self): 

254 '''Get the C{wrap} setting (C{bool}) or C{None}. 

255 ''' 

256 return self._wrap 

257 

258 

259class _HeightBase(_HeightNamed): # in .geoids 

260 '''(INTERNAL) Interpolator base class. 

261 ''' 

262 _k2interp2d = {-1: _linear_, # in .geoids._GeoidBase.__init__ 

263 -2: _linear_, # for backward compatibility 

264 -3: 'cubic', 

265 -5: 'quintic'} 

266 

267 def _as_xyllis4(self, llis, **wrap): 

268 # convert lli C{LatLon}s to tuples or C{NumPy} arrays of 

269 # C{SciPy} sphericals and determine the return type 

270 atype = self.numpy.array 

271 kwds = _xkwds(wrap, wrap=self._wrap, height=False) 

272 _as, llis = _as_llis2(llis) 

273 xis, yis, _ = zip(*_xyhs(llis=llis, **kwds)) # PYCHOK yield 

274 return _as, atype(xis), atype(yis), llis 

275 

276 def _ev(self, *args): # PYCHOK no cover 

277 '''(INTERNAL) I{Must be overloaded}.''' 

278 self._notOverloaded(*args) 

279 

280 def _evalls(self, llis, **wrap): # XXX single arg, not *args 

281 _as, xis, yis, _ = self._as_xyllis4(llis, **wrap) 

282 try: # SciPy .ev signature: y first, then x! 

283 return _as(self._ev(yis, xis)) 

284 except Exception as x: 

285 raise _SciPyIssue(x, self._ev_name) 

286 

287 def _ev2d(self, x, y): # PYCHOK no cover 

288 '''(INTERNAL) I{Must be overloaded}.''' 

289 self._notOverloaded(x, y) 

290 

291 @property_RO 

292 def _ev_name(self): 

293 '''(INTERNAL) Get the name of the C{.ev} method. 

294 ''' 

295 _ev = str(self._ev) 

296 if _scipy_ not in _ev: 

297 _ev = str(self._ev2d) 

298 # '<scipy.interpolate._interpolate.interp2d object at ...> 

299 # '<function _HeightBase._interp2d.<locals>._bisplev at ...> 

300 # '<bound method BivariateSpline.ev of ... object at ...> 

301 _ev = _ev[1:].split(None, 4) 

302 return Fmt.PAREN(_ev['sfb'.index(_ev[0][0])]) 

303 

304 def height(self, lats, lons, **wrap): 

305 '''Interpolate the height for one or several lat-/longitudes. 

306 

307 @arg lats: Latitude or latitudes (each C{degrees}). 

308 @arg lons: Longitude or longitudes (each C{degrees}). 

309 @kwarg wrap: Kewyord argument C{B{wrap}=False} (C{bool}). Use C{True} to 

310 wrap or I{normalize} all B{C{lats}} and B{C{lons}} locationts, 

311 overriding the B{C{knots}}' setting. 

312 

313 @return: A single interpolated height (C{float}) or a list of interpolated 

314 heights (each C{float}). 

315 

316 @raise HeightError: Insufficient or unequal number of B{C{lats}} and B{C{lons}}. 

317 

318 @raise SciPyError: A C{scipy} issue. 

319 

320 @raise SciPyWarning: A C{scipy} warning as exception. 

321 ''' 

322 lls = self._as_lls(lats, lons) # dup of _HeightIDW.height 

323 return self(lls, **wrap) # __call__(ll) or __call__(lls) 

324 

325 def _interp2d(self, xs, ys, hs, kind=-3): 

326 '''Create a C{scipy.interpolate.interp2d} or C{-.bisplrep/-ev} 

327 interpolator before, respectively since C{SciPy} version 1.14. 

328 ''' 

329 try: 

330 spi = self.scipy_interpolate 

331 if self._scipy_version() < (1, 14) and kind in self._k2interp2d: 

332 # SciPy.interpolate.interp2d kind 'linear', 'cubic' or 'quintic' 

333 # DEPRECATED since scipy 1.10, removed altogether in 1.14 

334 self._ev2d = spi.interp2d(xs, ys, hs, kind=self._k2interp2d[kind]) 

335 

336 else: # <https://scipy.GitHub.io/devdocs/tutorial/interpolate/interp_transition_guide.html> 

337 k = self._kxky(abs(kind)) 

338 # spi.RectBivariateSpline needs strictly ordered xs and ys 

339 r = spi.bisplrep(xs, ys, hs.T, kx=k, ky=k) 

340 

341 def _bisplev(x, y): 

342 return spi.bisplev(x, y, r) # .T 

343 

344 self._ev2d = _bisplev 

345 

346 except Exception as x: 

347 raise _SciPyIssue(x, self._ev_name) 

348 

349 def _kxky(self, kind): 

350 return Int_(kind=kind, low=1, high=5, Error=self._Error) 

351 

352 def _np_sp2(self, throwarnings=False): # PYCHOK no cover 

353 '''(INTERNAL) Import C{numpy} and C{scipy}, once. 

354 ''' 

355 # raise SciPyWarnings, but not if 

356 # scipy has already been imported 

357 if throwarnings: # PYCHOK no cover 

358 import sys 

359 if _scipy_ not in sys.modules: 

360 import warnings 

361 warnings.filterwarnings(_error_) 

362 return self.numpy, self.scipy 

363 

364 @property_ROver 

365 def numpy(self): 

366 '''Get the C{numpy} module or C{None}. 

367 ''' 

368 return _xnumpy(type(self), 1, 9) # overwrite property_ROver 

369 

370 @property_ROver 

371 def scipy(self): 

372 '''Get the C{scipy} module or C{None}. 

373 ''' 

374 return _xscipy(type(self), 1, 2) # overwrite property_ROver 

375 

376 @property_ROver 

377 def scipy_interpolate(self): 

378 '''Get the C{scipy.interpolate} module or C{None}. 

379 ''' 

380 _ = self.scipy 

381 import scipy.interpolate as spi # scipy 1.2.2 

382 return spi # overwrite property_ROver 

383 

384 def _scipy_version(self, **n): 

385 '''Get the C{scipy} version as 2- or 3-tuple C{(major, minor, micro)}. 

386 ''' 

387 return _MODS.internals._version2(self.scipy.version.version, **n) 

388 

389 def _xyhs3(self, knots, wrap=False, **name): 

390 # convert knot C{LatLon}s to tuples or C{NumPy} arrays and C{SciPy} sphericals 

391 xs, ys, hs = zip(*_xyhs(knots=knots, wrap=wrap)) # PYCHOK yield 

392 n = len(hs) 

393 if n < self.kmin: 

394 raise _InsufficientError(self.kmin, nots=n) 

395 if name: 

396 self.name = name 

397 return map1(self.numpy.array, xs, ys, hs) 

398 

399 

400class HeightCubic(_HeightBase): 

401 '''Height interpolator based on C{SciPy} U{interp2d<https://docs.SciPy.org/ 

402 doc/scipy/reference/generated/scipy.interpolate.interp2d.html>} 

403 C{kind='cubic'} or U{bisplrep/-ev<https://docs.SciPy.org/doc/scipy/ 

404 reference/generated/scipy.interpolate.interp2d.html>} C{kx=ky=3}. 

405 ''' 

406 _kind = -3 

407 _kmin = 16 

408 

409 def __init__(self, knots, **name_wrap): 

410 '''New L{HeightCubic} interpolator. 

411 

412 @arg knots: The points with known height (C{LatLon}s). 

413 @kwarg name_wrap: Optional C{B{name}=NN} for this height interpolator (C{str}) 

414 and keyword argument C{b{wrap}=False} to wrap or I{normalize} all 

415 B{C{knots}} and B{C{llis}} locations iff C{True} (C{bool}). 

416 

417 @raise HeightError: Insufficient number of B{C{knots}} or invalid B{C{knot}}. 

418 

419 @raise ImportError: Package C{numpy} or C{scipy} not found or not installed. 

420 

421 @raise SciPyError: A C{scipy} issue. 

422 

423 @raise SciPyWarning: A C{scipy} warning as exception. 

424 ''' 

425 xs_yx_hs = self._xyhs3(knots, **name_wrap) 

426 self._interp2d(*xs_yx_hs, kind=self._kind) 

427 

428 def __call__(self, *llis, **wrap): 

429 '''Interpolate the height for one or several locations. 

430 

431 @see: L{Here<_HeightBase.__call__>} for further details. 

432 ''' 

433 return self._evalls(llis, **wrap) 

434 

435 def _ev(self, yis, xis): # PYCHOK overwritten with .RectBivariateSpline.ev 

436 # to make SciPy .interp2d single (x, y) signature 

437 # match SciPy .ev signature(ys, xs), flipped multiples 

438 return map(self._ev2d, xis, yis) 

439 

440 

441class HeightLinear(HeightCubic): 

442 '''Height interpolator based on C{SciPy} U{interp2d<https://docs.SciPy.org/ 

443 doc/scipy/reference/generated/scipy.interpolate.interp2d.html>} 

444 C{kind='linear'} or U{bisplrep/-ev<https://docs.SciPy.org/doc/scipy/ 

445 reference/generated/scipy.interpolate.interp2d.html>} C{kx=ky=1}. 

446 ''' 

447 _kind = -1 

448 _kmin = 2 

449 

450 def __init__(self, knots, **name_wrap): 

451 '''New L{HeightLinear} interpolator. 

452 

453 @see: L{Here<HeightCubic.__init__>} for all details. 

454 ''' 

455 HeightCubic.__init__(self, knots, **name_wrap) 

456 

457 if _FOR_DOCS: 

458 __call__ = HeightCubic.__call__ 

459 height = HeightCubic.height 

460 

461 

462class HeightLSQBiSpline(_HeightBase): 

463 '''Height interpolator using C{SciPy} U{LSQSphereBivariateSpline 

464 <https://docs.SciPy.org/doc/scipy/reference/generated/scipy. 

465 interpolate.LSQSphereBivariateSpline.html>}. 

466 ''' 

467 _kmin = 16 # k = 3, always 

468 

469 def __init__(self, knots, weight=None, low=1e-4, **name_wrap): 

470 '''New L{HeightLSQBiSpline} interpolator. 

471 

472 @arg knots: The points with known height (C{LatLon}s). 

473 @kwarg weight: Optional weight or weights for each B{C{knot}} 

474 (C{scalar} or C{scalar}s). 

475 @kwarg low: Optional lower bound for I{ordered knots} (C{radians}). 

476 @kwarg name_wrap: Optional C{B{name}=NN} for this height interpolator 

477 (C{str}) and keyword argument C{b{wrap}=False} to wrap or 

478 I{normalize} all B{C{knots}} and B{C{llis}} locations iff 

479 C{True} (C{bool}). 

480 

481 @raise HeightError: Insufficient number of B{C{knots}} or an invalid 

482 B{C{knot}}, B{C{weight}} or B{C{eps}}. 

483 

484 @raise LenError: Unequal number of B{C{knots}} and B{C{weight}}s. 

485 

486 @raise ImportError: Package C{numpy} or C{scipy} not found or not 

487 installed. 

488 

489 @raise SciPyError: A C{scipy} issue. 

490 

491 @raise SciPyWarning: A C{scipy} warning as exception. 

492 ''' 

493 np = self.numpy 

494 spi = self.scipy_interpolate 

495 

496 xs, ys, hs = self._xyhs3(knots, **name_wrap) 

497 n = len(hs) 

498 

499 w = weight 

500 if isscalar(w): 

501 w = float(w) 

502 if w <= 0: 

503 raise HeightError(weight=w) 

504 w = (w,) * n 

505 elif w is not None: 

506 m, w = len2(w) 

507 if m != n: 

508 raise LenError(HeightLSQBiSpline, weight=m, nots=n) 

509 m, i = min2(*map(float, w)) 

510 if m <= 0: # PYCHOK no cover 

511 raise HeightError(Fmt.INDEX(weight=i), m) # PYLINT E1120 

512 try: 

513 if not EPS < low < (PI_2 - EPS): # 1e-4 like SciPy example 

514 raise HeightError(low=low) 

515 ps = np.array(_orderedup(xs, low, PI2 - low)) 

516 ts = np.array(_orderedup(ys, low, PI - low)) 

517 self._ev = spi.LSQSphereBivariateSpline(ys, xs, hs, 

518 ts, ps, eps=EPS, w=w).ev 

519 except Exception as x: 

520 raise _SciPyIssue(x, self._ev_name) 

521 

522 def __call__(self, *llis, **wrap): 

523 '''Interpolate the height for one or several locations. 

524 

525 @see: L{Here<_HeightBase.__call__>} for further details. 

526 ''' 

527 return self._evalls(llis, **wrap) 

528 

529 

530class HeightSmoothBiSpline(_HeightBase): 

531 '''Height interpolator using C{SciPy} U{SmoothSphereBivariateSpline 

532 <https://docs.SciPy.org/doc/scipy/reference/generated/scipy. 

533 interpolate.SmoothSphereBivariateSpline.html>}. 

534 ''' 

535 _kmin = 16 # k = 3, always 

536 

537 def __init__(self, knots, smooth=4, **name_wrap): 

538 '''New L{HeightSmoothBiSpline} interpolator. 

539 

540 @arg knots: The points with known height (C{LatLon}s). 

541 @kwarg smooth: Spline smoothing factor (C{scalar}), default C{4}. 

542 @kwarg name_wrap: Optional C{B{name}=NN} for this height interpolator 

543 (C{str}) and keyword argument C{b{wrap}=False} to wrap or 

544 I{normalize} all B{C{knots}} and B{C{llis}} locations iff 

545 C{True} (C{bool}). 

546 

547 @raise HeightError: Insufficient number of B{C{knots}} or an invalid 

548 B{C{knot}} or B{C{s}}. 

549 

550 @raise ImportError: Package C{numpy} or C{scipy} not found or not 

551 installed. 

552 

553 @raise SciPyError: A C{scipy} issue. 

554 

555 @raise SciPyWarning: A C{scipy} warning as exception. 

556 ''' 

557 spi = self.scipy_interpolate 

558 

559 s, name_wrap = _xkwds_pop2(name_wrap, s=smooth) 

560 s = Float_(smooth=s, Error=HeightError, low=0) 

561 

562 xs, ys, hs = self._xyhs3(knots, **name_wrap) 

563 try: 

564 self._ev = spi.SmoothSphereBivariateSpline(ys, xs, hs, 

565 eps=EPS, s=s).ev 

566 except Exception as x: 

567 raise _SciPyIssue(x, self._ev_name) 

568 

569 def __call__(self, *llis, **wrap): 

570 '''Interpolate the height for one or several locations. 

571 

572 @see: L{Here<_HeightBase.__call__>} for further details. 

573 ''' 

574 return self._evalls(llis, **wrap) 

575 

576 

577class _HeightIDW(_HeightNamed): 

578 '''(INTERNAL) Base class for U{Inverse Distance Weighting 

579 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) height 

580 interpolators. 

581 

582 @see: U{IDW<https://www.Geo.FU-Berlin.DE/en/v/soga/Geodata-analysis/ 

583 geostatistics/Inverse-Distance-Weighting/index.html>}, 

584 U{SHEPARD_INTERP_2D<https://People.SC.FSU.edu/~jburkardt/c_src/ 

585 shepard_interp_2d/shepard_interp_2d.html>} and other C{_HeightIDW*} 

586 classes. 

587 ''' 

588 _beta = 0 # fidw inverse power 

589 _func = None # formy function 

590 _knots = () # knots list or tuple 

591 _kwds = {} # func_ options 

592 

593 def __init__(self, knots, beta=2, **name__kwds): 

594 '''New C{_HeightIDW*} interpolator. 

595 

596 @arg knots: The points with known height (C{LatLon}s). 

597 @kwarg beta: Inverse distance power (C{int} 1, 2, or 3). 

598 @kwarg name__kwds: Optional C{B{name}=NN} for this height interpolator 

599 (C{str}) and any keyword arguments for the distance function, 

600 retrievable with property C{kwds}. 

601 

602 @raise HeightError: Insufficient number of B{C{knots}} or an invalid 

603 B{C{knot}} or B{C{beta}}. 

604 ''' 

605 name, kwds = _name2__(**name__kwds) 

606 if name: 

607 self.name = name 

608 

609 n, self._knots = len2(knots) 

610 if n < self.kmin: 

611 raise _InsufficientError(self.kmin, nots=n) 

612 self.beta = beta 

613 self._kwds = kwds or {} 

614 

615 def __call__(self, *llis, **wrap): 

616 '''Interpolate the height for one or several locations. 

617 

618 @arg llis: One or more locations (C{LatLon}s), all positional. 

619 @kwarg wrap: If C{True}, wrap or I{normalize} all B{C{llis}} 

620 locations (C{bool}). 

621 

622 @return: A single interpolated height (C{float}) or a list 

623 or tuple of interpolated heights (C{float}s). 

624 

625 @raise HeightError: Insufficient number of B{C{llis}}, an 

626 invalid B{C{lli}} or L{pygeodesy.fidw} 

627 issue. 

628 ''' 

629 def _xy2(wrap=False): 

630 _w, i = _Wrap._latlonop(wrap), None # PYLINT E1120 

631 try: # like _xyhs above, but degrees 

632 for i, ll in enumerate(llis): 

633 yield _w(ll.lon, ll.lat) 

634 except Exception as x: 

635 i = Fmt.INDEX(llis=i) # PYLINT E1120 

636 raise HeightError(i, ll, cause=x) 

637 

638 _as, llis = _as_llis2(llis) 

639 return _as(map(self._hIDW, *zip(*_xy2(**wrap)))) 

640 

641 @property_RO 

642 def adjust(self): 

643 '''Get the C{adjust} setting (C{bool}) or C{None}. 

644 ''' 

645 return _xkwds_get(self._kwds, adjust=None) 

646 

647 @property 

648 def beta(self): 

649 '''Get the inverse distance power (C{int}). 

650 ''' 

651 return self._beta 

652 

653 @beta.setter # PYCHOK setter! 

654 def beta(self, beta): 

655 '''Set the inverse distance power (C{int} 1, 2, or 3). 

656 

657 @raise HeightError: Invalid B{C{beta}}. 

658 ''' 

659 self._beta = Int_(beta=beta, Error=HeightError, low=1, high=3) 

660 

661 @property_RO 

662 def datum(self): 

663 '''Get the C{datum} setting or the default (L{Datum}). 

664 ''' 

665 return _xkwds_get(self._kwds, datum=self._datum) 

666 

667 def _datum_setter(self, datum): 

668 '''(INTERNAL) Set the default C{datum}. 

669 ''' 

670 d = datum or _xattr(self._knots[0], datum=None) 

671 if d and d is not self._datum: 

672 self._datum = _ellipsoidal_datum(d, name=self.name) 

673 

674 def _distances(self, x, y): 

675 '''(INTERNAL) Yield distances to C{(x, y)}. 

676 ''' 

677 _f, kwds, i = self._func, self._kwds, None 

678 if not callable(_f): # PYCHOK no cover 

679 self._notOverloaded(distance_function=_f) 

680 try: 

681 for i, k in enumerate(self._knots): 

682 yield _f(y, x, k.lat, k.lon, **kwds) # PYLINT E1102 

683 except Exception as x: # PYLINT W0613 

684 i = Fmt.INDEX(knots=i) # PYLINT E1120 

685 raise HeightError(i, k, cause=x) 

686 

687 def _distancesTo(self, _To): 

688 '''(INTERNAL) Yield distances C{_To}. 

689 ''' 

690 i = None 

691 try: 

692 for i, k in enumerate(self._knots): 

693 yield _To(k) 

694 except Exception as x: 

695 i = Fmt.INDEX(knots=i) # PYLINT E1120 

696 raise HeightError(i, k, cause=x) 

697 

698 def height(self, lats, lons, **wrap): 

699 '''Interpolate the height for one or several lat-/longitudes. 

700 

701 @arg lats: Latitude or latitudes (each C{degrees}). 

702 @arg lons: Longitude or longitudes (each C{degrees}). 

703 @kwarg wrap: Keyword argument C{B{wrap}=False} (C{bool}). Use 

704 C{B{wrap}=True} to wrap or I{normalize} all B{C{lats}} 

705 and B{C{lons}}. 

706 

707 @return: A single interpolated height (C{float}) or a list of 

708 interpolated heights (each C{float}). 

709 

710 @raise HeightError: Insufficient or unequal number of B{C{lats}} 

711 and B{C{lons}} or a L{pygeodesy.fidw} issue. 

712 ''' 

713 lls = self._as_lls(lats, lons) # dup of _HeightBase.height 

714 return self(lls, **wrap) # __call__(ll) or __call__(lls) 

715 

716 @Property_RO 

717 def _heights(self): 

718 '''(INTERNAL) Get the knots' heights. 

719 ''' 

720 return tuple(_xattr(k, height=0) for k in self.knots) 

721 

722 def _hIDW(self, x, y): 

723 '''(INTERNAL) Return the IDW-interpolated height at 

724 location (x, y), both C{degrees} or C{radians}. 

725 ''' 

726 ds, hs = self._distances(x, y), self._heights 

727 try: 

728 return _MODS.fmath.fidw(hs, ds, beta=self.beta) 

729 except (TypeError, ValueError) as e: 

730 raise HeightError(x=x, y=y, cause=e) 

731 

732 @property_RO 

733 def hypot(self): 

734 '''Get the C{hypot} setting (C{callable}) or C{None}. 

735 ''' 

736 return _xkwds_get(self._kwds, hypot=None) 

737 

738 @property_RO 

739 def knots(self): 

740 '''Get the B{C{knots}} (C{list} or C{tuple}). 

741 ''' 

742 return self._knots 

743 

744 @property_RO 

745 def kwds(self): 

746 '''Get the optional keyword arguments (C{dict}). 

747 ''' 

748 return self._kwds 

749 

750 @property_RO 

751 def limit(self): 

752 '''Get the C{limit} setting (C{degrees}) or C{None}. 

753 ''' 

754 return _xkwds_get(self._kwds, limit=None) 

755 

756 @property_RO 

757 def nots(self): 

758 '''Get the number of B{C{knots}} (C{int}). 

759 ''' 

760 return len(self.knots) 

761 

762 @property_RO 

763 def radius(self): 

764 '''Get the C{radius} setting (C{bool}) or C{None}. 

765 ''' 

766 return _xkwds_get(self._kwds, radius=None) 

767 

768 @property_RO 

769 def scaled(self): 

770 '''Get the C{scaled} setting (C{bool}) or C{None}. 

771 ''' 

772 return _xkwds_get(self._kwds, scaled=None) 

773 

774 @property_RO 

775 def wrap(self): 

776 '''Get the C{wrap} setting or the default (C{bool}) or C{None}. 

777 ''' 

778 return _xkwds_get(self._kwds, wrap=self._wrap) 

779 

780 

781class HeightIDWcosineLaw(_HeightIDW): 

782 '''Height interpolator using U{Inverse Distance Weighting 

783 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) 

784 and function L{pygeodesy.cosineLaw}. 

785 

786 @note: See note at function L{pygeodesy.vincentys_}. 

787 ''' 

788 def __init__(self, knots, beta=2, **name__corr_earth_datum_radius_wrap): 

789 '''New L{HeightIDWcosineLaw} interpolator. 

790 

791 @kwarg name__corr_earth_datum_radius_wrap: Optional C{B{name}=NN} 

792 for this height interpolator (C{str}) and any keyword 

793 arguments for function L{pygeodesy.cosineLaw}. 

794 

795 @see: L{Here<_HeightIDW.__init__>} for further details. 

796 ''' 

797 _HeightIDW.__init__(self, knots, beta=beta, **name__corr_earth_datum_radius_wrap) 

798 self._func = _formy.cosineLaw 

799 

800 if _FOR_DOCS: 

801 __call__ = _HeightIDW.__call__ 

802 height = _HeightIDW.height 

803 

804 

805class HeightIDWdistanceTo(_HeightIDW): 

806 '''Height interpolator using U{Inverse Distance Weighting 

807 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) 

808 and the points' C{LatLon.distanceTo} method. 

809 ''' 

810 def __init__(self, knots, beta=2, **name__distanceTo_kwds): 

811 '''New L{HeightIDWdistanceTo} interpolator. 

812 

813 @kwarg name__distanceTo_kwds: Optional C{B{name}=NN} for this 

814 height interpolator (C{str}) and keyword arguments 

815 for B{C{knots}}' method C{LatLon.distanceTo}. 

816 

817 @see: L{Here<_HeightIDW.__init__>} for further details. 

818 

819 @note: All B{C{points}} I{must} be instances of the same 

820 ellipsoidal or spherical C{LatLon} class, I{not 

821 checked}. 

822 ''' 

823 _HeightIDW.__init__(self, knots, beta=beta, **name__distanceTo_kwds) 

824 ks0 = _distanceTo(HeightError, knots=self._knots)[0] 

825 # use knots[0] class and datum to create compatible points 

826 # in ._as_lls instead of class LatLon_ and datum None 

827 self._datum = ks0.datum 

828 self._LLiC = ks0.classof # type(ks0) 

829 

830 def _distances(self, x, y): 

831 '''(INTERNAL) Yield distances to C{(x, y)}. 

832 ''' 

833 kwds, ll = self._kwds, self._LLiC(y, x) 

834 

835 def _To(k): 

836 return k.distanceTo(ll, **kwds) 

837 

838 return self._distancesTo(_To) 

839 

840 if _FOR_DOCS: 

841 __call__ = _HeightIDW.__call__ 

842 height = _HeightIDW.height 

843 

844 

845class HeightIDWequirectangular(_HeightIDW): 

846 '''Height interpolator using U{Inverse Distance Weighting 

847 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) 

848 and function L{pygeodesy.equirectangular4}. 

849 ''' 

850 def __init__(self, knots, beta=2, **name__adjust_limit_wrap): # XXX beta=1 

851 '''New L{HeightIDWequirectangular} interpolator. 

852 

853 @kwarg name__adjust_limit_wrap: Optional C{B{name}=NN} for this 

854 height interpolator (C{str}) and keyword arguments 

855 for function L{pygeodesy.equirectangular4}. 

856 

857 @see: L{Here<_HeightIDW.__init__>} for further details. 

858 ''' 

859 _HeightIDW.__init__(self, knots, beta=beta, **name__adjust_limit_wrap) 

860 

861 def _distances(self, x, y): 

862 '''(INTERNAL) Yield distances to C{(x, y)}. 

863 ''' 

864 _f, kwds = _formy.equirectangular4, self._kwds 

865 

866 def _To(k): 

867 return _f(y, x, k.lat, k.lon, **kwds).distance2 

868 

869 return self._distancesTo(_To) 

870 

871 if _FOR_DOCS: 

872 __call__ = _HeightIDW.__call__ 

873 height = _HeightIDW.height 

874 

875 

876class HeightIDWeuclidean(_HeightIDW): 

877 '''Height interpolator using U{Inverse Distance Weighting 

878 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) 

879 and function L{pygeodesy.euclidean_}. 

880 ''' 

881 def __init__(self, knots, beta=2, **name__adjust_radius_wrap): 

882 '''New L{HeightIDWeuclidean} interpolator. 

883 

884 @kwarg name__adjust_radius_wrap: Optional C{B{name}=NN} for this 

885 height interpolator (C{str}) and keyword arguments 

886 for function function L{pygeodesy.euclidean}. 

887 

888 @see: L{Here<_HeightIDW.__init__>} for further details. 

889 ''' 

890 _HeightIDW.__init__(self, knots, beta=beta, **name__adjust_radius_wrap) 

891 self._func = _formy.euclidean 

892 

893 if _FOR_DOCS: 

894 __call__ = _HeightIDW.__call__ 

895 height = _HeightIDW.height 

896 

897 

898class HeightIDWexact(_HeightIDW): 

899 '''Height interpolator using U{Inverse Distance Weighting 

900 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) 

901 and method L{GeodesicExact.Inverse}. 

902 ''' 

903 def __init__(self, knots, beta=2, datum=None, **name__wrap): 

904 '''New L{HeightIDWexact} interpolator. 

905 

906 @kwarg datum: Datum to override the default C{Datums.WGS84} and 

907 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid}, 

908 L{Ellipsoid2} or L{a_f2Tuple}). 

909 @kwarg name__wrap: Optional C{B{name}=NN} for this height interpolator 

910 (C{str}) and a keyword argument for method C{Inverse1} of 

911 class L{geodesicx.GeodesicExact}. 

912 

913 @raise TypeError: Invalid B{C{datum}}. 

914 

915 @see: L{Here<_HeightIDW.__init__>} for further details. 

916 ''' 

917 _HeightIDW.__init__(self, knots, beta=beta, **name__wrap) 

918 self._datum_setter(datum) 

919 self._func = self.datum.ellipsoid.geodesicx.Inverse1 

920 

921 if _FOR_DOCS: 

922 __call__ = _HeightIDW.__call__ 

923 height = _HeightIDW.height 

924 

925 

926class HeightIDWflatLocal(_HeightIDW): 

927 '''Height interpolator using U{Inverse Distance Weighting 

928 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) and 

929 the function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}. 

930 ''' 

931 def __init__(self, knots, beta=2, **name__datum_hypot_scaled_wrap): 

932 '''New L{HeightIDWflatLocal}/L{HeightIDWhubeny} interpolator. 

933 

934 @kwarg name__datum_hypot_scaled_wrap: Optional C{B{name}=NN} 

935 for this height interpolator (C{str}) and any 

936 keyword arguments for L{pygeodesy.flatLocal}. 

937 

938 @see: L{HeightIDW<_HeightIDW.__init__>} for further details. 

939 ''' 

940 _HeightIDW.__init__(self, knots, beta=beta, 

941 **name__datum_hypot_scaled_wrap) 

942 self._func = _formy.flatLocal 

943 

944 if _FOR_DOCS: 

945 __call__ = _HeightIDW.__call__ 

946 height = _HeightIDW.height 

947 

948 

949class HeightIDWflatPolar(_HeightIDW): 

950 '''Height interpolator using U{Inverse Distance Weighting 

951 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) 

952 and function L{pygeodesy.flatPolar_}. 

953 ''' 

954 def __init__(self, knots, beta=2, **name__radius_wrap): 

955 '''New L{HeightIDWflatPolar} interpolator. 

956 

957 @kwarg name__radius_wrap: Optional C{B{name}=NN} for this 

958 height interpolator (C{str}) and any keyword 

959 arguments for function L{pygeodesy.flatPolar}. 

960 

961 @see: L{Here<_HeightIDW.__init__>} for further details. 

962 ''' 

963 _HeightIDW.__init__(self, knots, beta=beta, **name__radius_wrap) 

964 self._func = _formy.flatPolar 

965 

966 if _FOR_DOCS: 

967 __call__ = _HeightIDW.__call__ 

968 height = _HeightIDW.height 

969 

970 

971class HeightIDWhaversine(_HeightIDW): 

972 '''Height interpolator using U{Inverse Distance Weighting 

973 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) 

974 and function L{pygeodesy.haversine_}. 

975 

976 @note: See note at function L{pygeodesy.vincentys_}. 

977 ''' 

978 def __init__(self, knots, beta=2, **name__radius_wrap): 

979 '''New L{HeightIDWhaversine} interpolator. 

980 

981 @kwarg name__radius_wrap: Optional C{B{name}=NN} for this 

982 height interpolator (C{str}) and any keyword 

983 arguments for function L{pygeodesy.haversine}. 

984 

985 @see: L{Here<_HeightIDW.__init__>} for further details. 

986 ''' 

987 _HeightIDW.__init__(self, knots, beta=beta, **name__radius_wrap) 

988 self._func = _formy.haversine 

989 

990 if _FOR_DOCS: 

991 __call__ = _HeightIDW.__call__ 

992 height = _HeightIDW.height 

993 

994 

995class HeightIDWhubeny(HeightIDWflatLocal): # for Karl Hubeny 

996 if _FOR_DOCS: 

997 __doc__ = HeightIDWflatLocal.__doc__ 

998 __init__ = HeightIDWflatLocal.__init__ 

999 __call__ = HeightIDWflatLocal.__call__ 

1000 height = HeightIDWflatLocal.height 

1001 

1002 

1003class HeightIDWkarney(_HeightIDW): 

1004 '''Height interpolator using U{Inverse Distance Weighting 

1005 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) and 

1006 I{Karney}'s U{geographiclib<https://PyPI.org/project/geographiclib>} 

1007 method U{geodesic.Geodesic.Inverse<https://GeographicLib.SourceForge.io/ 

1008 Python/doc/code.html#geographiclib.geodesic.Geodesic.Inverse>}. 

1009 ''' 

1010 def __init__(self, knots, beta=2, datum=None, **name__wrap): 

1011 '''New L{HeightIDWkarney} interpolator. 

1012 

1013 @kwarg datum: Datum to override the default C{Datums.WGS84} and 

1014 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid}, 

1015 L{Ellipsoid2} or L{a_f2Tuple}). 

1016 @kwarg name__wrap: Optional C{B{name}=NN} for this height interpolator 

1017 (C{str}) and a keyword argument for method C{Inverse1} of 

1018 class L{geodesicw.Geodesic}. 

1019 

1020 @raise ImportError: Package U{geographiclib 

1021 <https://PyPI.org/project/geographiclib>} missing. 

1022 

1023 @raise TypeError: Invalid B{C{datum}}. 

1024 

1025 @see: L{Here<_HeightIDW.__init__>} for further details. 

1026 ''' 

1027 _HeightIDW.__init__(self, knots, beta=beta, **name__wrap) 

1028 self._datum_setter(datum) 

1029 self._func = self.datum.ellipsoid.geodesic.Inverse1 

1030 

1031 if _FOR_DOCS: 

1032 __call__ = _HeightIDW.__call__ 

1033 height = _HeightIDW.height 

1034 

1035 

1036class HeightIDWthomas(_HeightIDW): 

1037 '''Height interpolator using U{Inverse Distance Weighting 

1038 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) 

1039 and function L{pygeodesy.thomas_}. 

1040 ''' 

1041 def __init__(self, knots, beta=2, **name__datum_wrap): 

1042 '''New L{HeightIDWthomas} interpolator. 

1043 

1044 @kwarg name__datum_wrap: Optional C{B{name}=NN} for this 

1045 height interpolator (C{str}) and any keyword 

1046 arguments for function L{pygeodesy.thomas}. 

1047 

1048 @see: L{Here<_HeightIDW.__init__>} for further details. 

1049 ''' 

1050 _HeightIDW.__init__(self, knots, beta=beta, **name__datum_wrap) 

1051 self._func = _formy.thomas 

1052 

1053 if _FOR_DOCS: 

1054 __call__ = _HeightIDW.__call__ 

1055 height = _HeightIDW.height 

1056 

1057 

1058class HeightIDWvincentys(_HeightIDW): 

1059 '''Height interpolator using U{Inverse Distance Weighting 

1060 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) 

1061 and function L{pygeodesy.vincentys_}. 

1062 

1063 @note: See note at function L{pygeodesy.vincentys_}. 

1064 ''' 

1065 def __init__(self, knots, beta=2, **name__radius_wrap): 

1066 '''New L{HeightIDWvincentys} interpolator. 

1067 

1068 @kwarg name__radius_wrap: Optional C{B{name}=NN} for this 

1069 height interpolator (C{str}) and any keyword 

1070 arguments for function L{pygeodesy.vincentys}. 

1071 

1072 @see: L{Here<_HeightIDW.__init__>} for further details. 

1073 ''' 

1074 _HeightIDW.__init__(self, knots, beta=beta, **name__radius_wrap) 

1075 self._func = _formy.vincentys 

1076 

1077 if _FOR_DOCS: 

1078 __call__ = _HeightIDW.__call__ 

1079 height = _HeightIDW.height 

1080 

1081 

1082__all__ += _ALL_DOCS(_HeightBase, _HeightIDW, _HeightNamed) 

1083 

1084# **) MIT License 

1085# 

1086# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved. 

1087# 

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

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

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

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

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

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

1094# 

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

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

1097# 

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

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

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

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

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

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

1104# OTHER DEALINGS IN THE SOFTWARE.