Coverage for pygeodesy/hausdorff.py: 95%
235 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-25 13:15 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-25 13:15 -0400
2# -*- coding: utf-8 -*-
4u'''Hausdorff distances.
6Classes L{Hausdorff}, L{HausdorffDegrees}, L{HausdorffRadians},
7L{HausdorffCosineLaw}, L{HausdorffDistanceTo}, L{HausdorffEquirectangular},
8L{HausdorffEuclidean}, L{HausdorffFlatLocal}, L{HausdorffFlatPolar},
9L{HausdorffHaversine}, L{HausdorffHubeny}, L{HausdorffKarney},
10L{HausdorffThomas} and L{HausdorffVincentys} to compute U{Hausdorff
11<https://WikiPedia.org/wiki/Hausdorff_distance>} distances between two
12sets of C{LatLon}, C{NumPy}, C{tuples} or other types of points.
14Only L{HausdorffDistanceTo} -iff used with L{ellipsoidalKarney.LatLon}
15points- and L{HausdorffKarney} requires installation of I{Charles Karney}'s
16U{geographiclib<https://PyPI.org/project/geographiclib>}.
18Typical usage is as follows. First, create a C{Hausdorff} calculator
19from a given set of C{LatLon} points, called the C{model} or C{template}
20points.
22C{h = HausdorffXyz(point1s, ...)}
24Get the C{directed} or C{symmetric} Hausdorff distance to a second set
25of C{LatLon} points, named the C{target} points, by using
27C{t6 = h.directed(point2s)}
29respectively
31C{t6 = h.symmetric(point2s)}.
33Or, use function C{hausdorff_} with a proper C{distance} function and
34optionally a C{point} function passed as keyword arguments as follows
36C{t6 = hausdorff_(point1s, point2s, ..., distance=..., point=...)}.
38In all cases, the returned result C{t6} is a L{Hausdorff6Tuple}.
40For C{(lat, lon, ...)} points in a C{NumPy} array or plain C{tuples},
41wrap the points in a L{Numpy2LatLon} respectively L{Tuple2LatLon}
42instance, more details in the documentation thereof.
44For other points, create a L{Hausdorff} sub-class with the appropriate
45C{distance} method overloading L{Hausdorff.distance} and optionally a
46C{point} method overriding L{Hausdorff.point} as the next example.
48 >>> from pygeodesy import Hausdorff, hypot_
49 >>>
50 >>> class H3D(Hausdorff):
51 >>> """Custom Hausdorff example.
52 >>> """
53 >>> def distance(self, p1, p2):
54 >>> return hypot_(p1.x - p2.x, p1.y - p2.y, p1.z - p2.z)
55 >>>
56 >>> h3D = H3D(xyz1, ..., units="...")
57 >>> d6 = h3D.directed(xyz2)
59Transcribed from the original SciPy U{Directed Hausdorff Code
60<https://GitHub.com/scipy/scipy/blob/master/scipy/spatial/_hausdorff.pyx>}
61version 0.19.0, Copyright (C) Tyler Reddy, Richard Gowers, and Max Linke,
622016, distributed under the same BSD license as SciPy, including C{early
63breaking} and C{random sampling} as in U{Abdel Aziz Taha, Allan Hanbury
64"An Efficient Algorithm for Calculating the Exact Hausdorff Distance"
65<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}, IEEE Trans. Pattern
66Analysis Machine Intelligence (PAMI), vol 37, no 11, pp 2153-2163, Nov 2015.
67'''
69# from pygeodesy.basics import _isin # from .datums
70from pygeodesy.constants import INF, NINF, _0_0
71from pygeodesy.datums import _ellipsoidal_datum, _WGS84, _isin
72from pygeodesy.errors import PointsError, _xattr, _xcallable, _xkwds, _xkwds_get
73# from pygeodesy import formy as _formy # _MODS.into
74from pygeodesy.interns import NN, _i_, _j_, _units_
75# from pygeodesy.iters import points2 as _points2 # from .points
76from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _FOR_DOCS
77from pygeodesy.named import _name2__, _Named, _NamedTuple, _Pass
78# from pygeodesy.namedTuples import PhiLam2Tuple # from .points
79from pygeodesy.points import _distanceTo, PhiLam2Tuple, points2 as _points2, radians
80from pygeodesy.props import Property, Property_RO, property_doc_, property_RO
81from pygeodesy.units import Float, Number_
82from pygeodesy import unitsBase as _unitsBase # _Str_..., _xUnit, _xUnits
84# from math import radians # from .points
85from random import Random
87__all__ = _ALL_LAZY.hausdorff
88__version__ = '25.04.21'
90_formy = _MODS.into(formy=__name__)
93class HausdorffError(PointsError):
94 '''Hausdorff issue.
95 '''
96 pass
99class Hausdorff(_Named):
100 '''Hausdorff base class, requires method L{Hausdorff.distance} to
101 be overloaded.
102 '''
103 _datum = _WGS84
104 _func = None # formy function/property
105 _kwds = {} # func_ options
106 _model = ()
107 _seed = None
108 _units = _unitsBase._Str_NN # XXX Str to _Pass and for backward compatibility
110 def __init__(self, point1s, seed=None, units=NN, **name__kwds):
111 '''New C{Hausdorff...} calculator.
113 @arg point1s: Initial set of points, aka the C{model} or C{template}
114 (C{LatLon}[], C{Numpy2LatLon}[], C{Tuple2LatLon}[] or
115 C{other}[]).
116 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False}
117 for no U{random sampling<https://Publik.TUWien.ac.AT/files/
118 PubDat_247739.pdf>}.
119 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
120 @kwarg name__kwds: Optional calculator/interpolator C{B{name}=NN} (C{str})
121 and keyword arguments for the distance function, retrievable
122 with property C{kwds}.
124 @raise HausdorffError: Insufficient number of B{C{point1s}} or an invalid
125 B{C{point1}}, B{C{seed}} or B{C{units}}.
126 '''
127 name, kwds = _name2__(**name__kwds) # name__=self.__class__
128 if name:
129 self.name = name
131 _, self._model = self._points2(point1s)
132 if seed:
133 self.seed = seed
134 if units: # and not self.units:
135 self.units = units
136 if kwds:
137 self._kwds = kwds
139 @Property_RO
140 def adjust(self):
141 '''Get the adjust setting (C{bool} or C{None} if not applicable).
142 '''
143 return _xkwds_get(self._kwds, adjust=None)
145 @Property_RO
146 def datum(self):
147 '''Get the datum of this calculator (L{Datum} or C{None} if not applicable).
148 '''
149 return self._datum
151 def _datum_setter(self, datum):
152 '''(INTERNAL) Set the datum.
153 '''
154 d = datum or _xattr(self._model[0], datum=datum)
155 if not _isin(d, None, self._datum): # PYCHOK no cover
156 self._datum = _ellipsoidal_datum(d, name=self.name)
158 def directed(self, point2s, early=True):
159 '''Compute only the C{forward Hausdorff} distance.
161 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[],
162 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
163 @kwarg early: Enable or disable U{early breaking<https://
164 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
166 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
168 @raise HausdorffError: Insufficient number of B{C{point2s}} or
169 an invalid B{C{point2}}.
171 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}.
172 '''
173 return self._hausdorff_(point2s, False, early, self.distance)
175 def distance(self, point1, point2):
176 '''Return the distance between B{C{point1}} and B{C{point2}},
177 subject to the supplied optional keyword arguments, see
178 property C{kwds}.
179 '''
180 return self._func(point1.lat, point1.lon,
181 point2.lat, point2.lon, **self._kwds)
183 @Property
184 def _func(self):
185 '''(INTERNAL) I{Must be overloaded}.'''
186 self._notOverloaded(**self.kwds)
188 @_func.setter_ # PYCHOK setter_underscore!
189 def _func(self, func):
190 return _formy._Propy(func, 4, self.kwds)
192 def _hausdorff_(self, point2s, both, early, distance):
193 _, ps2 = self._points2(point2s)
194 return _hausdorff_(self._model, ps2, both, early, self.seed,
195 self.units, distance, self.point)
197 @property_RO
198 def kwds(self):
199 '''Get the supplied, optional keyword arguments (C{dict}).
200 '''
201 return self._kwds
203 def point(self, point):
204 '''Convert a C{model} or C{target} point for the C{.distance} method.
205 '''
206 return point # pass thru
208 def _points2(self, points):
209 '''(INTERNAL) Check a set of points.
210 '''
211 return _points2(points, closed=False, Error=HausdorffError)
213 @property_doc_(''' the random sampling seed (C{Random}).''')
214 def seed(self):
215 '''Get the random sampling seed (C{any} or C{None}).
216 '''
217 return self._seed
219 @seed.setter # PYCHOK setter!
220 def seed(self, seed):
221 '''Set the random sampling seed (C{Random(seed)}) or
222 C{None}, C{0} or C{False} for no U{random sampling
223 <https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
225 @raise HausdorffError: Invalid B{C{seed}}.
226 '''
227 if seed:
228 try:
229 Random(seed)
230 except (TypeError, ValueError) as x:
231 raise HausdorffError(seed=seed, cause=x)
232 self._seed = seed
233 else:
234 self._seed = None
236 def symmetric(self, point2s, early=True):
237 '''Compute the combined C{forward and reverse Hausdorff} distance.
239 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[],
240 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
241 @kwarg early: Enable or disable U{early breaking<https://
242 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
244 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
246 @raise HausdorffError: Insufficient number of B{C{point2s}} or
247 an invalid B{C{point2}}.
249 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}.
250 '''
251 return self._hausdorff_(point2s, True, early, self.distance)
253 @property_doc_(''' the distance units (C{Unit} or C{str}).''')
254 def units(self):
255 '''Get the distance units (C{Unit} or C{str}).
256 '''
257 return self._units
259 @units.setter # PYCHOK setter!
260 def units(self, units):
261 '''Set the distance units (C{Unit} or C{str}).
263 @raise TypeError: Invalid B{C{units}}.
264 '''
265 self._units = _unitsBase._xUnits(units, Base=Float)
267 @Property_RO
268 def wrap(self):
269 '''Get the wrap setting (C{bool} or C{None} if not applicable).
270 '''
271 return _xkwds_get(self._kwds, adjust=None)
274class HausdorffDegrees(Hausdorff):
275 '''L{Hausdorff} base class for distances from C{LatLon}
276 points in C{degrees}.
277 '''
278 _units = _unitsBase._Str_degrees
280 if _FOR_DOCS:
281 __init__ = Hausdorff.__init__
282 directed = Hausdorff.directed
283 symmetric = Hausdorff.symmetric
285 def distance(self, point1, point2): # PYCHOK no cover
286 '''I{Must be overloaded}.'''
287 self._notOverloaded(point1, point2)
290class HausdorffRadians(Hausdorff):
291 '''L{Hausdorff} base class for distances from C{LatLon}
292 points converted from C{degrees} to C{radians}.
293 '''
294 _units = _unitsBase._Str_radians
296 if _FOR_DOCS:
297 __init__ = Hausdorff.__init__
298 directed = Hausdorff.directed
299 symmetric = Hausdorff.symmetric
301 def distance(self, point1, point2): # PYCHOK no cover
302 '''I{Must be overloaded}.'''
303 self._notOverloaded(point1, point2)
305 def point(self, point):
306 '''Return B{C{point}} as L{PhiLam2Tuple} to maintain
307 I{backward compatibility} of L{HausdorffRadians}.
309 @return: A L{PhiLam2Tuple}C{(phi, lam)}.
310 '''
311 try:
312 return point.philam
313 except AttributeError:
314 return PhiLam2Tuple(radians(point.lat), radians(point.lon))
317class _HausdorffMeterRadians(Hausdorff):
318 '''(INTERNAL) Returning C{meter} or C{radians} depending on
319 the optional keyword arguments supplied at instantiation
320 of the C{Hausdorff*} sub-class.
321 '''
322 _units = _unitsBase._Str_meter
323 _units_ = _unitsBase._Str_radians
325 def directed(self, point2s, early=True):
326 '''Overloaded method L{Hausdorff.directed} to determine
327 the distance function and units from the optional
328 keyword arguments given at this instantiation, see
329 property C{kwds}.
331 @see: L{Hausdorff.directed} for other details.
332 '''
333 return self._hausdorff_(point2s, False, early, _formy._radistance(self))
335 def symmetric(self, point2s, early=True):
336 '''Overloaded method L{Hausdorff.symmetric} to determine
337 the distance function and units from the optional
338 keyword arguments given at this instantiation, see
339 property C{kwds}.
341 @see: L{Hausdorff.symmetric} for other details.
342 '''
343 return self._hausdorff_(point2s, True, early, _formy._radistance(self))
345 @Property
346 def _func_(self): # see _formy._radistance
347 '''(INTERNAL) I{Must be overloaded}.'''
348 self._notOverloaded(**self.kwds)
350 @_func_.setter_ # PYCHOK setter_underscore!
351 def _func_(self, func):
352 return _formy._Propy(func, 3, self.kwds)
355class HausdorffCosineLaw(_HausdorffMeterRadians):
356 '''Compute the C{Hausdorff} distance with function L{pygeodesy.cosineLaw_}.
358 @note: See note at function L{pygeodesy.vincentys_}.
359 '''
360 def __init__(self, point1s, **seed_name__corr_earth_wrap):
361 '''New L{HausdorffCosineLaw} calculator.
363 @kwarg seed_name__corr_earth_wrap: Optional C{B{seed}=None} and
364 C{B{name}=NN} and keyword arguments for function
365 L{pygeodesy.cosineLaw}.
367 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
368 B{C{seed}}, B{C{name}} and other exceptions.
369 '''
370 Hausdorff.__init__(self, point1s, **seed_name__corr_earth_wrap)
371 self._func = _formy.cosineLaw
372 self._func_ = _formy.cosineLaw_
374 if _FOR_DOCS:
375 directed = Hausdorff.directed
376 symmetric = Hausdorff.symmetric
379class HausdorffDistanceTo(Hausdorff):
380 '''Compute the C{Hausdorff} distance the points' C{LatLon.distanceTo} method.
381 '''
382 _units = _unitsBase._Str_meter
384 def __init__(self, point1s, **seed_name__distanceTo_kwds):
385 '''New L{HausdorffDistanceTo} calculator.
387 @kwarg seed_name__distanceTo_kwds: Optional C{B{seed}=None} and
388 C{B{name}=NN} and keyword arguments for each
389 B{C{point1s}}' C{LatLon.distanceTo} method.
391 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
392 B{C{seed}}, B{C{name}} and other exceptions.
394 @note: All C{model}, C{template} and C{target} B{C{points}}
395 I{must} be instances of the same ellipsoidal or
396 spherical C{LatLon} class.
397 '''
398 Hausdorff.__init__(self, point1s, **seed_name__distanceTo_kwds)
400 if _FOR_DOCS:
401 directed = Hausdorff.directed
402 symmetric = Hausdorff.symmetric
404 def distance(self, p1, p2):
405 '''Return the distance in C{meter}.
406 '''
407 return p1.distanceTo(p2, **self._kwds)
409 def _points2(self, points):
410 '''(INTERNAL) Check a set of points.
411 '''
412 np, ps = Hausdorff._points2(self, points)
413 return np, _distanceTo(HausdorffError, points=ps)
416class HausdorffEquirectangular(Hausdorff):
417 '''Compute the C{Hausdorff} distance with function L{pygeodesy.equirectangular}.
418 '''
419 _units = _unitsBase._Str_degrees2
421 def __init__(self, point1s, **seed_name__adjust_limit_wrap):
422 '''New L{HausdorffEquirectangular} calculator.
424 @kwarg seed_name__adjust_limit_wrap: Optional C{B{seed}=None} and
425 C{B{name}=NN} and keyword arguments for function
426 L{pygeodesy.equirectangular} I{with default}
427 C{B{limit}=0} for I{backward compatibility}.
429 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
430 B{C{seed}}, B{C{name}} and other exceptions.
431 '''
432 Hausdorff.__init__(self, point1s, **_xkwds(seed_name__adjust_limit_wrap,
433 limit=0))
434 self._func = _formy._equirectangular # helper
436 if _FOR_DOCS:
437 directed = Hausdorff.directed
438 symmetric = Hausdorff.symmetric
441class HausdorffEuclidean(_HausdorffMeterRadians):
442 '''Compute the C{Hausdorff} distance with function L{pygeodesy.euclidean_}.
443 '''
444 def __init__(self, point1s, **seed_name__adjust_radius_wrap):
445 '''New L{HausdorffEuclidean} calculator.
447 @kwarg seed_name__adjust_radius_wrap: Optional C{B{seed}=None}
448 and C{B{name}=NN} and keyword arguments for
449 function L{pygeodesy.euclidean}.
451 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
452 B{C{seed}}, B{C{name}} and other exceptions.
453 '''
454 Hausdorff.__init__(self, point1s, **seed_name__adjust_radius_wrap)
455 self._func = _formy.euclidean
456 self._func_ = _formy.euclidean_
458 if _FOR_DOCS:
459 directed = Hausdorff.directed
460 symmetric = Hausdorff.symmetric
463class HausdorffExact(Hausdorff):
464 '''Compute the C{Hausdorff} distance with method L{GeodesicExact}C{.Inverse}.
465 '''
466 _units = _unitsBase._Str_degrees
468 def __init__(self, point1s, datum=None, **seed_name__wrap):
469 '''New L{HausdorffKarney} calculator.
471 @kwarg datum: Datum to override the default C{Datums.WGS84} and first
472 B{C{point1s}}' datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2}
473 or L{a_f2Tuple}).
474 @kwarg seed_name__wrap: Optional C{B{seed}=None} and C{B{name}=NN} and
475 keyword argument for method C{Inverse1} of class
476 L{geodesicx.GeodesicExact}.
478 @raise TypeError: Invalid B{C{datum}}.
480 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, B{C{seed}},
481 B{C{name}} and other exceptions.
482 '''
483 Hausdorff.__init__(self, point1s, **seed_name__wrap)
484 self._datum_setter(datum)
485 self._func = self.datum.ellipsoid.geodesicx.Inverse1 # note -x
487 if _FOR_DOCS:
488 directed = Hausdorff.directed
489 symmetric = Hausdorff.symmetric
492class HausdorffFlatLocal(_HausdorffMeterRadians):
493 '''Compute the C{Hausdorff} distance with function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}.
494 '''
495 _units = _unitsBase._Str_radians2
497 def __init__(self, point1s, **seed_name__datum_scaled_wrap):
498 '''New L{HausdorffFlatLocal}/L{HausdorffHubeny} calculator.
500 @kwarg seed_name__datum_scaled_wrap: Optional C{B{seed}=None} and
501 C{B{name}=NN} and keyword arguments for function
502 L{pygeodesy.flatLocal}.
504 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
505 B{C{seed}}, B{C{name}} and other exceptions.
507 @note: The distance C{units} are C{radians squared}, not C{radians}.
508 '''
509 Hausdorff.__init__(self, point1s, **seed_name__datum_scaled_wrap)
510 self._func = _formy.flatLocal
511 self._func_ = self.datum.ellipsoid._hubeny_2
513 if _FOR_DOCS:
514 directed = Hausdorff.directed
515 symmetric = Hausdorff.symmetric
518class HausdorffFlatPolar(_HausdorffMeterRadians):
519 '''Compute the C{Hausdorff} distance with function L{pygeodesy.flatPolar_}.
520 '''
521 _wrap = False
523 def __init__(self, points, **seed_name__radius_wrap):
524 '''New L{HausdorffFlatPolar} calculator.
526 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
527 and C{B{name}=NN} and keyword arguments
528 for function L{pygeodesy.flatPolar}.
530 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
531 B{C{seed}}, B{C{name}} and other exceptions.
532 '''
533 Hausdorff.__init__(self, points, **seed_name__radius_wrap)
534 self._func = _formy.flatPolar
535 self._func_ = _formy.flatPolar_
537 if _FOR_DOCS:
538 directed = Hausdorff.directed
539 symmetric = Hausdorff.symmetric
542class HausdorffHaversine(_HausdorffMeterRadians):
543 '''Compute the C{Hausdorff} distance with function L{pygeodesy.haversine_}.
545 @note: See note under L{HausdorffVincentys}.
546 '''
547 _wrap = False
549 def __init__(self, points, **seed_name__radius_wrap):
550 '''New L{HausdorffHaversine} calculator.
552 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
553 and C{B{name}=NN} and keyword arguments
554 for function L{pygeodesy.haversine}.
556 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
557 B{C{seed}}, B{C{name}} and other exceptions.
558 '''
559 Hausdorff.__init__(self, points, **seed_name__radius_wrap)
560 self._func = _formy.haversine
561 self._func_ = _formy.haversine_
563 if _FOR_DOCS:
564 directed = Hausdorff.directed
565 symmetric = Hausdorff.symmetric
568class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny
569 if _FOR_DOCS:
570 __doc__ = HausdorffFlatLocal.__doc__
571 __init__ = HausdorffFlatLocal.__init__
572 directed = HausdorffFlatLocal.directed
573 distance = HausdorffFlatLocal.distance
574 symmetric = HausdorffFlatLocal.symmetric
577class HausdorffKarney(Hausdorff):
578 '''Compute the C{Hausdorff} distance with I{Karney}'s U{geographiclib
579 <https://PyPI.org/project/geographiclib>} U{geodesic.Geodesic
580 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}C{.Inverse}
581 method.
582 '''
583 _units = _unitsBase._Str_degrees
585 def __init__(self, point1s, datum=None, **seed_name__wrap):
586 '''New L{HausdorffKarney} calculator.
588 @kwarg datum: Datum to override the default C{Datums.WGS84} and
589 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid},
590 L{Ellipsoid2} or L{a_f2Tuple}).
591 @kwarg seed_name__wrap: Optional C{B{seed}=None} and C{B{name}=NN}
592 and keyword arguments for method C{Inverse1} of
593 class L{geodesicw.Geodesic}.
595 @raise ImportError: Package U{geographiclib
596 <https://PyPI.org/project/geographiclib>} missing.
598 @raise TypeError: Invalid B{C{datum}}.
600 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
601 B{C{seed}}, B{C{name}} and other exceptions.
602 '''
603 Hausdorff.__init__(self, point1s, **seed_name__wrap)
604 self._datum_setter(datum)
605 self._func = self.datum.ellipsoid.geodesic.Inverse1
608class HausdorffThomas(_HausdorffMeterRadians):
609 '''Compute the C{Hausdorff} distance with function L{pygeodesy.thomas_}.
610 '''
611 def __init__(self, point1s, **seed_name__datum_wrap):
612 '''New L{HausdorffThomas} calculator.
614 @kwarg seed_name__datum_wrap: Optional C{B{seed}=None}
615 and C{B{name}=NN} and keyword arguments
616 for function L{pygeodesy.thomas}.
618 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
619 B{C{seed}}, B{C{name}} and other exceptions.
620 '''
621 Hausdorff.__init__(self, point1s, **seed_name__datum_wrap)
622 self._func = _formy.thomas
623 self._func_ = _formy.thomas_
625 if _FOR_DOCS:
626 directed = Hausdorff.directed
627 symmetric = Hausdorff.symmetric
630class HausdorffVincentys(_HausdorffMeterRadians):
631 '''Compute the C{Hausdorff} distance with function L{pygeodesy.vincentys_}.
633 @note: See note at function L{pygeodesy.vincentys_}.
634 '''
635 _wrap = False
637 def __init__(self, point1s, **seed_name__radius_wrap):
638 '''New L{HausdorffVincentys} calculator.
640 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
641 and C{B{name}=NN} and keyword arguments
642 for function L{pygeodesy.vincentys}.
644 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
645 B{C{seed}}, B{C{name}} and other exceptions.
646 '''
647 Hausdorff.__init__(self, point1s, **seed_name__radius_wrap)
648 self._func = _formy.vincentys
649 self._func_ = _formy.vincentys_
651 if _FOR_DOCS:
652 directed = Hausdorff.directed
653 symmetric = Hausdorff.symmetric
656def _hausdorff_(ps1, ps2, both, early, seed, units, distance, point):
657 '''(INTERNAL) Core of function L{hausdorff_} and methods C{directed}
658 and C{symmetric} of classes C{hausdorff.Hausdorff...}.
659 '''
660 # shuffling the points generally increases the
661 # chance of an early break in the inner j loop
662 rr = randomrangenerator(seed) if seed else range
664 hd = NINF
665 mn = m = hi = hj = 0
666 md = _0_0
668 # forward or forward and backward
669 for fb in range(2 if both else 1):
670 ji, n = False, len(ps2)
671 for i in rr(len(ps1)):
672 p1 = point(ps1[i])
673 dh, dj = INF, 0
674 for j in rr(n):
675 p2 = point(ps2[j])
676 d = distance(p1, p2)
677 if early and d < hd:
678 break # early
679 elif d < dh:
680 dh, dj = d, j
681 else: # no early break
682 if hd < dh:
683 hd = dh
684 hi = i
685 hj = dj
686 ji = fb
687 md += dh
688 mn += 1
689 m += 1
690 if ji: # swap indices
691 hi, hj = hj, hi
692 # swap model and target
693 ps1, ps2 = ps2, ps1
695 md = None if mn < m else (md / float(m))
696 return Hausdorff6Tuple(hd, hi, hj, m, md, units)
699def _point(p):
700 '''Default B{C{point}} callable for function L{hausdorff_}.
702 @arg p: The original C{model} or C{target} point (C{any}).
704 @return: The point, suitable for the L{hausdorff_}
705 B{C{distance}} callable.
706 '''
707 return p
710def hausdorff_(model, target, both=False, early=True, seed=None, units=NN,
711 distance=None, point=_point):
712 '''Compute the C{directed} or C{symmetric} U{Hausdorff
713 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance between 2 sets of points
714 with or without U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
715 and U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
717 @arg model: First set of points (C{LatLon}[], C{Numpy2LatLon}[],
718 C{Tuple2LatLon}[] or C{other}[]).
719 @arg target: Second set of points (C{LatLon}[], C{Numpy2LatLon}[],
720 C{Tuple2LatLon}[] or C{other}[]).
721 @kwarg both: Return the C{directed} (forward only) or the C{symmetric}
722 (combined forward and reverse) C{Hausdorff} distance (C{bool}).
723 @kwarg early: Enable or disable U{early breaking<https://Publik.TUWien.ac.AT/
724 files/PubDat_247739.pdf>} (C{bool}).
725 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False} for no
726 U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
727 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
728 @kwarg distance: Callable returning the distance between a B{C{model}}
729 and B{C{target}} point (signature C{(point1, point2)}).
730 @kwarg point: Callable returning the B{C{model}} or B{C{target}} point
731 suitable for B{C{distance}} (signature C{(point)}).
733 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
735 @raise HausdorffError: Insufficient number of B{C{model}} or B{C{target}} points.
737 @raise TypeError: If B{C{distance}} or B{C{point}} is not callable.
738 '''
739 _xcallable(distance=distance, point=point)
741 _, ps1 = _points2(model, closed=False, Error=HausdorffError) # PYCHOK non-sequence
742 _, ps2 = _points2(target, closed=False, Error=HausdorffError) # PYCHOK non-sequence
743 return _hausdorff_(ps1, ps2, both, early, seed, units, distance, point)
746class Hausdorff6Tuple(_NamedTuple):
747 '''6-Tuple C{(hd, i, j, mn, md, units)} with the U{Hausdorff
748 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance C{hd},
749 indices C{i} and C{j}, the total count C{mn}, the C{I{mean}
750 Hausdorff} distance C{md} and the class or name of both distance
751 C{units}.
753 For C{directed Hausdorff} distances, count C{mn} is the number
754 of model points considered. For C{symmetric Hausdorff} distances
755 count C{mn} twice that.
757 Indices C{i} and C{j} are the C{model} respectively C{target}
758 point with the C{hd} distance.
760 Mean distance C{md} is C{None} if an C{early break} occurred and
761 U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
762 was enabled by keyword argument C{early=True}.
763 '''
764 _Names_ = ('hd', _i_, _j_, 'mn', 'md', _units_)
765 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass)
767 def toUnits(self, **Error_name): # PYCHOK expected
768 '''Overloaded C{_NamedTuple.toUnits} for C{hd} and C{md} units.
769 '''
770 u = list(Hausdorff6Tuple._Units_)
771 u[0] = U = _unitsBase._xUnit(self.units, Float) # PYCHOK expected
772 u[4] = _Pass if self.md is None else U # PYCHOK expected
773 return _NamedTuple.toUnits(self.reUnit(*u), **Error_name) # PYCHOK self
776def randomrangenerator(seed):
777 '''Return a C{seed}ed random range function generator.
779 @arg seed: Initial, internal L{Random} state (C{hashable}
780 or C{None}).
782 @note: L{Random} with C{B{seed} is None} seeds from the
783 current time or from a platform-specific randomness
784 source, if available.
786 @return: A function to generate random ranges.
788 @example:
790 >>> rrange = randomrangenerator('R')
791 >>> for r in rrange(n):
792 >>> ... # r is random in 0..n-1
793 '''
794 R = Random(seed)
796 def _range(n, *stop_step):
797 '''Like standard L{range}C{start, stop=..., step=...)},
798 except the returned values are in random order.
800 @note: Especially C{range(n)} behaves like standard
801 L{Random.sample}C{(range(n), n)} but avoids
802 creating a tuple with the entire C{population}
803 and a list containing all sample values (for
804 large C{n}).
805 '''
806 if stop_step:
807 s = range(n, *stop_step)
809 elif n > 32:
810 r = R.randrange # Random._randbelow
811 s = set()
812 for _ in range(n - 32):
813 i = r(n)
814 while i in s:
815 i = r(n)
816 s.add(i)
817 yield i
818 s = set(range(n)) - s # [i for i in range(n) if i not in s]
819 else:
820 s = range(n)
822 s = list(s)
823 R.shuffle(s)
824 while s:
825 yield s.pop(0)
827 return _range
829# **) MIT License
830#
831# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
832#
833# Permission is hereby granted, free of charge, to any person obtaining a
834# copy of this software and associated documentation files (the "Software"),
835# to deal in the Software without restriction, including without limitation
836# the rights to use, copy, modify, merge, publish, distribute, sublicense,
837# and/or sell copies of the Software, and to permit persons to whom the
838# Software is furnished to do so, subject to the following conditions:
839#
840# The above copyright notice and this permission notice shall be included
841# in all copies or substantial portions of the Software.
842#
843# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
844# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
845# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
846# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
847# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
848# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
849# OTHER DEALINGS IN THE SOFTWARE.