Coverage for pygeodesy / heights.py: 95%
320 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-08-27 13:47 -0400
« prev ^ index » next coverage.py v7.14.0, created at 2026-08-27 13:47 -0400
2# -*- coding: utf-8 -*-
4u'''Height interpolations from C{knots}, points with known height.
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}.
14Typical usage
15=============
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.
20C{>>> ...}
222. Select one of the C{Height} classes for height interpolation
24C{>>> from pygeodesy import HeightCubic as HeightXyz # or an other Height... class}
263. Instantiate a height interpolator with the C{knots} and use keyword
27arguments to select different interpolation options
29C{>>> hinterpolator = HeightXyz(knots, **options)}
314. Get the interpolated height of C{LatLon} location(s) with
33C{>>> ll = LatLon(1, 2, ...)}
35C{>>> h = hinterpolator(ll)}
37or
39C{>>> h0, h1, h2, ... = hinterpolator(ll0, ll1, ll2, ...)}
41or a list, tuple, generator, etc. of C{LatLon}s
43C{>>> hs = hinterpolator(lls)}
455. For separate lat- and longitudes invoke the C{height} method as
47C{>>> h = hinterpolator.height(lat, lon)}
49or as 2 lists, 2 tuples, etc.
51C{>>> hs = hinterpolator.height(lats, lons)}
53or for several positionals use the C{height_} method
55C{>>> h1, h2, ... = hinterpolator.height_(lat1, lon1, lat2, lon2, ...)}
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.
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}.
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 ;
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
93# from math import radians # from .points
95__all__ = _ALL_LAZY.heights
96__version__ = '26.08.26'
98_error_ = 'error'
99_formy = _MODS.into(formy=__name__)
100_linear_ = 'linear'
101_llis_ = 'llis'
104class HeightError(PointsError):
105 '''Height interpolator C{Height...} or interpolation issue.
106 '''
107 pass
110def _alist(ais):
111 # return list of floats, not numpy.float64s
112 return list(map(float, ais))
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>
125def _atuple(ais):
126 # return tuple of floats, not numpy.float64s
127 return tuple(map(float, ais))
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))
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
148 if n < m:
149 raise _InsufficientError(m, Error=Error, llis=n)
150 return _as, llis
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)
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
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
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)
177 raise HeightError(i, ll, cause=x)
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
187 _LLiC = LatLon_ # ._height class
188 _np_sp = None # (numpy, scipy)
189 _wrap = None # wrap knots and llis
191 def __call__(self, *llis, **wrap): # PYCHOK no cover
192 '''Interpolate the height for one or several locations. I{Must be overloaded}.
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.
198 @return: A single interpolated height (C{float}) or a list or tuple of
199 interpolated heights (each C{float}).
201 @raise HeightError: Insufficient number of B{C{llis}} or an invalid B{C{lli}}.
203 @raise SciPyError: A C{scipy} issue.
205 @raise SciPyWarning: A C{scipy} warning as exception.
206 '''
207 self._notOverloaded(callername='__call__', *llis, **wrap)
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
222 @property_RO
223 def datum(self):
224 '''Get the C{datum} setting or the default (L{Datum}).
225 '''
226 return self._datum
228 def height(self, lats, lons, **wrap): # PYCHOK no cover
229 '''I{Must be overloaded}.'''
230 self._notOverloaded(lats, lons, **wrap)
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})}.
236 @arg latlons: Alternating lat-/longitude pairs (each C{degrees}),
237 all positional.
239 @see: Method C{height} for further details.
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))
246 @property_RO
247 def kmin(self):
248 '''Get the minimum number of knots (C{int}).
249 '''
250 return self._kmin
252 @property_RO
253 def wrap(self):
254 '''Get the C{wrap} setting (C{bool}) or C{None}.
255 '''
256 return self._wrap
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'}
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
276 def _ev(self, *args): # PYCHOK no cover
277 '''(INTERNAL) I{Must be overloaded}.'''
278 self._notOverloaded(*args)
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)
287 def _ev2d(self, x, y): # PYCHOK no cover
288 '''(INTERNAL) I{Must be overloaded}.'''
289 self._notOverloaded(x, y)
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])])
304 def height(self, lats, lons, **wrap):
305 '''Interpolate the height for one or several lat-/longitudes.
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.
313 @return: A single interpolated height (C{float}) or a list of interpolated
314 heights (each C{float}).
316 @raise HeightError: Insufficient or unequal number of B{C{lats}} and B{C{lons}}.
318 @raise SciPyError: A C{scipy} issue.
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)
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])
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)
341 def _bisplev(x, y):
342 return spi.bisplev(x, y, r) # .T
344 self._ev2d = _bisplev
346 except Exception as x:
347 raise _SciPyIssue(x, self._ev_name)
349 def _kxky(self, kind):
350 return Int_(kind=kind, low=1, high=5, Error=self._Error)
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
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
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
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
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)
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)
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
409 def __init__(self, knots, **name_wrap):
410 '''New L{HeightCubic} interpolator.
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}).
417 @raise HeightError: Insufficient number of B{C{knots}} or invalid B{C{knot}}.
419 @raise ImportError: Package C{numpy} or C{scipy} not found or not installed.
421 @raise SciPyError: A C{scipy} issue.
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)
428 def __call__(self, *llis, **wrap):
429 '''Interpolate the height for one or several locations.
431 @see: L{Here<_HeightBase.__call__>} for further details.
432 '''
433 return self._evalls(llis, **wrap)
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)
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
450 def __init__(self, knots, **name_wrap):
451 '''New L{HeightLinear} interpolator.
453 @see: L{Here<HeightCubic.__init__>} for all details.
454 '''
455 HeightCubic.__init__(self, knots, **name_wrap)
457 if _FOR_DOCS:
458 __call__ = HeightCubic.__call__
459 height = HeightCubic.height
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
469 def __init__(self, knots, weight=None, low=1e-4, **name_wrap):
470 '''New L{HeightLSQBiSpline} interpolator.
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}).
481 @raise HeightError: Insufficient number of B{C{knots}} or an invalid
482 B{C{knot}}, B{C{weight}} or B{C{eps}}.
484 @raise LenError: Unequal number of B{C{knots}} and B{C{weight}}s.
486 @raise ImportError: Package C{numpy} or C{scipy} not found or not
487 installed.
489 @raise SciPyError: A C{scipy} issue.
491 @raise SciPyWarning: A C{scipy} warning as exception.
492 '''
493 np = self.numpy
494 spi = self.scipy_interpolate
496 xs, ys, hs = self._xyhs3(knots, **name_wrap)
497 n = len(hs)
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)
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)
522 def __call__(self, *llis, **wrap):
523 '''Interpolate the height for one or several locations.
525 @see: L{Here<_HeightBase.__call__>} for further details.
526 '''
527 return self._evalls(llis, **wrap)
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
537 def __init__(self, knots, smooth=4, **name_wrap):
538 '''New L{HeightSmoothBiSpline} interpolator.
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}).
547 @raise HeightError: Insufficient number of B{C{knots}} or an invalid
548 B{C{knot}} or B{C{s}}.
550 @raise ImportError: Package C{numpy} or C{scipy} not found or not
551 installed.
553 @raise SciPyError: A C{scipy} issue.
555 @raise SciPyWarning: A C{scipy} warning as exception.
556 '''
557 spi = self.scipy_interpolate
559 s, name_wrap = _xkwds_pop2(name_wrap, s=smooth)
560 s = Float_(smooth=s, Error=HeightError, low=0)
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)
569 def __call__(self, *llis, **wrap):
570 '''Interpolate the height for one or several locations.
572 @see: L{Here<_HeightBase.__call__>} for further details.
573 '''
574 return self._evalls(llis, **wrap)
577class _HeightIDW(_HeightNamed):
578 '''(INTERNAL) Base class for U{Inverse Distance Weighting
579 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) height
580 interpolators.
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
593 def __init__(self, knots, beta=2, **name__kwds):
594 '''New C{_HeightIDW*} interpolator.
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}.
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
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 {}
615 def __call__(self, *llis, **wrap):
616 '''Interpolate the height for one or several locations.
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}).
622 @return: A single interpolated height (C{float}) or a list
623 or tuple of interpolated heights (C{float}s).
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 = _Wrap._latlonop(wrap)
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)
636 raise HeightError(i, ll, cause=x)
638 _as, llis = _as_llis2(llis)
639 return _as(map(self._hIDW, *zip(*_xy2(**wrap))))
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)
647 @property
648 def beta(self):
649 '''Get the inverse distance power (C{int}).
650 '''
651 return self._beta
653 @beta.setter # PYCHOK setter!
654 def beta(self, beta):
655 '''Set the inverse distance power (C{int} 1, 2, or 3).
657 @raise HeightError: Invalid B{C{beta}}.
658 '''
659 self._beta = Int_(beta=beta, Error=HeightError, low=1, high=3)
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)
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)
674 def _distances(self, x, y):
675 '''(INTERNAL) Yield distances to C{(x, y)}.
676 '''
677 _f, kwds = self._func, self._kwds
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)
683 except Exception as x:
684 i = Fmt.INDEX(knots=i)
685 raise HeightError(i, k, cause=x)
687 def _distancesTo(self, _To):
688 '''(INTERNAL) Yield distances C{_To}.
689 '''
690 try:
691 for i, k in enumerate(self._knots):
692 yield _To(k)
693 except Exception as x:
694 i = Fmt.INDEX(knots=i)
695 raise HeightError(i, k, cause=x)
697 def height(self, lats, lons, **wrap):
698 '''Interpolate the height for one or several lat-/longitudes.
700 @arg lats: Latitude or latitudes (each C{degrees}).
701 @arg lons: Longitude or longitudes (each C{degrees}).
702 @kwarg wrap: Keyword argument C{B{wrap}=False} (C{bool}). Use
703 C{B{wrap}=True} to wrap or I{normalize} all B{C{lats}}
704 and B{C{lons}}.
706 @return: A single interpolated height (C{float}) or a list of
707 interpolated heights (each C{float}).
709 @raise HeightError: Insufficient or unequal number of B{C{lats}}
710 and B{C{lons}} or a L{pygeodesy.fidw} issue.
711 '''
712 lls = self._as_lls(lats, lons) # dup of _HeightBase.height
713 return self(lls, **wrap) # __call__(ll) or __call__(lls)
715 @Property_RO
716 def _heights(self):
717 '''(INTERNAL) Get the knots' heights.
718 '''
719 return tuple(_xattr(k, height=0) for k in self.knots)
721 def _hIDW(self, x, y):
722 '''(INTERNAL) Return the IDW-interpolated height at
723 location (x, y), both C{degrees} or C{radians}.
724 '''
725 ds, hs = self._distances(x, y), self._heights
726 try:
727 return _MODS.fmath.fidw(hs, ds, beta=self.beta)
728 except (TypeError, ValueError) as e:
729 raise HeightError(x=x, y=y, cause=e)
731 @property_RO
732 def hypot(self):
733 '''Get the C{hypot} setting (C{callable}) or C{None}.
734 '''
735 return _xkwds_get(self._kwds, hypot=None)
737 @property_RO
738 def knots(self):
739 '''Get the B{C{knots}} (C{list} or C{tuple}).
740 '''
741 return self._knots
743 @property_RO
744 def kwds(self):
745 '''Get the optional keyword arguments (C{dict}).
746 '''
747 return self._kwds
749 @property_RO
750 def limit(self):
751 '''Get the C{limit} setting (C{degrees}) or C{None}.
752 '''
753 return _xkwds_get(self._kwds, limit=None)
755 @property_RO
756 def nots(self):
757 '''Get the number of B{C{knots}} (C{int}).
758 '''
759 return len(self.knots)
761 @property_RO
762 def radius(self):
763 '''Get the C{radius} setting (C{bool}) or C{None}.
764 '''
765 return _xkwds_get(self._kwds, radius=None)
767 @property_RO
768 def scaled(self):
769 '''Get the C{scaled} setting (C{bool}) or C{None}.
770 '''
771 return _xkwds_get(self._kwds, scaled=None)
773 @property_RO
774 def wrap(self):
775 '''Get the C{wrap} setting or the default (C{bool}) or C{None}.
776 '''
777 return _xkwds_get(self._kwds, wrap=self._wrap)
780class HeightIDWcosineLaw(_HeightIDW):
781 '''Height interpolator using U{Inverse Distance Weighting
782 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW)
783 and function L{pygeodesy.cosineLaw}.
785 @note: See note at function L{pygeodesy.vincentys_}.
786 '''
787 def __init__(self, knots, beta=2, **name__corr_earth_datum_radius_wrap):
788 '''New L{HeightIDWcosineLaw} interpolator.
790 @kwarg name__corr_earth_datum_radius_wrap: Optional C{B{name}=NN}
791 for this height interpolator (C{str}) and any keyword
792 arguments for function L{pygeodesy.cosineLaw}.
794 @see: L{Here<_HeightIDW.__init__>} for further details.
795 '''
796 _HeightIDW.__init__(self, knots, beta=beta, **name__corr_earth_datum_radius_wrap)
797 self._func = _formy.cosineLaw
799 if _FOR_DOCS:
800 __call__ = _HeightIDW.__call__
801 height = _HeightIDW.height
804class HeightIDWdistanceTo(_HeightIDW):
805 '''Height interpolator using U{Inverse Distance Weighting
806 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW)
807 and the points' C{LatLon.distanceTo} method.
808 '''
809 def __init__(self, knots, beta=2, **name__distanceTo_kwds):
810 '''New L{HeightIDWdistanceTo} interpolator.
812 @kwarg name__distanceTo_kwds: Optional C{B{name}=NN} for this
813 height interpolator (C{str}) and keyword arguments
814 for B{C{knots}}' method C{LatLon.distanceTo}.
816 @see: L{Here<_HeightIDW.__init__>} for further details.
818 @note: All B{C{points}} I{must} be instances of the same
819 ellipsoidal or spherical C{LatLon} class, I{not
820 checked}.
821 '''
822 _HeightIDW.__init__(self, knots, beta=beta, **name__distanceTo_kwds)
823 ks0 = _distanceTo(HeightError, knots=self._knots)[0]
824 # use knots[0] class and datum to create compatible points
825 # in ._as_lls instead of class LatLon_ and datum None
826 self._datum = ks0.datum
827 self._LLiC = ks0.classof # type(ks0)
829 def _distances(self, x, y):
830 '''(INTERNAL) Yield distances to C{(x, y)}.
831 '''
832 kwds, ll = self._kwds, self._LLiC(y, x)
834 def _To(k):
835 return k.distanceTo(ll, **kwds)
837 return self._distancesTo(_To)
839 if _FOR_DOCS:
840 __call__ = _HeightIDW.__call__
841 height = _HeightIDW.height
844class HeightIDWequirectangular(_HeightIDW):
845 '''Height interpolator using U{Inverse Distance Weighting
846 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW)
847 and function L{pygeodesy.equirectangular4}.
848 '''
849 def __init__(self, knots, beta=2, **name__adjust_limit_wrap): # XXX beta=1
850 '''New L{HeightIDWequirectangular} interpolator.
852 @kwarg name__adjust_limit_wrap: Optional C{B{name}=NN} for this
853 height interpolator (C{str}) and keyword arguments
854 for function L{pygeodesy.equirectangular4}.
856 @see: L{Here<_HeightIDW.__init__>} for further details.
857 '''
858 _HeightIDW.__init__(self, knots, beta=beta, **name__adjust_limit_wrap)
860 def _distances(self, x, y):
861 '''(INTERNAL) Yield distances to C{(x, y)}.
862 '''
863 _f, kwds = _formy.equirectangular4, self._kwds
865 def _To(k):
866 return _f(y, x, k.lat, k.lon, **kwds).distance2
868 return self._distancesTo(_To)
870 if _FOR_DOCS:
871 __call__ = _HeightIDW.__call__
872 height = _HeightIDW.height
875class HeightIDWeuclidean(_HeightIDW):
876 '''Height interpolator using U{Inverse Distance Weighting
877 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW)
878 and function L{pygeodesy.euclidean_}.
879 '''
880 def __init__(self, knots, beta=2, **name__adjust_radius_wrap):
881 '''New L{HeightIDWeuclidean} interpolator.
883 @kwarg name__adjust_radius_wrap: Optional C{B{name}=NN} for this
884 height interpolator (C{str}) and keyword arguments
885 for function function L{pygeodesy.euclidean}.
887 @see: L{Here<_HeightIDW.__init__>} for further details.
888 '''
889 _HeightIDW.__init__(self, knots, beta=beta, **name__adjust_radius_wrap)
890 self._func = _formy.euclidean
892 if _FOR_DOCS:
893 __call__ = _HeightIDW.__call__
894 height = _HeightIDW.height
897class HeightIDWexact(_HeightIDW):
898 '''Height interpolator using U{Inverse Distance Weighting
899 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW)
900 and method L{GeodesicExact.Inverse}.
901 '''
902 def __init__(self, knots, beta=2, datum=None, **name__wrap):
903 '''New L{HeightIDWexact} interpolator.
905 @kwarg datum: Datum to override the default C{Datums.WGS84} and
906 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid},
907 L{Ellipsoid2} or L{a_f2Tuple}).
908 @kwarg name__wrap: Optional C{B{name}=NN} for this height interpolator
909 (C{str}) and a keyword argument for method C{Inverse1} of
910 class L{geodesicx.GeodesicExact}.
912 @raise TypeError: Invalid B{C{datum}}.
914 @see: L{Here<_HeightIDW.__init__>} for further details.
915 '''
916 _HeightIDW.__init__(self, knots, beta=beta, **name__wrap)
917 self._datum_setter(datum)
918 self._func = self.datum.ellipsoid.geodesicx.Inverse1
920 if _FOR_DOCS:
921 __call__ = _HeightIDW.__call__
922 height = _HeightIDW.height
925class HeightIDWflatLocal(_HeightIDW):
926 '''Height interpolator using U{Inverse Distance Weighting
927 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) and
928 the function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}.
929 '''
930 def __init__(self, knots, beta=2, **name__datum_hypot_scaled_wrap):
931 '''New L{HeightIDWflatLocal}/L{HeightIDWhubeny} interpolator.
933 @kwarg name__datum_hypot_scaled_wrap: Optional C{B{name}=NN}
934 for this height interpolator (C{str}) and any
935 keyword arguments for L{pygeodesy.flatLocal}.
937 @see: L{HeightIDW<_HeightIDW.__init__>} for further details.
938 '''
939 _HeightIDW.__init__(self, knots, beta=beta,
940 **name__datum_hypot_scaled_wrap)
941 self._func = _formy.flatLocal
943 if _FOR_DOCS:
944 __call__ = _HeightIDW.__call__
945 height = _HeightIDW.height
948class HeightIDWflatPolar(_HeightIDW):
949 '''Height interpolator using U{Inverse Distance Weighting
950 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW)
951 and function L{pygeodesy.flatPolar_}.
952 '''
953 def __init__(self, knots, beta=2, **name__radius_wrap):
954 '''New L{HeightIDWflatPolar} interpolator.
956 @kwarg name__radius_wrap: Optional C{B{name}=NN} for this
957 height interpolator (C{str}) and any keyword
958 arguments for function L{pygeodesy.flatPolar}.
960 @see: L{Here<_HeightIDW.__init__>} for further details.
961 '''
962 _HeightIDW.__init__(self, knots, beta=beta, **name__radius_wrap)
963 self._func = _formy.flatPolar
965 if _FOR_DOCS:
966 __call__ = _HeightIDW.__call__
967 height = _HeightIDW.height
970class HeightIDWhaversine(_HeightIDW):
971 '''Height interpolator using U{Inverse Distance Weighting
972 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW)
973 and function L{pygeodesy.haversine_}.
975 @note: See note at function L{pygeodesy.vincentys_}.
976 '''
977 def __init__(self, knots, beta=2, **name__radius_wrap):
978 '''New L{HeightIDWhaversine} interpolator.
980 @kwarg name__radius_wrap: Optional C{B{name}=NN} for this
981 height interpolator (C{str}) and any keyword
982 arguments for function L{pygeodesy.haversine}.
984 @see: L{Here<_HeightIDW.__init__>} for further details.
985 '''
986 _HeightIDW.__init__(self, knots, beta=beta, **name__radius_wrap)
987 self._func = _formy.haversine
989 if _FOR_DOCS:
990 __call__ = _HeightIDW.__call__
991 height = _HeightIDW.height
994class HeightIDWhubeny(HeightIDWflatLocal): # for Karl Hubeny
995 if _FOR_DOCS:
996 __doc__ = HeightIDWflatLocal.__doc__
997 __init__ = HeightIDWflatLocal.__init__
998 __call__ = HeightIDWflatLocal.__call__
999 height = HeightIDWflatLocal.height
1002class HeightIDWkarney(_HeightIDW):
1003 '''Height interpolator using U{Inverse Distance Weighting
1004 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW) and
1005 I{Karney}'s U{geographiclib<https://PyPI.org/project/geographiclib>}
1006 method U{geodesic.Geodesic.Inverse<https://GeographicLib.SourceForge.io/
1007 Python/doc/code.html#geographiclib.geodesic.Geodesic.Inverse>}.
1008 '''
1009 def __init__(self, knots, beta=2, datum=None, **name__wrap):
1010 '''New L{HeightIDWkarney} interpolator.
1012 @kwarg datum: Datum to override the default C{Datums.WGS84} and
1013 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid},
1014 L{Ellipsoid2} or L{a_f2Tuple}).
1015 @kwarg name__wrap: Optional C{B{name}=NN} for this height interpolator
1016 (C{str}) and a keyword argument for method C{Inverse1} of
1017 class L{geodesicw.Geodesic}.
1019 @raise ImportError: Package U{geographiclib
1020 <https://PyPI.org/project/geographiclib>} missing.
1022 @raise TypeError: Invalid B{C{datum}}.
1024 @see: L{Here<_HeightIDW.__init__>} for further details.
1025 '''
1026 _HeightIDW.__init__(self, knots, beta=beta, **name__wrap)
1027 self._datum_setter(datum)
1028 self._func = self.datum.ellipsoid.geodesic.Inverse1
1030 if _FOR_DOCS:
1031 __call__ = _HeightIDW.__call__
1032 height = _HeightIDW.height
1035class HeightIDWthomas(_HeightIDW):
1036 '''Height interpolator using U{Inverse Distance Weighting
1037 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW)
1038 and function L{pygeodesy.thomas_}.
1039 '''
1040 def __init__(self, knots, beta=2, **name__datum_wrap):
1041 '''New L{HeightIDWthomas} interpolator.
1043 @kwarg name__datum_wrap: Optional C{B{name}=NN} for this
1044 height interpolator (C{str}) and any keyword
1045 arguments for function L{pygeodesy.thomas}.
1047 @see: L{Here<_HeightIDW.__init__>} for further details.
1048 '''
1049 _HeightIDW.__init__(self, knots, beta=beta, **name__datum_wrap)
1050 self._func = _formy.thomas
1052 if _FOR_DOCS:
1053 __call__ = _HeightIDW.__call__
1054 height = _HeightIDW.height
1057class HeightIDWvincentys(_HeightIDW):
1058 '''Height interpolator using U{Inverse Distance Weighting
1059 <https://WikiPedia.org/wiki/Inverse_distance_weighting>} (IDW)
1060 and function L{pygeodesy.vincentys_}.
1062 @note: See note at function L{pygeodesy.vincentys_}.
1063 '''
1064 def __init__(self, knots, beta=2, **name__radius_wrap):
1065 '''New L{HeightIDWvincentys} interpolator.
1067 @kwarg name__radius_wrap: Optional C{B{name}=NN} for this
1068 height interpolator (C{str}) and any keyword
1069 arguments for function L{pygeodesy.vincentys}.
1071 @see: L{Here<_HeightIDW.__init__>} for further details.
1072 '''
1073 _HeightIDW.__init__(self, knots, beta=beta, **name__radius_wrap)
1074 self._func = _formy.vincentys
1076 if _FOR_DOCS:
1077 __call__ = _HeightIDW.__call__
1078 height = _HeightIDW.height
1081__all__ += _ALL_DOCS(_HeightBase, _HeightIDW, _HeightNamed)
1083# **) MIT License
1084#
1085# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.
1086#
1087# Permission is hereby granted, free of charge, to any person obtaining a
1088# copy of this software and associated documentation files (the "Software"),
1089# to deal in the Software without restriction, including without limitation
1090# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1091# and/or sell copies of the Software, and to permit persons to whom the
1092# Software is furnished to do so, subject to the following conditions:
1093#
1094# The above copyright notice and this permission notice shall be included
1095# in all copies or substantial portions of the Software.
1096#
1097# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1098# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1099# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1100# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1101# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1102# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1103# OTHER DEALINGS IN THE SOFTWARE.