Coverage for pygeodesy/hausdorff.py: 95%
243 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-08-02 18:24 -0400
« prev ^ index » next coverage.py v7.6.0, created at 2024-08-02 18:24 -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 as _points # 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, 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__ = '24.06.15'
91class HausdorffError(PointsError):
92 '''Hausdorff issue.
93 '''
94 pass
97class Hausdorff(_Named):
98 '''Hausdorff base class, requires method L{Hausdorff.distance} to
99 be overloaded.
100 '''
101 _datum = _WGS84
102 _func = None # formy function/property
103 _kwds = {} # func_ options
104 _model = ()
105 _seed = None
106 _units = _unitsBase._Str_NN # XXX Str to _Pass and for backward compatibility
108 def __init__(self, point1s, seed=None, units=NN, **name__kwds):
109 '''New C{Hausdorff...} calculator.
111 @arg point1s: Initial set of points, aka the C{model} or C{template}
112 (C{LatLon}[], C{Numpy2LatLon}[], C{Tuple2LatLon}[] or
113 C{other}[]).
114 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False}
115 for no U{random sampling<https://Publik.TUWien.ac.AT/files/
116 PubDat_247739.pdf>}.
117 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
118 @kwarg name__kwds: Optional calculator/interpolator C{B{name}=NN} (C{str})
119 and keyword arguments for the distance function, retrievable
120 with property C{kwds}.
122 @raise HausdorffError: Insufficient number of B{C{point1s}} or an invalid
123 B{C{point1}}, B{C{seed}} or B{C{units}}.
124 '''
125 name, kwds = _name2__(**name__kwds) # name__=self.__class__
126 if name:
127 self.name = name
129 _, self._model = self._points2(point1s)
130 if seed:
131 self.seed = seed
132 if units: # and not self.units:
133 self.units = units
134 if kwds:
135 self._kwds = kwds
137 @Property_RO
138 def adjust(self):
139 '''Get the adjust setting (C{bool} or C{None} if not applicable).
140 '''
141 return _xkwds_get(self._kwds, adjust=None)
143 @Property_RO
144 def datum(self):
145 '''Get the datum of this calculator (L{Datum} or C{None} if not applicable).
146 '''
147 return self._datum
149 def _datum_setter(self, datum):
150 '''(INTERNAL) Set the datum.
151 '''
152 d = datum or _xattr(self._model[0], datum=datum)
153 if d not in (None, self._datum): # PYCHOK no cover
154 self._datum = _ellipsoidal_datum(d, name=self.name)
156 def directed(self, point2s, early=True):
157 '''Compute only the C{forward Hausdorff} distance.
159 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[],
160 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
161 @kwarg early: Enable or disable U{early breaking<https://
162 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
164 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
166 @raise HausdorffError: Insufficient number of B{C{point2s}} or
167 an invalid B{C{point2}}.
169 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}.
170 '''
171 return self._hausdorff_(point2s, False, early, self.distance)
173 def distance(self, point1, point2):
174 '''Return the distance between B{C{point1}} and B{C{point2s}},
175 subject to the supplied optional keyword arguments, see
176 property C{kwds}.
177 '''
178 return self._func(point1.lat, point1.lon,
179 point2.lat, point2.lon, **self._kwds)
181 @Property
182 def _func(self):
183 '''(INTERNAL) I{Must be overloaded}.'''
184 self._notOverloaded(**self.kwds)
186 @_func.setter_ # PYCHOK setter_underscore!
187 def _func(self, func):
188 return _formy._Propy(func, 4, self.kwds)
190 def _hausdorff_(self, point2s, both, early, distance):
191 _, ps2 = self._points2(point2s)
192 return _hausdorff_(self._model, ps2, both, early, self.seed,
193 self.units, distance, self.point)
195 @property_RO
196 def kwds(self):
197 '''Get the supplied, optional keyword arguments (C{dict}).
198 '''
199 return self._kwds
201 def point(self, point):
202 '''Convert a C{model} or C{target} point for the C{.distance} method.
203 '''
204 return point # pass thru
206 def _points2(self, points):
207 '''(INTERNAL) Check a set of points.
208 '''
209 return _points2(points, closed=False, Error=HausdorffError)
211 @property_doc_(''' the random sampling seed (C{Random}).''')
212 def seed(self):
213 '''Get the random sampling seed (C{any} or C{None}).
214 '''
215 return self._seed
217 @seed.setter # PYCHOK setter!
218 def seed(self, seed):
219 '''Set the random sampling seed (C{Random(seed)}) or
220 C{None}, C{0} or C{False} for no U{random sampling
221 <https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
223 @raise HausdorffError: Invalid B{C{seed}}.
224 '''
225 if seed:
226 try:
227 Random(seed)
228 except (TypeError, ValueError) as x:
229 raise HausdorffError(seed=seed, cause=x)
230 self._seed = seed
231 else:
232 self._seed = None
234 def symmetric(self, point2s, early=True):
235 '''Compute the combined C{forward and reverse Hausdorff} distance.
237 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[],
238 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
239 @kwarg early: Enable or disable U{early breaking<https://
240 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
242 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
244 @raise HausdorffError: Insufficient number of B{C{point2s}} or
245 an invalid B{C{point2}}.
247 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}.
248 '''
249 return self._hausdorff_(point2s, True, early, self.distance)
251 @property_doc_(''' the distance units (C{Unit} or C{str}).''')
252 def units(self):
253 '''Get the distance units (C{Unit} or C{str}).
254 '''
255 return self._units
257 @units.setter # PYCHOK setter!
258 def units(self, units):
259 '''Set the distance units (C{Unit} or C{str}).
261 @raise TypeError: Invalid B{C{units}}.
262 '''
263 self._units = _unitsBase._xUnits(units, Base=Float)
265 @Property_RO
266 def wrap(self):
267 '''Get the wrap setting (C{bool} or C{None} if not applicable).
268 '''
269 return _xkwds_get(self._kwds, adjust=None)
272class HausdorffDegrees(Hausdorff):
273 '''L{Hausdorff} base class for distances from C{LatLon}
274 points in C{degrees}.
275 '''
276 _units = _unitsBase._Str_degrees
278 if _FOR_DOCS:
279 __init__ = Hausdorff.__init__
280 directed = Hausdorff.directed
281 symmetric = Hausdorff.symmetric
283 def distance(self, point1, point2): # PYCHOK no cover
284 '''I{Must be overloaded}.'''
285 self._notOverloaded(point1, point2)
288class HausdorffRadians(Hausdorff):
289 '''L{Hausdorff} base class for distances from C{LatLon}
290 points converted from C{degrees} to C{radians}.
291 '''
292 _units = _unitsBase._Str_radians
294 if _FOR_DOCS:
295 __init__ = Hausdorff.__init__
296 directed = Hausdorff.directed
297 symmetric = Hausdorff.symmetric
299 def distance(self, point1, point2): # PYCHOK no cover
300 '''I{Must be overloaded}.'''
301 self._notOverloaded(point1, point2)
303 def point(self, point):
304 '''Return B{C{point}} as L{PhiLam2Tuple} to maintain
305 I{backward compatibility} of L{HausdorffRadians}.
307 @return: A L{PhiLam2Tuple}C{(phi, lam)}.
308 '''
309 try:
310 return point.philam
311 except AttributeError:
312 return PhiLam2Tuple(radians(point.lat), radians(point.lon))
315class _HausdorffMeterRadians(Hausdorff):
316 '''(INTERNAL) Returning C{meter} or C{radians} depending on
317 the optional keyword arguments supplied at instantiation
318 of the C{Hausdorff*} sub-class.
319 '''
320 _units = _unitsBase._Str_meter
321 _units_ = _unitsBase._Str_radians
323 def directed(self, point2s, early=True):
324 '''Overloaded method L{Hausdorff.directed} to determine
325 the distance function and units from the optional
326 keyword arguments given at this instantiation, see
327 property C{kwds}.
329 @see: L{Hausdorff.directed} for other details.
330 '''
331 return self._hausdorff_(point2s, False, early, _formy._radistance(self))
333 def symmetric(self, point2s, early=True):
334 '''Overloaded method L{Hausdorff.symmetric} to determine
335 the distance function and units from the optional
336 keyword arguments given at this instantiation, see
337 property C{kwds}.
339 @see: L{Hausdorff.symmetric} for other details.
340 '''
341 return self._hausdorff_(point2s, True, early, _formy._radistance(self))
343 @Property
344 def _func_(self):
345 '''(INTERNAL) I{Must be overloaded}.'''
346 self._notOverloaded(**self.kwds)
348 @_func_.setter_ # PYCHOK setter_underscore!
349 def _func_(self, func):
350 return _formy._Propy(func, 3, self.kwds)
353class HausdorffCosineAndoyerLambert(_HausdorffMeterRadians):
354 '''Compute the C{Hausdorff} distance based on the I{angular} distance
355 in C{radians} from function L{pygeodesy.cosineAndoyerLambert}.
356 '''
357 def __init__(self, point1s, **seed_name__datum_wrap):
358 '''New L{HausdorffCosineAndoyerLambert} calculator.
360 @kwarg seed_name__datum_wrap: Optional C{B{seed}=None} and
361 C{B{name}=NN} and keyword arguments for
362 function L{pygeodesy.cosineAndoyerLambert}.
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__datum_wrap)
368 self._func = _formy.cosineAndoyerLambert
369 self._func_ = _formy.cosineAndoyerLambert_
371 if _FOR_DOCS:
372 directed = Hausdorff.directed
373 symmetric = Hausdorff.symmetric
376class HausdorffCosineForsytheAndoyerLambert(_HausdorffMeterRadians):
377 '''Compute the C{Hausdorff} distance based on the I{angular} distance
378 in C{radians} from function L{pygeodesy.cosineForsytheAndoyerLambert}.
379 '''
380 def __init__(self, point1s, **seed_name__datum_wrap):
381 '''New L{HausdorffCosineForsytheAndoyerLambert} calculator.
383 @kwarg seed_name__datum_wrap: Optional C{B{seed}=None} and
384 C{B{name}=NN} and keyword arguments for function
385 L{pygeodesy.cosineForsytheAndoyerLambert}.
387 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
388 B{C{seed}}, B{C{name}} and other exceptions.
389 '''
390 Hausdorff.__init__(self, point1s, **seed_name__datum_wrap)
391 self._func = _formy.cosineForsytheAndoyerLambert
392 self._func_ = _formy.cosineForsytheAndoyerLambert_
394 if _FOR_DOCS:
395 directed = Hausdorff.directed
396 symmetric = Hausdorff.symmetric
399class HausdorffCosineLaw(_HausdorffMeterRadians):
400 '''Compute the C{Hausdorff} distance based on the I{angular}
401 distance in C{radians} from function L{pygeodesy.cosineLaw_}.
403 @note: See note at function L{pygeodesy.vincentys_}.
404 '''
405 def __init__(self, point1s, **seed_name__radius_wrap):
406 '''New L{HausdorffCosineLaw} calculator.
408 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None} and
409 C{B{name}=NN} and keyword arguments for
410 function L{pygeodesy.cosineLaw}.
412 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
413 B{C{seed}}, B{C{name}} and other exceptions.
414 '''
415 Hausdorff.__init__(self, point1s, **seed_name__radius_wrap)
416 self._func = _formy.cosineLaw
417 self._func_ = _formy.cosineLaw_
419 if _FOR_DOCS:
420 directed = Hausdorff.directed
421 symmetric = Hausdorff.symmetric
424class HausdorffDistanceTo(Hausdorff):
425 '''Compute the C{Hausdorff} distance based on the distance from the
426 points' C{LatLon.distanceTo} method, conventionally in C{meter}.
427 '''
428 _units = _unitsBase._Str_meter
430 def __init__(self, point1s, **seed_name__distanceTo_kwds):
431 '''New L{HausdorffDistanceTo} calculator.
433 @kwarg seed_name__distanceTo_kwds: Optional C{B{seed}=None} and
434 C{B{name}=NN} and keyword arguments for each
435 B{C{point1s}}' C{LatLon.distanceTo} method.
437 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
438 B{C{seed}}, B{C{name}} and other exceptions.
440 @note: All C{model}, C{template} and C{target} B{C{points}}
441 I{must} be instances of the same ellipsoidal or
442 spherical C{LatLon} class.
443 '''
444 Hausdorff.__init__(self, point1s, **seed_name__distanceTo_kwds)
446 if _FOR_DOCS:
447 directed = Hausdorff.directed
448 symmetric = Hausdorff.symmetric
450 def distance(self, p1, p2):
451 '''Return the distance in C{meter}.
452 '''
453 return p1.distanceTo(p2, **self._kwds)
455 def _points2(self, points):
456 '''(INTERNAL) Check a set of points.
457 '''
458 np, ps = Hausdorff._points2(self, points)
459 return np, _distanceTo(HausdorffError, points=ps)
462class HausdorffEquirectangular(Hausdorff):
463 '''Compute the C{Hausdorff} distance based on the C{equirectangular} distance
464 in C{radians squared} like function L{pygeodesy.equirectangular}.
465 '''
466 _units = _unitsBase._Str_degrees2
468 def __init__(self, point1s, **seed_name__adjust_limit_wrap):
469 '''New L{HausdorffEquirectangular} calculator.
471 @kwarg seed_name__adjust_limit_wrap: Optional C{B{seed}=None} and
472 C{B{name}=NN} and keyword arguments for function
473 L{pygeodesy.equirectangular} I{with default}
474 C{B{limit}=0} for I{backward compatibility}.
476 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
477 B{C{seed}}, B{C{name}} and other exceptions.
478 '''
479 Hausdorff.__init__(self, point1s, **_xkwds(seed_name__adjust_limit_wrap,
480 limit=0))
481 self._func = _formy._equirectangular # helper
483 if _FOR_DOCS:
484 directed = Hausdorff.directed
485 symmetric = Hausdorff.symmetric
488class HausdorffEuclidean(_HausdorffMeterRadians):
489 '''Compute the C{Hausdorff} distance based on the C{Euclidean}
490 distance in C{radians} from function L{pygeodesy.euclidean_}.
491 '''
492 def __init__(self, point1s, **seed_name__adjust_radius_wrap):
493 '''New L{HausdorffEuclidean} calculator.
495 @kwarg seed_name__adjust_radius_wrap: Optional C{B{seed}=None}
496 and C{B{name}=NN} and keyword arguments for
497 function L{pygeodesy.euclidean}.
499 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
500 B{C{seed}}, B{C{name}} and other exceptions.
501 '''
502 Hausdorff.__init__(self, point1s, **seed_name__adjust_radius_wrap)
503 self._func = _formy.euclidean
504 self._func_ = _formy.euclidean_
506 if _FOR_DOCS:
507 directed = Hausdorff.directed
508 symmetric = Hausdorff.symmetric
511class HausdorffExact(Hausdorff):
512 '''Compute the C{Hausdorff} distance based on the I{angular}
513 distance in C{degrees} from method L{GeodesicExact}C{.Inverse}.
514 '''
515 _units = _unitsBase._Str_degrees
517 def __init__(self, point1s, datum=None, **seed_name__wrap):
518 '''New L{HausdorffKarney} calculator.
520 @kwarg datum: Datum to override the default C{Datums.WGS84} and first
521 B{C{point1s}}' datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2}
522 or L{a_f2Tuple}).
523 @kwarg seed_name__wrap: Optional C{B{seed}=None} and C{B{name}=NN} and
524 keyword argument for method C{Inverse1} of class
525 L{geodesicx.GeodesicExact}.
527 @raise TypeError: Invalid B{C{datum}}.
529 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, B{C{seed}},
530 B{C{name}} and other exceptions.
531 '''
532 Hausdorff.__init__(self, point1s, **seed_name__wrap)
533 self._datum_setter(datum)
534 self._func = self.datum.ellipsoid.geodesicx.Inverse1 # note -x
536 if _FOR_DOCS:
537 directed = Hausdorff.directed
538 symmetric = Hausdorff.symmetric
541class HausdorffFlatLocal(_HausdorffMeterRadians):
542 '''Compute the C{Hausdorff} distance based on the I{angular} distance in
543 C{radians squared} like function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}.
544 '''
545 _units = _unitsBase._Str_radians2
547 def __init__(self, point1s, **seed_name__datum_scaled_wrap):
548 '''New L{HausdorffFlatLocal}/L{HausdorffHubeny} calculator.
550 @kwarg seed_name__datum_scaled_wrap: Optional C{B{seed}=None} and
551 C{B{name}=NN} and keyword arguments for function
552 L{pygeodesy.flatLocal}.
554 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
555 B{C{seed}}, B{C{name}} and other exceptions.
557 @note: The distance C{units} are C{radians squared}, not C{radians}.
558 '''
559 Hausdorff.__init__(self, point1s, **seed_name__datum_scaled_wrap)
560 self._func = _formy.flatLocal
561 self._func_ = self.datum.ellipsoid._hubeny_2
563 if _FOR_DOCS:
564 directed = Hausdorff.directed
565 symmetric = Hausdorff.symmetric
568class HausdorffFlatPolar(_HausdorffMeterRadians):
569 '''Compute the C{Hausdorff} distance based on the I{angular}
570 distance in C{radians} from function L{pygeodesy.flatPolar_}.
571 '''
572 _wrap = False
574 def __init__(self, points, **seed_name__radius_wrap):
575 '''New L{HausdorffFlatPolar} calculator.
577 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
578 and C{B{name}=NN} and keyword arguments
579 for function L{pygeodesy.flatPolar}.
581 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
582 B{C{seed}}, B{C{name}} and other exceptions.
583 '''
584 Hausdorff.__init__(self, points, **seed_name__radius_wrap)
585 self._func = _formy.flatPolar
586 self._func_ = _formy.flatPolar_
588 if _FOR_DOCS:
589 directed = Hausdorff.directed
590 symmetric = Hausdorff.symmetric
593class HausdorffHaversine(_HausdorffMeterRadians):
594 '''Compute the C{Hausdorff} distance based on the I{angular}
595 distance in C{radians} from function L{pygeodesy.haversine_}.
597 @note: See note under L{HausdorffVincentys}.
598 '''
599 _wrap = False
601 def __init__(self, points, **seed_name__radius_wrap):
602 '''New L{HausdorffHaversine} calculator.
604 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
605 and C{B{name}=NN} and keyword arguments
606 for function L{pygeodesy.haversine}.
608 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
609 B{C{seed}}, B{C{name}} and other exceptions.
610 '''
611 Hausdorff.__init__(self, points, **seed_name__radius_wrap)
612 self._func = _formy.haversine
613 self._func_ = _formy.haversine_
615 if _FOR_DOCS:
616 directed = Hausdorff.directed
617 symmetric = Hausdorff.symmetric
620class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny
621 if _FOR_DOCS:
622 __doc__ = HausdorffFlatLocal.__doc__
623 __init__ = HausdorffFlatLocal.__init__
624 directed = HausdorffFlatLocal.directed
625 distance = HausdorffFlatLocal.distance
626 symmetric = HausdorffFlatLocal.symmetric
629class HausdorffKarney(Hausdorff):
630 '''Compute the C{Hausdorff} distance based on the I{angular}
631 distance in C{degrees} from I{Karney}'s U{geographiclib
632 <https://PyPI.org/project/geographiclib>} U{Geodesic
633 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}
634 Inverse method.
635 '''
636 _units = _unitsBase._Str_degrees
638 def __init__(self, point1s, datum=None, **seed_name__wrap):
639 '''New L{HausdorffKarney} calculator.
641 @kwarg datum: Datum to override the default C{Datums.WGS84} and
642 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid},
643 L{Ellipsoid2} or L{a_f2Tuple}).
644 @kwarg seed_name__wrap: Optional C{B{seed}=None} and C{B{name}=NN}
645 and keyword arguments for method C{Inverse1} of
646 class L{geodesicw.Geodesic}.
648 @raise ImportError: Package U{geographiclib
649 <https://PyPI.org/project/geographiclib>} missing.
651 @raise TypeError: Invalid B{C{datum}}.
653 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
654 B{C{seed}}, B{C{name}} and other exceptions.
655 '''
656 Hausdorff.__init__(self, point1s, **seed_name__wrap)
657 self._datum_setter(datum)
658 self._func = self.datum.ellipsoid.geodesic.Inverse1
661class HausdorffThomas(_HausdorffMeterRadians):
662 '''Compute the C{Hausdorff} distance based on the I{angular}
663 distance in C{radians} from function L{pygeodesy.thomas_}.
664 '''
665 def __init__(self, point1s, **seed_name__datum_wrap):
666 '''New L{HausdorffThomas} calculator.
668 @kwarg seed_name__datum_wrap: Optional C{B{seed}=None}
669 and C{B{name}=NN} and keyword arguments
670 for function L{pygeodesy.thomas}.
672 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
673 B{C{seed}}, B{C{name}} and other exceptions.
674 '''
675 Hausdorff.__init__(self, point1s, **seed_name__datum_wrap)
676 self._func = _formy.thomas
677 self._func_ = _formy.thomas_
679 if _FOR_DOCS:
680 directed = Hausdorff.directed
681 symmetric = Hausdorff.symmetric
684class HausdorffVincentys(_HausdorffMeterRadians):
685 '''Compute the C{Hausdorff} distance based on the I{angular}
686 distance in C{radians} from function L{pygeodesy.vincentys_}.
688 @note: See note at function L{pygeodesy.vincentys_}.
689 '''
690 _wrap = False
692 def __init__(self, point1s, **seed_name__radius_wrap):
693 '''New L{HausdorffVincentys} calculator.
695 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None}
696 and C{B{name}=NN} and keyword arguments
697 for function L{pygeodesy.vincentys}.
699 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
700 B{C{seed}}, B{C{name}} and other exceptions.
701 '''
702 Hausdorff.__init__(self, point1s, **seed_name__radius_wrap)
703 self._func = _formy.vincentys
704 self._func_ = _formy.vincentys_
706 if _FOR_DOCS:
707 directed = Hausdorff.directed
708 symmetric = Hausdorff.symmetric
711def _hausdorff_(ps1, ps2, both, early, seed, units, distance, point):
712 '''(INTERNAL) Core of function L{hausdorff_} and methods C{directed}
713 and C{symmetric} of classes C{hausdorff.Hausdorff...}.
714 '''
715 # shuffling the points generally increases the
716 # chance of an early break in the inner j loop
717 rr = randomrangenerator(seed) if seed else range
719 hd = NINF
720 hi = hj = m = mn = 0
721 md = _0_0
723 # forward or forward and backward
724 for fb in range(2 if both else 1):
725 n = len(ps2)
726 for i in rr(len(ps1)):
727 p1 = point(ps1[i])
728 dh, dj = INF, 0
729 for j in rr(n):
730 p2 = point(ps2[j])
731 d = distance(p1, p2)
732 if early and d < hd:
733 break # early
734 elif d < dh:
735 dh, dj = d, j
736 else: # no early break
737 if hd < dh:
738 hd = dh
739 if fb:
740 hi, hj = dj, i
741 else:
742 hi, hj = i, dj
743 md += dh
744 mn += 1
745 m += 1
746 # swap model and target
747 ps1, ps2 = ps2, ps1
749 md = None if mn < m else (md / float(m))
750 return Hausdorff6Tuple(hd, hi, hj, m, md, units)
753def _point(p):
754 '''Default B{C{point}} callable for function L{hausdorff_}.
756 @arg p: The original C{model} or C{target} point (C{any}).
758 @return: The point, suitable for the L{hausdorff_}
759 B{C{distance}} callable.
760 '''
761 return p
764def hausdorff_(model, target, both=False, early=True, seed=None, units=NN,
765 distance=None, point=_point):
766 '''Compute the C{directed} or C{symmetric} U{Hausdorff
767 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance between 2 sets of points
768 with or without U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
769 and U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
771 @arg model: First set of points (C{LatLon}[], C{Numpy2LatLon}[],
772 C{Tuple2LatLon}[] or C{other}[]).
773 @arg target: Second set of points (C{LatLon}[], C{Numpy2LatLon}[],
774 C{Tuple2LatLon}[] or C{other}[]).
775 @kwarg both: Return the C{directed} (forward only) or the C{symmetric}
776 (combined forward and reverse) C{Hausdorff} distance (C{bool}).
777 @kwarg early: Enable or disable U{early breaking<https://Publik.TUWien.ac.AT/
778 files/PubDat_247739.pdf>} (C{bool}).
779 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False} for no
780 U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
781 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
782 @kwarg distance: Callable returning the distance between a B{C{model}}
783 and B{C{target}} point (signature C{(point1, point2)}).
784 @kwarg point: Callable returning the B{C{model}} or B{C{target}} point
785 suitable for B{C{distance}} (signature C{(point)}).
787 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
789 @raise HausdorffError: Insufficient number of B{C{model}} or B{C{target}} points.
791 @raise TypeError: If B{C{distance}} or B{C{point}} is not callable.
792 '''
793 _xcallable(distance=distance, point=point)
795 _, ps1 = _points2(model, closed=False, Error=HausdorffError) # PYCHOK non-sequence
796 _, ps2 = _points2(target, closed=False, Error=HausdorffError) # PYCHOK non-sequence
797 return _hausdorff_(ps1, ps2, both, early, seed, units, distance, point)
800class Hausdorff6Tuple(_NamedTuple):
801 '''6-Tuple C{(hd, i, j, mn, md, units)} with the U{Hausdorff
802 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance C{hd},
803 indices C{i} and C{j}, the total count C{mn}, the C{I{mean}
804 Hausdorff} distance C{md} and the class or name of both distance
805 C{units}.
807 For C{directed Hausdorff} distances, count C{mn} is the number
808 of model points considered. For C{symmetric Hausdorff} distances
809 count C{mn} twice that.
811 Indices C{i} and C{j} are the C{model} respectively C{target}
812 point with the C{hd} distance.
814 Mean distance C{md} is C{None} if an C{early break} occurred and
815 U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
816 was enabled by keyword argument C{early=True}.
817 '''
818 _Names_ = ('hd', _i_, _j_, 'mn', 'md', _units_)
819 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass)
821 def toUnits(self, **Error_name): # PYCHOK expected
822 '''Overloaded C{_NamedTuple.toUnits} for C{hd} and C{md} units.
823 '''
824 u = list(Hausdorff6Tuple._Units_)
825 u[0] = U = _unitsBase._xUnit(self.units, Float) # PYCHOK expected
826 u[4] = _Pass if self.md is None else U # PYCHOK expected
827 return _NamedTuple.toUnits(self.reUnit(*u), **Error_name) # PYCHOK self
830def randomrangenerator(seed):
831 '''Return a C{seed}ed random range function generator.
833 @arg seed: Initial, internal L{Random} state (C{hashable}
834 or C{None}).
836 @note: L{Random} with C{B{seed} is None} seeds from the
837 current time or from a platform-specific randomness
838 source, if available.
840 @return: A function to generate random ranges.
842 @example:
844 >>> rrange = randomrangenerator('R')
845 >>> for r in rrange(n):
846 >>> ... # r is random in 0..n-1
847 '''
848 R = Random(seed)
850 def _range(n, *stop_step):
851 '''Like standard L{range}C{start, stop=..., step=...)},
852 except the returned values are in random order.
854 @note: Especially C{range(n)} behaves like standard
855 L{Random.sample}C{(range(n), n)} but avoids
856 creating a tuple with the entire C{population}
857 and a list containing all sample values (for
858 large C{n}).
859 '''
860 if stop_step:
861 s = range(n, *stop_step)
863 elif n > 32:
864 r = R.randrange # Random._randbelow
865 s = set()
866 for _ in range(n - 32):
867 i = r(n)
868 while i in s:
869 i = r(n)
870 s.add(i)
871 yield i
872 s = set(range(n)) - s # [i for i in range(n) if i not in s]
873 else:
874 s = range(n)
876 s = list(s)
877 R.shuffle(s)
878 while s:
879 yield s.pop(0)
881 return _range
883# **) MIT License
884#
885# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
886#
887# Permission is hereby granted, free of charge, to any person obtaining a
888# copy of this software and associated documentation files (the "Software"),
889# to deal in the Software without restriction, including without limitation
890# the rights to use, copy, modify, merge, publish, distribute, sublicense,
891# and/or sell copies of the Software, and to permit persons to whom the
892# Software is furnished to do so, subject to the following conditions:
893#
894# The above copyright notice and this permission notice shall be included
895# in all copies or substantial portions of the Software.
896#
897# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
898# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
899# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
900# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
901# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
902# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
903# OTHER DEALINGS IN THE SOFTWARE.