Coverage for pygeodesy/frechet.py: 96%

261 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-29 12:40 -0400

1 

2# -*- coding: utf-8 -*- 

3 

4u'''Fréchet distances. 

5 

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. 

12 

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>}. 

16 

17Typical usage is as follows. First, create a C{Frechet} calculator from one 

18set of C{LatLon} points. 

19 

20C{f = FrechetXyz(point1s, ...)} 

21 

22Get the I{discrete} Fréchet distance to another set of C{LatLon} points 

23by 

24 

25C{t6 = f.discrete(point2s)} 

26 

27Or, use function C{frechet_} with a proper C{distance} function passed 

28as keyword arguments as follows 

29 

30C{t6 = frechet_(point1s, point2s, ..., distance=...)}. 

31 

32In both cases, the returned result C{t6} is a L{Frechet6Tuple}. 

33 

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. 

37 

38For other points, create a L{Frechet} sub-class with the appropriate 

39C{distance} method overloading L{Frechet.distance} as in this example. 

40 

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) 

51 

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. 

57 

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. 

63 

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. 

68 

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. 

73 

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''' 

81 

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 

93from pygeodesy.namedTuples import PhiLam2Tuple 

94from pygeodesy.points import _distanceTo, _fractional, \ 

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 

99 

100from collections import defaultdict as _defaultdict 

101# from math import radians # from .points 

102 

103__all__ = _ALL_LAZY.frechet 

104__version__ = '25.05.26' 

105 

106_formy = _MODS.into(formy=__name__) 

107 

108 

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 

119 

120 

121class FrechetError(PointsError): 

122 '''Fréchet issue. 

123 ''' 

124 pass 

125 

126 

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 

138 

139 def __init__(self, point1s, fraction=None, units=NN, **name__kwds): 

140 '''New C{Frechet...} calculator/interpolator. 

141 

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}. 

152 

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__=type(self) 

157 if name: 

158 self.name = name 

159 

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 

167 

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) 

173 

174 @property_RO 

175 def datum(self): 

176 '''Get the datum (L{Datum} or C{None} if not applicable). 

177 ''' 

178 return self._datum 

179 

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) 

186 

187 def discrete(self, point2s, fraction=None, recursive=False): 

188 '''Compute the C{forward, discrete Fréchet} distance. 

189 

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. 

198 

199 @return: A L{Frechet6Tuple}C{(fd, fi1, fi2, r, n, units)}. 

200 

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}. 

204 

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) 

210 

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 

216 

217 f2 = _fraction(fraction, n2) 

218 p2 = self.points_fraction if f2 < EPS1 else self.points_ # PYCHOK attr 

219 

220 f1 = self.fraction 

221 p1 = self.points_fraction if f1 < EPS1 else self.points_ # PYCHOK attr 

222 

223 def _dF(fi1, fi2): 

224 return distance(p1(ps1, fi1), p2(ps2, fi2)) 

225 

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 

238 

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) 

246 

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 

252 

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. 

258 

259 @raise FrechetError: Invalid B{C{fraction}}. 

260 ''' 

261 self._f1 = _fraction(fraction, self._n1) 

262 

263# def _func(self, *args, **kwds): # PYCHOK no cover 

264# '''(INTERNAL) I{Must be overloaded}.''' 

265# self._notOverloaded(*args, **kwds) 

266 

267 @Property 

268 def _func(self): 

269 '''(INTERNAL) I{Must be overloaded}.''' 

270 self._notOverloaded(**self.kwds) 

271 

272 @_func.setter_ # PYCHOK setter_underscore! 

273 def _func(self, func): 

274 return _formy._Propy(func, 4, self.kwds) 

275 

276 @property_RO 

277 def kwds(self): 

278 '''Get the supplied, optional keyword arguments (C{dict}). 

279 ''' 

280 return self._kwds 

281 

282 def point(self, point): 

283 '''Convert a point for the C{.distance} method. 

284 

285 @arg point: The point to convert ((C{LatLon}, L{Numpy2LatLon}, 

286 L{Tuple2LatLon} or C{other}). 

287 

288 @return: The converted B{C{point}}. 

289 ''' 

290 return point # pass thru 

291 

292 def points_(self, points, i): 

293 '''Get and convert a point for the C{.distance} method. 

294 

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}). 

298 

299 @return: The converted B{C{points[i]}}. 

