Coverage for pygeodesy/hausdorff.py: 96%
244 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-25 12:04 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-25 12:04 -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(point1s, ...)}
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(point2s)}
30respectively
32C{t6 = h.symmetric(point2s)}.
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_(point1s, point2s, ..., 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 PointsError, _xattr, _xcallable, _xkwds, _xkwds_get
73import pygeodesy.formy as _formy
74from pygeodesy.interns import NN, _i_, _j_, _units_
75# from pygeodesy.iters import points2 # from .points
76from pygeodesy.lazily import _ALL_LAZY, _FOR_DOCS
77from pygeodesy.named import _name2__, _Named, _NamedTuple, _Pass
78# from pygeodesy.namedTuples import PhiLam2Tuple # from .points
79from pygeodesy.points import _distanceTo, points2 as _points2, PhiLam2Tuple, radians
80from pygeodesy.props import Property_RO, property_doc_, property_RO
81from pygeodesy.units import Float, Number_, _xUnit, _xUnits
82from pygeodesy.unitsBase import _Str_degrees, _Str_degrees2, _Str_meter, _Str_NN, \
83 _Str_radians, _Str_radians2
85# from math import radians # from .points
86from random import Random
88__all__ = _ALL_LAZY.hausdorff
89__version__ = '24.05.24'
92class HausdorffError(PointsError):
93 '''Hausdorff issue.
94 '''
95 pass
98class Hausdorff(_Named):
99 '''Hausdorff base class, requires method L{Hausdorff.distance} to
100 be overloaded.
101 '''
102 _datum = _WGS84
103 _func = None # formy function/property
104 _kwds = {} # func_ options
105 _model = ()
106 _seed = None
107 _units = _Str_NN # XXX Str to _Pass and for backward compatibility
109 def __init__(self, point1s, seed=None, units=NN, **name__kwds):
110 '''New C{Hausdorff...} calculator.
112 @arg point1s: Initial set of points, aka the C{model} or C{template}
113 (C{LatLon}[], C{Numpy2LatLon}[], C{Tuple2LatLon}[] or
114 C{other}[]).
115 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False}
116 for no U{random sampling<https://Publik.TUWien.ac.AT/files/
117 PubDat_247739.pdf>}.
118 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
119 @kwarg name__kwds: Optional calculator/interpolator C{B{name}=NN} (C{str})
120 and keyword arguments for the distance function, retrievable
121 with property C{kwds}.
123 @raise HausdorffError: Insufficient number of B{C{point1s}} or an invalid
124 B{C{point1}}, B{C{seed}} or B{C{units}}.
125 '''
126 name, kwds = _name2__(**name__kwds) # name__=self.__class__
127 if name:
128 self.name = name
130 _, self._model = self._points2(point1s)
131 if seed:
132 self.seed = seed
133 if units: # and not self.units:
134 self.units = units
135 if kwds:
136 self._kwds = kwds
138 @Property_RO
139 def adjust(self):
140 '''Get the adjust setting (C{bool} or C{None} if not applicable).
141 '''
142 return _xkwds_get(self._kwds, adjust=None)
144 @Property_RO
145 def datum(self):
146 '''Get the datum of this calculator (L{Datum} or C{None} if not applicable).
147 '''
148 return self._datum
150 def _datum_setter(self, datum):
151 '''(INTERNAL) Set the datum.
152 '''
153 d = datum or _xattr(self._model[0], datum=datum)
154 if d not in (None, self._datum): # PYCHOK no cover
155 self._datum = _ellipsoidal_datum(d, name=self.name)
157 def directed(self, point2s, early=True):
158 '''Compute only the C{forward Hausdorff} distance.
160 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[],
161 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
162 @kwarg early: Enable or disable U{early breaking<https://
163 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
165 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
167 @raise HausdorffError: Insufficient number of B{C{point2s}} or
168 an invalid B{C{point2}}.
170 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}.
171 '''
172 return self._hausdorff_(point2s, False, early, self.distance)
174 def distance(self, point1, point2):
175 '''Return the distance between B{C{point1}} and B{C{point2s}},
176 subject to the supplied optional keyword arguments, see
177 property C{kwds}.
178 '''
179 return self._func(point1.lat, point1.lon,
180 point2.lat, point2.lon, **self._kwds)
182 @property
183 def _func(self):
184 '''(INTERNAL) I{Must be overloaded}.'''
185 return _formy._Propy(self, 0, _func=None)
187 @_func.setter # PYCHOK setter!
188 def _func(self, func):
189 _formy._Propy(self, 4, _func=func)
191 def _hausdorff_(self, point2s, both, early, distance):
192 _, ps2 = self._points2(point2s)
193 return _hausdorff_(self._model, ps2, both, early, self.seed,
194 self.units, distance, self.point)
196 @property_RO
197 def kwds(self):
198 '''Get the supplied, optional keyword arguments (C{dict}).
199 '''
200 return self._kwds
202 def point(self, point):
203 '''Convert a C{model} or C{target} point for the C{.distance} method.
204 '''
205 return point # pass thru
207 def _points2(self, points):
208 '''(INTERNAL) Check a set of points.
209 '''
210 return _points2(points, closed=False, Error=HausdorffError)
212 @property_doc_(''' the random sampling seed (C{Random}).''')
213 def seed(self):
214 '''Get the random sampling seed (C{any} or C{None}).
215 '''
216 return self._seed
218 @seed.setter # PYCHOK setter!
219 def seed(self, seed):
220 '''Set the random sampling seed (C{Random(seed)}) or
221 C{None}, C{0} or C{False} for no U{random sampling
222 <https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
224 @raise HausdorffError: Invalid B{C{seed}}.
225 '''
226 if seed:
227 try:
228 Random(seed)
229 except (TypeError, ValueError) as x:
230 raise HausdorffError(seed=seed, cause=x)
231 self._seed = seed
232 else:
233 self._seed = None
235 def symmetric(self, point2s, early=True):
236 '''Compute the combined C{forward and reverse Hausdorff} distance.
238 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[],
239 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
240 @kwarg early: Enable or disable U{early breaking<https://
241 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
243 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
245 @raise HausdorffError: Insufficient number of B{C{point2s}} or
246 an invalid B{C{point2}}.
248 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}.
249 '''
250 return self._hausdorff_(point2s, True, early, self.distance)
252 @property_doc_(''' the distance units (C{Unit} or C{str}).''')
253 def units(self):
254 '''Get the distance units (C{Unit} or C{str}).
255 '''
256 return self._units
258 @units.setter # PYCHOK setter!
259 def units(self, units):
260 '''Set the distance units (C{Unit} or C{str}).
262 @raise TypeError: Invalid B{C{units}}.
263 '''
264 self._units = _xUnits(units, Base=Float)
266 @Property_RO
267 def wrap(self):
268 '''Get the wrap setting (C{bool} or C{None} if not applicable).
269 '''
270 return _xkwds_get(self._kwds, adjust=None)
273class HausdorffDegrees(Hausdorff):
274 '''L{Hausdorff} base class for distances from C{LatLon}
275 points in C{degrees}.
276 '''
277 _units = _Str_degrees
279 if _FOR_DOCS:
280 __init__ = Hausdorff.__init__
281 directed = Hausdorff.directed
282 symmetric = Hausdorff.symmetric
284 def distance(self, point1, point2): # PYCHOK no cover
285 '''I{Must be overloaded}.'''
286 self._notOverloaded(point1, point2)
289class HausdorffRadians(Hausdorff):
290 '''L{Hausdorff} base class for distances from C{LatLon}
291 points converted from C{degrees} to C{radians}.
292 '''
293 _units = _Str_radians
295 if _FOR_DOCS:
296 __init__ = Hausdorff.__init__
297 directed = Hausdorff.directed
298 symmetric = Hausdorff.symmetric
300 def distance(self, point1, point2): # PYCHOK no cover
301 '''I{Must be overloaded}.'''
302 self._notOverloaded(point1, point2)
304 def point(self, point):
305 '''Return B{C{point}} as L{PhiLam2Tuple} to maintain
306 I{backward compatibility} of L{HausdorffRadians}.
308 @return: A L{PhiLam2Tuple}C{(phi, lam)}.
309 '''
310 try:
311 return point.philam
312 except AttributeError:
313 return PhiLam2Tuple(radians(point.lat), radians(point.lon))
316class _HausdorffMeterRadians(Hausdorff):
317 '''(INTERNAL) Returning C{meter} or C{radians} depending on
318 the optional keyword arguments supplied at instantiation
319 of the C{Hausdorff*} sub-class.
320 '''
321 _units = _Str_meter
322 _units_ = _Str_radians
324 def directed(self, point2s, early=True):
325 '''Overloaded method L{Hausdorff.directed} to determine
326 the distance function and units from the optional
327 keyword arguments given at this instantiation, see
328 property C{kwds}.
330 @see: L{Hausdorff.directed} for other details.
331 '''
332 return self._hausdorff_(point2s, False, early, _formy._radistance(self))
334 def symmetric(self, point2s, early=True):
335 '''Overloaded method L{Hausdorff.symmetric} to determine
336 the distance function and units from the optional
337 keyword arguments given at this instantiation, see
338 property C{kwds}.
340 @see: L{Hausdorff.symmetric} for other details.
341 '''
342 return self._hausdorff_(point2s, True, early, _formy._radistance(self))
344 @property
345 def _func_(self):
346 '''(INTERNAL) I{Must be overloaded}.'''
347 return _formy._Propy(self, 0, _func_=None)
349 @_func_.setter # PYCHOK setter!
350 def _func_(self, func):
351 _formy._Propy(self, 3,_func_=func)
354class HausdorffCosineAndoyerLambert(_HausdorffMeterRadians):
355 '''Compute the C{Hausdorff} distance based on the I{angular} distance
356 in C{radians} from function L{pygeodesy.cosineAndoyerLambert}.
357 '''
358 def __init__(self, point1s, **seed_name__datum_wrap):
359 '''New L{HausdorffCosineAndoyerLambert} calculator.
361 @kwarg seed_name__datum_wrap: Optional C{B{seed}=None} and
362 C{B{name}=NN} and keyword arguments for
363 function L{pygeodesy.cosineAndoyerLambert}.
365 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
366 B{C{seed}}, B{C{name}} and other exceptions.
367 '''
368 Hausdorff.__init__(self, point1s, **seed_name__datum_wrap)
369 self._func = _formy.cosineAndoyerLambert
370 self._func_ = _formy.cosineAndoyerLambert_
372 if _FOR_DOCS:
373 directed = Hausdorff.directed
374 symmetric = Hausdorff.symmetric
377class HausdorffCosineForsytheAndoyerLambert(_HausdorffMeterRadians):
378 '''Compute the C{Hausdorff} distance based on the I{angular} distance
379 in C{radians} from function L{pygeodesy.cosineForsytheAndoyerLambert}.
380 '''
381 def __init__(self, point1s, **seed_name__datum_wrap):
382 '''New L{HausdorffCosineForsytheAndoyerLambert} calculator.
384 @kwarg seed_name__datum_wrap: Optional C{B{seed}=None} and
385 C{B{name}=NN} and keyword arguments for function
386 L{pygeodesy.cosineForsytheAndoyerLambert}.
388 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
389 B{C{seed}}, B{C{name}} and other exceptions.
390 '''
391 Hausdorff.__init__(self, point1s, **seed_name__datum_wrap)
392 self._func = _formy.cosineForsytheAndoyerLambert
393 self._func_ = _formy.cosineForsytheAndoyerLambert_
395 if _FOR_DOCS:
396 directed = Hausdorff.directed
397 symmetric = Hausdorff.symmetric
400class HausdorffCosineLaw(_HausdorffMeterRadians):
401 '''Compute the C{Hausdorff} distance based on the I{angular}
402 distance in C{radians} from function L{pygeodesy.cosineLaw_}.
404 @note: See note at function L{pygeodesy.vincentys_}.
405 '''
406 def __init__(self, point1s, **seed_name__radius_wrap):
407 '''New L{HausdorffCosineLaw} calculator.
409 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None} and
410 C{B{name}=NN} and keyword arguments for
411 function L{pygeodesy.cosineLaw}.
413 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
414 B{C{seed}}, B{C{name}} and other exceptions.
415 '''
416 Hausdorff.__init__(self, point1s, **seed_name__radius_wrap)
417 self._func = _formy.cosineLaw
418 self._func_ = _formy.cosineLaw_
420 if _FOR_DOCS:
421 directed = Hausdorff.directed
422 symmetric = Hausdorff.symmetric
425class HausdorffDistanceTo(Hausdorff):
426 '''Compute the C{Hausdorff} distance based on the distance from the
427 points' C{LatLon.distanceTo} method, conventionally in C{meter}.
428 '''
429 _units = _Str_meter
431 def __init__(self, point1s, **seed_name__distanceTo_kwds):
432 '''New L{HausdorffDistanceTo} calculator.
434 @kwarg seed_name__distanceTo_kwds: Optional C{B{seed}=None} and
435 C{B{name}=NN} and keyword arguments for each
436 B{C{point1s}}' C{LatLon.distanceTo} method.
438 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
439 B{C{seed}}, B{C{name}} and other exceptions.
441 @note: All C{model}, C{template} and C{target} B{C{points}}
442 I{must} be instances of the same ellipsoidal or
443 spherical C{LatLon} class.
444 '''
445 Hausdorff.__init__(self, point1s, **seed_name__distanceTo_kwds)
447 if _FOR_DOCS:
448 directed = Hausdorff.directed
449 symmetric = Hausdorff.symmetric
451 def distance(self, p1, p2):
452 '''Return the distance in C{meter}.
453 '''
454 return p1.distanceTo(p2, **self._kwds)
456 def _points2(self, points):
457 '''(INTERNAL) Check a set of points.
458 '''
459 np, ps = Hausdorff._points2(self, points)
460 return np, _distanceTo(HausdorffError, points=ps)
463class HausdorffEquirectangular(Hausdorff):
464 '''Compute the C{Hausdorff} distance based on the C{equirectangular} distance
465 in C{radians squared} like function L{pygeodesy.equirectangular_}.
466 '''
467 _units = _Str_degrees2
469 def __init__(self, point1s, **seed_name__adjust_limit_wrap):
470 '''New L{HausdorffEquirectangular} calculator.
472 @kwarg seed_name__adjust_limit_wrap: Optional C{B{seed}=None} and
473 C{B{name}=NN} and keyword arguments for function
474 L{pygeodesy.equirectangular_} I{with default}
475 C{B{limit}=0} for I{backward compatibility}.
477 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
478 B{C{seed}}, B{C{name}} and other exceptions.
479 '''
480 Hausdorff.__init__(self, point1s, **_xkwds(seed_name__adjust_limit_wrap,
481 limit=0))
482 self._func = _formy._equirectangular # helper
484 if _FOR_DOCS:
485 directed = Hausdorff.directed
486 symmetric = Hausdorff.symmetric
489class HausdorffEuclidean(_HausdorffMeterRadians):
490 '''Compute the C{Hausdorff} distance based on the C{Euclidean}
491 distance in C{radians} from function L{pygeodesy.euclidean_}.
492 '''
493 def __init__(self, point1s, **seed_name__adjust_radius_wrap):
494 '''New L{HausdorffEuclidean} calculator.
496 @kwarg seed_name__adjust_radius_wrap: Optional C{B{seed}=None}
497 and C{B{name}=NN} and keyword arguments for
498 function L{pygeodesy.euclidean}.
500 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
501 B{C{seed}}, B{C{name}} and other exceptions.
502 '''
503 Hausdorff.__init__(self, point1s, **seed_name__adjust_radius_wrap)
504 self._func = _formy.euclidean
505 self._func_ = _formy.euclidean_
507 if _FOR_DOCS:
508 directed = Hausdorff.directed
509 symmetric = Hausdorff.symmetric
512class HausdorffExact(Hausdorff):
513 '''Compute the C{Hausdorff} distance based on the I{angular}
514 distance in C{degrees} from method L{GeodesicExact}C{.Inverse}.
515 '''
516 _units = _Str_degrees
518 def __init__(self, point1s, datum=None, **seed_name__wrap):
519 '''New L{HausdorffKarney} calculator.
521 @kwarg datum: Datum to override the default C{Datums.WGS84} and first
522 B{C{point1s}}' datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2}
523 or L{a_f2Tuple}).
524 @kwarg seed_name__wrap: Optional C{B{seed}=None} and C{B{name}=NN} and
525 keyword argument for method C{Inverse1} of class
526 L{geodesicx.GeodesicExact}.
528 @raise TypeError: Invalid B{C{datum}}.
530 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, B{C{seed}},
531 B{C{name}} and other exceptions.
532 '''
533 Hausdorff.__init__(self, point1s, **seed_name__wrap)
534 self._datum_setter(datum)
535 self._func = self.datum.ellipsoid.geodesicx.Inverse1 # note -x
537 if _FOR_DOCS:
538 directed = Hausdorff.directed
539 symmetric = Hausdorff.symmetric
542class HausdorffFlatLocal(_HausdorffMeterRadians):
543 '''Compute the C{Hausdorff} distance based on the I{angular} distance in
544 C{radians squared} like function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}.
545 '''
546 _units = _Str_radians2
548 def __init__(self, point1s, **seed_name__datum_scaled_wrap):
549 '''New L{HausdorffFlatLocal}/L{HausdorffHubeny} calculator.
551 @kwarg seed_name__datum_scaled_wrap: Optional C{B{seed}=None} and
552 C{B{name}=NN} and keyword arguments for function
553 L{pygeodesy.flatLocal}.
555 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
556 B{C{seed}}, B{C{name}} and other exceptions.
558 @note: The distance C{units} are C{radians squared}, not C{radians}.
559 '''
560 Hausdorff.__init__(self, point1s, **seed_name__datum_scaled_wrap)
561 self._func = _formy.flatLocal
562 self._func_ = self.datum.ellipsoid._hubeny_2
564 if _FOR_DOCS:
565 directed = Hausdorff.directed
566 symmetric = Hausdorff.symmetric
569class HausdorffFlatPolar(_HausdorffMeterRadians):
570 '''Compute the C{Hausdorff} distance based on the I{angular}
571 distance in C{radians} from function L{pygeodesy.flatPolar_}.
572 '''
573 _wrap = False
575 def __init__(self, points, **seed_name__radius_wrap):
576 '''New L{HausdorffFlatPolar} calculator.
578 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
579 and C{B{name}=NN} and keyword arguments
580 for function L{pygeodesy.flatPolar}.
582 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
583 B{C{seed}}, B{C{name}} and other exceptions.
584 '''
585 Hausdorff.__init__(self, points, **seed_name__radius_wrap)
586 self._func = _formy.flatPolar
587 self._func_ = _formy.flatPolar_
589 if _FOR_DOCS:
590 directed = Hausdorff.directed
591 symmetric = Hausdorff.symmetric
594class HausdorffHaversine(_HausdorffMeterRadians):
595 '''Compute the C{Hausdorff} distance based on the I{angular}
596 distance in C{radians} from function L{pygeodesy.haversine_}.
598 @note: See note under L{HausdorffVincentys}.
599 '''
600 _wrap = False
602 def __init__(self, points, **seed_name__radius_wrap):
603 '''New L{HausdorffHaversine} calculator.
605 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
606 and C{B{name}=NN} and keyword arguments
607 for function L{pygeodesy.haversine}.
609 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
610 B{C{seed}}, B{C{name}} and other exceptions.
611 '''
612 Hausdorff.__init__(self, points, **seed_name__radius_wrap)
613 self._func = _formy.haversine
614 self._func_ = _formy.haversine_
616 if _FOR_DOCS:
617 directed = Hausdorff.directed
618 symmetric = Hausdorff.symmetric
621class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny
622 if _FOR_DOCS:
623 __doc__ = HausdorffFlatLocal.__doc__
624 __init__ = HausdorffFlatLocal.__init__
625 directed = HausdorffFlatLocal.directed
626 distance = HausdorffFlatLocal.distance
627 symmetric = HausdorffFlatLocal.symmetric
630class HausdorffKarney(Hausdorff):
631 '''Compute the C{Hausdorff} distance based on the I{angular}
632 distance in C{degrees} from I{Karney}'s U{geographiclib
633 <https://PyPI.org/project/geographiclib>} U{Geodesic
634 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}
635 Inverse method.
636 '''
637 _units = _Str_degrees
639 def __init__(self, point1s, datum=None, **seed_name__wrap):
640 '''New L{HausdorffKarney} calculator.
642 @kwarg datum: Datum to override the default C{Datums.WGS84} and
643 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid},
644 L{Ellipsoid2} or L{a_f2Tuple}).
645 @kwarg seed_name__wrap: Optional C{B{seed}=None} and C{B{name}=NN}
646 and keyword arguments for method C{Inverse1} of
647 class L{geodesicw.Geodesic}.
649 @raise ImportError: Package U{geographiclib
650 <https://PyPI.org/project/geographiclib>} missing.
652 @raise TypeError: Invalid B{C{datum}}.
654 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
655 B{C{seed}}, B{C{name}} and other exceptions.
656 '''
657 Hausdorff.__init__(self, point1s, **seed_name__wrap)
658 self._datum_setter(datum)
659 self._func = self.datum.ellipsoid.geodesic.Inverse1
662class HausdorffThomas(_HausdorffMeterRadians):
663 '''Compute the C{Hausdorff} distance based on the I{angular}
664 distance in C{radians} from function L{pygeodesy.thomas_}.
665 '''
666 def __init__(self, point1s, **seed_name__datum_wrap):
667 '''New L{HausdorffThomas} calculator.
669 @kwarg seed_name__datum_wrap: Optional C{B{seed}=None}
670 and C{B{name}=NN} and keyword arguments
671 for function L{pygeodesy.thomas}.
673 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
674 B{C{seed}}, B{C{name}} and other exceptions.
675 '''
676 Hausdorff.__init__(self, point1s, **seed_name__datum_wrap)
677 self._func = _formy.thomas
678 self._func_ = _formy.thomas_
680 if _FOR_DOCS:
681 directed = Hausdorff.directed
682 symmetric = Hausdorff.symmetric
685class HausdorffVincentys(_HausdorffMeterRadians):
686 '''Compute the C{Hausdorff} distance based on the I{angular}
687 distance in C{radians} from function L{pygeodesy.vincentys_}.
689 @note: See note at function L{pygeodesy.vincentys_}.
690 '''
691 _wrap = False
693 def __init__(self, point1s, **seed_name__radius_wrap):
694 '''New L{HausdorffVincentys} calculator.
696 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
697 and C{B{name}=NN} and keyword arguments
698 for function L{pygeodesy.vincentys}.
700 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
701 B{C{seed}}, B{C{name}} and other exceptions.
702 '''
703 Hausdorff.__init__(self, point1s, **seed_name__radius_wrap)
704 self._func = _formy.vincentys
705 self._func_ = _formy.vincentys_
707 if _FOR_DOCS:
708 directed = Hausdorff.directed
709 symmetric = Hausdorff.symmetric
712def _hausdorff_(ps1, ps2, both, early, seed, units, distance, point):
713 '''(INTERNAL) Core of function L{hausdorff_} and methods C{directed}
714 and C{symmetric} of classes C{hausdorff.Hausdorff...}.
715 '''
716 # shuffling the points generally increases the
717 # chance of an early break in the inner j loop
718 rr = randomrangenerator(seed) if seed else range
720 hd = NINF
721 hi = hj = m = mn = 0
722 md = _0_0
724 # forward or forward and backward
725 for fb in range(2 if both else 1):
726 n = len(ps2)
727 for i in rr(len(ps1)):
728 p1 = point(ps1[i])
729 dh, dj = INF, 0
730 for j in rr(n):
731 p2 = point(ps2[j])
732 d = distance(p1, p2)
733 if early and d < hd:
734 break # early
735 elif d < dh:
736 dh, dj = d, j
737 else: # no early break
738 if hd < dh:
739 hd = dh
740 if fb:
741 hi, hj = dj, i
742 else:
743 hi, hj = i, dj
744 md += dh
745 mn += 1
746 m += 1
747 # swap model and target
748 ps1, ps2 = ps2, ps1
750 md = None if mn < m else (md / float(m))
751 return Hausdorff6Tuple(hd, hi, hj, m, md, units)
754def _point(p):
755 '''Default B{C{point}} callable for function L{hausdorff_}.
757 @arg p: The original C{model} or C{target} point (C{any}).
759 @return: The point, suitable for the L{hausdorff_}
760 B{C{distance}} callable.
761 '''
762 return p
765def hausdorff_(model, target, both=False, early=True, seed=None, units=NN,
766 distance=None, point=_point):
767 '''Compute the C{directed} or C{symmetric} U{Hausdorff
768 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance between 2 sets of points
769 with or without U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
770 and U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
772 @arg model: First set of points (C{LatLon}[], C{Numpy2LatLon}[],
773 C{Tuple2LatLon}[] or C{other}[]).
774 @arg target: Second set of points (C{LatLon}[], C{Numpy2LatLon}[],
775 C{Tuple2LatLon}[] or C{other}[]).
776 @kwarg both: Return the C{directed} (forward only) or the C{symmetric}
777 (combined forward and reverse) C{Hausdorff} distance (C{bool}).
778 @kwarg early: Enable or disable U{early breaking<https://Publik.TUWien.ac.AT/
779 files/PubDat_247739.pdf>} (C{bool}).
780 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False} for no
781 U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
782 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
783 @kwarg distance: Callable returning the distance between a B{C{model}}
784 and B{C{target}} point (signature C{(point1, point2)}).
785 @kwarg point: Callable returning the B{C{model}} or B{C{target}} point
786 suitable for B{C{distance}} (signature C{(point)}).
788 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
790 @raise HausdorffError: Insufficient number of B{C{model}} or B{C{target}} points.
792 @raise TypeError: If B{C{distance}} or B{C{point}} is not callable.
793 '''
794 _xcallable(distance=distance, point=point)
796 _, ps1 = _points2(model, closed=False, Error=HausdorffError) # PYCHOK non-sequence
797 _, ps2 = _points2(target, closed=False, Error=HausdorffError) # PYCHOK non-sequence
798 return _hausdorff_(ps1, ps2, both, early, seed, units, distance, point)
801class Hausdorff6Tuple(_NamedTuple):
802 '''6-Tuple C{(hd, i, j, mn, md, units)} with the U{Hausdorff
803 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance C{hd},
804 indices C{i} and C{j}, the total count C{mn}, the C{I{mean}
805 Hausdorff} distance C{md} and the class or name of both distance
806 C{units}.
808 For C{directed Hausdorff} distances, count C{mn} is the number
809 of model points considered. For C{symmetric Hausdorff} distances
810 count C{mn} twice that.
812 Indices C{i} and C{j} are the C{model} respectively C{target}
813 point with the C{hd} distance.
815 Mean distance C{md} is C{None} if an C{early break} occurred and
816 U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
817 was enabled by keyword argument C{early=True}.
818 '''
819 _Names_ = ('hd', _i_, _j_, 'mn', 'md', _units_)
820 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass)
822 def toUnits(self, **Error): # PYCHOK expected
823 '''Overloaded C{_NamedTuple.toUnits} for C{hd} and C{md} units.
824 '''
825 U = _xUnit(self.units, Float) # PYCHOK expected
826 M = _Pass if self.md is None else U # PYCHOK expected
827 self._Units_ = (U,) + Hausdorff6Tuple._Units_[1:4] \
828 + (M,) + Hausdorff6Tuple._Units_[5:]
829 return _NamedTuple.toUnits(self, **Error)
832def randomrangenerator(seed):
833 '''Return a C{seed}ed random range function generator.
835 @arg seed: Initial, internal L{Random} state (C{hashable}
836 or C{None}).
838 @note: L{Random} with C{B{seed} is None} seeds from the
839 current time or from a platform-specific randomness
840 source, if available.
842 @return: A function to generate random ranges.
844 @example:
846 >>> rrange = randomrangenerator('R')
847 >>> for r in rrange(n):
848 >>> ... # r is random in 0..n-1
849 '''
850 R = Random(seed)
852 def _range(n, *stop_step):
853 '''Like standard L{range}C{start, stop=..., step=...)},
854 except the returned values are in random order.
856 @note: Especially C{range(n)} behaves like standard
857 L{Random.sample}C{(range(n), n)} but avoids
858 creating a tuple with the entire C{population}
859 and a list containing all sample values (for
860 large C{n}).
861 '''
862 if stop_step:
863 s = range(n, *stop_step)
865 elif n > 32:
866 r = R.randrange # Random._randbelow
867 s = set()
868 for _ in range(n - 32):
869 i = r(n)
870 while i in s:
871 i = r(n)
872 s.add(i)
873 yield i
874 s = set(range(n)) - s # [i for i in range(n) if i not in s]
875 else:
876 s = range(n)
878 s = list(s)
879 R.shuffle(s)
880 while s:
881 yield s.pop(0)
883 return _range
885# **) MIT License
886#
887# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
888#
889# Permission is hereby granted, free of charge, to any person obtaining a
890# copy of this software and associated documentation files (the "Software"),
891# to deal in the Software without restriction, including without limitation
892# the rights to use, copy, modify, merge, publish, distribute, sublicense,
893# and/or sell copies of the Software, and to permit persons to whom the
894# Software is furnished to do so, subject to the following conditions:
895#
896# The above copyright notice and this permission notice shall be included
897# in all copies or substantial portions of the Software.
898#
899# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
900# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
901# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
902# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
903# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
904# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
905# OTHER DEALINGS IN THE SOFTWARE.