Coverage for pygeodesy/hausdorff.py: 95%
233 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-09 11:05 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-09 11:05 -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'''
69from pygeodesy.constants import INF, NINF, _0_0
70from pygeodesy.datums import _ellipsoidal_datum, _WGS84
71from pygeodesy.errors import PointsError, _xattr, _xcallable, _xkwds, _xkwds_get
72import pygeodesy.formy as _formy
73from pygeodesy.interns import NN, _i_, _j_, _units_
74# from pygeodesy.iters import points2 as _points # from .points
75from pygeodesy.lazily import _ALL_LAZY, _FOR_DOCS
76from pygeodesy.named import _name2__, _Named, _NamedTuple, _Pass
77# from pygeodesy.namedTuples import PhiLam2Tuple # from .points
78from pygeodesy.points import _distanceTo, PhiLam2Tuple, points2 as _points2, radians
79from pygeodesy.props import Property, Property_RO, property_doc_, property_RO
80from pygeodesy.units import Float, Number_
81from pygeodesy import unitsBase as _unitsBase # _Str_..., _xUnit, _xUnits
83# from math import radians # from .points
84from random import Random
86__all__ = _ALL_LAZY.hausdorff
87__version__ = '24.12.31'
90class HausdorffError(PointsError):
91 '''Hausdorff issue.
92 '''
93 pass
96class Hausdorff(_Named):
97 '''Hausdorff base class, requires method L{Hausdorff.distance} to
98 be overloaded.
99 '''
100 _datum = _WGS84
101 _func = None # formy function/property
102 _kwds = {} # func_ options
103 _model = ()
104 _seed = None
105 _units = _unitsBase._Str_NN # XXX Str to _Pass and for backward compatibility
107 def __init__(self, point1s, seed=None, units=NN, **name__kwds):
108 '''New C{Hausdorff...} calculator.
110 @arg point1s: Initial set of points, aka the C{model} or C{template}
111 (C{LatLon}[], C{Numpy2LatLon}[], C{Tuple2LatLon}[] or
112 C{other}[]).
113 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False}
114 for no U{random sampling<https://Publik.TUWien.ac.AT/files/
115 PubDat_247739.pdf>}.
116 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
117 @kwarg name__kwds: Optional calculator/interpolator C{B{name}=NN} (C{str})
118 and keyword arguments for the distance function, retrievable
119 with property C{kwds}.
121 @raise HausdorffError: Insufficient number of B{C{point1s}} or an invalid
122 B{C{point1}}, B{C{seed}} or B{C{units}}.
123 '''
124 name, kwds = _name2__(**name__kwds) # name__=self.__class__
125 if name:
126 self.name = name
128 _, self._model = self._points2(point1s)
129 if seed:
130 self.seed = seed
131 if units: # and not self.units:
132 self.units = units
133 if kwds:
134 self._kwds = kwds
136 @Property_RO
137 def adjust(self):
138 '''Get the adjust setting (C{bool} or C{None} if not applicable).
139 '''
140 return _xkwds_get(self._kwds, adjust=None)
142 @Property_RO
143 def datum(self):
144 '''Get the datum of this calculator (L{Datum} or C{None} if not applicable).
145 '''
146 return self._datum
148 def _datum_setter(self, datum):
149 '''(INTERNAL) Set the datum.
150 '''
151 d = datum or _xattr(self._model[0], datum=datum)
152 if d not in (None, self._datum): # PYCHOK no cover
153 self._datum = _ellipsoidal_datum(d, name=self.name)
155 def directed(self, point2s, early=True):
156 '''Compute only the C{forward Hausdorff} distance.
158 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[],
159 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
160 @kwarg early: Enable or disable U{early breaking<https://
161 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
163 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
165 @raise HausdorffError: Insufficient number of B{C{point2s}} or
166 an invalid B{C{point2}}.
168 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}.
169 '''
170 return self._hausdorff_(point2s, False, early, self.distance)
172 def distance(self, point1, point2):
173 '''Return the distance between B{C{point1}} and B{C{point2}},
174 subject to the supplied optional keyword arguments, see
175 property C{kwds}.
176 '''
177 return self._func(point1.lat, point1.lon,
178 point2.lat, point2.lon, **self._kwds)
180 @Property
181 def _func(self):
182 '''(INTERNAL) I{Must be overloaded}.'''
183 self._notOverloaded(**self.kwds)
185 @_func.setter_ # PYCHOK setter_underscore!
186 def _func(self, func):
187 return _formy._Propy(func, 4, self.kwds)
189 def _hausdorff_(self, point2s, both, early, distance):
190 _, ps2 = self._points2(point2s)
191 return _hausdorff_(self._model, ps2, both, early, self.seed,
192 self.units, distance, self.point)
194 @property_RO
195 def kwds(self):
196 '''Get the supplied, optional keyword arguments (C{dict}).
197 '''
198 return self._kwds
200 def point(self, point):
201 '''Convert a C{model} or C{target} point for the C{.distance} method.
202 '''
203 return point # pass thru
205 def _points2(self, points):
206 '''(INTERNAL) Check a set of points.
207 '''
208 return _points2(points, closed=False, Error=HausdorffError)
210 @property_doc_(''' the random sampling seed (C{Random}).''')
211 def seed(self):
212 '''Get the random sampling seed (C{any} or C{None}).
213 '''
214 return self._seed
216 @seed.setter # PYCHOK setter!
217 def seed(self, seed):
218 '''Set the random sampling seed (C{Random(seed)}) or
219 C{None}, C{0} or C{False} for no U{random sampling
220 <https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
222 @raise HausdorffError: Invalid B{C{seed}}.
223 '''
224 if seed:
225 try:
226 Random(seed)
227 except (TypeError, ValueError) as x:
228 raise HausdorffError(seed=seed, cause=x)
229 self._seed = seed
230 else:
231 self._seed = None
233 def symmetric(self, point2s, early=True):
234 '''Compute the combined C{forward and reverse Hausdorff} distance.
236 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[],
237 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
238 @kwarg early: Enable or disable U{early breaking<https://
239 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
241 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
243 @raise HausdorffError: Insufficient number of B{C{point2s}} or
244 an invalid B{C{point2}}.
246 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}.
247 '''
248 return self._hausdorff_(point2s, True, early, self.distance)
250 @property_doc_(''' the distance units (C{Unit} or C{str}).''')
251 def units(self):
252 '''Get the distance units (C{Unit} or C{str}).
253 '''
254 return self._units
256 @units.setter # PYCHOK setter!
257 def units(self, units):
258 '''Set the distance units (C{Unit} or C{str}).
260 @raise TypeError: Invalid B{C{units}}.
261 '''
262 self._units = _unitsBase._xUnits(units, Base=Float)
264 @Property_RO
265 def wrap(self):
266 '''Get the wrap setting (C{bool} or C{None} if not applicable).
267 '''
268 return _xkwds_get(self._kwds, adjust=None)
271class HausdorffDegrees(Hausdorff):
272 '''L{Hausdorff} base class for distances from C{LatLon}
273 points in C{degrees}.
274 '''
275 _units = _unitsBase._Str_degrees
277 if _FOR_DOCS:
278 __init__ = Hausdorff.__init__
279 directed = Hausdorff.directed
280 symmetric = Hausdorff.symmetric
282 def distance(self, point1, point2): # PYCHOK no cover
283 '''I{Must be overloaded}.'''
284 self._notOverloaded(point1, point2)
287class HausdorffRadians(Hausdorff):
288 '''L{Hausdorff} base class for distances from C{LatLon}
289 points converted from C{degrees} to C{radians}.
290 '''
291 _units = _unitsBase._Str_radians
293 if _FOR_DOCS:
294 __init__ = Hausdorff.__init__
295 directed = Hausdorff.directed
296 symmetric = Hausdorff.symmetric
298 def distance(self, point1, point2): # PYCHOK no cover
299 '''I{Must be overloaded}.'''
300 self._notOverloaded(point1, point2)
302 def point(self, point):
303 '''Return B{C{point}} as L{PhiLam2Tuple} to maintain
304 I{backward compatibility} of L{HausdorffRadians}.
306 @return: A L{PhiLam2Tuple}C{(phi, lam)}.
307 '''
308 try:
309 return point.philam
310 except AttributeError:
311 return PhiLam2Tuple(radians(point.lat), radians(point.lon))
314class _HausdorffMeterRadians(Hausdorff):
315 '''(INTERNAL) Returning C{meter} or C{radians} depending on
316 the optional keyword arguments supplied at instantiation
317 of the C{Hausdorff*} sub-class.
318 '''
319 _units = _unitsBase._Str_meter
320 _units_ = _unitsBase._Str_radians
322 def directed(self, point2s, early=True):
323 '''Overloaded method L{Hausdorff.directed} to determine
324 the distance function and units from the optional
325 keyword arguments given at this instantiation, see
326 property C{kwds}.
328 @see: L{Hausdorff.directed} for other details.
329 '''
330 return self._hausdorff_(point2s, False, early, _formy._radistance(self))
332 def symmetric(self, point2s, early=True):
333 '''Overloaded method L{Hausdorff.symmetric} to determine
334 the distance function and units from the optional
335 keyword arguments given at this instantiation, see
336 property C{kwds}.
338 @see: L{Hausdorff.symmetric} for other details.
339 '''
340 return self._hausdorff_(point2s, True, early, _formy._radistance(self))
342 @Property
343 def _func_(self): # see _formy._radistance
344 '''(INTERNAL) I{Must be overloaded}.'''
345 self._notOverloaded(**self.kwds)
347 @_func_.setter_ # PYCHOK setter_underscore!
348 def _func_(self, func):
349 return _formy._Propy(func, 3, self.kwds)
352class HausdorffCosineLaw(_HausdorffMeterRadians):
353 '''Compute the C{Hausdorff} distance with function L{pygeodesy.cosineLaw_}.
355 @note: See note at function L{pygeodesy.vincentys_}.
356 '''
357 def __init__(self, point1s, **seed_name__corr_earth_wrap):
358 '''New L{HausdorffCosineLaw} calculator.
360 @kwarg seed_name__corr_earth_wrap: Optional C{B{seed}=None} and
361 C{B{name}=NN} and keyword arguments for function
362 L{pygeodesy.cosineLaw}.
364 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
365 B{C{seed}}, B{C{name}} and other exceptions.
366 '''
367 Hausdorff.__init__(self, point1s, **seed_name__corr_earth_wrap)
368 self._func = _formy.cosineLaw
369 self._func_ = _formy.cosineLaw_
371 if _FOR_DOCS:
372 directed = Hausdorff.directed
373 symmetric = Hausdorff.symmetric
376class HausdorffDistanceTo(Hausdorff):
377 '''Compute the C{Hausdorff} distance the points' C{LatLon.distanceTo} method.
378 '''
379 _units = _unitsBase._Str_meter
381 def __init__(self, point1s, **seed_name__distanceTo_kwds):
382 '''New L{HausdorffDistanceTo} calculator.
384 @kwarg seed_name__distanceTo_kwds: Optional C{B{seed}=None} and
385 C{B{name}=NN} and keyword arguments for each
386 B{C{point1s}}' C{LatLon.distanceTo} method.
388 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
389 B{C{seed}}, B{C{name}} and other exceptions.
391 @note: All C{model}, C{template} and C{target} B{C{points}}
392 I{must} be instances of the same ellipsoidal or
393 spherical C{LatLon} class.
394 '''
395 Hausdorff.__init__(self, point1s, **seed_name__distanceTo_kwds)
397 if _FOR_DOCS:
398 directed = Hausdorff.directed
399 symmetric = Hausdorff.symmetric
401 def distance(self, p1, p2):
402 '''Return the distance in C{meter}.
403 '''
404 return p1.distanceTo(p2, **self._kwds)
406 def _points2(self, points):
407 '''(INTERNAL) Check a set of points.
408 '''
409 np, ps = Hausdorff._points2(self, points)
410 return np, _distanceTo(HausdorffError, points=ps)
413class HausdorffEquirectangular(Hausdorff):
414 '''Compute the C{Hausdorff} distance with function L{pygeodesy.equirectangular}.
415 '''
416 _units = _unitsBase._Str_degrees2
418 def __init__(self, point1s, **seed_name__adjust_limit_wrap):
419 '''New L{HausdorffEquirectangular} calculator.
421 @kwarg seed_name__adjust_limit_wrap: Optional C{B{seed}=None} and
422 C{B{name}=NN} and keyword arguments for function
423 L{pygeodesy.equirectangular} I{with default}
424 C{B{limit}=0} for I{backward compatibility}.
426 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
427 B{C{seed}}, B{C{name}} and other exceptions.
428 '''
429 Hausdorff.__init__(self, point1s, **_xkwds(seed_name__adjust_limit_wrap,
430 limit=0))
431 self._func = _formy._equirectangular # helper
433 if _FOR_DOCS:
434 directed = Hausdorff.directed
435 symmetric = Hausdorff.symmetric
438class HausdorffEuclidean(_HausdorffMeterRadians):
439 '''Compute the C{Hausdorff} distance with function L{pygeodesy.euclidean_}.
440 '''
441 def __init__(self, point1s, **seed_name__adjust_radius_wrap):
442 '''New L{HausdorffEuclidean} calculator.
444 @kwarg seed_name__adjust_radius_wrap: Optional C{B{seed}=None}
445 and C{B{name}=NN} and keyword arguments for
446 function L{pygeodesy.euclidean}.
448 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
449 B{C{seed}}, B{C{name}} and other exceptions.
450 '''
451 Hausdorff.__init__(self, point1s, **seed_name__adjust_radius_wrap)
452 self._func = _formy.euclidean
453 self._func_ = _formy.euclidean_
455 if _FOR_DOCS:
456 directed = Hausdorff.directed
457 symmetric = Hausdorff.symmetric
460class HausdorffExact(Hausdorff):
461 '''Compute the C{Hausdorff} distance with method L{GeodesicExact}C{.Inverse}.
462 '''
463 _units = _unitsBase._Str_degrees
465 def __init__(self, point1s, datum=None, **seed_name__wrap):
466 '''New L{HausdorffKarney} calculator.
468 @kwarg datum: Datum to override the default C{Datums.WGS84} and first
469 B{C{point1s}}' datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2}
470 or L{a_f2Tuple}).
471 @kwarg seed_name__wrap: Optional C{B{seed}=None} and C{B{name}=NN} and
472 keyword argument for method C{Inverse1} of class
473 L{geodesicx.GeodesicExact}.
475 @raise TypeError: Invalid B{C{datum}}.
477 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, B{C{seed}},
478 B{C{name}} and other exceptions.
479 '''
480 Hausdorff.__init__(self, point1s, **seed_name__wrap)
481 self._datum_setter(datum)
482 self._func = self.datum.ellipsoid.geodesicx.Inverse1 # note -x
484 if _FOR_DOCS:
485 directed = Hausdorff.directed
486 symmetric = Hausdorff.symmetric
489class HausdorffFlatLocal(_HausdorffMeterRadians):
490 '''Compute the C{Hausdorff} distance with function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}.
491 '''
492 _units = _unitsBase._Str_radians2
494 def __init__(self, point1s, **seed_name__datum_scaled_wrap):
495 '''New L{HausdorffFlatLocal}/L{HausdorffHubeny} calculator.
497 @kwarg seed_name__datum_scaled_wrap: Optional C{B{seed}=None} and
498 C{B{name}=NN} and keyword arguments for function
499 L{pygeodesy.flatLocal}.
501 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
502 B{C{seed}}, B{C{name}} and other exceptions.
504 @note: The distance C{units} are C{radians squared}, not C{radians}.
505 '''
506 Hausdorff.__init__(self, point1s, **seed_name__datum_scaled_wrap)
507 self._func = _formy.flatLocal
508 self._func_ = self.datum.ellipsoid._hubeny_2
510 if _FOR_DOCS:
511 directed = Hausdorff.directed
512 symmetric = Hausdorff.symmetric
515class HausdorffFlatPolar(_HausdorffMeterRadians):
516 '''Compute the C{Hausdorff} distance with function L{pygeodesy.flatPolar_}.
517 '''
518 _wrap = False
520 def __init__(self, points, **seed_name__radius_wrap):
521 '''New L{HausdorffFlatPolar} calculator.
523 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
524 and C{B{name}=NN} and keyword arguments
525 for function L{pygeodesy.flatPolar}.
527 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
528 B{C{seed}}, B{C{name}} and other exceptions.
529 '''
530 Hausdorff.__init__(self, points, **seed_name__radius_wrap)
531 self._func = _formy.flatPolar
532 self._func_ = _formy.flatPolar_
534 if _FOR_DOCS:
535 directed = Hausdorff.directed
536 symmetric = Hausdorff.symmetric
539class HausdorffHaversine(_HausdorffMeterRadians):
540 '''Compute the C{Hausdorff} distance with function L{pygeodesy.haversine_}.
542 @note: See note under L{HausdorffVincentys}.
543 '''
544 _wrap = False
546 def __init__(self, points, **seed_name__radius_wrap):
547 '''New L{HausdorffHaversine} calculator.
549 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
550 and C{B{name}=NN} and keyword arguments
551 for function L{pygeodesy.haversine}.
553 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
554 B{C{seed}}, B{C{name}} and other exceptions.
555 '''
556 Hausdorff.__init__(self, points, **seed_name__radius_wrap)
557 self._func = _formy.haversine
558 self._func_ = _formy.haversine_
560 if _FOR_DOCS:
561 directed = Hausdorff.directed
562 symmetric = Hausdorff.symmetric
565class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny
566 if _FOR_DOCS:
567 __doc__ = HausdorffFlatLocal.__doc__
568 __init__ = HausdorffFlatLocal.__init__
569 directed = HausdorffFlatLocal.directed
570 distance = HausdorffFlatLocal.distance
571 symmetric = HausdorffFlatLocal.symmetric
574class HausdorffKarney(Hausdorff):
575 '''Compute the C{Hausdorff} distance with I{Karney}'s U{geographiclib
576 <https://PyPI.org/project/geographiclib>} U{geodesic.Geodesic
577 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}C{.Inverse}
578 method.
579 '''
580 _units = _unitsBase._Str_degrees
582 def __init__(self, point1s, datum=None, **seed_name__wrap):
583 '''New L{HausdorffKarney} calculator.
585 @kwarg datum: Datum to override the default C{Datums.WGS84} and
586 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid},
587 L{Ellipsoid2} or L{a_f2Tuple}).
588 @kwarg seed_name__wrap: Optional C{B{seed}=None} and C{B{name}=NN}
589 and keyword arguments for method C{Inverse1} of
590 class L{geodesicw.Geodesic}.
592 @raise ImportError: Package U{geographiclib
593 <https://PyPI.org/project/geographiclib>} missing.
595 @raise TypeError: Invalid B{C{datum}}.
597 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
598 B{C{seed}}, B{C{name}} and other exceptions.
599 '''
600 Hausdorff.__init__(self, point1s, **seed_name__wrap)
601 self._datum_setter(datum)
602 self._func = self.datum.ellipsoid.geodesic.Inverse1
605class HausdorffThomas(_HausdorffMeterRadians):
606 '''Compute the C{Hausdorff} distance with function L{pygeodesy.thomas_}.
607 '''
608 def __init__(self, point1s, **seed_name__datum_wrap):
609 '''New L{HausdorffThomas} calculator.
611 @kwarg seed_name__datum_wrap: Optional C{B{seed}=None}
612 and C{B{name}=NN} and keyword arguments
613 for function L{pygeodesy.thomas}.
615 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
616 B{C{seed}}, B{C{name}} and other exceptions.
617 '''
618 Hausdorff.__init__(self, point1s, **seed_name__datum_wrap)
619 self._func = _formy.thomas
620 self._func_ = _formy.thomas_
622 if _FOR_DOCS:
623 directed = Hausdorff.directed
624 symmetric = Hausdorff.symmetric
627class HausdorffVincentys(_HausdorffMeterRadians):
628 '''Compute the C{Hausdorff} distance with function L{pygeodesy.vincentys_}.
630 @note: See note at function L{pygeodesy.vincentys_}.
631 '''
632 _wrap = False
634 def __init__(self, point1s, **seed_name__radius_wrap):
635 '''New L{HausdorffVincentys} calculator.
637 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
638 and C{B{name}=NN} and keyword arguments
639 for function L{pygeodesy.vincentys}.
641 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
642 B{C{seed}}, B{C{name}} and other exceptions.
643 '''
644 Hausdorff.__init__(self, point1s, **seed_name__radius_wrap)
645 self._func = _formy.vincentys
646 self._func_ = _formy.vincentys_
648 if _FOR_DOCS:
649 directed = Hausdorff.directed
650 symmetric = Hausdorff.symmetric
653def _hausdorff_(ps1, ps2, both, early, seed, units, distance, point):
654 '''(INTERNAL) Core of function L{hausdorff_} and methods C{directed}
655 and C{symmetric} of classes C{hausdorff.Hausdorff...}.
656 '''
657 # shuffling the points generally increases the
658 # chance of an early break in the inner j loop
659 rr = randomrangenerator(seed) if seed else range
661 hd = NINF
662 hi = hj = m = mn = 0
663 md = _0_0
665 # forward or forward and backward
666 for fb in range(2 if both else 1):
667 n = len(ps2)
668 for i in rr(len(ps1)):
669 p1 = point(ps1[i])
670 dh, dj = INF, 0
671 for j in rr(n):
672 p2 = point(ps2[j])
673 d = distance(p1, p2)
674 if early and d < hd:
675 break # early
676 elif d < dh:
677 dh, dj = d, j
678 else: # no early break
679 if hd < dh:
680 hd = dh
681 if fb:
682 hi, hj = dj, i
683 else:
684 hi, hj = i, dj
685 md += dh
686 mn += 1
687 m += 1
688 # swap model and target
689 ps1, ps2 = ps2, ps1
691 md = None if mn < m else (md / float(m))
692 return Hausdorff6Tuple(hd, hi, hj, m, md, units)
695def _point(p):
696 '''Default B{C{point}} callable for function L{hausdorff_}.
698 @arg p: The original C{model} or C{target} point (C{any}).
700 @return: The point, suitable for the L{hausdorff_}
701 B{C{distance}} callable.
702 '''
703 return p
706def hausdorff_(model, target, both=False, early=True, seed=None, units=NN,
707 distance=None, point=_point):
708 '''Compute the C{directed} or C{symmetric} U{Hausdorff
709 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance between 2 sets of points
710 with or without U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
711 and U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
713 @arg model: First set of points (C{LatLon}[], C{Numpy2LatLon}[],
714 C{Tuple2LatLon}[] or C{other}[]).
715 @arg target: Second set of points (C{LatLon}[], C{Numpy2LatLon}[],
716 C{Tuple2LatLon}[] or C{other}[]).
717 @kwarg both: Return the C{directed} (forward only) or the C{symmetric}
718 (combined forward and reverse) C{Hausdorff} distance (C{bool}).
719 @kwarg early: Enable or disable U{early breaking<https://Publik.TUWien.ac.AT/
720 files/PubDat_247739.pdf>} (C{bool}).
721 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False} for no
722 U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
723 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
724 @kwarg distance: Callable returning the distance between a B{C{model}}
725 and B{C{target}} point (signature C{(point1, point2)}).
726 @kwarg point: Callable returning the B{C{model}} or B{C{target}} point
727 suitable for B{C{distance}} (signature C{(point)}).
729 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
731 @raise HausdorffError: Insufficient number of B{C{model}} or B{C{target}} points.
733 @raise TypeError: If B{C{distance}} or B{C{point}} is not callable.
734 '''
735 _xcallable(distance=distance, point=point)
737 _, ps1 = _points2(model, closed=False, Error=HausdorffError) # PYCHOK non-sequence
738 _, ps2 = _points2(target, closed=False, Error=HausdorffError) # PYCHOK non-sequence
739 return _hausdorff_(ps1, ps2, both, early, seed, units, distance, point)
742class Hausdorff6Tuple(_NamedTuple):
743 '''6-Tuple C{(hd, i, j, mn, md, units)} with the U{Hausdorff
744 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance C{hd},
745 indices C{i} and C{j}, the total count C{mn}, the C{I{mean}
746 Hausdorff} distance C{md} and the class or name of both distance
747 C{units}.
749 For C{directed Hausdorff} distances, count C{mn} is the number
750 of model points considered. For C{symmetric Hausdorff} distances
751 count C{mn} twice that.
753 Indices C{i} and C{j} are the C{model} respectively C{target}
754 point with the C{hd} distance.
756 Mean distance C{md} is C{None} if an C{early break} occurred and
757 U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
758 was enabled by keyword argument C{early=True}.
759 '''
760 _Names_ = ('hd', _i_, _j_, 'mn', 'md', _units_)
761 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass)
763 def toUnits(self, **Error_name): # PYCHOK expected
764 '''Overloaded C{_NamedTuple.toUnits} for C{hd} and C{md} units.
765 '''
766 u = list(Hausdorff6Tuple._Units_)
767 u[0] = U = _unitsBase._xUnit(self.units, Float) # PYCHOK expected
768 u[4] = _Pass if self.md is None else U # PYCHOK expected
769 return _NamedTuple.toUnits(self.reUnit(*u), **Error_name) # PYCHOK self
772def randomrangenerator(seed):
773 '''Return a C{seed}ed random range function generator.
775 @arg seed: Initial, internal L{Random} state (C{hashable}
776 or C{None}).
778 @note: L{Random} with C{B{seed} is None} seeds from the
779 current time or from a platform-specific randomness
780 source, if available.
782 @return: A function to generate random ranges.
784 @example:
786 >>> rrange = randomrangenerator('R')
787 >>> for r in rrange(n):
788 >>> ... # r is random in 0..n-1
789 '''
790 R = Random(seed)
792 def _range(n, *stop_step):
793 '''Like standard L{range}C{start, stop=..., step=...)},
794 except the returned values are in random order.
796 @note: Especially C{range(n)} behaves like standard
797 L{Random.sample}C{(range(n), n)} but avoids
798 creating a tuple with the entire C{population}
799 and a list containing all sample values (for
800 large C{n}).
801 '''
802 if stop_step:
803 s = range(n, *stop_step)
805 elif n > 32:
806 r = R.randrange # Random._randbelow
807 s = set()
808 for _ in range(n - 32):
809 i = r(n)
810 while i in s:
811 i = r(n)
812 s.add(i)
813 yield i
814 s = set(range(n)) - s # [i for i in range(n) if i not in s]
815 else:
816 s = range(n)
818 s = list(s)
819 R.shuffle(s)
820 while s:
821 yield s.pop(0)
823 return _range
825# **) MIT License
826#
827# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
828#
829# Permission is hereby granted, free of charge, to any person obtaining a
830# copy of this software and associated documentation files (the "Software"),
831# to deal in the Software without restriction, including without limitation
832# the rights to use, copy, modify, merge, publish, distribute, sublicense,
833# and/or sell copies of the Software, and to permit persons to whom the
834# Software is furnished to do so, subject to the following conditions:
835#
836# The above copyright notice and this permission notice shall be included
837# in all copies or substantial portions of the Software.
838#
839# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
840# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
841# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
842# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
843# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
844# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
845# OTHER DEALINGS IN THE SOFTWARE.