300 ''' 

301 return self.point(points[i]) 

302 

303 def points_fraction(self, points, fi): 

304 '''Get and convert a I{fractional} point for the C{.distance} method. 

305 

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}). 

309 

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 

313 

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) 

318 

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 

324 

325 @units.setter # PYCHOK setter! 

326 def units(self, units): 

327 '''Set the distance units (C{Unit} or C{str}). 

328 

329 @raise TypeError: Invalid B{C{units}}. 

330 ''' 

331 self._units = _unitsBase._xUnits(units, Base=Float) 

332 

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) 

338 

339 

340class FrechetDegrees(Frechet): 

341 '''DEPRECATED, use an other C{Frechet*} class. 

342 ''' 

343 _units = _unitsBase._Str_degrees 

344 

345 if _FOR_DOCS: 

346 __init__ = Frechet.__init__ 

347 discrete = Frechet.discrete 

348 

349 def distance(self, point1, point2, *args, **kwds): # PYCHOK no cover 

350 '''I{Must be overloaded}.''' 

351 self._notOverloaded(point1, point2, *args, **kwds) 

352 

353 

354class FrechetRadians(Frechet): 

355 '''DEPRECATED, use an other C{Frechet*} class. 

356 ''' 

357 _units = _unitsBase._Str_radians 

358 

359 if _FOR_DOCS: 

360 __init__ = Frechet.__init__ 

361 discrete = Frechet.discrete 

362 

363 def distance(self, point1, point2, *args, **kwds): # PYCHOK no cover 

364 '''I{Must be overloaded}.''' 

365 self._notOverloaded(point1, point2, *args, **kwds) 

366 

367 def point(self, point): 

368 '''Return B{C{point}} as L{PhiLam2Tuple} to maintain 

369 I{backward compatibility} of L{FrechetRadians}. 

370 

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)) 

377 

378 

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 

386 

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}. 

392 

393 @see: Method L{Frechet.discrete} for other details. 

394 ''' 

395 _rad = _formy._radistance(self) 

396 return self._discrete(point2s, fraction, _rad, recursive) 

397 

398 @Property 

399 def _func_(self): # see _formy._radistance 

400 '''(INTERNAL) I{Must be overloaded}.''' 

401 self._notOverloaded(**self.kwds) 

402 

403 @_func_.setter_ # PYCHOK setter_underscore! 

404 def _func_(self, func): 

405 return _formy._Propy(func, 3, self.kwds) 

406 

407 

408class FrechetCosineLaw(_FrechetMeterRadians): 

409 '''Compute the C{Frechet} distance with functionn L{pygeodesy.cosineLaw}. 

410 

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. 

415 

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}. 

419 

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_ 

426 

427 if _FOR_DOCS: 

428 discrete = Frechet.discrete 

429 

430 

431class FrechetDistanceTo(Frechet): # FrechetMeter 

432 '''Compute the C{Frechet} distance with the point1s' C{LatLon.distanceTo} method. 

433 ''' 

434 _units = _unitsBase._Str_meter 

435 

436 def __init__(self, point1s, **fraction_name__distanceTo_kwds): 

437 '''New L{FrechetDistanceTo} calculator/interpolator. 

438 

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. 

442 

443 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}}, 

444 B{C{name}} and other exceptions. 

445 

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) 

450 

451 if _FOR_DOCS: 

452 discrete = Frechet.discrete 

453 

454 def distance(self, p1, p2): 

455 '''Return the distance in C{meter}. 

456 ''' 

457 return p1.distanceTo(p2, **self._kwds) 

458 

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) 

464 

465 

466class FrechetEquirectangular(Frechet): 

467 '''Compute the C{Frechet} distance with function L{pygeodesy.equirectangular}. 

468 ''' 

469 _units = _unitsBase._Str_radians2 

470 

471 def __init__(self, point1s, **fraction_name__adjust_limit_wrap): 

472 '''New L{FrechetEquirectangular} calculator/interpolator. 

473 

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}. 

478 

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 

485 

486 if _FOR_DOCS: 

487 discrete = Frechet.discrete 

488 

489 

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. 

495 

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}. 

499 

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_ 

506 

507 if _FOR_DOCS: 

508 discrete = Frechet.discrete 

509 

510 

511class FrechetExact(Frechet): 

512 '''Compute the C{Frechet} distance with method L{GeodesicExact}C{.Inverse}. 

