Coverage for pygeodesy/frechet.py: 96%
261 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
2# -*- coding: utf-8 -*-
4u'''Fréchet distances.
6Classes L{Frechet}, L{FrechetDegrees}, L{FrechetRadians}, L{FrechetCosineLaw},
7L{FrechetDistanceTo}, L{FrechetEquirectangular}, L{FrechetEuclidean},
8L{FrechetExact}, L{FrechetFlatLocal}, L{FrechetFlatPolar}, L{FrechetHaversine},
9L{FrechetHubeny}, L{FrechetKarney}, L{FrechetThomas} and L{FrechetVincentys}
10to compute I{discrete} U{Fréchet<https://WikiPedia.org/wiki/Frechet_distance>}
11distances between two sets of C{LatLon}, C{NumPy}, C{tuples} or other types of points.
13Only L{FrechetDistanceTo} -iff used with L{ellipsoidalKarney.LatLon}
14points- and L{FrechetKarney} requires installation of I{Charles Karney}'s
15U{geographiclib<https://PyPI.org/project/geographiclib>}.
17Typical usage is as follows. First, create a C{Frechet} calculator from one
18set of C{LatLon} points.
20C{f = FrechetXyz(point1s, ...)}
22Get the I{discrete} Fréchet distance to another set of C{LatLon} points
23by
25C{t6 = f.discrete(point2s)}
27Or, use function C{frechet_} with a proper C{distance} function passed
28as keyword arguments as follows
30C{t6 = frechet_(point1s, point2s, ..., distance=...)}.
32In both cases, the returned result C{t6} is a L{Frechet6Tuple}.
34For C{(lat, lon, ...)} points in a C{NumPy} array or plain C{tuples},
35wrap the points in a L{Numpy2LatLon} respectively L{Tuple2LatLon}
36instance, more details in the documentation thereof.
38For other points, create a L{Frechet} sub-class with the appropriate
39C{distance} method overloading L{Frechet.distance} as in this example.
41 >>> from pygeodesy import Frechet, hypot_
42 >>>
43 >>> class F3D(Frechet):
44 >>> """Custom Frechet example.
45 >>> """
46 >>> def distance(self, p1, p2):
47 >>> return hypot_(p1.x - p2.x, p1.y - p2.y, p1.z - p2.z)
48 >>>
49 >>> f3D = F3D(xyz1, ..., units="...")
50 >>> t6 = f3D.discrete(xyz2)
52Transcribed from the original U{Computing Discrete Fréchet Distance
53<https://www.kr.TUWien.ac.AT/staff/eiter/et-archive/cdtr9464.pdf>} by
54Eiter, T. and Mannila, H., 1994, April 25, Technical Report CD-TR 94/64,
55Information Systems Department/Christian Doppler Laboratory for Expert
56Systems, Technical University Vienna, Austria.
58This L{Frechet.discrete} implementation optionally generates intermediate
59points for each point set separately. For example, using keyword argument
60C{fraction=0.5} adds one additional point halfway between each pair of
61points. Or using C{fraction=0.1} interpolates nine additional points
62between each points pair.
64The L{Frechet6Tuple} attributes C{fi1} and/or C{fi2} will be I{fractional}
65indices of type C{float} if keyword argument C{fraction} is used. Otherwise,
66C{fi1} and/or C{fi2} are simply type C{int} indices into the respective
67points set.
69For example, C{fractional} index value 2.5 means an intermediate point
70halfway between points[2] and points[3]. Use function L{fractional}
71to obtain the intermediate point for a I{fractional} index in the
72corresponding set of points.
74The C{Fréchet} distance was introduced in 1906 by U{Maurice Fréchet
75<https://WikiPedia.org/wiki/Maurice_Rene_Frechet>}, see U{reference
76[6]<https://www.kr.TUWien.ac.AT/staff/eiter/et-archive/cdtr9464.pdf>}.
77It is a measure of similarity between curves that takes into account the
78location and ordering of the points. Therefore, it is often a better metric
79than the well-known C{Hausdorff} distance, see the L{hausdorff} module.
80'''
82from pygeodesy.basics import _isin, isscalar, typename
83from pygeodesy.constants import EPS, EPS1, INF, NINF
84from pygeodesy.datums import _ellipsoidal_datum, _WGS84
85from pygeodesy.errors import PointsError, _xattr, _xcallable, \
86 _xkwds, _xkwds_get
87# from pygeodesy import formy as _formy # _MODS.into
88# from pygeodesy.internals import typename # from .basics
89from pygeodesy.interns import _DMAIN_, _DOT_, _n_, NN, _units_
90# from pygeodesy.iters import points2 as _points2 # from .points
91from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _FOR_DOCS
92from pygeodesy.named import _name2__, _Named, _NamedTuple, _Pass
93# from pygeodesy.namedTuples import PhiLam2Tuple # from .points
94from pygeodesy.points import _distanceTo, _fractional, PhiLam2Tuple, \
95 points2 as _points2, radians
96from pygeodesy.props import Property, property_doc_, property_RO
97from pygeodesy.units import FIx, Float, Number_
98from pygeodesy import unitsBase as _unitsBase # _Str_..., _xUnit, _xUnits
100from collections import defaultdict as _defaultdict
101# from math import radians # from .points
103__all__ = _ALL_LAZY.frechet
104__version__ = '25.04.21'
106_formy = _MODS.into(formy=__name__)
109def _fraction(fraction, n):
110 f = 1 # int, no fractional indices
111 if _isin(fraction, None, 1):
112 pass
113 elif not (isscalar(fraction) and EPS < fraction < EPS1
114 and (float(n) - fraction) < n):
115 raise FrechetError(fraction=fraction)
116 elif fraction < EPS1:
117 f = float(fraction)
118 return f
121class FrechetError(PointsError):
122 '''Fréchet issue.
123 '''
124 pass
127class Frechet(_Named):
128 '''Frechet base class, requires method L{Frechet.distance} to
129 be overloaded.
130 '''
131 _datum = _WGS84
132 _func = None # formy function/property
133 _f1 = 1
134 _kwds = {} # func_ options
135 _n1 = 0
136 _ps1 = None
137 _units = _unitsBase._Str_NN # XXX Str to _Pass and for backward compatibility
139 def __init__(self, point1s, fraction=None, units=NN, **name__kwds):
140 '''New C{Frechet...} calculator/interpolator.
142 @arg point1s: First set of points (C{LatLon}[], L{Numpy2LatLon}[],
143 L{Tuple2LatLon}[] or C{other}[]).
144 @kwarg fraction: Index fraction (C{float} in L{EPS}..L{EPS1}) to
145 interpolate intermediate B{C{point1s}} or use C{None},
146 C{0} or C{1} for no intermediate B{C{point1s}} and no
147 I{fractional} indices.
148 @kwarg units: Optional distance units (C{Unit} or C{str}).
149 @kwarg name__kwds: Optional C{B{name}=NN} for this calculator/interpolator
150 (C{str}) and any keyword arguments for the distance function,
151 retrievable with property C{kwds}.
153 @raise FrechetError: Insufficient number of B{C{point1s}} or an invalid
154 B{C{point1}}, B{C{fraction}} or B{C{units}}.
155 '''
156 name, kwds = _name2__(**name__kwds) # name__=self.__class__
157 if name:
158 self.name = name
160 self._n1, self._ps1 = self._points2(point1s)
161 if fraction:
162 self.fraction = fraction
163 if units: # and not self.units:
164 self.units = units
165 if kwds:
166 self._kwds = kwds
168 @property_RO
169 def adjust(self):
170 '''Get the C{adjust} setting (C{bool} or C{None}).
171 '''
172 return _xkwds_get(self._kwds, adjust=None)
174 @property_RO
175 def datum(self):
176 '''Get the datum (L{Datum} or C{None} if not applicable).
177 '''
178 return self._datum
180 def _datum_setter(self, datum):
181 '''(INTERNAL) Set the datum.
182 '''
183 d = datum or _xattr(self._ps1[0], datum=None)
184 if d and d is not self._datum: # PYCHOK no cover
185 self._datum = _ellipsoidal_datum(d, name=self.name)
187 def discrete(self, point2s, fraction=None, recursive=False):
188 '''Compute the C{forward, discrete Fréchet} distance.
190 @arg point2s: Second set of points (C{LatLon}[], L{Numpy2LatLon}[],
191 L{Tuple2LatLon}[] or C{other}[]).
192 @kwarg fraction: Index fraction (C{float} in L{EPS}..L{EPS1}) to
193 interpolate intermediate B{C{point2s}} or use
194 C{None}, C{0} or C{1} for no intermediate
195 B{C{point2s}} and no I{fractional} indices.
196 @kwarg recursive: Use C{True} for backward compatibility (C{bool})
197 or with I{fractional} indices.
199 @return: A L{Frechet6Tuple}C{(fd, fi1, fi2, r, n, units)}.
201 @raise FrechetError: Insufficient number of B{C{point2s}} or
202 an invalid B{C{point2}} or B{C{fraction}}
203 or C{non-B{recursive}} and I{fractional}.
205 @raise RecursionError: Recursion depth exceeded, see U{sys.getrecursionlimit
206 <https://docs.Python.org/3/library/sys.html#sys.getrecursionlimit>},
207 only with C{B{recursive}=True}.
208 '''
209 return self._discrete(point2s, fraction, self.distance, recursive)
211 def _discrete(self, point2s, fraction, distance, recursive):
212 '''(INTERNAL) Detailed C{discrete} with C{distance}.
213 '''
214 n2, ps2 = self._points2(point2s)
215 n1, ps1 = self._n1, self._ps1
217 f2 = _fraction(fraction, n2)
218 p2 = self.points_fraction if f2 < EPS1 else self.points_ # PYCHOK attr
220 f1 = self.fraction
221 p1 = self.points_fraction if f1 < EPS1 else self.points_ # PYCHOK attr
223 def _dF(fi1, fi2):
224 return distance(p1(ps1, fi1), p2(ps2, fi2))
226 try:
227 if recursive or not f1 == f2 == 1:
228 t = _frechetR(n1, f1, n2, f2, _dF, self.units)
229 else: # elif f1 == f2 == 1:
230 t = _frechetDP(n1, n2, _dF, self.units, False)
231# else:
232# f = fraction or self.fraction
233# raise FrechetError(fraction=f, txt='non-recursive')
234 except TypeError as x:
235 t = _DOT_(self.classname, typename(self.discrete))
236 raise FrechetError(t, cause=x)
237 return t
239 def distance(self, point1, point2):
240 '''Return the distance between B{C{point1}} and B{C{point2s}},
241 subject to the supplied optional keyword arguments, see
242 property C{kwds}.
243 '''
244 return self._func(point1.lat, point1.lon,
245 point2.lat, point2.lon, **self._kwds)
247 @property_doc_(''' the index fraction (C{float}).''')
248 def fraction(self):
249 '''Get the index fraction (C{float} or C{1}).
250 '''
251 return self._f1
253 @fraction.setter # PYCHOK setter!
254 def fraction(self, fraction):
255 '''Set the index fraction (C{float} in C{EPS}..L{EPS1}) to interpolate
256 intermediate B{C{point1s}} or use C{None}, C{0} or C{1} for no
257 intermediate B{C{point1s}} and no I{fractional} indices.
259 @raise FrechetError: Invalid B{C{fraction}}.
260 '''
261 self._f1 = _fraction(fraction, self._n1)
263# def _func(self, *args, **kwds): # PYCHOK no cover
264# '''(INTERNAL) I{Must be overloaded}.'''
265# self._notOverloaded(*args, **kwds)
267 @Property
268 def _func(self):
269 '''(INTERNAL) I{Must be overloaded}.'''
270 self._notOverloaded(**self.kwds)
272 @_func.setter_ # PYCHOK setter_underscore!
273 def _func(self, func):
274 return _formy._Propy(func, 4, self.kwds)
276 @property_RO
277 def kwds(self):
278 '''Get the supplied, optional keyword arguments (C{dict}).
279 '''
280 return self._kwds
282 def point(self, point):
283 '''Convert a point for the C{.distance} method.
285 @arg point: The point to convert ((C{LatLon}, L{Numpy2LatLon},
286 L{Tuple2LatLon} or C{other}).
288 @return: The converted B{C{point}}.
289 '''
290 return point # pass thru
292 def points_(self, points, i):
293 '''Get and convert a point for the C{.distance} method.
295 @arg points: The orignal B{C{points}} to convert ((C{LatLon}[],
296 L{Numpy2LatLon}[], L{Tuple2LatLon}[] or C{other}[]).
297 @arg i: The B{C{points}} index (C{int}).
299 @return: The converted B{C{points[i]}}.
300 '''
301 return self.point(points[i])
303 def points_fraction(self, points, fi):
304 '''Get and convert a I{fractional} point for the C{.distance} method.
306 @arg points: The orignal B{C{points}} to convert ((C{LatLon}[],
307 L{Numpy2LatLon}[], L{Tuple2LatLon}[] or C{other}[]).
308 @arg fi: The I{fractional} index in B{C{points}} (C{float} or C{int}).
310 @return: The interpolated, converted, intermediate B{C{points[fi]}}.
311 '''
312 return self.point(_fractional(points, fi, None, wrap=None, dup=True)) # was=self.wrap
314 def _points2(self, points):
315 '''(INTERNAL) Check a set of points, overloaded in L{FrechetDistanceTo}.
316 '''
317 return _points2(points, closed=False, Error=FrechetError)
319 @property_doc_(''' the distance units (C{Unit} or C{str}).''')
320 def units(self):
321 '''Get the distance units (C{Unit} or C{str}).
322 '''
323 return self._units
325 @units.setter # PYCHOK setter!
326 def units(self, units):
327 '''Set the distance units (C{Unit} or C{str}).
329 @raise TypeError: Invalid B{C{units}}.
330 '''
331 self._units = _unitsBase._xUnits(units, Base=Float)
333 @property_RO
334 def wrap(self):
335 '''Get the C{wrap} setting (C{bool} or C{None}).
336 '''
337 return _xkwds_get(self._kwds, wrap=None)
340class FrechetDegrees(Frechet):
341 '''DEPRECATED, use an other C{Frechet*} class.
342 '''
343 _units = _unitsBase._Str_degrees
345 if _FOR_DOCS:
346 __init__ = Frechet.__init__
347 discrete = Frechet.discrete
349 def distance(self, point1, point2, *args, **kwds): # PYCHOK no cover
350 '''I{Must be overloaded}.'''
351 self._notOverloaded(point1, point2, *args, **kwds)
354class FrechetRadians(Frechet):
355 '''DEPRECATED, use an other C{Frechet*} class.
356 '''
357 _units = _unitsBase._Str_radians
359 if _FOR_DOCS:
360 __init__ = Frechet.__init__
361 discrete = Frechet.discrete
363 def distance(self, point1, point2, *args, **kwds): # PYCHOK no cover
364 '''I{Must be overloaded}.'''
365 self._notOverloaded(point1, point2, *args, **kwds)
367 def point(self, point):
368 '''Return B{C{point}} as L{PhiLam2Tuple} to maintain
369 I{backward compatibility} of L{FrechetRadians}.
371 @return: A L{PhiLam2Tuple}C{(phi, lam)}.
372 '''
373 try:
374 return point.philam
375 except AttributeError:
376 return PhiLam2Tuple(radians(point.lat), radians(point.lon))
379class _FrechetMeterRadians(Frechet):
380 '''(INTERNAL) Returning C{meter} or C{radians} depending on
381 the optional keyword arguments supplied at instantiation
382 of the C{Frechet*} sub-class.
383 '''
384 _units = _unitsBase._Str_meter
385 _units_ = _unitsBase._Str_radians
387 def discrete(self, point2s, fraction=None, recursive=False):
388 '''Overloaded method L{Frechet.discrete} to determine
389 the distance function and units from the optional
390 keyword arguments given at this instantiation, see
391 property C{kwds}.
393 @see: Method L{Frechet.discrete} for other details.
394 '''
395 _rad = _formy._radistance(self)
396 return self._discrete(point2s, fraction, _rad, recursive)
398 @Property
399 def _func_(self): # see _formy._radistance
400 '''(INTERNAL) I{Must be overloaded}.'''
401 self._notOverloaded(**self.kwds)
403 @_func_.setter_ # PYCHOK setter_underscore!
404 def _func_(self, func):
405 return _formy._Propy(func, 3, self.kwds)
408class FrechetCosineLaw(_FrechetMeterRadians):
409 '''Compute the C{Frechet} distance with functionn L{pygeodesy.cosineLaw}.
411 @note: See note at function L{pygeodesy.vincentys_}.
412 '''
413 def __init__(self, point1s, **fraction_name__corr_earth_wrap):
414 '''New L{FrechetCosineLaw} calculator/interpolator.
416 @kwarg fraction_name__corr_earth_wrap: Optional
417 C{B{fraction}=None} and C{B{name}=NN} and keyword
418 arguments for function L{pygeodesy.cosineLaw}.
420 @see: L{Frechet.__init__} for details about B{C{point1s}},
421 B{C{fraction}}, B{C{name}} and other exceptions.
422 '''
423 Frechet.__init__(self, point1s, **fraction_name__corr_earth_wrap)
424 self._func = _formy.cosineLaw
425 self._func_ = _formy.cosineLaw_
427 if _FOR_DOCS:
428 discrete = Frechet.discrete
431class FrechetDistanceTo(Frechet): # FrechetMeter
432 '''Compute the C{Frechet} distance with the point1s' C{LatLon.distanceTo} method.
433 '''
434 _units = _unitsBase._Str_meter
436 def __init__(self, point1s, **fraction_name__distanceTo_kwds):
437 '''New L{FrechetDistanceTo} calculator/interpolator.
439 @kwarg fraction_name__distanceTo_kwds: Optional C{B{fraction}=None}
440 and C{B{name}=NN} and keyword arguments for
441 each B{C{point1s}}' C{LatLon.distanceTo} method.
443 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}},
444 B{C{name}} and other exceptions.
446 @note: All B{C{point1s}} I{must} be instances of the same ellipsoidal
447 or spherical C{LatLon} class.
448 '''
449 Frechet.__init__(self, point1s, **fraction_name__distanceTo_kwds)
451 if _FOR_DOCS:
452 discrete = Frechet.discrete
454 def distance(self, p1, p2):
455 '''Return the distance in C{meter}.
456 '''
457 return p1.distanceTo(p2, **self._kwds)
459 def _points2(self, points):
460 '''(INTERNAL) Check a set of points.
461 '''
462 np, ps = Frechet._points2(self, points)
463 return np, _distanceTo(FrechetError, points=ps)
466class FrechetEquirectangular(Frechet):
467 '''Compute the C{Frechet} distance with function L{pygeodesy.equirectangular}.
468 '''
469 _units = _unitsBase._Str_radians2
471 def __init__(self, point1s, **fraction_name__adjust_limit_wrap):
472 '''New L{FrechetEquirectangular} calculator/interpolator.
474 @kwarg fraction_name__adjust_limit_wrap: Optional C{B{fraction}=None}
475 and C{B{name}=NN} and keyword arguments for
476 function L{pygeodesy.equirectangular} I{with
477 default} C{B{limit}=0} for I{backward compatibility}.
479 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}},
480 B{C{name}} and other exceptions.
481 '''
482 Frechet.__init__(self, point1s, **_xkwds(fraction_name__adjust_limit_wrap,
483 limit=0))
484 self._func = _formy._equirectangular # helper
486 if _FOR_DOCS:
487 discrete = Frechet.discrete
490class FrechetEuclidean(_FrechetMeterRadians):
491 '''Compute the C{Frechet} distance with function L{pygeodesy.euclidean}.
492 '''
493 def __init__(self, point1s, **fraction_name__adjust_radius_wrap): # was=True
494 '''New L{FrechetEuclidean} calculator/interpolator.
496 @kwarg fraction_name__adjust_radius_wrap: Optional C{B{fraction}=None}
497 and C{B{name}=NN} and keyword arguments for
498 function L{pygeodesy.euclidean}.
500 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}},
501 B{C{name}} and other exceptions.
502 '''
503 Frechet.__init__(self, point1s, **fraction_name__adjust_radius_wrap)
504 self._func = _formy.euclidean
505 self._func_ = _formy.euclidean_
507 if _FOR_DOCS:
508 discrete = Frechet.discrete
511class FrechetExact(Frechet):
512 '''Compute the C{Frechet} distance with method L{GeodesicExact}C{.Inverse}.
513 '''
514 _units = _unitsBase._Str_degrees
516 def __init__(self, point1s, datum=None, **fraction_name__wrap):
517 '''New L{FrechetExact} calculator/interpolator.
519 @kwarg datum: Datum to override the default C{Datums.WGS84} and first
520 B{C{point1s}}' datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2}
521 or L{a_f2Tuple}).
522 @kwarg fraction_name__wrap: Optional C{B{fraction}=None} and C{B{name}=NN}
523 and keyword argument for method C{Inverse1} of class
524 L{geodesicx.GeodesicExact}.
526 @raise TypeError: Invalid B{C{datum}}.
528 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}},
529 B{C{name}} and other exceptions.
530 '''
531 Frechet.__init__(self, point1s, **fraction_name__wrap)
532 self._datum_setter(datum)
533 self._func = self.datum.ellipsoid.geodesicx.Inverse1 # note -x
535 if _FOR_DOCS:
536 discrete = Frechet.discrete
539class FrechetFlatLocal(_FrechetMeterRadians):
540 '''Compute the C{Frechet} distance with function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny}.
541 '''
542 _units_ = _unitsBase._Str_radians2 # see L{flatLocal_}
544 def __init__(self, point1s, **fraction_name__datum_scaled_wrap):
545 '''New L{FrechetFlatLocal}/L{FrechetHubeny} calculator/interpolator.
547 @kwarg fraction_name__datum_scaled_wrap: Optional C{B{fraction}=None}
548 and C{B{name}=NN} and keyword arguments for
549 function L{pygeodesy.flatLocal}.
551 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}},
552 B{C{name}} and other exceptions.
554 @note: The distance C{units} are C{radians squared}, not C{radians}.
555 '''
556 Frechet.__init__(self, point1s, **fraction_name__datum_scaled_wrap)
557 self._func = _formy.flatLocal
558 self._func_ = self.datum.ellipsoid._hubeny_2
560 if _FOR_DOCS:
561 discrete = Frechet.discrete
564class FrechetFlatPolar(_FrechetMeterRadians):
565 '''Compute the C{Frechet} distance with function L{flatPolar_}.
566 '''
567 def __init__(self, point1s, **fraction_name__radius_wrap):
568 '''New L{FrechetFlatPolar} calculator/interpolator.
570 @kwarg fraction_name__radius_wrap: Optional C{B{fraction}=None}
571 and C{B{name}=NN} and keyword arguments
572 for function L{pygeodesy.flatPolar}.
574 @see: L{Frechet.__init__} for details about B{C{point1s}},
575 B{C{fraction}}, B{C{name}} and other exceptions.
576 '''
577 Frechet.__init__(self, point1s, **fraction_name__radius_wrap)
578 self._func = _formy.flatPolar
579 self._func_ = _formy.flatPolar_
581 if _FOR_DOCS:
582 discrete = Frechet.discrete
585class FrechetHaversine(_FrechetMeterRadians):
586 '''Compute the C{Frechet} distance with function L{pygeodesy.haversine_}.
588 @note: See note at function L{pygeodesy.vincentys_}.
589 '''
590 def __init__(self, point1s, **fraction_name__radius_wrap):
591 '''New L{FrechetHaversine} calculator/interpolator.
593 @kwarg fraction_name__radius_wrap: Optional C{B{fraction}=None}
594 and C{B{name}=NN} and keyword arguments
595 for function L{pygeodesy.haversine}.
597 @see: L{Frechet.__init__} for details about B{C{point1s}},
598 B{C{fraction}}, B{C{name}} and other exceptions.
599 '''
600 Frechet.__init__(self, point1s, **fraction_name__radius_wrap)
601 self._func = _formy.haversine
602 self._func_ = _formy.haversine_
604 if _FOR_DOCS:
605 discrete = Frechet.discrete
608class FrechetHubeny(FrechetFlatLocal): # for Karl Hubeny
609 if _FOR_DOCS:
610 __doc__ = FrechetFlatLocal.__doc__
611 __init__ = FrechetFlatLocal.__init__
612 discrete = FrechetFlatLocal.discrete
613 distance = FrechetFlatLocal.discrete
616class FrechetKarney(Frechet):
617 '''Compute the C{Frechet} distance with I{Karney}'s U{geographiclib
618 <https://PyPI.org/project/geographiclib>} U{geodesic.Geodesic
619 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}C{.Inverse}
620 method.
621 '''
622 _units = _unitsBase._Str_degrees
624 def __init__(self, point1s, datum=None, **fraction_name__wrap):
625 '''New L{FrechetKarney} calculator/interpolator.
627 @kwarg datum: Datum to override the default C{Datums.WGS84} and
628 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid},
629 L{Ellipsoid2} or L{a_f2Tuple}).
630 @kwarg fraction_name__wrap: Optional C{B{fraction}=None} and
631 C{B{name}=NN} and keyword arguments for
632 method C{Inverse1} of class L{geodesicw.Geodesic}.
634 @raise ImportError: Package U{geographiclib
635 <https://PyPI.org/project/geographiclib>} missing.
637 @raise TypeError: Invalid B{C{datum}}.
639 @see: L{Frechet.__init__} for details about B{C{point1s}},
640 B{C{fraction}}, B{C{name}} and other exceptions.
641 '''
642 Frechet.__init__(self, point1s, **fraction_name__wrap)
643 self._datum_setter(datum)
644 self._func = self.datum.ellipsoid.geodesic.Inverse1
646 if _FOR_DOCS:
647 discrete = Frechet.discrete
650class FrechetThomas(_FrechetMeterRadians):
651 '''Compute the C{Frechet} distance with function L{pygeodesy.thomas_}.
652 '''
653 def __init__(self, point1s, **fraction_name__datum_wrap):
654 '''New L{FrechetThomas} calculator/interpolator.
656 @kwarg fraction_name__datum_wrap: Optional C{B{fraction}=None}
657 and C{B{name}=NN} and keyword arguments
658 for function L{pygeodesy.thomas}.
660 @see: L{Frechet.__init__} for details about B{C{point1s}},
661 B{C{fraction}}, B{C{name}} and other exceptions.
662 '''
663 Frechet.__init__(self, point1s, **fraction_name__datum_wrap)
664 self._func = _formy.thomas
665 self._func_ = _formy.thomas_
667 if _FOR_DOCS:
668 discrete = Frechet.discrete
671class Frechet6Tuple(_NamedTuple):
672 '''6-Tuple C{(fd, fi1, fi2, r, n, units)} with the I{discrete}
673 U{Fréchet<https://WikiPedia.org/wiki/Frechet_distance>} distance
674 C{fd}, I{fractional} indices C{fi1} and C{fi2} as C{FIx}, the
675 recursion depth C{r}, the number of distances computed C{n} and
676 the L{units} class or name of the distance C{units}.
678 Empirically, the recursion depth C{r ≈ 2 * sqrt(len(point1s) *
679 len(point2s))} or C{0} if non-recursive, see function L{frechet_}.
681 If I{fractional} indices C{fi1} and C{fi2} are C{int}, the
682 returned C{fd} is the distance between C{point1s[fi1]} and
683 C{point2s[fi2]}. For C{float} indices, the distance is
684 between an intermediate point along C{point1s[int(fi1)]} and
685 C{point1s[int(fi1) + 1]} respectively an intermediate point
686 along C{point2s[int(fi2)]} and C{point2s[int(fi2) + 1]}.
688 Use function L{fractional} to compute the point at a
689 I{fractional} index.
690 '''
691 _Names_ = ('fd', 'fi1', 'fi2', 'r', _n_, _units_)
692 _Units_ = (_Pass, FIx, FIx, Number_, Number_, _Pass)
694 def toUnits(self, **Error_name): # PYCHOK expected
695 '''Overloaded C{_NamedTuple.toUnits} for C{fd} units.
696 '''
697 U = _unitsBase._xUnit(self.units, Float) # PYCHOK expected
698 return _NamedTuple.toUnits(self.reUnit(U), **Error_name) # PYCHOK self
700# def __gt__(self, other):
701# _xinstanceof(Frechet6Tuple, other=other)
702# return self if self.fd > other.fd else other # PYCHOK .fd=[0]
703#
704# def __lt__(self, other):
705# _xinstanceof(Frechet6Tuple, other=other)
706# return self if self.fd < other.fd else other # PYCHOK .fd=[0]
709class FrechetVincentys(_FrechetMeterRadians):
710 '''Compute the C{Frechet} distance with function L{pygeodesy.vincentys_}.
712 @note: See note at function L{pygeodesy.vincentys_}.
713 '''
714 def __init__(self, point1s, **fraction_name__radius_wrap):
715 '''New L{FrechetVincentys} calculator/interpolator.
717 @kwarg fraction_name__radius_wrap: Optional C{B{fraction}=None}
718 and C{B{name}=NN} and keyword arguments
719 for function L{pygeodesy.vincentys}.
721 @see: L{Frechet.__init__} for details about B{C{point1s}},
722 B{C{fraction}}, B{C{name}} and other exceptions.
723 '''
724 Frechet.__init__(self, point1s, **fraction_name__radius_wrap)
725 self._func = _formy.vincentys
726 self._func_ = _formy.vincentys_
728 if _FOR_DOCS:
729 discrete = Frechet.discrete
732def frechet_(point1s, point2s, distance=None, units=NN, recursive=False):
733 '''Compute the I{discrete} U{Fréchet<https://WikiPedia.org/wiki/Frechet_distance>}
734 distance between two paths, each given as a set of points.
736 @arg point1s: First set of points (C{LatLon}[], L{Numpy2LatLon}[],
737 L{Tuple2LatLon}[] or C{other}[]).
738 @arg point2s: Second set of points (C{LatLon}[], L{Numpy2LatLon}[],
739 L{Tuple2LatLon}[] or C{other}[]).
740 @kwarg distance: Callable returning the distance between a B{C{point1s}}
741 and a B{C{point2s}} point (signature C{(point1, point2)}).
742 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
743 @kwarg recursive: Use C{True} for backward compatibility (C{bool}).
745 @return: A L{Frechet6Tuple}C{(fd, fi1, fi2, r, n, units)} where C{fi1}
746 and C{fi2} are type C{int} indices into B{C{point1s}} respectively
747 B{C{point2s}}.
749 @raise FrechetError: Insufficient number of B{C{point1s}} or B{C{point2s}}.
751 @raise RecursionError: Recursion depth exceeded, see U{sys.getrecursionlimit()
752 <https://docs.Python.org/3/library/sys.html#sys.getrecursionlimit>},
753 only with C{B{recursive}=True}.
755 @raise TypeError: If B{C{distance}} is not a callable.
757 @note: Function L{frechet_} does I{not} support I{fractional} indices for intermediate
758 B{C{point1s}} and B{C{point2s}}.
760 @see: Non-recursive U{dp_frechet_dist
761 <https://GitHub.com/cjekel/similarity_measures/issues/6#issuecomment-544350039>}.
762 '''
763 _xcallable(distance=distance)
765 n1, ps1 = _points2(point1s, closed=False, Error=FrechetError)
766 n2, ps2 = _points2(point2s, closed=False, Error=FrechetError)
768 def _dF(i1, i2):
769 return distance(ps1[i1], ps2[i2])
771 if recursive:
772 t = _frechetR(n1, 1, n2, 1, _dF, units)
773 else:
774 s = n1 < n2
775 if s: # n2, ps2 as shortest
776 n1, ps1, n2, ps2 = n2, ps2, n1, ps1
777 t = _frechetDP(n1, n2, _dF, units, s)
778 return t
781def _frechet3(r, t, s): # return tuple r if equal
782 if s[0] < r[0]:
783 r = s
784 return t if t[0] < r[0] else r
787def _frechetDP(ni, nj, dF, units, swap):
788 '''(INTERNAL) DP core of function L{frechet_} and
789 method C{discrete} of C{Frechet...} classes.
790 '''
791 def _max2(r, *t): # return tuple r if equal
792 return t if t[0] > r[0] else r
794 _min3 = _frechet3
796 d = dF(0, 0)
797 t = (d, 0, 0)
798 r = [t] * nj # nj-list of 3-tuples
799 for j in range(1, nj):
800 d = max(d, dF(0, j))
801 r[j] = (d, 0, j)
802 for i in range(1, ni):
803 t1j1 = r[0] # == r[i-1][0]
804 r[0] = t = _max2(t1j1, dF(i, 0), i, 0)
805 for j in range(1, nj):
806 t1j = r[j] # == r[i-1][j]
807 t = _min3(t1j, t, t1j1) # == r[i-1][j-1]
808 r[j] = t = _max2(t, dF(i, j), i, j)
809 t1j1 = t1j
810 d, i, j = t # == r[nj-1]
811 if swap:
812 i, j = j, i
813# del r
814 return Frechet6Tuple(d, i, j, 0, (ni * nj), units)
817def _frechetR(ni, fi, nj, fj, dF, units): # MCCABE 14
818 '''(INTERNAL) Recursive core of function L{frechet_}
819 and method C{discrete} of C{Frechet...} classes.
820 '''
821 class iF(dict): # index, depth ints and floats cache
822 def __call__(self, i):
823 return dict.setdefault(self, i, i)
825 _min3 = _frechet3
827 iF = iF() # PYCHOK once
828 tF = _defaultdict(dict) # tuple[i][j]
830 def _rF(i, j, r): # recursive Fréchet
831 i = iF(i)
832 j = iF(j)
833 try:
834 return tF[i][j]
835 except KeyError:
836 pass
837 r = iF(r + 1)
838 try:
839 if i > 0:
840 if j > 0:
841 t = _min3(_rF(i - fi, j, r),
842 _rF(i - fi, j - fj, r),
843 _rF(i, j - fj, r))
844 elif j < 0:
845 raise IndexError
846 else: # j == 0
847 t = _rF(i - fi, 0, r)
848 elif i < 0:
849 raise IndexError
851 elif j > 0: # i == 0
852 t = _rF(0, j - fj, r)
853 elif j < 0: # i == 0
854 raise IndexError
855 else: # i == j == 0
856 t = (NINF, i, j, r)
858 d = dF(i, j)
859 if d > t[0]:
860 t = (d, i, j, r)
861 except IndexError:
862 t = (INF, i, j, r)
863 tF[i][j] = t
864 return t
866 d, i, j, r = _rF(ni - 1, nj - 1, 0)
867 n = sum(map(len, tF.values())) # (ni * nj) <= n < (ni * nj * 2)
868# del iF, tF
869 return Frechet6Tuple(d, i, j, r, n, units)
872if __name__ == _DMAIN_:
874 def _main():
875 from time import time
876 from pygeodesy import euclid, printf # randomrangenerator
877 _r = range # randomrangenerator('R')
879 def _d(p1, p2):
880 return euclid(p1[0] - p2[0], p1[1] - p2[1])
882 p1 = tuple(zip(_r(-90, 90, 4), _r(-180, 180, 8))) # 45
883 p2 = tuple(zip(_r(-89, 90, 3), _r(-179, 180, 6))) # 60
884 ss = []
885 for r, u in ((True, 'R'), (False, 'DP')):
886 s = time()
887 t = frechet_(p1, p2, _d, recursive=r, units=u)
888 s = time() - s
889 printf('# %r in %.3f ms', t, (s * 1e3))
890 ss.append(s)
892 from pygeodesy.internals import _versions
893 printf('# %s %.2fX', _versions(), (ss[0] / ss[1]))
895 _main()
897# % python3.13 -m pygeodesy.frechet
898# Frechet6Tuple(fd=3.828427, fi1=2, fi2=3, r=99, n=2700, units='R') in 3.575 ms
899# Frechet6Tuple(fd=3.828427, fi1=2, fi2=3, r=0, n=2700, units='DP') in 0.704 ms
900# pygeodesy 25.4.25 Python 3.13.3 64bit arm64 macOS 15.4.1 5.08X
902# % python2.7 -m pygeodesy.frechet
903# Frechet6Tuple(fd=3.828427, fi1=2, fi2=3, r=99, n=2700, units='R') in 7.030 ms
904# Frechet6Tuple(fd=3.828427, fi1=2, fi2=3, r=0, n=2700, units='DP') in 1.536 ms
905# pygeodesy 25.4.25 Python 2.7.18 64bit arm64_x86_64 macOS 10.16 4.58X
907# **) MIT License
908#
909# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
910#
911# Permission is hereby granted, free of charge, to any person obtaining a
912# copy of this software and associated documentation files (the "Software"),
913# to deal in the Software without restriction, including without limitation
914# the rights to use, copy, modify, merge, publish, distribute, sublicense,
915# and/or sell copies of the Software, and to permit persons to whom the
916# Software is furnished to do so, subject to the following conditions:
917#
918# The above copyright notice and this permission notice shall be included
919# in all copies or substantial portions of the Software.
920#
921# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
922# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
923# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
924# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
925# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
926# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
927# OTHER DEALINGS IN THE SOFTWARE.