Coverage for pygeodesy/frechet.py: 96%

1

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

3

4 u'''Fréchet distances.

5

6 Classes L{Frechet}, L{FrechetDegrees}, L{FrechetRadians}, L{FrechetCosineLaw},

7 L{FrechetDistanceTo}, L{FrechetEquirectangular}, L{FrechetEuclidean},

8 L{FrechetExact}, L{FrechetFlatLocal}, L{FrechetFlatPolar}, L{FrechetHaversine},

9 L{FrechetHubeny}, L{FrechetKarney}, L{FrechetThomas} and L{FrechetVincentys}

10 to compute I{discrete} U{Fréchet<https://WikiPedia.org/wiki/Frechet_distance>}

11 distances between two sets of C{LatLon}, C{NumPy}, C{tuples} or other types of points.

12

13 Only L{FrechetDistanceTo} -iff used with L{ellipsoidalKarney.LatLon}

14 points- and L{FrechetKarney} requires installation of I{Charles Karney}'s

15 U{geographiclib<https://PyPI.org/project/geographiclib>}.

16

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

18 set of C{LatLon} points.

19

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

21

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

23 by

24

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

26

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

28 as keyword arguments as follows

29

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

31

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

33

34 For C{(lat, lon, ...)} points in a C{NumPy} array or plain C{tuples},

35 wrap the points in a L{Numpy2LatLon} respectively L{Tuple2LatLon}

36 instance, more details in the documentation thereof.

37

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

39 C{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

52 Transcribed from the original U{Computing Discrete Fréchet Distance

53<https://www.kr.TUWien.ac.AT/staff/eiter/et-archive/cdtr9464.pdf>} by

54 Eiter, T. and Mannila, H., 1994, April 25, Technical Report CD-TR 94/64,

55 Information Systems Department/Christian Doppler Laboratory for Expert

56 Systems, Technical University Vienna, Austria.

57

58 This L{Frechet.discrete} implementation optionally generates intermediate

59 points for each point set separately. For example, using keyword argument

60 C{fraction=0.5} adds one additional point halfway between each pair of

61 points. Or using C{fraction=0.1} interpolates nine additional points

62 between each points pair.

63

64 The L{Frechet6Tuple} attributes C{fi1} and/or C{fi2} will be I{fractional}

65 indices of type C{float} if keyword argument C{fraction} is used. Otherwise,

66 C{fi1} and/or C{fi2} are simply type C{int} indices into the respective

67 points set.

68

69 For example, C{fractional} index value 2.5 means an intermediate point

70 halfway between points[2] and points[3]. Use function L{fractional}

71 to obtain the intermediate point for a I{fractional} index in the

72 corresponding set of points.

73

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

77 It is a measure of similarity between curves that takes into account the

78 location and ordering of the points. Therefore, it is often a better metric

79 than the well-known C{Hausdorff} distance, see the L{hausdorff} module.

80'''

81

82 from pygeodesy.basics import _isin, isscalar, typename

83 from pygeodesy.constants import EPS, EPS1, INF, NINF

84 from pygeodesy.datums import _ellipsoidal_datum, _WGS84

85 from 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

89 from pygeodesy.interns import _DMAIN_, _DOT_, _n_, NN, _units_

90# from pygeodesy.iters import points2 as _points2 # from .points

91 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _FOR_DOCS

92 from pygeodesy.named import _name2__, _Named, _NamedTuple, _Pass

93 from pygeodesy.namedTuples import PhiLam2Tuple

94 from pygeodesy.points import _distanceTo, _fractional, \

95 points2 as _points2, radians

96 from pygeodesy.props import Property, property_doc_, property_RO

97 from pygeodesy.units import FIx, Float, Number_

98 from pygeodesy import unitsBase as _unitsBase # _Str_..., _xUnit, _xUnits

99

100 from 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

109 def _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

121 class FrechetError(PointsError):

122 '''Fréchet issue.

123 '''

124 pass

125

126

127 class 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

340 class 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

354 class 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

379 class _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

408 class 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

431 class 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

466 class 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

490 class 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

511 class 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

539 class 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

564 class 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

585 class 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

608 class 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

616 class 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

650 class 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

671 class 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

709 class 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

732 def 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

781 def _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

787 def _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

817 def _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

872 if __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-2026 -- 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.