Coverage for pygeodesy/hausdorff.py: 96%
266 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-02 08:40 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-02 08:40 -0400
2# -*- coding: utf-8 -*-
4u'''Hausdorff distances.
6Classes L{Hausdorff}, L{HausdorffDegrees}, L{HausdorffRadians},
7L{HausdorffCosineAndoyerLambert}, L{HausdorffCosineForsytheAndoyerLambert},
8L{HausdorffCosineLaw}, L{HausdorffDistanceTo}, L{HausdorffEquirectangular},
9L{HausdorffEuclidean}, L{HausdorffFlatLocal}, L{HausdorffFlatPolar},
10L{HausdorffHaversine}, L{HausdorffHubeny}, L{HausdorffKarney},
11L{HausdorffThomas} and L{HausdorffVincentys} to compute U{Hausdorff
12<https://WikiPedia.org/wiki/Hausdorff_distance>} distances between two
13sets of C{LatLon}, C{NumPy}, C{tuples} or other types of points.
15Only L{HausdorffDistanceTo} -iff used with L{ellipsoidalKarney.LatLon}
16points- and L{HausdorffKarney} requires installation of I{Charles Karney}'s
17U{geographiclib<https://PyPI.org/project/geographiclib>}.
19Typical usage is as follows. First, create a C{Hausdorff} calculator
20from a given set of C{LatLon} points, called the C{model} or C{template}
21points.
23C{h = HausdorffXyz(points1, ...)}
25Get the C{directed} or C{symmetric} Hausdorff distance to a second set
26of C{LatLon} points, named the C{target} points, by using
28C{t6 = h.directed(points2)}
30respectively
32C{t6 = h.symmetric(points2)}.
34Or, use function C{hausdorff_} with a proper C{distance} function and
35optionally a C{point} function passed as keyword arguments as follows
37C{t6 = hausdorff_(points1, points2, ..., distance=..., point=...)}.
39In all cases, the returned result C{t6} is a L{Hausdorff6Tuple}.
41For C{(lat, lon, ...)} points in a C{NumPy} array or plain C{tuples},
42wrap the points in a L{Numpy2LatLon} respectively L{Tuple2LatLon}
43instance, more details in the documentation thereof.
45For other points, create a L{Hausdorff} sub-class with the appropriate
46C{distance} method overloading L{Hausdorff.distance} and optionally a
47C{point} method overriding L{Hausdorff.point} as the next example.
49 >>> from pygeodesy import Hausdorff, hypot_
50 >>>
51 >>> class H3D(Hausdorff):
52 >>> """Custom Hausdorff example.
53 >>> """
54 >>> def distance(self, p1, p2):
55 >>> return hypot_(p1.x - p2.x, p1.y - p2.y, p1.z - p2.z)
56 >>>
57 >>> h3D = H3D(xyz1, ..., units="...")
58 >>> d6 = h3D.directed(xyz2)
60Transcribed from the original SciPy U{Directed Hausdorff Code
61<https://GitHub.com/scipy/scipy/blob/master/scipy/spatial/_hausdorff.pyx>}
62version 0.19.0, Copyright (C) Tyler Reddy, Richard Gowers, and Max Linke,
632016, distributed under the same BSD license as SciPy, including C{early
64breaking} and C{random sampling} as in U{Abdel Aziz Taha, Allan Hanbury
65"An Efficient Algorithm for Calculating the Exact Hausdorff Distance"
66<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}, IEEE Trans. Pattern
67Analysis Machine Intelligence (PAMI), vol 37, no 11, pp 2153-2163, Nov 2015.
68'''
70from pygeodesy.constants import INF, NINF, _0_0
71from pygeodesy.datums import _ellipsoidal_datum, _WGS84
72from pygeodesy.errors import _IsnotError, PointsError
73# from pygeodesy.fmath import hypot2 # from .formy
74from pygeodesy.formy import cosineAndoyerLambert_, cosineForsytheAndoyerLambert_, \
75 cosineLaw_, euclidean_, flatPolar_, haversine_, \
76 hypot2, thomas_, vincentys_, _scale_rad, unrollPI
77from pygeodesy.interns import NN, _datum_, _distanceTo_, _i_, _j_, _points_, _units_
78# from pygeodesy.iters import points2 # from .points
79from pygeodesy.lazily import _ALL_LAZY, _FOR_DOCS
80from pygeodesy.named import _Named, _NamedTuple, notOverloaded, _Pass
81from pygeodesy.namedTuples import PhiLam2Tuple
82from pygeodesy.points import points2, Property_RO, property_doc_
83# from pygeodesy.props import Property_RO, property_doc_ # from .points
84from pygeodesy.streprs import _boolkwds, Fmt
85from pygeodesy.units import Float, Number_, _xUnit, _xUnits
86from pygeodesy.unitsBase import _Str_degrees, _Str_meter, _Str_NN, \
87 _Str_radians, _Str_radians2
88# from pygeodesy.utily import unrollPI # from .formy
90from math import radians
91from random import Random
93__all__ = _ALL_LAZY.hausdorff
94__version__ = '22.09.24'
97class HausdorffError(PointsError):
98 '''Hausdorff issue.
99 '''
100 pass
103class Hausdorff(_Named):
104 '''Hausdorff base class, requires method L{Hausdorff.distance} to
105 be overloaded.
106 '''
107 _adjust = None # not applicable
108 _datum = None # not applicable
109 _model = ()
110 _seed = None
111 _units = _Str_NN # XXX Str to _Pass and for backward compatibility
112 _wrap = None # not applicable
114 def __init__(self, points, seed=None, name=NN, units=NN, **wrap_adjust):
115 '''New C{Hausdorff...} calculator.
117 @arg points: Initial set of points, aka the C{model} or
118 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
119 C{Tuple2LatLon}[] or C{other}[]).
120 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0}
121 or C{False} for no U{random sampling<https://
122 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
123 @kwarg name: Optional name for this interpolator (C{str}).
124 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
125 @kwarg wrap_adjust: Optionally, C{wrap} and unroll longitudes, iff
126 applicable (C{bool}) and C{adjust} wrapped,
127 unrolled longitudinal delta by the cosine
128 of the mean latitude, iff applicable.
130 @raise HausdorffError: Insufficient number of B{C{points}} or
131 an invalid B{C{point}}, B{C{seed}} or
132 B{C{wrap}} or B{C{ajust}} not applicable.
133 '''
134 _, self._model = self._points2(points)
135 if seed:
136 self.seed = seed
137 if name:
138 self.name = name
139 if units: # and not self.units:
140 self.units = units
141 if wrap_adjust:
142 _boolkwds(self, **wrap_adjust)
144 @Property_RO
145 def adjust(self):
146 '''Get the adjust setting (C{bool} or C{None} if not applicable).
147 '''
148 return self._adjust
150 @Property_RO
151 def datum(self):
152 '''Get the datum of this calculator (L{Datum} or C{None} if not applicable).
153 '''
154 return self._datum
156 def _datum_setter(self, datum):
157 '''(INTERNAL) Set the datum.
159 @raise TypeError: Invalid B{C{datum}}.
160 '''
161 d = datum or getattr(self._model[0], _datum_, datum)
162 if d not in (None, self._datum): # PYCHOK no cover
163 self._datum = _ellipsoidal_datum(d, name=self.name)
165 def directed(self, points, early=True):
166 '''Compute only the C{forward Hausdorff} distance.
168 @arg points: Second set of points, aka the C{target} (C{LatLon}[],
169 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
170 @kwarg early: Enable or disable U{early breaking<https://
171 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
173 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
175 @raise HausdorffError: Insufficient number of B{C{points}} or
176 an invalid B{C{point}}.
178 @note: See B{C{points}} note at L{HausdorffDistanceTo}.
179 '''
180 _, ps2 = self._points2(points)
181 return _hausdorff_(self._model, ps2, False, early, self.seed,
182 self.units, self.distance, self.point)
184 def distance(self, point1, point2):
185 '''(INTERNAL) I{Must be overloaded}, see function C{notOverloaded}.
186 '''
187 notOverloaded(self, point1, point2) # PYCHOK no cover
189 def point(self, point):
190 '''Convert a C{model} or C{target} point for the C{.distance} method.
191 '''
192 return point # pass thru
194 def _points2(self, points):
195 '''(INTERNAL) Check a set of points.
196 '''
197 return points2(points, closed=False, Error=HausdorffError)
199 @property_doc_(''' the random sampling seed (C{Random}).''')
200 def seed(self):
201 '''Get the random sampling seed (C{any} or C{None}).
202 '''
203 return self._seed
205 @seed.setter # PYCHOK setter!
206 def seed(self, seed):
207 '''Set the random sampling seed (C{Random(seed)}) or
208 C{None}, C{0} or C{False} for no U{random sampling
209 <https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
211 @raise HausdorffError: Invalid B{C{seed}}.
212 '''
213 if seed:
214 try:
215 Random(seed)
216 except (TypeError, ValueError) as x:
217 raise HausdorffError(seed=seed, cause=x)
218 self._seed = seed
219 else:
220 self._seed = None
222 def symmetric(self, points, early=True):
223 '''Compute the combined C{forward and reverse Hausdorff} distance.
225 @arg points: Second set of points, aka the C{target} (C{LatLon}[],
226 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
227 @kwarg early: Enable or disable U{early breaking<https://
228 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
230 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
232 @raise HausdorffError: Insufficient number of B{C{points}} or
233 an invalid B{C{point}}.
235 @note: See B{C{points}} note at L{HausdorffDistanceTo}.
236 '''
237 _, ps2 = self._points2(points)
238 return _hausdorff_(self._model, ps2, True, early, self.seed,
239 self.units, self.distance, self.point)
241 @property_doc_(''' the distance units (C{Unit} or C{str}).''')
242 def units(self):
243 '''Get the distance units (C{Unit} or C{str}).
244 '''
245 return self._units
247 @units.setter # PYCHOK setter!
248 def units(self, units):
249 '''Set the distance units (C{Unit} or C{str}).
251 @raise TypeError: Invalid B{C{units}}.
252 '''
253 self._units = _xUnits(units, Base=Float)
255 @Property_RO
256 def wrap(self):
257 '''Get the wrap setting (C{bool} or C{None} if not applicable).
258 '''
259 return self._wrap
262class HausdorffDegrees(Hausdorff):
263 '''L{Hausdorff} base class for distances from C{LatLon}
264 points in C{degrees}.
265 '''
266 _units = _Str_degrees
268 if _FOR_DOCS:
269 __init__ = Hausdorff.__init__
270 directed = Hausdorff.directed
271 symmetric = Hausdorff.symmetric
274class HausdorffRadians(Hausdorff):
275 '''L{Hausdorff} base class for distances from C{LatLon}
276 points converted from C{degrees} to C{radians}.
277 '''
278 _units = _Str_radians
280 if _FOR_DOCS:
281 __init__ = Hausdorff.__init__
282 directed = Hausdorff.directed
283 symmetric = Hausdorff.symmetric
285 def point(self, point):
286 '''Convert C{(lat, lon)} point in degrees to C{(a, b)}
287 in radians.
289 @return: An L{PhiLam2Tuple}C{(phi, lam)}.
290 '''
291 try:
292 return point.philam
293 except AttributeError:
294 return PhiLam2Tuple(radians(point.lat), radians(point.lon))
297class HausdorffCosineAndoyerLambert(HausdorffRadians):
298 '''Compute the C{Hausdorff} distance based on the I{angular} distance
299 in C{radians} from function L{pygeodesy.cosineAndoyerLambert_}.
301 @see: L{HausdorffCosineForsytheAndoyerLambert}, L{HausdorffDistanceTo},
302 L{HausdorffExact}, L{HausdorffFlatLocal}, L{HausdorffHubeny},
303 L{HausdorffThomas} and L{HausdorffKarney}.
304 '''
305 _datum = _WGS84
306 _wrap = False
308 def __init__(self, points, datum=None, wrap=False, seed=None, name=NN):
309 '''New L{HausdorffCosineAndoyerLambert} calculator.
311 @arg points: Initial set of points, aka the C{model} or
312 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
313 C{Tuple2LatLon}[] or C{other}[]).
314 @kwarg datum: Optional datum overriding the default C{Datums.WGS84}
315 and first B{C{points}}' datum (L{Datum}, L{Ellipsoid},
316 L{Ellipsoid2} or L{a_f2Tuple}).
317 @kwarg wrap: Optionally, wrap and L{pygeodesy.unrollPI} longitudes
318 (C{bool}).
319 @kwarg seed: Random seed (C{any}) or C{None}, C{0} or
320 C{False} for no U{random sampling<https://
321 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
322 @kwarg name: Optional name for this interpolator (C{str}).
324 @raise HausdorffError: Insufficient number of B{C{points}} or
325 invalid B{C{seed}}.
327 @raise TypeError: Invalid B{C{datum}}.
328 '''
329 HausdorffRadians.__init__(self, points, seed=seed, name=name,
330 wrap=wrap)
331 self._datum_setter(datum)
333 if _FOR_DOCS:
334 directed = Hausdorff.directed
335 symmetric = Hausdorff.symmetric
337 def distance(self, p1, p2):
338 '''Return the L{pygeodesy.cosineAndoyerLambert_} distance in C{radians}.
339 '''
340 r, _ = unrollPI(p1.lam, p2.lam, wrap=self._wrap)
341 return cosineAndoyerLambert_(p2.phi, p1.phi, r, datum=self._datum)
344class HausdorffCosineForsytheAndoyerLambert(HausdorffRadians):
345 '''Compute the C{Hausdorff} distance based on the I{angular} distance
346 in C{radians} from function L{pygeodesy.cosineForsytheAndoyerLambert_}.
348 @see: L{HausdorffCosineAndoyerLambert}, L{HausdorffDistanceTo},
349 L{HausdorffExact}, L{HausdorffFlatLocal}, L{HausdorffHubeny},
350 L{HausdorffThomas} and L{HausdorffKarney}.
351 '''
352 _datum = _WGS84
353 _wrap = False
355 def __init__(self, points, datum=None, wrap=False, seed=None, name=NN):
356 '''New L{HausdorffCosineForsytheAndoyerLambert} calculator.
358 @arg points: Initial set of points, aka the C{model} or
359 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
360 C{Tuple2LatLon}[] or C{other}[]).
361 @kwarg datum: Optional datum overriding the default C{Datums.WGS84}
362 and first B{C{points}}' datum (L{Datum}, L{Ellipsoid},
363 L{Ellipsoid2} or L{a_f2Tuple}).
364 @kwarg wrap: Optionally, wrap and L{pygeodesy.unrollPI} longitudes
365 (C{bool}).
366 @kwarg seed: Random seed (C{any}) or C{None}, C{0} or
367 C{False} for no U{random sampling<https://
368 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
369 @kwarg name: Optional name for this interpolator (C{str}).
371 @raise HausdorffError: Insufficient number of B{C{points}} or
372 invalid B{C{seed}}.
374 @raise TypeError: Invalid B{C{datum}}.
375 '''
376 HausdorffRadians.__init__(self, points, seed=seed, name=name,
377 wrap=wrap)
378 self._datum_setter(datum)
380 if _FOR_DOCS:
381 directed = Hausdorff.directed
382 symmetric = Hausdorff.symmetric
384 def distance(self, p1, p2):
385 '''Return the L{pygeodesy.cosineForsytheAndoyerLambert_} distance in C{radians}.
386 '''
387 r, _ = unrollPI(p1.lam, p2.lam, wrap=self._wrap)
388 return cosineForsytheAndoyerLambert_(p2.phi, p1.phi, r, datum=self._datum)
391class HausdorffCosineLaw(HausdorffRadians):
392 '''Compute the C{Hausdorff} distance based on the I{angular}
393 distance in C{radians} from function L{pygeodesy.cosineLaw_}.
395 @note: See note at function L{pygeodesy.vincentys_}.
397 @see: L{HausdorffEquirectangular}, L{HausdorffEuclidean},
398 L{HausdorffExact}, L{HausdorffFlatLocal}, L{HausdorffHubeny},
399 L{HausdorffFlatPolar}, L{HausdorffHaversine}, L{HausdorffKarney}
400 and L{HausdorffVincentys}.
401 '''
402 _wrap = False
404 def __init__(self, points, wrap=False, seed=None, name=NN):
405 '''New L{HausdorffCosineLaw} calculator.
407 @arg points: Initial set of points, aka the C{model} or
408 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
409 C{Tuple2LatLon}[] or C{other}[]).
410 @kwarg wrap: Optionally, wrap and L{pygeodesy.unrollPI}
411 longitudes (C{bool}).
412 @kwarg seed: Random seed (C{any}) or C{None}, C{0} or
413 C{False} for no U{random sampling<https://
414 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
415 @kwarg name: Optional name for this interpolator (C{str}).
417 @raise HausdorffError: Insufficient number of B{C{points}} or
418 invalid B{C{seed}}.
419 '''
420 HausdorffRadians.__init__(self, points, seed=seed, name=name,
421 wrap=wrap)
423 if _FOR_DOCS:
424 directed = Hausdorff.directed
425 symmetric = Hausdorff.symmetric
427 def distance(self, p1, p2):
428 '''Return the L{pygeodesy.cosineLaw_} distance in C{radians}.
429 '''
430 d, _ = unrollPI(p1.lam, p2.lam, wrap=self._wrap)
431 return cosineLaw_(p2.phi, p1.phi, d)
434class HausdorffDistanceTo(Hausdorff):
435 '''Compute the C{Hausdorff} distance based on the distance from the
436 points' C{LatLon.distanceTo} method, conventionally in C{meter}.
438 @see: L{HausdorffCosineAndoyerLambert},
439 L{HausdorffCosineForsytheAndoyerLambert},
440 L{HausdorffExact}, L{HausdorffFlatLocal},
441 L{HausdorffHubeny}, L{HausdorffThomas} and
442 L{HausdorffKarney}.
443 '''
444 _distanceTo_kwds = {}
445 _units = _Str_meter
447 def __init__(self, points, seed=None, name=NN, **distanceTo_kwds):
448 '''New L{HausdorffDistanceTo} calculator.
450 @arg points: Initial set of points, aka the C{model} or
451 C{template} (C{LatLon}[]).
452 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0}
453 or C{False} for no U{random sampling<https://
454 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
455 @kwarg name: Optional name for this interpolator (C{str}).
456 @kwarg distanceTo_kwds: Optional keyword arguments for the
457 B{C{points}}' C{LatLon.distanceTo}
458 method.
460 @raise HausdorffError: Insufficient number of B{C{points}} or
461 an invalid B{C{point}} or B{C{seed}}.
463 @raise ImportError: Package U{geographiclib
464 <https://PyPI.org/project/geographiclib>} missing
465 iff B{C{points}} are L{ellipsoidalKarney.LatLon}s.
467 @note: All C{model}, C{template} and C{target} B{C{points}}
468 I{must} be instances of the same ellipsoidal or
469 spherical C{LatLon} class.
470 '''
471 Hausdorff.__init__(self, points, seed=seed, name=name)
472 if distanceTo_kwds:
473 self._distanceTo_kwds = distanceTo_kwds
475 if _FOR_DOCS:
476 directed = Hausdorff.directed
477 symmetric = Hausdorff.symmetric
479 def distance(self, p1, p2):
480 '''Return the distance in C{meter}.
481 '''
482 return p1.distanceTo(p2, **self._distanceTo_kwds)
484 def _points2(self, points):
485 '''(INTERNAL) Check a set of points.
486 '''
487 np, ps = Hausdorff._points2(self, points)
488 for i, p in enumerate(ps):
489 if not callable(getattr(p, _distanceTo_, None)):
490 i = Fmt.SQUARE(_points_, i)
491 raise HausdorffError(i, p, txt=_distanceTo_)
492 return np, ps
495class HausdorffEquirectangular(HausdorffRadians):
496 '''Compute the C{Hausdorff} distance based on the C{equirectangular} distance
497 in C{radians squared} like function L{pygeodesy.equirectangular_}.
499 @see: L{HausdorffCosineLaw}, L{HausdorffEuclidean}
500 L{HausdorffExact}, L{HausdorffFlatPolar},
501 L{HausdorffHaversine} and L{HausdorffVincentys}.
502 '''
503 _adjust = True
504 _units = _Str_radians2
505 _wrap = False
507 def __init__(self, points, adjust=True, wrap=False, seed=None, name=NN):
508 '''New L{HausdorffEquirectangular} calculator.
510 @arg points: Initial set of points, aka the C{model} or
511 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
512 C{Tuple2LatLon}[] or C{other}[]).
513 @kwarg adjust: Adjust the wrapped, unrolled longitudinal
514 delta by the cosine of the mean latitude (C{bool}).
515 @kwarg wrap: Optionally, wrap and L{pygeodesy.unrollPI} longitudes
516 (C{bool}).
517 @kwarg seed: Random seed (C{any}) or C{None}, C{0} or
518 C{False} for no U{random sampling<https://
519 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
520 @kwarg name: Optional name for this interpolator (C{str}).
522 @raise HausdorffError: Insufficient number of B{C{points}} or
523 invalid B{C{seed}}.
524 '''
525 HausdorffRadians.__init__(self, points, seed=seed, name=name,
526 adjust=adjust, wrap=wrap)
528 if _FOR_DOCS:
529 directed = Hausdorff.directed
530 symmetric = Hausdorff.symmetric
532 def distance(self, p1, p2):
533 '''Return the L{pygeodesy.equirectangular_} distance in C{radians squared}.
534 '''
535 r, _ = unrollPI(p1.lam, p2.lam, wrap=self._wrap)
536 if self._adjust:
537 r *= _scale_rad(p1.phi, p2.phi)
538 return hypot2(r, p2.phi - p1.phi) # like equirectangular_ d2
541class HausdorffEuclidean(HausdorffRadians):
542 '''Compute the C{Hausdorff} distance based on the C{Euclidean}
543 distance in C{radians} from function L{pygeodesy.euclidean_}.
545 @see: L{HausdorffCosineLaw}, L{HausdorffEquirectangular},
546 L{HausdorffExact}, L{HausdorffFlatPolar},
547 L{HausdorffHaversine} and L{HausdorffVincentys}.
548 '''
549 _adjust = True
550 _wrap = True
552 def __init__(self, points, adjust=True, seed=None, name=NN):
553 '''New L{HausdorffEuclidean} calculator.
555 @arg points: Initial set of points, aka the C{model} or
556 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
557 C{Tuple2LatLon}[] or C{other}[]).
558 @kwarg adjust: Adjust the wrapped, unrolled longitudinal
559 delta by the cosine of the mean latitude (C{bool}).
560 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0}
561 or C{False} for no U{random sampling<https://
562 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
563 @kwarg name: Optional name for this interpolator (C{str}).
565 @raise HausdorffError: Insufficient number of B{C{points}} or
566 invalid B{C{seed}}.
567 '''
568 HausdorffRadians.__init__(self, points, seed=seed, name=name,
569 wrap=True)
570 if not adjust:
571 self._adjust = False
573 if _FOR_DOCS:
574 directed = Hausdorff.directed
575 symmetric = Hausdorff.symmetric
577 def distance(self, p1, p2):
578 '''Return the L{pygeodesy.euclidean_} distance in C{radians}.
579 '''
580 return euclidean_(p2.phi, p1.phi, p2.lam - p1.lam, adjust=self._adjust)
583class HausdorffExact(HausdorffDegrees):
584 '''Compute the C{Hausdorff} distance based on the I{angular}
585 distance in C{degrees} from method L{GeodesicExact}C{.Inverse}.
587 @see: L{HausdorffCosineAndoyerLambert},
588 L{HausdorffCosineForsytheAndoyerLambert},
589 L{HausdorffDistanceTo}, L{HausdorffFlatLocal},
590 L{HausdorffHubeny}, L{HausdorffKarney} and
591 L{HausdorffThomas}.
592 '''
593 _datum = _WGS84
594 _Inverse1 = None
595 _units = _Str_degrees
596 _wrap = False
598 def __init__(self, points, datum=None, wrap=False, seed=None, name=NN):
599 '''New L{HausdorffKarney} calculator.
601 @arg points: Initial set of points, aka the C{model} or
602 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
603 C{Tuple2LatLon}[] or C{other}[]).
604 @kwarg datum: Optional datum overriding the default C{Datums.WGS84}
605 and first B{C{points}}' datum (L{Datum}, L{Ellipsoid},
606 L{Ellipsoid2} or L{a_f2Tuple}).
607 @kwarg wrap: Optionally, wrap and L{pygeodesy.unroll180} longitudes
608 (C{bool}).
609 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0}
610 or C{False} for no U{random sampling<https://
611 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
612 @kwarg name: Optional name for this interpolator (C{str}).
614 @raise HausdorffError: Insufficient number of B{C{points}} or
615 invalid B{C{seed}}.
617 @raise ImportError: Package U{geographiclib
618 <https://PyPI.org/project/geographiclib>} missing.
620 @raise TypeError: Invalid B{C{datum}}.
621 '''
622 HausdorffDegrees.__init__(self, points, seed=seed, name=name,
623 wrap=wrap)
624 self._datum_setter(datum)
625 self._Inverse1 = self.datum.ellipsoid.geodesicx.Inverse1 # note -x
627 if _FOR_DOCS:
628 directed = Hausdorff.directed
629 symmetric = Hausdorff.symmetric
631 def distance(self, p1, p2):
632 '''Return the non-negative I{angular} distance in C{degrees}.
633 '''
634 return self._Inverse1(p1.lat, p1.lon, p2.lat, p2.lon, wrap=self._wrap)
637class HausdorffFlatLocal(HausdorffRadians):
638 '''Compute the C{Hausdorff} distance based on the I{angular} distance in
639 C{radians squared} like function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}.
641 @see: L{HausdorffCosineAndoyerLambert},
642 L{HausdorffCosineForsytheAndoyerLambert},
643 L{HausdorffDistanceTo}, L{HausdorffExact},
644 L{HausdorffHubeny}, L{HausdorffThomas} and
645 L{HausdorffKarney}.
646 '''
647 _datum = _WGS84
648 _units = _Str_radians2
649 _wrap = False
651 def __init__(self, points, datum=None, wrap=False, seed=None, name=NN):
652 '''New L{HausdorffFlatLocal}/L{HausdorffHubeny} calculator.
654 @arg points: Initial set of points, aka the C{model} or
655 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
656 C{Tuple2LatLon}[] or C{other}[]).
657 @kwarg datum: Optional datum overriding the default C{Datums.WGS84}
658 and first B{C{points}}' datum (L{Datum}, L{Ellipsoid},
659 L{Ellipsoid2} or L{a_f2Tuple}).
660 @kwarg wrap: Optionally, wrap and L{pygeodesy.unrollPI} longitudes
661 (C{bool}).
662 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0}
663 or C{False} for no U{random sampling<https://
664 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
665 @kwarg name: Optional name for this interpolator (C{str}).
667 @raise HausdorffError: Insufficient number of B{C{points}} or
668 invalid B{C{seed}}.
670 @raise TypeError: Invalid B{C{datum}}.
671 '''
672 HausdorffRadians.__init__(self, points, seed=seed, name=name,
673 wrap=wrap)
674 self._datum_setter(datum)
676 if _FOR_DOCS:
677 directed = Hausdorff.directed
678 symmetric = Hausdorff.symmetric
680 def distance(self, p1, p2):
681 '''Return the L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_} distance
682 in C{radians squared}.
683 '''
684 d, _ = unrollPI(p1.lam, p2.lam, wrap=self._wrap)
685 return self._hubeny_2(p2.phi, p1.phi, d)
687 @Property_RO
688 def _hubeny_2(self):
689 '''(INTERNAL) Get and cache the C{.datum.ellipsoid._hubeny_2} method.
690 '''
691 return self.datum.ellipsoid._hubeny_2
694class HausdorffFlatPolar(HausdorffRadians):
695 '''Compute the C{Hausdorff} distance based on the I{angular}
696 distance in C{radians} from function L{pygeodesy.flatPolar_}.
698 @see: L{HausdorffCosineLaw}, L{HausdorffEquirectangular},
699 L{HausdorffEuclidean}, L{HausdorffExact},
700 L{HausdorffHaversine} and L{HausdorffVincentys}.
701 '''
702 _wrap = False
704 def __init__(self, points, wrap=False, seed=None, name=NN):
705 '''New L{HausdorffFlatPolar} calculator.
707 @arg points: Initial set of points, aka the C{model} or
708 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
709 C{Tuple2LatLon}[] or C{other}[]).
710 @kwarg wrap: Optionally, wrap and L{pygeodesy.unrollPI} longitudes
711 (C{bool}).
712 @kwarg seed: Random seed (C{any}) or C{None}, C{0} or
713 C{False} for no U{random sampling<https://
714 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
715 @kwarg name: Optional name for this interpolator (C{str}).
717 @raise HausdorffError: Insufficient number of B{C{points}} or
718 invalid B{C{seed}}.
719 '''
720 HausdorffRadians.__init__(self, points, seed=seed, name=name,
721 wrap=wrap)
723 if _FOR_DOCS:
724 directed = Hausdorff.directed
725 symmetric = Hausdorff.symmetric
727 def distance(self, p1, p2):
728 '''Return the L{pygeodesy.flatPolar_} distance in C{radians}.
729 '''
730 r, _ = unrollPI(p1.lam, p2.lam, wrap=self._wrap)
731 return flatPolar_(p2.phi, p1.phi, r)
734class HausdorffHaversine(HausdorffRadians):
735 '''Compute the C{Hausdorff} distance based on the I{angular}
736 distance in C{radians} from function L{pygeodesy.haversine_}.
738 @note: See note under L{HausdorffVincentys}.
740 @see: L{HausdorffEquirectangular}, L{HausdorffEuclidean},
741 L{HausdorffExact}, L{HausdorffFlatPolar} and
742 L{HausdorffVincentys}.
743 '''
744 _wrap = False
746 def __init__(self, points, wrap=False, seed=None, name=NN):
747 '''New L{HausdorffHaversine} calculator.
749 @arg points: Initial set of points, aka the C{model} or
750 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
751 C{Tuple2LatLon}[] or C{other}[]).
752 @kwarg wrap: Optionally, wrap and L{pygeodesy.unrollPI}
753 longitudes (C{bool}).
754 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0}
755 or C{False} for no U{random sampling<https://
756 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
757 @kwarg name: Optional name for this interpolator (C{str}).
759 @raise HausdorffError: Insufficient number of B{C{points}} or
760 invalid B{C{seed}}.
761 '''
762 HausdorffRadians.__init__(self, points, seed=seed, name=name,
763 wrap=wrap)
765 if _FOR_DOCS:
766 directed = Hausdorff.directed
767 symmetric = Hausdorff.symmetric
769 def distance(self, p1, p2):
770 '''Return the L{pygeodesy.haversine_} distance in C{radians}.
771 '''
772 r, _ = unrollPI(p1.lam, p2.lam, wrap=self._wrap)
773 return haversine_(p2.phi, p1.phi, r)
776class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny
777 if _FOR_DOCS:
778 __doc__ = HausdorffFlatLocal.__doc__
779 __init__ = HausdorffFlatLocal.__init__
780 directed = HausdorffFlatLocal.directed
781 distance = HausdorffFlatLocal.distance
782 symmetric = HausdorffFlatLocal.symmetric
785class HausdorffKarney(HausdorffExact):
786 '''Compute the C{Hausdorff} distance based on the I{angular}
787 distance in C{degrees} from I{Karney}'s U{geographiclib
788 <https://PyPI.org/project/geographiclib>} U{Geodesic
789 <https://GeographicLib.SourceForge.io/C++/doc/python/code.html>}
790 Inverse method.
792 @see: L{HausdorffCosineAndoyerLambert},
793 L{HausdorffCosineForsytheAndoyerLambert},
794 L{HausdorffDistanceTo}, L{HausdorffExact},
795 L{HausdorffFlatLocal}, L{HausdorffHubeny} and
796 L{HausdorffThomas}.
797 '''
798 def __init__(self, points, datum=None, wrap=False, seed=None, name=NN):
799 '''New L{HausdorffKarney} calculator.
801 @arg points: Initial set of points, aka the C{model} or
802 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
803 C{Tuple2LatLon}[] or C{other}[]).
804 @kwarg datum: Optional datum overriding the default C{Datums.WGS84}
805 and first B{C{points}}' datum (L{Datum}, L{Ellipsoid},
806 L{Ellipsoid2} or L{a_f2Tuple}).
807 @kwarg wrap: Optionally, wrap and L{pygeodesy.unroll180} longitudes
808 (C{bool}).
809 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0}
810 or C{False} for no U{random sampling<https://
811 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
812 @kwarg name: Optional name for this interpolator (C{str}).
814 @raise HausdorffError: Insufficient number of B{C{points}} or
815 invalid B{C{seed}}.
817 @raise ImportError: Package U{geographiclib
818 <https://PyPI.org/project/geographiclib>} missing.
820 @raise TypeError: Invalid B{C{datum}}.
821 '''
822 HausdorffDegrees.__init__(self, points, seed=seed, name=name,
823 wrap=wrap)
824 self._datum_setter(datum)
825 self._Inverse1 = self.datum.ellipsoid.geodesic.Inverse1
828class HausdorffThomas(HausdorffRadians):
829 '''Compute the C{Hausdorff} distance based on the I{angular}
830 distance in C{radians} from function L{pygeodesy.thomas_}.
832 @see: L{HausdorffCosineAndoyerLambert},
833 L{HausdorffCosineForsytheAndoyerLambert},
834 L{HausdorffDistanceTo}, L{HausdorffExact},
835 L{HausdorffFlatLocal}, L{HausdorffHubeny}
836 and L{HausdorffKarney}.
837 '''
838 _datum = _WGS84
839 _wrap = False
841 def __init__(self, points, datum=None, wrap=False, seed=None, name=NN):
842 '''New L{HausdorffThomas} calculator.
844 @arg points: Initial set of points, aka the C{model} or
845 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
846 C{Tuple2LatLon}[] or C{other}[]).
847 @kwarg datum: Optional datum overriding the default C{Datums.WGS84}
848 and first B{C{points}}' datum (L{Datum}, L{Ellipsoid},
849 L{Ellipsoid2} or L{a_f2Tuple}).
850 @kwarg wrap: Optionally, wrap and L{pygeodesy.unrollPI} longitudes
851 (C{bool}).
852 @kwarg seed: Random seed (C{any}) or C{None}, C{0} or
853 C{False} for no U{random sampling<https://
854 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
855 @kwarg name: Optional name for this interpolator (C{str}).
857 @raise HausdorffError: Insufficient number of B{C{points}} or
858 invalid B{C{seed}}.
860 @raise TypeError: Invalid B{C{datum}}.
861 '''
862 HausdorffRadians.__init__(self, points, seed=seed, name=name,
863 wrap=wrap)
864 self._datum_setter(datum)
866 if _FOR_DOCS:
867 directed = Hausdorff.directed
868 symmetric = Hausdorff.symmetric
870 def distance(self, p1, p2):
871 '''Return the L{pygeodesy.thomas_} distance in C{radians}.
872 '''
873 r, _ = unrollPI(p1.lam, p2.lam, wrap=self._wrap)
874 return thomas_(p2.phi, p1.phi, r, datum=self._datum)
877class HausdorffVincentys(HausdorffRadians):
878 '''Compute the C{Hausdorff} distance based on the I{angular}
879 distance in C{radians} from function L{pygeodesy.vincentys_}.
881 @note: See note at function L{pygeodesy.vincentys_}.
883 @see: L{HausdorffCosineLaw}, L{HausdorffEquirectangular},
884 L{HausdorffEuclidean}, L{HausdorffExact},
885 L{HausdorffFlatPolar} and L{HausdorffHaversine}.
886 '''
887 _wrap = False
889 def __init__(self, points, wrap=False, seed=None, name=NN):
890 '''New L{HausdorffVincentys} calculator.
892 @arg points: Initial set of points, aka the C{model} or
893 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
894 C{Tuple2LatLon}[] or C{other}[]).
895 @kwarg wrap: Optionally, wrap and L{pygeodesy.unrollPI}
896 longitudes (C{bool}).
897 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0}
898 or C{False} for no U{random sampling<https://
899 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
900 @kwarg name: Optional name for this interpolator (C{str}).
902 @raise HausdorffError: Insufficient number of B{C{points}} or
903 invalid B{C{seed}}.
904 '''
905 HausdorffRadians.__init__(self, points, seed=seed, name=name,
906 wrap=wrap)
908 if _FOR_DOCS:
909 directed = Hausdorff.directed
910 symmetric = Hausdorff.symmetric
912 def distance(self, p1, p2):
913 '''Return the L{pygeodesy.vincentys_} distance in C{radians}.
914 '''
915 r, _ = unrollPI(p1.lam, p2.lam, wrap=self._wrap)
916 return vincentys_(p2.phi, p1.phi, r)
919def _hausdorff_(ps1, ps2, both, early, seed, units, distance, point):
920 '''(INTERNAL) Core of function L{hausdorff_} and methods C{directed}
921 and C{symmetric} of classes C{hausdorff.Hausdorff...}.
922 '''
923 # shuffling the points generally increases the
924 # chance of an early break in the inner j loop
925 rr = randomrangenerator(seed) if seed else range
927 hd = NINF
928 hi = hj = m = mn = 0
929 md = _0_0
931 # forward or forward and backward
932 for fb in range(2 if both else 1):
933 n = len(ps2)
934 for i in rr(len(ps1)):
935 p1 = point(ps1[i])
936 dh, dj = INF, 0
937 for j in rr(n):
938 p2 = point(ps2[j])
939 d = distance(p1, p2)
940 if early and d < hd:
941 break # early
942 elif d < dh:
943 dh, dj = d, j
944 else: # no early break
945 if hd < dh:
946 hd = dh
947 if fb:
948 hi, hj = dj, i
949 else:
950 hi, hj = i, dj
951 md += dh
952 mn += 1
953 m += 1
954 # swap model and target
955 ps1, ps2 = ps2, ps1
957 md = None if mn < m else (md / float(m))
958 return Hausdorff6Tuple(hd, hi, hj, m, md, units)
961def _point(p):
962 '''Default B{C{point}} callable for function L{hausdorff_}.
964 @arg p: The original C{model} or C{target} point (C{any}).
966 @return: The point, suitable for the L{hausdorff_}
967 B{C{distance}} callable.
968 '''
969 return p
972def hausdorff_(model, target, both=False, early=True, seed=None, units=NN,
973 distance=None, point=_point):
974 '''Compute the C{directed} or C{symmetric} U{Hausdorff
975 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance between 2 sets of points
976 with or without U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
977 and U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
979 @arg model: First set of points (C{LatLon}[], C{Numpy2LatLon}[],
980 C{Tuple2LatLon}[] or C{other}[]).
981 @arg target: Second set of points (C{LatLon}[], C{Numpy2LatLon}[],
982 C{Tuple2LatLon}[] or C{other}[]).
983 @kwarg both: Return the C{directed} (forward only) or the C{symmetric}
984 (combined forward and reverse) C{Hausdorff} distance (C{bool}).
985 @kwarg early: Enable or disable U{early breaking<https://Publik.TUWien.ac.AT/
986 files/PubDat_247739.pdf>} (C{bool}).
987 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False} for no
988 U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
989 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
990 @kwarg distance: Callable returning the distance between a B{C{model}}
991 and B{C{target}} point (signature C{(point1, point2)}).
992 @kwarg point: Callable returning the B{C{model}} or B{C{target}} point
993 suitable for B{C{distance}} (signature C{(point)}).
995 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
997 @raise HausdorffError: Insufficient number of B{C{model}} or B{C{target}} points.
999 @raise TypeError: If B{C{distance}} or B{C{point}} is not callable.
1000 '''
1001 if not callable(distance):
1002 raise _IsnotError(callable.__name__, distance=distance)
1003 if not callable(point):
1004 raise _IsnotError(callable.__name__, point=point)
1006 _, ps1 = points2(model, closed=False, Error=HausdorffError) # PYCHOK non-sequence
1007 _, ps2 = points2(target, closed=False, Error=HausdorffError) # PYCHOK non-sequence
1008 return _hausdorff_(ps1, ps2, both, early, seed, units, distance, point)
1011class Hausdorff6Tuple(_NamedTuple):
1012 '''6-Tuple C{(hd, i, j, mn, md, units)} with the U{Hausdorff
1013 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance C{hd},
1014 indices C{i} and C{j}, the total count C{mn}, the C{I{mean}
1015 Hausdorff} distance C{md} and the class or name of both distance
1016 C{units}.
1018 For C{directed Hausdorff} distances, count C{mn} is the number
1019 of model points considered. For C{symmetric Hausdorff} distances
1020 count C{mn} twice that.
1022 Indices C{i} and C{j} are the C{model} respectively C{target}
1023 point with the C{hd} distance.
1025 Mean distance C{md} is C{None} if an C{early break} occurred and
1026 U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
1027 was enabled by keyword argument C{early=True}.
1028 '''
1029 _Names_ = ('hd', _i_, _j_, 'mn', 'md', _units_)
1030 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass)
1032 def toUnits(self, **Error): # PYCHOK expected
1033 '''Overloaded C{_NamedTuple.toUnits} for C{hd} and C{md} units.
1034 '''
1035 U = _xUnit(self.units, Float) # PYCHOK expected
1036 M = _Pass if self.md is None else U # PYCHOK expected
1037 self._Units_ = (U,) + Hausdorff6Tuple._Units_[1:4] \
1038 + (M,) + Hausdorff6Tuple._Units_[5:]
1039 return _NamedTuple.toUnits(self, **Error)
1042def randomrangenerator(seed):
1043 '''Return a C{seed}ed random range function generator.
1045 @arg seed: Initial, internal L{Random} state (C{hashable}
1046 or C{None}).
1048 @note: L{Random} with C{B{seed} is None} seeds from the
1049 current time or from a platform-specific randomness
1050 source, if available.
1052 @return: A function to generate random ranges.
1054 @example:
1056 >>> rrange = randomrangenerator('R')
1057 >>> for r in rrange(n):
1058 >>> ... # r is random in 0..n-1
1059 '''
1060 R = Random(seed)
1062 def _range(n, *stop_step):
1063 '''Like standard L{range}C{start, stop=..., step=...)},
1064 except the returned values are in random order.
1066 @note: Especially C{range(n)} behaves like standard
1067 L{Random.sample}C{(range(n), n)} but avoids
1068 creating a tuple with the entire C{population}
1069 and a list containing all sample values (for
1070 large C{n}).
1071 '''
1072 if stop_step:
1073 s = range(n, *stop_step)
1075 elif n > 32:
1076 r = R.randrange # Random._randbelow
1077 s = set()
1078 for _ in range(n - 32):
1079 i = r(n)
1080 while i in s:
1081 i = r(n)
1082 s.add(i)
1083 yield i
1084 s = set(range(n)) - s # [i for i in range(n) if i not in s]
1085 else:
1086 s = range(n)
1088 s = list(s)
1089 R.shuffle(s)
1090 while s:
1091 yield s.pop(0)
1093 return _range
1095# **) MIT License
1096#
1097# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
1098#
1099# Permission is hereby granted, free of charge, to any person obtaining a
1100# copy of this software and associated documentation files (the "Software"),
1101# to deal in the Software without restriction, including without limitation
1102# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1103# and/or sell copies of the Software, and to permit persons to whom the
1104# Software is furnished to do so, subject to the following conditions:
1105#
1106# The above copyright notice and this permission notice shall be included
1107# in all copies or substantial portions of the Software.
1108#
1109# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1110# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1111# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1112# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1113# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1114# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1115# OTHER DEALINGS IN THE SOFTWARE.