513 ''' 

514 _units = _unitsBase._Str_degrees 

515 

516 def __init__(self, point1s, datum=None, **fraction_name__wrap): 

517 '''New L{FrechetExact} calculator/interpolator. 

518 

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}. 

525 

526 @raise TypeError: Invalid B{C{datum}}. 

527 

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 

534 

535 if _FOR_DOCS: 

536 discrete = Frechet.discrete 

537 

538 

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_} 

543 

544 def __init__(self, point1s, **fraction_name__datum_scaled_wrap): 

545 '''New L{FrechetFlatLocal}/L{FrechetHubeny} calculator/interpolator. 

546 

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}. 

550 

551 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}}, 

552 B{C{name}} and other exceptions. 

553 

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 

559 

560 if _FOR_DOCS: 

561 discrete = Frechet.discrete 

562 

563 

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. 

569 

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}. 

573 

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_ 

580 

581 if _FOR_DOCS: 

582 discrete = Frechet.discrete 

583 

584 

585class FrechetHaversine(_FrechetMeterRadians): 

586 '''Compute the C{Frechet} distance with function L{pygeodesy.haversine_}. 

587 

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. 

592 

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}. 

596 

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_ 

603 

604 if _FOR_DOCS: 

605 discrete = Frechet.discrete 

606 

607 

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 

614 

615 

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 

623 

624 def __init__(self, point1s, datum=None, **fraction_name__wrap): 

625 '''New L{FrechetKarney} calculator/interpolator. 

626 

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}. 

633 

634 @raise ImportError: Package U{geographiclib 

635 <https://PyPI.org/project/geographiclib>} missing. 

636 

637 @raise TypeError: Invalid B{C{datum}}. 

638 

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 

645 

646 if _FOR_DOCS: 

647 discrete = Frechet.discrete 

648 

649 

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. 

655 

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}. 

659 

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_ 

666 

667 if _FOR_DOCS: 

668 discrete = Frechet.discrete 

669 

670 

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}. 

677 

678 Empirically, the recursion depth C{r ≈ 2 * sqrt(len(point1s) * 

679 len(point2s))} or C{0} if non-recursive, see function L{frechet_}. 

680 

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]}. 

687 

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) 

693 

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 

699 

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] 

707 

708 

709class FrechetVincentys(_FrechetMeterRadians): 

710 '''Compute the C{Frechet} distance with function L{pygeodesy.vincentys_}. 

711 

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. 

716 

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}. 

720 

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_ 

727 

728 if _FOR_DOCS: 

729 discrete = Frechet.discrete 

730 

731 

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. 

735 

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}). 

744 

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}}. 

748 

749 @raise FrechetError: Insufficient number of B{C{point1s}} or B{C{point2s}}. 

750 

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}. 

754 

755 @raise TypeError: If B{C{distance}} is not a callable. 

756 

757 @note: Function L{frechet_} does I{not} support I{fractional} indices for intermediate 

758 B{C{point1s}} and B{C{point2s}}. 

759 

760 @see: Non-recursive U{dp_frechet_dist 

761 <https://GitHub.com/cjekel/similarity_measures/issues/6#issuecomment-544350039>}. 

762 ''' 

763 _xcallable(distance=distance) 

764 

765 n1, ps1 = _points2(point1s, closed=False, Error=FrechetError) 

766 n2, ps2 = _points2(point2s, closed=False, Error=FrechetError) 

767 

768 def _dF(i1, i2): 

769 return distance(ps1[i1], ps2[i2]) 

770 

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 

779 

780 

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 

785 

786 

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 

793 

794 _min3 = _frechet3 

795 

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) 

815 

816 

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) 

824 

825 _min3 = _frechet3 

826 

827 iF = iF() # PYCHOK once 

828 tF = _defaultdict(dict) # tuple[i][j] 

829 

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 

850 

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) 

857 

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 

865 

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) 

870 

871 

872if __name__ == _DMAIN_: 

873 

874 def _main(): 

875 from time import time 

876 from pygeodesy import euclid, printf # randomrangenerator 

877 _r = range # randomrangenerator('R') 

878 

879 def _d(p1, p2): 

880 return euclid(p1[0] - p2[0], p1[1] - p2[1]) 

881 

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) 

891 

892 from pygeodesy.internals import _versions 

893 printf('# %s %.2fX', _versions(), (ss[0] / ss[1])) 

894 

895 _main() 

896 

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 

901 

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 

906 

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.