Coverage for pygeodesy/hausdorff.py: 96%
233 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-10-04 14:05 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-10-04 14:05 -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.09.22'
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 '''Return the distance in C{degrees} between B{C{point1}} and B{C{point2}}.
276 I{Must be overloaded}.'''
277 notOverloaded(self, point1, point2)
280class HausdorffRadians(Hausdorff):
281 '''L{Hausdorff} base class for distances from C{LatLon}
282 points converted from C{degrees} to C{radians}.
283 '''
284 _units = _Str_radians
286 if _FOR_DOCS:
287 __init__ = Hausdorff.__init__
288 directed = Hausdorff.directed
289 symmetric = Hausdorff.symmetric
291 def distance(self, point1, point2): # PYCHOK no cover
292 '''Return the distance in C{radians} between B{C{point1}} and B{C{point2}}.
293 I{Must be overloaded}.'''
294 notOverloaded(self, point1, point2)
296 def point(self, point):
297 '''Return B{C{point}} as L{PhiLam2Tuple} to maintain
298 I{backward compatibility} of L{HausdorffRadians}.
300 @return: A L{PhiLam2Tuple}C{(phi, lam)}.
301 '''
302 try:
303 return point.philam
304 except AttributeError:
305 return PhiLam2Tuple(radians(point.lat), radians(point.lon))
308class _HausdorffMeterRadians(Hausdorff):
309 '''(INTERNAL) Returning C{meter} or C{radians} depending on
310 the optional keyword arguments supplied at instantiation
311 of the C{Hausdorff*} sub-class.
312 '''
313 _units = _Str_meter
314 _units_ = _Str_radians
316 def directed(self, point2s, early=True):
317 '''Overloaded method L{Hausdorff.directed} to determine
318 the distance function and units from the optional
319 keyword arguments given at this instantiation, see
320 property C{kwds}.
322 @see: L{Hausdorff.directed} for other details.
323 '''
324 return self._hausdorff_(point2s, False, early, _formy._radistance(self))
326 def symmetric(self, point2s, early=True):
327 '''Overloaded method L{Hausdorff.symmetric} to determine
328 the distance function and units from the optional
329 keyword arguments given at this instantiation, see
330 property C{kwds}.
332 @see: L{Hausdorff.symmetric} for other details.
333 '''
334 return self._hausdorff_(point2s, True, early, _formy._radistance(self))
336 def _func_(self, *args, **kwds): # PYCHOK no cover
337 '''(INTERNAL) I{Must be overloaded}.'''
338 notOverloaded(self, *args, **kwds)
341class HausdorffCosineAndoyerLambert(_HausdorffMeterRadians):
342 '''Compute the C{Hausdorff} distance based on the I{angular} distance
343 in C{radians} from function L{pygeodesy.cosineAndoyerLambert}.
344 '''
345 def __init__(self, point1s, seed=None, name=NN, **datum_wrap):
346 '''New L{HausdorffCosineAndoyerLambert} calculator.
348 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
349 B{C{seed}}, B{C{name}} and other exceptions.
351 @kwarg datum_wrap: Optional keyword arguments for function
352 L{pygeodesy.cosineAndoyerLambert}.
353 '''
354 Hausdorff.__init__(self, point1s, seed=seed, name=name,
355 **datum_wrap)
356 self._func = _formy.cosineAndoyerLambert
357 self._func_ = _formy.cosineAndoyerLambert_
359 if _FOR_DOCS:
360 directed = Hausdorff.directed
361 symmetric = Hausdorff.symmetric
364class HausdorffCosineForsytheAndoyerLambert(_HausdorffMeterRadians):
365 '''Compute the C{Hausdorff} distance based on the I{angular} distance
366 in C{radians} from function L{pygeodesy.cosineForsytheAndoyerLambert}.
367 '''
368 def __init__(self, point1s, seed=None, name=NN, **datum_wrap):
369 '''New L{HausdorffCosineForsytheAndoyerLambert} calculator.
371 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
372 B{C{seed}}, B{C{name}} and other exceptions.
374 @kwarg datum_wrap: Optional keyword arguments for function
375 L{pygeodesy.cosineAndoyerLambert}.
376 '''
377 Hausdorff.__init__(self, point1s, seed=seed, name=name,
378 **datum_wrap)
379 self._func = _formy.cosineForsytheAndoyerLambert
380 self._func_ = _formy.cosineForsytheAndoyerLambert_
382 if _FOR_DOCS:
383 directed = Hausdorff.directed
384 symmetric = Hausdorff.symmetric
387class HausdorffCosineLaw(_HausdorffMeterRadians):
388 '''Compute the C{Hausdorff} distance based on the I{angular}
389 distance in C{radians} from function L{pygeodesy.cosineLaw_}.
391 @note: See note at function L{pygeodesy.vincentys_}.
392 '''
393 def __init__(self, point1s, seed=None, name=NN, **radius_wrap):
394 '''New L{HausdorffCosineLaw} calculator.
396 @kwarg radius_wrap: Optional keyword arguments for function
397 L{pygeodesy.cosineLaw}.
399 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
400 B{C{seed}}, B{C{name}} and other exceptions.
401 '''
402 Hausdorff.__init__(self, point1s, seed=seed, name=name,
403 **radius_wrap)
404 self._func = _formy.cosineLaw
405 self._func_ = _formy.cosineLaw_
407 if _FOR_DOCS:
408 directed = Hausdorff.directed
409 symmetric = Hausdorff.symmetric
412class HausdorffDistanceTo(Hausdorff):
413 '''Compute the C{Hausdorff} distance based on the distance from the
414 points' C{LatLon.distanceTo} method, conventionally in C{meter}.
415 '''
416 _units = _Str_meter
418 def __init__(self, point1s, seed=None, name=NN, **distanceTo_kwds):
419 '''New L{HausdorffDistanceTo} calculator.
421 @kwarg distanceTo_kwds: Optional keyword arguments for each
422 B{C{point1s}}' C{LatLon.distanceTo}
423 method.
425 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
426 B{C{seed}}, B{C{name}} and other exceptions.
428 @note: All C{model}, C{template} and C{target} B{C{points}}
429 I{must} be instances of the same ellipsoidal or
430 spherical C{LatLon} class.
431 '''
432 Hausdorff.__init__(self, point1s, seed=seed, name=name,
433 **distanceTo_kwds)
435 if _FOR_DOCS:
436 directed = Hausdorff.directed
437 symmetric = Hausdorff.symmetric
439 def distance(self, p1, p2):
440 '''Return the distance in C{meter}.
441 '''
442 return p1.distanceTo(p2, **self._kwds)
444 def _points2(self, points):
445 '''(INTERNAL) Check a set of points.
446 '''
447 np, ps = Hausdorff._points2(self, points)
448 return np, _distanceTo(HausdorffError, points=ps)
451class HausdorffEquirectangular(Hausdorff):
452 '''Compute the C{Hausdorff} distance based on the C{equirectangular} distance
453 in C{radians squared} like function L{pygeodesy.equirectangular_}.
454 '''
455 _units = _Str_degrees2
457 def __init__(self, point1s, seed=None, name=NN, **adjust_limit_wrap):
458 '''New L{HausdorffEquirectangular} calculator.
460 @kwarg adjust_limit_wrap: Optional keyword arguments for function
461 L{pygeodesy.equirectangular_} I{with default}
462 C{B{limit}=0} for I{backward compatibility}.
464 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
465 B{C{seed}}, B{C{name}} and other exceptions.
466 '''
467 adjust_limit_wrap = _xkwds(adjust_limit_wrap, limit=0)
468 Hausdorff.__init__(self, point1s, seed=seed, name=name,
469 **adjust_limit_wrap)
470 self._func = _formy._equirectangular # helper
472 if _FOR_DOCS:
473 directed = Hausdorff.directed
474 symmetric = Hausdorff.symmetric
477class HausdorffEuclidean(_HausdorffMeterRadians):
478 '''Compute the C{Hausdorff} distance based on the C{Euclidean}
479 distance in C{radians} from function L{pygeodesy.euclidean_}.
480 '''
481 def __init__(self, point1s, seed=None, name=NN, **adjust_wrap):
482 '''New L{HausdorffEuclidean} calculator.
484 @kwarg adjust_radius_wrap: Optional keyword arguments for
485 function L{pygeodesy.euclidean}.
487 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
488 B{C{seed}}, B{C{name}} and other exceptions.
489 '''
490 Hausdorff.__init__(self, point1s, seed=seed, name=name,
491 **adjust_wrap)
492 self._func = _formy.euclidean
493 self._func_ = _formy.euclidean_
495 if _FOR_DOCS:
496 directed = Hausdorff.directed
497 symmetric = Hausdorff.symmetric
500class HausdorffExact(Hausdorff):
501 '''Compute the C{Hausdorff} distance based on the I{angular}
502 distance in C{degrees} from method L{GeodesicExact}C{.Inverse}.
503 '''
504 _units = _Str_degrees
506 def __init__(self, point1s, seed=None, name=NN, datum=None, **wrap):
507 '''New L{HausdorffKarney} calculator.
509 @kwarg datum: Datum to override the default C{Datums.WGS84} and
510 first B{C{point1s}}' datum (L{Datum}, L{Ellipsoid},
511 L{Ellipsoid2} or L{a_f2Tuple}).
512 @kwarg wrap: Optional keyword argument for method C{Inverse1}
513 of class L{geodesicx.GeodesicExact}.
515 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
516 B{C{seed}}, B{C{name}} and other exceptions.
518 @raise TypeError: Invalid B{C{datum}}.
519 '''
520 Hausdorff.__init__(self, point1s, seed=seed, name=name,
521 **wrap)
522 self._datum_setter(datum)
523 self._func = self.datum.ellipsoid.geodesicx.Inverse1 # note -x
525 if _FOR_DOCS:
526 directed = Hausdorff.directed
527 symmetric = Hausdorff.symmetric
530class HausdorffFlatLocal(_HausdorffMeterRadians):
531 '''Compute the C{Hausdorff} distance based on the I{angular} distance in
532 C{radians squared} like function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}.
533 '''
534 _units = _Str_radians2
536 def __init__(self, point1s, seed=None, name=NN, **datum_scaled_wrap):
537 '''New L{HausdorffFlatLocal}/L{HausdorffHubeny} calculator.
539 @kwarg datum_scaled_wrap: Optional keyword arguments for
540 function L{pygeodesy.flatLocal}.
542 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
543 B{C{seed}}, B{C{name}} and other exceptions.
545 @note: The distance C{units} are C{radians squared}, not C{radians}.
546 '''
547 Hausdorff.__init__(self, point1s, seed=seed, name=name,
548 **datum_scaled_wrap)
549 self._func = _formy.flatLocal
550 self._func_ = self.datum.ellipsoid._hubeny_2
552 if _FOR_DOCS:
553 directed = Hausdorff.directed
554 symmetric = Hausdorff.symmetric
557class HausdorffFlatPolar(_HausdorffMeterRadians):
558 '''Compute the C{Hausdorff} distance based on the I{angular}
559 distance in C{radians} from function L{pygeodesy.flatPolar_}.
560 '''
561 _wrap = False
563 def __init__(self, points, seed=None, name=NN, **radius_wrap):
564 '''New L{HausdorffFlatPolar} calculator.
566 @kwarg radius_wrap: Optional keyword arguments for function
567 L{pygeodesy.flatPolar}.
569 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
570 B{C{seed}}, B{C{name}} and other exceptions.
571 '''
572 Hausdorff.__init__(self, points, seed=seed, name=name,
573 **radius_wrap)
574 self._func = _formy.flatPolar
575 self._func_ = _formy.flatPolar_
577 if _FOR_DOCS:
578 directed = Hausdorff.directed
579 symmetric = Hausdorff.symmetric
582class HausdorffHaversine(_HausdorffMeterRadians):
583 '''Compute the C{Hausdorff} distance based on the I{angular}
584 distance in C{radians} from function L{pygeodesy.haversine_}.
586 @note: See note under L{HausdorffVincentys}.
587 '''
588 _wrap = False
590 def __init__(self, points, seed=None, name=NN, **radius_wrap):
591 '''New L{HausdorffHaversine} calculator.
593 @kwarg radius_wrap: Optional keyword arguments for function
594 L{pygeodesy.haversine}.
596 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
597 B{C{seed}}, B{C{name}} and other exceptions.
598 '''
599 Hausdorff.__init__(self, points, seed=seed, name=name,
600 **radius_wrap)
601 self._func = _formy.haversine
602 self._func_ = _formy.haversine_
604 if _FOR_DOCS:
605 directed = Hausdorff.directed
606 symmetric = Hausdorff.symmetric
609class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny
610 if _FOR_DOCS:
611 __doc__ = HausdorffFlatLocal.__doc__
612 __init__ = HausdorffFlatLocal.__init__
613 directed = HausdorffFlatLocal.directed
614 distance = HausdorffFlatLocal.distance
615 symmetric = HausdorffFlatLocal.symmetric
618class HausdorffKarney(Hausdorff):
619 '''Compute the C{Hausdorff} distance based on the I{angular}
620 distance in C{degrees} from I{Karney}'s U{geographiclib
621 <https://PyPI.org/project/geographiclib>} U{Geodesic
622 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}
623 Inverse method.
624 '''
625 _units = _Str_degrees
627 def __init__(self, point1s, datum=None, seed=None, name=NN, **wrap):
628 '''New L{HausdorffKarney} calculator.
630 @kwarg datum: Datum to override the default C{Datums.WGS84} and
631 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid},
632 L{Ellipsoid2} or L{a_f2Tuple}).
633 @kwarg wrap: Optional keyword argument for method C{Inverse1}
634 of class L{geodesicw.Geodesic}.
636 @raise ImportError: Package U{geographiclib
637 <https://PyPI.org/project/geographiclib>} missing.
639 @raise TypeError: Invalid B{C{datum}}.
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=seed, name=name,
645 **wrap)
646 self._datum_setter(datum)
647 self._func = self.datum.ellipsoid.geodesic.Inverse1
650class HausdorffThomas(_HausdorffMeterRadians):
651 '''Compute the C{Hausdorff} distance based on the I{angular}
652 distance in C{radians} from function L{pygeodesy.thomas_}.
653 '''
654 def __init__(self, point1s, seed=None, name=NN, **datum_wrap):
655 '''New L{HausdorffThomas} calculator.
657 @kwarg datum_wrap: Optional keyword argument for function
658 L{pygeodesy.thomas}.
660 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
661 B{C{seed}}, B{C{name}} and other exceptions.
662 '''
663 Hausdorff.__init__(self, point1s, seed=seed, name=name,
664 **datum_wrap)
665 self._func = _formy.thomas
666 self._func_ = _formy.thomas_
668 if _FOR_DOCS:
669 directed = Hausdorff.directed
670 symmetric = Hausdorff.symmetric
673class HausdorffVincentys(_HausdorffMeterRadians):
674 '''Compute the C{Hausdorff} distance based on the I{angular}
675 distance in C{radians} from function L{pygeodesy.vincentys_}.
677 @note: See note at function L{pygeodesy.vincentys_}.
678 '''
679 _wrap = False
681 def __init__(self, point1s, seed=None, name=NN, **radius_wrap):
682 '''New L{HausdorffVincentys} calculator.
684 @kwarg radius_wrap: Optional keyword arguments for function
685 L{pygeodesy.vincentys}.
687 @see: L{Hausdorff.__init__} for details about B{C{point1s}},
688 B{C{seed}}, B{C{name}} and other exceptions.
689 '''
690 Hausdorff.__init__(self, point1s, seed=seed, name=name,
691 **radius_wrap)
692 self._func = _formy.vincentys
693 self._func_ = _formy.vincentys_
695 if _FOR_DOCS:
696 directed = Hausdorff.directed
697 symmetric = Hausdorff.symmetric
700def _hausdorff_(ps1, ps2, both, early, seed, units, distance, point):
701 '''(INTERNAL) Core of function L{hausdorff_} and methods C{directed}
702 and C{symmetric} of classes C{hausdorff.Hausdorff...}.
703 '''
704 # shuffling the points generally increases the
705 # chance of an early break in the inner j loop
706 rr = randomrangenerator(seed) if seed else range
708 hd = NINF
709 hi = hj = m = mn = 0
710 md = _0_0
712 # forward or forward and backward
713 for fb in range(2 if both else 1):
714 n = len(ps2)
715 for i in rr(len(ps1)):
716 p1 = point(ps1[i])
717 dh, dj = INF, 0
718 for j in rr(n):
719 p2 = point(ps2[j])
720 d = distance(p1, p2)
721 if early and d < hd:
722 break # early
723 elif d < dh:
724 dh, dj = d, j
725 else: # no early break
726 if hd < dh:
727 hd = dh
728 if fb:
729 hi, hj = dj, i
730 else:
731 hi, hj = i, dj
732 md += dh
733 mn += 1
734 m += 1
735 # swap model and target
736 ps1, ps2 = ps2, ps1
738 md = None if mn < m else (md / float(m))
739 return Hausdorff6Tuple(hd, hi, hj, m, md, units)
742def _point(p):
743 '''Default B{C{point}} callable for function L{hausdorff_}.
745 @arg p: The original C{model} or C{target} point (C{any}).
747 @return: The point, suitable for the L{hausdorff_}
748 B{C{distance}} callable.
749 '''
750 return p
753def hausdorff_(model, target, both=False, early=True, seed=None, units=NN,
754 distance=None, point=_point):
755 '''Compute the C{directed} or C{symmetric} U{Hausdorff
756 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance between 2 sets of points
757 with or without U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
758 and U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
760 @arg model: First set of points (C{LatLon}[], C{Numpy2LatLon}[],
761 C{Tuple2LatLon}[] or C{other}[]).
762 @arg target: Second set of points (C{LatLon}[], C{Numpy2LatLon}[],
763 C{Tuple2LatLon}[] or C{other}[]).
764 @kwarg both: Return the C{directed} (forward only) or the C{symmetric}
765 (combined forward and reverse) C{Hausdorff} distance (C{bool}).
766 @kwarg early: Enable or disable U{early breaking<https://Publik.TUWien.ac.AT/
767 files/PubDat_247739.pdf>} (C{bool}).
768 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False} for no
769 U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}.
770 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
771 @kwarg distance: Callable returning the distance between a B{C{model}}
772 and B{C{target}} point (signature C{(point1, point2)}).
773 @kwarg point: Callable returning the B{C{model}} or B{C{target}} point
774 suitable for B{C{distance}} (signature C{(point)}).
776 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}.
778 @raise HausdorffError: Insufficient number of B{C{model}} or B{C{target}} points.
780 @raise TypeError: If B{C{distance}} or B{C{point}} is not callable.
781 '''
782 if not callable(distance):
783 raise _IsnotError(callable.__name__, distance=distance)
784 if not callable(point):
785 raise _IsnotError(callable.__name__, point=point)
787 _, ps1 = _points2(model, closed=False, Error=HausdorffError) # PYCHOK non-sequence
788 _, ps2 = _points2(target, closed=False, Error=HausdorffError) # PYCHOK non-sequence
789 return _hausdorff_(ps1, ps2, both, early, seed, units, distance, point)
792class Hausdorff6Tuple(_NamedTuple):
793 '''6-Tuple C{(hd, i, j, mn, md, units)} with the U{Hausdorff
794 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance C{hd},
795 indices C{i} and C{j}, the total count C{mn}, the C{I{mean}
796 Hausdorff} distance C{md} and the class or name of both distance
797 C{units}.
799 For C{directed Hausdorff} distances, count C{mn} is the number
800 of model points considered. For C{symmetric Hausdorff} distances
801 count C{mn} twice that.
803 Indices C{i} and C{j} are the C{model} respectively C{target}
804 point with the C{hd} distance.
806 Mean distance C{md} is C{None} if an C{early break} occurred and
807 U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}
808 was enabled by keyword argument C{early=True}.
809 '''
810 _Names_ = ('hd', _i_, _j_, 'mn', 'md', _units_)
811 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass)
813 def toUnits(self, **Error): # PYCHOK expected
814 '''Overloaded C{_NamedTuple.toUnits} for C{hd} and C{md} units.
815 '''
816 U = _xUnit(self.units, Float) # PYCHOK expected
817 M = _Pass if self.md is None else U # PYCHOK expected
818 self._Units_ = (U,) + Hausdorff6Tuple._Units_[1:4] \
819 + (M,) + Hausdorff6Tuple._Units_[5:]
820 return _NamedTuple.toUnits(self, **Error)
823def randomrangenerator(seed):
824 '''Return a C{seed}ed random range function generator.
826 @arg seed: Initial, internal L{Random} state (C{hashable}
827 or C{None}).
829 @note: L{Random} with C{B{seed} is None} seeds from the
830 current time or from a platform-specific randomness
831 source, if available.
833 @return: A function to generate random ranges.
835 @example:
837 >>> rrange = randomrangenerator('R')
838 >>> for r in rrange(n):
839 >>> ... # r is random in 0..n-1
840 '''
841 R = Random(seed)
843 def _range(n, *stop_step):
844 '''Like standard L{range}C{start, stop=..., step=...)},
845 except the returned values are in random order.
847 @note: Especially C{range(n)} behaves like standard
848 L{Random.sample}C{(range(n), n)} but avoids
849 creating a tuple with the entire C{population}
850 and a list containing all sample values (for
851 large C{n}).
852 '''
853 if stop_step:
854 s = range(n, *stop_step)
856 elif n > 32:
857 r = R.randrange # Random._randbelow
858 s = set()
859 for _ in range(n - 32):
860 i = r(n)
861 while i in s:
862 i = r(n)
863 s.add(i)
864 yield i
865 s = set(range(n)) - s # [i for i in range(n) if i not in s]
866 else:
867 s = range(n)
869 s = list(s)
870 R.shuffle(s)
871 while s:
872 yield s.pop(0)
874 return _range
876# **) MIT License
877#
878# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
879#
880# Permission is hereby granted, free of charge, to any person obtaining a
881# copy of this software and associated documentation files (the "Software"),
882# to deal in the Software without restriction, including without limitation
883# the rights to use, copy, modify, merge, publish, distribute, sublicense,
884# and/or sell copies of the Software, and to permit persons to whom the
885# Software is furnished to do so, subject to the following conditions:
886#
887# The above copyright notice and this permission notice shall be included
888# in all copies or substantial portions of the Software.
889#
890# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
891# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
892# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
893# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
894# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
895# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
896# OTHER DEALINGS IN THE SOFTWARE.