Coverage for pygeodesy/hausdorff.py: 96%
233 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-06 15:27 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-06 15:27 -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 _IsnotError, PointsError, _xattr, _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 _Named, _NamedTuple, notOverloaded, _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__ = '23.08.06'
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
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, name=NN, units=NN, **kwds):
110 '''New C{Hausdorff...} calculator.
112 @arg point1s: Initial set of points, aka the C{model} or
113 C{template} (C{LatLon}[], C{Numpy2LatLon}[],
114 C{Tuple2LatLon}[] or C{other}[]).
115 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0}
116 or C{False} for no U{random sampling<https://
117 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
118 @kwarg name: Optional name for this interpolator (C{str}).
119 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
120 @kwarg kwds: Optional keyword argument for distance function,
121 retrievable with property C{kwds}.
123 @raise HausdorffError: Insufficient number of B{C{point1s}}
124 or an invalid B{C{point1}}, B{C{seed}}
125 or B{C{units}}.
126 '''
127 _, self._model = self._points2(point1s)
128 if seed:
129 self.seed = seed
130 if name:
131 self.name = name
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 def _hausdorff_(self, point2s, both, early, distance):
182 _, ps2 = self._points2(point2s)
183 return _hausdorff_(self._model, ps2, both, early, self.seed,
184 self.units, distance, self.point)
186 @property_RO
187 def kwds(self):
188 '''Get the supplied, optional keyword arguments (C{dict}).
189 '''
190 return self._kwds
192 def point(self, point):
193 '''Convert a C{model} or C{target} point for the C{.distance} method.
194 '''
195 return point # pass thru
197 def _points2(self, points):
198 '''(INTERNAL) Check a set of points.
199 '''
200 return _points2(points, closed=False, Error=HausdorffError)
202 @property_doc_(''' the random sampling seed (C{Random}).''')
203 def seed(self):
204 '''Get the random sampling seed (C{any} or C{None}).
205 '''
206 return self._seed
208 @seed.setter # PYCHOK setter!
209 def seed(self, seed):
210 '''Set the random sampling seed (C{Random(seed)}) or
211 C{None}, C{0} or C{False} for no U{random sampling
212 <https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
214 @raise HausdorffError: Invalid B{C{seed}}.
215 '''
216 if seed:
217 try:
218 Random(seed)
219 except (TypeError, ValueError) as x:
220 raise HausdorffError(seed=seed, cause=x)
221 self._seed = seed
222 else:
223 self._seed = None
225 def symmetric(self, point2s, early=True):
226 '''Compute the combined C{forward and reverse Hausdorff} distance.
228 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[],
229 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]).
230 @kwarg early: Enable or disable U{early breaking<https://
231 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}).
233 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
235 @raise HausdorffError: Insufficient number of B{C{point2s}} or
236 an invalid B{C{point2}}.
238 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}.
239 '''
240 return self._hausdorff_(point2s, True, early, self.distance)
242 @property_doc_(''' the distance units (C{Unit} or C{str}).''')
243 def units(self):
244 '''Get the distance units (C{Unit} or C{str}).
245 '''
246 return self._units
248 @units.setter # PYCHOK setter!
249 def units(self, units):
250 '''Set the distance units (C{Unit} or C{str}).
252 @raise TypeError: Invalid B{C{units}}.
253 '''
254 self._units = _xUnits(units, Base=Float)
256 @Property_RO
257 def wrap(self):
258 '''Get the wrap setting (C{bool} or C{None} if not applicable).
259 '''
260 return _xkwds_get(self._kwds, adjust=None)
263class HausdorffDegrees(Hausdorff):
264 '''L{Hausdorff} base class for distances from C{LatLon}
265 points in C{degrees}.
266 '''
267 _units = _Str_degrees
269 if _FOR_DOCS:
270 __init__ = Hausdorff.__init__
271 directed = Hausdorff.directed
272 symmetric = Hausdorff.symmetric
274 def distance(self, point1, point2): # PYCHOK no cover
275 '''I{Must be overloaded} to return the distance between
276 B{C{point1}} and B{C{point2}} in C{degrees}.
277 '''
278 notOverloaded(self, point1, point2)
281class HausdorffRadians(Hausdorff):
282 '''L{Hausdorff} base class for distances from C{LatLon}
283 points converted from C{degrees} to C{radians}.
284 '''
285 _units = _Str_radians
287 if _FOR_DOCS:
288 __init__ = Hausdorff.__init__
289 directed = Hausdorff.directed
290 symmetric = Hausdorff.symmetric
292 def distance(self, point1, point2): # PYCHOK no cover
293 '''I{Must be overloaded} to return the distance between
294 B{C{point1}} and B{C{point2}} in C{radians}.
295 '''
296 notOverloaded(self, point1, point2)
298 def point(self, point):
299 '''Return B{C{point}} as L{PhiLam2Tuple} to maintain
300 I{backward compatibility} of L{HausdorffRadians}.
302 @return: A L{PhiLam2Tuple}C{(phi, lam)}.
303 '''
304 try:
305 return point.philam
306 except AttributeError:
307 return PhiLam2Tuple(radians(point.lat), radians(point.lon))
310class _HausdorffMeterRadians(Hausdorff):
311 '''(INTERNAL) Returning C{meter} or C{radians} depending on
312 the optional keyword arguments supplied at instantiation
313 of the C{Hausdorff*} sub-class.
314 '''
315 _units = _Str_meter
316 _units_ = _Str_radians
318 def directed(self, point2s, early=True):
319 '''Overloaded method L{Hausdorff.directed} to determine
320 the distance function and units from the optional
321 keyword arguments given at this instantiation, see
322 property C{kwds}.
324 @see: L{Hausdorff.directed} for other details.
325 '''
326 return self._hausdorff_(point2s, False, early, _formy._radistance(self))
328 def symmetric(self, point2s, early=True):
329 '''Overloaded method L{Hausdorff.symmetric} to determine
330 the distance function and units from the optional
331 keyword arguments given at this instantiation, see
332 property C{kwds}.
334 @see: L{Hausdorff.symmetric} for other details.
335 '''
336 return self._hausdorff_(point2s, True, early, _formy._radistance(self))
338 def _func_(self, *args, **kwds): # PYCHOK no cover
339 notOverloaded(self, *args, **kwds)
342class HausdorffCosineAndoyerLambert(_HausdorffMeterRadians):
343 '''Compute the C{Hausdorff} distance based on the I{angular} distance
344 in C{radians} from function L{pygeodesy.cosineAndoyerLambert}.
345 '''
346 def __init__(self, point1s, seed=None, name=NN, **datum_wrap):
347 '''New L{HausdorffCosineAndoyerLambert} calculator.
349 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
350 B{C{seed}}, B{C{name}} and other exceptions.
352 @kwarg datum_wrap: Optional keyword arguments for function
353 L{pygeodesy.cosineAndoyerLambert}.
354 '''
355 Hausdorff.__init__(self, point1s, seed=seed, name=name,
356 **datum_wrap)
357 self._func = _formy.cosineAndoyerLambert
358 self._func_ = _formy.cosineAndoyerLambert_
360 if _FOR_DOCS:
361 directed = Hausdorff.directed
362 symmetric = Hausdorff.symmetric
365class HausdorffCosineForsytheAndoyerLambert(_HausdorffMeterRadians):
366 '''Compute the C{Hausdorff} distance based on the I{angular} distance
367 in C{radians} from function L{pygeodesy.cosineForsytheAndoyerLambert}.
368 '''
369 def __init__(self, point1s, seed=None, name=NN, **datum_wrap):
370 '''New L{HausdorffCosineForsytheAndoyerLambert} calculator.
372 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
373 B{C{seed}}, B{C{name}} and other exceptions.
375 @kwarg datum_wrap: Optional keyword arguments for function
376 L{pygeodesy.cosineAndoyerLambert}.
377 '''
378 Hausdorff.__init__(self, point1s, seed=seed, name=name,
379 **datum_wrap)
380 self._func = _formy.cosineForsytheAndoyerLambert
381 self._func_ = _formy.cosineForsytheAndoyerLambert_
383 if _FOR_DOCS:
384 directed = Hausdorff.directed
385 symmetric = Hausdorff.symmetric
388class HausdorffCosineLaw(_HausdorffMeterRadians):
389 '''Compute the C{Hausdorff} distance based on the I{angular}
390 distance in C{radians} from function L{pygeodesy.cosineLaw_}.
392 @note: See note at function L{pygeodesy.vincentys_}.
393 '''
394 def __init__(self, point1s, seed=None, name=NN, **radius_wrap):
395 '''New L{HausdorffCosineLaw} calculator.
397 @kwarg radius_wrap: Optional keyword arguments for function
398 L{pygeodesy.cosineLaw}.
400 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
401 B{C{seed}}, B{C{name}} and other exceptions.
402 '''
403 Hausdorff.__init__(self, point1s, seed=seed, name=name,
404 **radius_wrap)
405 self._func = _formy.cosineLaw
406 self._func_ = _formy.cosineLaw_
408 if _FOR_DOCS:
409 directed = Hausdorff.directed
410 symmetric = Hausdorff.symmetric
413class HausdorffDistanceTo(Hausdorff):
414 '''Compute the C{Hausdorff} distance based on the distance from the
415 points' C{LatLon.distanceTo} method, conventionally in C{meter}.
416 '''
417 _units = _Str_meter
419 def __init__(self, point1s, seed=None, name=NN, **distanceTo_kwds):
420 '''New L{HausdorffDistanceTo} calculator.
422 @kwarg distanceTo_kwds: Optional keyword arguments for each
423 B{C{point1s}}' C{LatLon.distanceTo}
424 method.
426 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
427 B{C{seed}}, B{C{name}} and other exceptions.
429 @note: All C{model}, C{template} and C{target} B{C{points}}
430 I{must} be instances of the same ellipsoidal or
431 spherical C{LatLon} class.
432 '''
433 Hausdorff.__init__(self, point1s, seed=seed, name=name,
434 **distanceTo_kwds)
436 if _FOR_DOCS:
437 directed = Hausdorff.directed
438 symmetric = Hausdorff.symmetric
440 def distance(self, p1, p2):
441 '''Return the distance in C{meter}.
442 '''
443 return p1.distanceTo(p2, **self._kwds)
445 def _points2(self, points):
446 '''(INTERNAL) Check a set of points.
447 '''
448 np, ps = Hausdorff._points2(self, points)
449 return np, _distanceTo(HausdorffError, points=ps)
452class HausdorffEquirectangular(Hausdorff):
453 '''Compute the C{Hausdorff} distance based on the C{equirectangular} distance
454 in C{radians squared} like function L{pygeodesy.equirectangular_}.
455 '''
456 _units = _Str_degrees2
458 def __init__(self, point1s, seed=None, name=NN, **adjust_limit_wrap):
459 '''New L{HausdorffEquirectangular} calculator.
461 @kwarg adjust_limit_wrap: Optional keyword arguments for function
462 L{pygeodesy.equirectangular_} I{with default}
463 C{B{limit}=0} for I{backward compatibility}.
465 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
466 B{C{seed}}, B{C{name}} and other exceptions.
467 '''
468 adjust_limit_wrap = _xkwds(adjust_limit_wrap, limit=0)
469 Hausdorff.__init__(self, point1s, seed=seed, name=name,
470 **adjust_limit_wrap)
471 self._func = _formy._equirectangular # helper
473 if _FOR_DOCS:
474 directed = Hausdorff.directed
475 symmetric = Hausdorff.symmetric
478class HausdorffEuclidean(_HausdorffMeterRadians):
479 '''Compute the C{Hausdorff} distance based on the C{Euclidean}
480 distance in C{radians} from function L{pygeodesy.euclidean_}.
481 '''
482 def __init__(self, point1s, seed=None, name=NN, **adjust_wrap):
483 '''New L{HausdorffEuclidean} calculator.
485 @kwarg adjust_radius_wrap: Optional keyword arguments for
486 function L{pygeodesy.euclidean}.
488 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
489 B{C{seed}}, B{C{name}} and other exceptions.
490 '''
491 Hausdorff.__init__(self, point1s, seed=seed, name=name,
492 **adjust_wrap)
493 self._func = _formy.euclidean
494 self._func_ = _formy.euclidean_
496 if _FOR_DOCS:
497 directed = Hausdorff.directed
498 symmetric = Hausdorff.symmetric
501class HausdorffExact(Hausdorff):
502 '''Compute the C{Hausdorff} distance based on the I{angular}
503 distance in C{degrees} from method L{GeodesicExact}C{.Inverse}.
504 '''
505 _units = _Str_degrees
507 def __init__(self, point1s, seed=None, name=NN, datum=None, **wrap):
508 '''New L{HausdorffKarney} calculator.
510 @kwarg datum: Datum to override the default C{Datums.WGS84} and
511 first B{C{point1s}}' datum (L{Datum}, L{Ellipsoid},
512 L{Ellipsoid2} or L{a_f2Tuple}).
513 @kwarg wrap: Optional keyword argument for method C{Inverse1}
514 of class L{geodesicx.GeodesicExact}.
516 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
517 B{C{seed}}, B{C{name}} and other exceptions.
519 @raise TypeError: Invalid B{C{datum}}.
520 '''
521 Hausdorff.__init__(self, point1s, seed=seed, name=name,
522 **wrap)
523 self._datum_setter(datum)
524 self._func = self.datum.ellipsoid.geodesicx.Inverse1 # note -x
526 if _FOR_DOCS:
527 directed = Hausdorff.directed
528 symmetric = Hausdorff.symmetric
531class HausdorffFlatLocal(_HausdorffMeterRadians):
532 '''Compute the C{Hausdorff} distance based on the I{angular} distance in
533 C{radians squared} like function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}.
534 '''
535 _units = _Str_radians2
537 def __init__(self, point1s, seed=None, name=NN, **datum_scaled_wrap):
538 '''New L{HausdorffFlatLocal}/L{HausdorffHubeny} calculator.
540 @kwarg datum_scaled_wrap: Optional keyword arguments for
541 function L{pygeodesy.flatLocal}.
543 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
544 B{C{seed}}, B{C{name}} and other exceptions.
546 @note: The distance C{units} are C{radians squared}, not C{radians}.
547 '''
548 Hausdorff.__init__(self, point1s, seed=seed, name=name,
549 **datum_scaled_wrap)
550 self._func = _formy.flatLocal
551 self._func_ = self.datum.ellipsoid._hubeny_2
553 if _FOR_DOCS:
554 directed = Hausdorff.directed
555 symmetric = Hausdorff.symmetric
558class HausdorffFlatPolar(_HausdorffMeterRadians):
559 '''Compute the C{Hausdorff} distance based on the I{angular}
560 distance in C{radians} from function L{pygeodesy.flatPolar_}.
561 '''
562 _wrap = False
564 def __init__(self, points, seed=None, name=NN, **radius_wrap):
565 '''New L{HausdorffFlatPolar} calculator.
567 @kwarg radius_wrap: Optional keyword arguments for function
568 L{pygeodesy.flatPolar}.
570 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
571 B{C{seed}}, B{C{name}} and other exceptions.
572 '''
573 Hausdorff.__init__(self, points, seed=seed, name=name,
574 **radius_wrap)
575 self._func = _formy.flatPolar
576 self._func_ = _formy.flatPolar_
578 if _FOR_DOCS:
579 directed = Hausdorff.directed
580 symmetric = Hausdorff.symmetric
583class HausdorffHaversine(_HausdorffMeterRadians):
584 '''Compute the C{Hausdorff} distance based on the I{angular}
585 distance in C{radians} from function L{pygeodesy.haversine_}.
587 @note: See note under L{HausdorffVincentys}.
588 '''
589 _wrap = False
591 def __init__(self, points, seed=None, name=NN, **radius_wrap):
592 '''New L{HausdorffHaversine} calculator.
594 @kwarg radius_wrap: Optional keyword arguments for function
595 L{pygeodesy.haversine}.
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, points, seed=seed, name=name,
601 **radius_wrap)
602 self._func = _formy.haversine
603 self._func_ = _formy.haversine_
605 if _FOR_DOCS:
606 directed = Hausdorff.directed
607 symmetric = Hausdorff.symmetric
610class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny
611 if _FOR_DOCS:
612 __doc__ = HausdorffFlatLocal.__doc__
613 __init__ = HausdorffFlatLocal.__init__
614 directed = HausdorffFlatLocal.directed
615 distance = HausdorffFlatLocal.distance
616 symmetric = HausdorffFlatLocal.symmetric
619class HausdorffKarney(Hausdorff):
620 '''Compute the C{Hausdorff} distance based on the I{angular}
621 distance in C{degrees} from I{Karney}'s U{geographiclib
622 <https://PyPI.org/project/geographiclib>} U{Geodesic
623 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}
624 Inverse method.
625 '''
626 _units = _Str_degrees
628 def __init__(self, point1s, datum=None, seed=None, name=NN, **wrap):
629 '''New L{HausdorffKarney} calculator.
631 @kwarg datum: Datum to override the default C{Datums.WGS84} and
632 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid},
633 L{Ellipsoid2} or L{a_f2Tuple}).
634 @kwarg wrap: Optional keyword argument for method C{Inverse1}
635 of class L{geodesicw.Geodesic}.
637 @raise ImportError: Package U{geographiclib
638 <https://PyPI.org/project/geographiclib>} missing.
640 @raise TypeError: Invalid B{C{datum}}.
642 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
643 B{C{seed}}, B{C{name}} and other exceptions.
644 '''
645 Hausdorff.__init__(self, point1s, seed=seed, name=name,
646 **wrap)
647 self._datum_setter(datum)
648 self._func = self.datum.ellipsoid.geodesic.Inverse1
651class HausdorffThomas(_HausdorffMeterRadians):
652 '''Compute the C{Hausdorff} distance based on the I{angular}
653 distance in C{radians} from function L{pygeodesy.thomas_}.
654 '''
655 def __init__(self, point1s, seed=None, name=NN, **datum_wrap):
656 '''New L{HausdorffThomas} calculator.
658 @kwarg datum_wrap: Optional keyword argument for function
659 L{pygeodesy.thomas}.
661 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
662 B{C{seed}}, B{C{name}} and other exceptions.
663 '''
664 Hausdorff.__init__(self, point1s, seed=seed, name=name,
665 **datum_wrap)
666 self._func = _formy.thomas
667 self._func_ = _formy.thomas_
669 if _FOR_DOCS:
670 directed = Hausdorff.directed
671 symmetric = Hausdorff.symmetric
674class HausdorffVincentys(_HausdorffMeterRadians):
675 '''Compute the C{Hausdorff} distance based on the I{angular}
676 distance in C{radians} from function L{pygeodesy.vincentys_}.
678 @note: See note at function L{pygeodesy.vincentys_}.
679 '''
680 _wrap = False
682 def __init__(self, point1s, seed=None, name=NN, **radius_wrap):
683 '''New L{HausdorffVincentys} calculator.
685 @kwarg radius_wrap: Optional keyword arguments for function
686 L{pygeodesy.vincentys}.
688 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
689 B{C{seed}}, B{C{name}} and other exceptions.
690 '''
691 Hausdorff.__init__(self, point1s, seed=seed, name=name,
692 **radius_wrap)
693 self._func = _formy.vincentys
694 self._func_ = _formy.vincentys_
696 if _FOR_DOCS:
697 directed = Hausdorff.directed
698 symmetric = Hausdorff.symmetric
701def _hausdorff_(ps1, ps2, both, early, seed, units, distance, point):
702 '''(INTERNAL) Core of function L{hausdorff_} and methods C{directed}
703 and C{symmetric} of classes C{hausdorff.Hausdorff...}.
704 '''
705 # shuffling the points generally increases the
706 # chance of an early break in the inner j loop
707 rr = randomrangenerator(seed) if seed else range
709 hd = NINF
710 hi = hj = m = mn = 0
711 md = _0_0
713 # forward or forward and backward
714 for fb in range(2 if both else 1):
715 n = len(ps2)
716 for i in rr(len(ps1)):
717 p1 = point(ps1[i])
718 dh, dj = INF, 0
719 for j in rr(n):
720 p2 = point(ps2[j])
721 d = distance(p1, p2)
722 if early and d < hd:
723 break # early
724 elif d < dh:
725 dh, dj = d, j
726 else: # no early break
727 if hd < dh:
728 hd = dh
729 if fb:
730 hi, hj = dj, i
731 else:
732 hi, hj = i, dj
733 md += dh
734 mn += 1
735 m += 1
736 # swap model and target
737 ps1, ps2 = ps2, ps1
739 md = None if mn < m else (md / float(m))
740 return Hausdorff6Tuple(hd, hi, hj, m, md, units)
743def _point(p):
744 '''Default B{C{point}} callable for function L{hausdorff_}.
746 @arg p: The original C{model} or C{target} point (C{any}).
748 @return: The point, suitable for the L{hausdorff_}
749 B{C{distance}} callable.
750 '''
751 return p
754def hausdorff_(model, target, both=False, early=True, seed=None, units=NN,
755 distance=None, point=_point):
756 '''Compute the C{directed} or C{symmetric} U{Hausdorff
757 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance between 2 sets of points
758 with or without U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
759 and U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
761 @arg model: First set of points (C{LatLon}[], C{Numpy2LatLon}[],
762 C{Tuple2LatLon}[] or C{other}[]).
763 @arg target: Second set of points (C{LatLon}[], C{Numpy2LatLon}[],
764 C{Tuple2LatLon}[] or C{other}[]).
765 @kwarg both: Return the C{directed} (forward only) or the C{symmetric}
766 (combined forward and reverse) C{Hausdorff} distance (C{bool}).
767 @kwarg early: Enable or disable U{early breaking<https://Publik.TUWien.ac.AT/
768 files/PubDat_247739.pdf>} (C{bool}).
769 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False} for no
770 U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
771 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
772 @kwarg distance: Callable returning the distance between a B{C{model}}
773 and B{C{target}} point (signature C{(point1, point2)}).
774 @kwarg point: Callable returning the B{C{model}} or B{C{target}} point
775 suitable for B{C{distance}} (signature C{(point)}).
777 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
779 @raise HausdorffError: Insufficient number of B{C{model}} or B{C{target}} points.
781 @raise TypeError: If B{C{distance}} or B{C{point}} is not callable.
782 '''
783 if not callable(distance):
784 raise _IsnotError(callable.__name__, distance=distance)
785 if not callable(point):
786 raise _IsnotError(callable.__name__, point=point)
788 _, ps1 = _points2(model, closed=False, Error=HausdorffError) # PYCHOK non-sequence
789 _, ps2 = _points2(target, closed=False, Error=HausdorffError) # PYCHOK non-sequence
790 return _hausdorff_(ps1, ps2, both, early, seed, units, distance, point)
793class Hausdorff6Tuple(_NamedTuple):
794 '''6-Tuple C{(hd, i, j, mn, md, units)} with the U{Hausdorff
795 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance C{hd},
796 indices C{i} and C{j}, the total count C{mn}, the C{I{mean}
797 Hausdorff} distance C{md} and the class or name of both distance
798 C{units}.
800 For C{directed Hausdorff} distances, count C{mn} is the number
801 of model points considered. For C{symmetric Hausdorff} distances
802 count C{mn} twice that.
804 Indices C{i} and C{j} are the C{model} respectively C{target}
805 point with the C{hd} distance.
807 Mean distance C{md} is C{None} if an C{early break} occurred and
808 U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
809 was enabled by keyword argument C{early=True}.
810 '''
811 _Names_ = ('hd', _i_, _j_, 'mn', 'md', _units_)
812 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass)
814 def toUnits(self, **Error): # PYCHOK expected
815 '''Overloaded C{_NamedTuple.toUnits} for C{hd} and C{md} units.
816 '''
817 U = _xUnit(self.units, Float) # PYCHOK expected
818 M = _Pass if self.md is None else U # PYCHOK expected
819 self._Units_ = (U,) + Hausdorff6Tuple._Units_[1:4] \
820 + (M,) + Hausdorff6Tuple._Units_[5:]
821 return _NamedTuple.toUnits(self, **Error)
824def randomrangenerator(seed):
825 '''Return a C{seed}ed random range function generator.
827 @arg seed: Initial, internal L{Random} state (C{hashable}
828 or C{None}).
830 @note: L{Random} with C{B{seed} is None} seeds from the
831 current time or from a platform-specific randomness
832 source, if available.
834 @return: A function to generate random ranges.
836 @example:
838 >>> rrange = randomrangenerator('R')
839 >>> for r in rrange(n):
840 >>> ... # r is random in 0..n-1
841 '''
842 R = Random(seed)
844 def _range(n, *stop_step):
845 '''Like standard L{range}C{start, stop=..., step=...)},
846 except the returned values are in random order.
848 @note: Especially C{range(n)} behaves like standard
849 L{Random.sample}C{(range(n), n)} but avoids
850 creating a tuple with the entire C{population}
851 and a list containing all sample values (for
852 large C{n}).
853 '''
854 if stop_step:
855 s = range(n, *stop_step)
857 elif n > 32:
858 r = R.randrange # Random._randbelow
859 s = set()
860 for _ in range(n - 32):
861 i = r(n)
862 while i in s:
863 i = r(n)
864 s.add(i)
865 yield i
866 s = set(range(n)) - s # [i for i in range(n) if i not in s]
867 else:
868 s = range(n)
870 s = list(s)
871 R.shuffle(s)
872 while s:
873 yield s.pop(0)
875 return _range
877# **) MIT License
878#
879# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
880#
881# Permission is hereby granted, free of charge, to any person obtaining a
882# copy of this software and associated documentation files (the "Software"),
883# to deal in the Software without restriction, including without limitation
884# the rights to use, copy, modify, merge, publish, distribute, sublicense,
885# and/or sell copies of the Software, and to permit persons to whom the
886# Software is furnished to do so, subject to the following conditions:
887#
888# The above copyright notice and this permission notice shall be included
889# in all copies or substantial portions of the Software.
890#
891# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
892# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
893# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
894# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
895# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
896# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
897# OTHER DEALINGS IN THE SOFTWARE.