Coverage for pygeodesy/hausdorff.py: 95%

233 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-06 12:20 -0500

1 

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

3 

4u'''Hausdorff distances. 

5 

6Classes L{Hausdorff}, L{HausdorffDegrees}, L{HausdorffRadians}, 

7L{HausdorffCosineLaw}, L{HausdorffDistanceTo}, L{HausdorffEquirectangular}, 

8L{HausdorffEuclidean}, L{HausdorffFlatLocal}, L{HausdorffFlatPolar}, 

9L{HausdorffHaversine}, L{HausdorffHubeny}, L{HausdorffKarney}, 

10L{HausdorffThomas} and L{HausdorffVincentys} to compute U{Hausdorff 

11<https://WikiPedia.org/wiki/Hausdorff_distance>} distances between two 

12sets of C{LatLon}, C{NumPy}, C{tuples} or other types of points. 

13 

14Only L{HausdorffDistanceTo} -iff used with L{ellipsoidalKarney.LatLon} 

15points- and L{HausdorffKarney} requires installation of I{Charles Karney}'s 

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

17 

18Typical usage is as follows. First, create a C{Hausdorff} calculator 

19from a given set of C{LatLon} points, called the C{model} or C{template} 

20points. 

21 

22C{h = HausdorffXyz(point1s, ...)} 

23 

24Get the C{directed} or C{symmetric} Hausdorff distance to a second set 

25of C{LatLon} points, named the C{target} points, by using 

26 

27C{t6 = h.directed(point2s)} 

28 

29respectively 

30 

31C{t6 = h.symmetric(point2s)}. 

32 

33Or, use function C{hausdorff_} with a proper C{distance} function and 

34optionally a C{point} function passed as keyword arguments as follows 

35 

36C{t6 = hausdorff_(point1s, point2s, ..., distance=..., point=...)}. 

37 

38In all cases, the returned result C{t6} is a L{Hausdorff6Tuple}. 

39 

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

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

42instance, more details in the documentation thereof. 

43 

44For other points, create a L{Hausdorff} sub-class with the appropriate 

45C{distance} method overloading L{Hausdorff.distance} and optionally a 

46C{point} method overriding L{Hausdorff.point} as the next example. 

47 

48 >>> from pygeodesy import Hausdorff, hypot_ 

49 >>> 

50 >>> class H3D(Hausdorff): 

51 >>> """Custom Hausdorff example. 

52 >>> """ 

53 >>> def distance(self, p1, p2): 

54 >>> return hypot_(p1.x - p2.x, p1.y - p2.y, p1.z - p2.z) 

55 >>> 

56 >>> h3D = H3D(xyz1, ..., units="...") 

57 >>> d6 = h3D.directed(xyz2) 

58 

59Transcribed from the original SciPy U{Directed Hausdorff Code 

60<https://GitHub.com/scipy/scipy/blob/master/scipy/spatial/_hausdorff.pyx>} 

61version 0.19.0, Copyright (C) Tyler Reddy, Richard Gowers, and Max Linke, 

622016, distributed under the same BSD license as SciPy, including C{early 

63breaking} and C{random sampling} as in U{Abdel Aziz Taha, Allan Hanbury 

64"An Efficient Algorithm for Calculating the Exact Hausdorff Distance" 

65<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}, IEEE Trans. Pattern 

66Analysis Machine Intelligence (PAMI), vol 37, no 11, pp 2153-2163, Nov 2015. 

67''' 

68 

69from pygeodesy.constants import INF, NINF, _0_0 

70from pygeodesy.datums import _ellipsoidal_datum, _WGS84 

71from pygeodesy.errors import PointsError, _xattr, _xcallable, _xkwds, _xkwds_get 

72import pygeodesy.formy as _formy 

73from pygeodesy.interns import NN, _i_, _j_, _units_ 

74# from pygeodesy.iters import points2 as _points # from .points 

75from pygeodesy.lazily import _ALL_LAZY, _FOR_DOCS 

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

77# from pygeodesy.namedTuples import PhiLam2Tuple # from .points 

78from pygeodesy.points import _distanceTo, PhiLam2Tuple, points2 as _points2, radians 

79from pygeodesy.props import Property, Property_RO, property_doc_, property_RO 

80from pygeodesy.units import Float, Number_ 

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

82 

83# from math import radians # from .points 

84from random import Random 

85 

86__all__ = _ALL_LAZY.hausdorff 

87__version__ = '24.12.31' 

88 

89 

90class HausdorffError(PointsError): 

91 '''Hausdorff issue. 

92 ''' 

93 pass 

94 

95 

96class Hausdorff(_Named): 

97 '''Hausdorff base class, requires method L{Hausdorff.distance} to 

98 be overloaded. 

99 ''' 

100 _datum = _WGS84 

101 _func = None # formy function/property 

102 _kwds = {} # func_ options 

103 _model = () 

104 _seed = None 

105 _units = _unitsBase._Str_NN # XXX Str to _Pass and for backward compatibility 

106 

107 def __init__(self, point1s, seed=None, units=NN, **name__kwds): 

108 '''New C{Hausdorff...} calculator. 

109 

110 @arg point1s: Initial set of points, aka the C{model} or C{template} 

111 (C{LatLon}[], C{Numpy2LatLon}[], C{Tuple2LatLon}[] or 

112 C{other}[]). 

113 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False} 

114 for no U{random sampling<https://Publik.TUWien.ac.AT/files/ 

115 PubDat_247739.pdf>}. 

116 @kwarg units: Optional, the distance units (C{Unit} or C{str}). 

117 @kwarg name__kwds: Optional calculator/interpolator C{B{name}=NN} (C{str}) 

118 and keyword arguments for the distance function, retrievable 

119 with property C{kwds}. 

120 

121 @raise HausdorffError: Insufficient number of B{C{point1s}} or an invalid 

122 B{C{point1}}, B{C{seed}} or B{C{units}}. 

123 ''' 

124 name, kwds = _name2__(**name__kwds) # name__=self.__class__ 

125 if name: 

126 self.name = name 

127 

128 _, self._model = self._points2(point1s) 

129 if seed: 

130 self.seed = seed 

131 if units: # and not self.units: 

132 self.units = units 

133 if kwds: 

134 self._kwds = kwds 

135 

136 @Property_RO 

137 def adjust(self): 

138 '''Get the adjust setting (C{bool} or C{None} if not applicable). 

139 ''' 

140 return _xkwds_get(self._kwds, adjust=None) 

141 

142 @Property_RO 

143 def datum(self): 

144 '''Get the datum of this calculator (L{Datum} or C{None} if not applicable). 

145 ''' 

146 return self._datum 

147 

148 def _datum_setter(self, datum): 

149 '''(INTERNAL) Set the datum. 

150 ''' 

151 d = datum or _xattr(self._model[0], datum=datum) 

152 if d not in (None, self._datum): # PYCHOK no cover 

153 self._datum = _ellipsoidal_datum(d, name=self.name) 

154 

155 def directed(self, point2s, early=True): 

156 '''Compute only the C{forward Hausdorff} distance. 

157 

158 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[], 

159 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]). 

160 @kwarg early: Enable or disable U{early breaking<https:// 

161 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}). 

162 

163 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}. 

164 

165 @raise HausdorffError: Insufficient number of B{C{point2s}} or 

166 an invalid B{C{point2}}. 

167 

168 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}. 

169 ''' 

170 return self._hausdorff_(point2s, False, early, self.distance) 

171 

172 def distance(self, point1, point2): 

173 '''Return the distance between B{C{point1}} and B{C{point2}}, 

174 subject to the supplied optional keyword arguments, see 

175 property C{kwds}. 

176 ''' 

177 return self._func(point1.lat, point1.lon, 

178 point2.lat, point2.lon, **self._kwds) 

179 

180 @Property 

181 def _func(self): 

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

183 self._notOverloaded(**self.kwds) 

184 

185 @_func.setter_ # PYCHOK setter_underscore! 

186 def _func(self, func): 

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

188 

189 def _hausdorff_(self, point2s, both, early, distance): 

190 _, ps2 = self._points2(point2s) 

191 return _hausdorff_(self._model, ps2, both, early, self.seed, 

192 self.units, distance, self.point) 

193 

194 @property_RO 

195 def kwds(self): 

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

197 ''' 

198 return self._kwds 

199 

200 def point(self, point): 

201 '''Convert a C{model} or C{target} point for the C{.distance} method. 

202 ''' 

203 return point # pass thru 

204 

205 def _points2(self, points): 

206 '''(INTERNAL) Check a set of points. 

207 ''' 

208 return _points2(points, closed=False, Error=HausdorffError) 

209 

210 @property_doc_(''' the random sampling seed (C{Random}).''') 

211 def seed(self): 

212 '''Get the random sampling seed (C{any} or C{None}). 

213 ''' 

214 return self._seed 

215 

216 @seed.setter # PYCHOK setter! 

217 def seed(self, seed): 

218 '''Set the random sampling seed (C{Random(seed)}) or 

219 C{None}, C{0} or C{False} for no U{random sampling 

220 <https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}. 

221 

222 @raise HausdorffError: Invalid B{C{seed}}. 

223 ''' 

224 if seed: 

225 try: 

226 Random(seed) 

227 except (TypeError, ValueError) as x: 

228 raise HausdorffError(seed=seed, cause=x) 

229 self._seed = seed 

230 else: 

231 self._seed = None 

232 

233 def symmetric(self, point2s, early=True): 

234 '''Compute the combined C{forward and reverse Hausdorff} distance. 

235 

236 @arg point2s: Second set of points, aka the C{target} (C{LatLon}[], 

237 C{Numpy2LatLon}[], C{Tuple2LatLon}[] or C{other}[]). 

238 @kwarg early: Enable or disable U{early breaking<https:// 

239 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} (C{bool}). 

240 

241 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}. 

242 

243 @raise HausdorffError: Insufficient number of B{C{point2s}} or 

244 an invalid B{C{point2}}. 

245 

246 @note: See B{C{point2s}} note at L{HausdorffDistanceTo}. 

247 ''' 

248 return self._hausdorff_(point2s, True, early, self.distance) 

249 

250 @property_doc_(''' the distance units (C{Unit} or C{str}).''') 

251 def units(self): 

252 '''Get the distance units (C{Unit} or C{str}). 

253 ''' 

254 return self._units 

255 

256 @units.setter # PYCHOK setter! 

257 def units(self, units): 

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

259 

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

261 ''' 

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

263 

264 @Property_RO 

265 def wrap(self): 

266 '''Get the wrap setting (C{bool} or C{None} if not applicable). 

267 ''' 

268 return _xkwds_get(self._kwds, adjust=None) 

269 

270 

271class HausdorffDegrees(Hausdorff): 

272 '''L{Hausdorff} base class for distances from C{LatLon} 

273 points in C{degrees}. 

274 ''' 

275 _units = _unitsBase._Str_degrees 

276 

277 if _FOR_DOCS: 

278 __init__ = Hausdorff.__init__ 

279 directed = Hausdorff.directed 

280 symmetric = Hausdorff.symmetric 

281 

282 def distance(self, point1, point2): # PYCHOK no cover 

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

284 self._notOverloaded(point1, point2) 

285 

286 

287class HausdorffRadians(Hausdorff): 

288 '''L{Hausdorff} base class for distances from C{LatLon} 

289 points converted from C{degrees} to C{radians}. 

290 ''' 

291 _units = _unitsBase._Str_radians 

292 

293 if _FOR_DOCS: 

294 __init__ = Hausdorff.__init__ 

295 directed = Hausdorff.directed 

296 symmetric = Hausdorff.symmetric 

297 

298 def distance(self, point1, point2): # PYCHOK no cover 

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

300 self._notOverloaded(point1, point2) 

301 

302 def point(self, point): 

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

304 I{backward compatibility} of L{HausdorffRadians}. 

305 

306 @return: A L{PhiLam2Tuple}C{(phi, lam)}. 

307 ''' 

308 try: 

309 return point.philam 

310 except AttributeError: 

311 return PhiLam2Tuple(radians(point.lat), radians(point.lon)) 

312 

313 

314class _HausdorffMeterRadians(Hausdorff): 

315 '''(INTERNAL) Returning C{meter} or C{radians} depending on 

316 the optional keyword arguments supplied at instantiation 

317 of the C{Hausdorff*} sub-class. 

318 ''' 

319 _units = _unitsBase._Str_meter 

320 _units_ = _unitsBase._Str_radians 

321 

322 def directed(self, point2s, early=True): 

323 '''Overloaded method L{Hausdorff.directed} to determine 

324 the distance function and units from the optional 

325 keyword arguments given at this instantiation, see 

326 property C{kwds}. 

327 

328 @see: L{Hausdorff.directed} for other details. 

329 ''' 

330 return self._hausdorff_(point2s, False, early, _formy._radistance(self)) 

331 

332 def symmetric(self, point2s, early=True): 

333 '''Overloaded method L{Hausdorff.symmetric} to determine 

334 the distance function and units from the optional 

335 keyword arguments given at this instantiation, see 

336 property C{kwds}. 

337 

338 @see: L{Hausdorff.symmetric} for other details. 

339 ''' 

340 return self._hausdorff_(point2s, True, early, _formy._radistance(self)) 

341 

342 @Property 

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

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

345 self._notOverloaded(**self.kwds) 

346 

347 @_func_.setter_ # PYCHOK setter_underscore! 

348 def _func_(self, func): 

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

350 

351 

352class HausdorffCosineLaw(_HausdorffMeterRadians): 

353 '''Compute the C{Hausdorff} distance with function L{pygeodesy.cosineLaw_}. 

354 

355 @note: See note at function L{pygeodesy.vincentys_}. 

356 ''' 

357 def __init__(self, point1s, **seed_name__corr_earth_wrap): 

358 '''New L{HausdorffCosineLaw} calculator. 

359 

360 @kwarg seed_name__corr_earth_wrap: Optional C{B{seed}=None} and 

361 C{B{name}=NN} and keyword arguments for function 

362 L{pygeodesy.cosineLaw}. 

363 

364 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, 

365 B{C{seed}}, B{C{name}} and other exceptions. 

366 ''' 

367 Hausdorff.__init__(self, point1s, **seed_name__corr_earth_wrap) 

368 self._func = _formy.cosineLaw 

369 self._func_ = _formy.cosineLaw_ 

370 

371 if _FOR_DOCS: 

372 directed = Hausdorff.directed 

373 symmetric = Hausdorff.symmetric 

374 

375 

376class HausdorffDistanceTo(Hausdorff): 

377 '''Compute the C{Hausdorff} distance the points' C{LatLon.distanceTo} method. 

378 ''' 

379 _units = _unitsBase._Str_meter 

380 

381 def __init__(self, point1s, **seed_name__distanceTo_kwds): 

382 '''New L{HausdorffDistanceTo} calculator. 

383 

384 @kwarg seed_name__distanceTo_kwds: Optional C{B{seed}=None} and 

385 C{B{name}=NN} and keyword arguments for each 

386 B{C{point1s}}' C{LatLon.distanceTo} method. 

387 

388 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, 

389 B{C{seed}}, B{C{name}} and other exceptions. 

390 

391 @note: All C{model}, C{template} and C{target} B{C{points}} 

392 I{must} be instances of the same ellipsoidal or 

393 spherical C{LatLon} class. 

394 ''' 

395 Hausdorff.__init__(self, point1s, **seed_name__distanceTo_kwds) 

396 

397 if _FOR_DOCS: 

398 directed = Hausdorff.directed 

399 symmetric = Hausdorff.symmetric 

400 

401 def distance(self, p1, p2): 

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

403 ''' 

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

405 

406 def _points2(self, points): 

407 '''(INTERNAL) Check a set of points. 

408 ''' 

409 np, ps = Hausdorff._points2(self, points) 

410 return np, _distanceTo(HausdorffError, points=ps) 

411 

412 

413class HausdorffEquirectangular(Hausdorff): 

414 '''Compute the C{Hausdorff} distance with function L{pygeodesy.equirectangular}. 

415 ''' 

416 _units = _unitsBase._Str_degrees2 

417 

418 def __init__(self, point1s, **seed_name__adjust_limit_wrap): 

419 '''New L{HausdorffEquirectangular} calculator. 

420 

421 @kwarg seed_name__adjust_limit_wrap: Optional C{B{seed}=None} and 

422 C{B{name}=NN} and keyword arguments for function 

423 L{pygeodesy.equirectangular} I{with default} 

424 C{B{limit}=0} for I{backward compatibility}. 

425 

426 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, 

427 B{C{seed}}, B{C{name}} and other exceptions. 

428 ''' 

429 Hausdorff.__init__(self, point1s, **_xkwds(seed_name__adjust_limit_wrap, 

430 limit=0)) 

431 self._func = _formy._equirectangular # helper 

432 

433 if _FOR_DOCS: 

434 directed = Hausdorff.directed 

435 symmetric = Hausdorff.symmetric 

436 

437 

438class HausdorffEuclidean(_HausdorffMeterRadians): 

439 '''Compute the C{Hausdorff} distance with function L{pygeodesy.euclidean_}. 

440 ''' 

441 def __init__(self, point1s, **seed_name__adjust_radius_wrap): 

442 '''New L{HausdorffEuclidean} calculator. 

443 

444 @kwarg seed_name__adjust_radius_wrap: Optional C{B{seed}=None} 

445 and C{B{name}=NN} and keyword arguments for 

446 function L{pygeodesy.euclidean}. 

447 

448 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, 

449 B{C{seed}}, B{C{name}} and other exceptions. 

450 ''' 

451 Hausdorff.__init__(self, point1s, **seed_name__adjust_radius_wrap) 

452 self._func = _formy.euclidean 

453 self._func_ = _formy.euclidean_ 

454 

455 if _FOR_DOCS: 

456 directed = Hausdorff.directed 

457 symmetric = Hausdorff.symmetric 

458 

459 

460class HausdorffExact(Hausdorff): 

461 '''Compute the C{Hausdorff} distance with method L{GeodesicExact}C{.Inverse}. 

462 ''' 

463 _units = _unitsBase._Str_degrees 

464 

465 def __init__(self, point1s, datum=None, **seed_name__wrap): 

466 '''New L{HausdorffKarney} calculator. 

467 

468 @kwarg datum: Datum to override the default C{Datums.WGS84} and first 

469 B{C{point1s}}' datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2} 

470 or L{a_f2Tuple}). 

471 @kwarg seed_name__wrap: Optional C{B{seed}=None} and C{B{name}=NN} and 

472 keyword argument for method C{Inverse1} of class 

473 L{geodesicx.GeodesicExact}. 

474 

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

476 

477 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, B{C{seed}}, 

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

479 ''' 

480 Hausdorff.__init__(self, point1s, **seed_name__wrap) 

481 self._datum_setter(datum) 

482 self._func = self.datum.ellipsoid.geodesicx.Inverse1 # note -x 

483 

484 if _FOR_DOCS: 

485 directed = Hausdorff.directed 

486 symmetric = Hausdorff.symmetric 

487 

488 

489class HausdorffFlatLocal(_HausdorffMeterRadians): 

490 '''Compute the C{Hausdorff} distance with function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}. 

491 ''' 

492 _units = _unitsBase._Str_radians2 

493 

494 def __init__(self, point1s, **seed_name__datum_scaled_wrap): 

495 '''New L{HausdorffFlatLocal}/L{HausdorffHubeny} calculator. 

496 

497 @kwarg seed_name__datum_scaled_wrap: Optional C{B{seed}=None} and 

498 C{B{name}=NN} and keyword arguments for function 

499 L{pygeodesy.flatLocal}. 

500 

501 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, 

502 B{C{seed}}, B{C{name}} and other exceptions. 

503 

504 @note: The distance C{units} are C{radians squared}, not C{radians}. 

505 ''' 

506 Hausdorff.__init__(self, point1s, **seed_name__datum_scaled_wrap) 

507 self._func = _formy.flatLocal 

508 self._func_ = self.datum.ellipsoid._hubeny_2 

509 

510 if _FOR_DOCS: 

511 directed = Hausdorff.directed 

512 symmetric = Hausdorff.symmetric 

513 

514 

515class HausdorffFlatPolar(_HausdorffMeterRadians): 

516 '''Compute the C{Hausdorff} distance with function L{pygeodesy.flatPolar_}. 

517 ''' 

518 _wrap = False 

519 

520 def __init__(self, points, **seed_name__radius_wrap): 

521 '''New L{HausdorffFlatPolar} calculator. 

522 

523 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None} 

524 and C{B{name}=NN} and keyword arguments 

525 for function L{pygeodesy.flatPolar}. 

526 

527 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, 

528 B{C{seed}}, B{C{name}} and other exceptions. 

529 ''' 

530 Hausdorff.__init__(self, points, **seed_name__radius_wrap) 

531 self._func = _formy.flatPolar 

532 self._func_ = _formy.flatPolar_ 

533 

534 if _FOR_DOCS: 

535 directed = Hausdorff.directed 

536 symmetric = Hausdorff.symmetric 

537 

538 

539class HausdorffHaversine(_HausdorffMeterRadians): 

540 '''Compute the C{Hausdorff} distance with function L{pygeodesy.haversine_}. 

541 

542 @note: See note under L{HausdorffVincentys}. 

543 ''' 

544 _wrap = False 

545 

546 def __init__(self, points, **seed_name__radius_wrap): 

547 '''New L{HausdorffHaversine} calculator. 

548 

549 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None} 

550 and C{B{name}=NN} and keyword arguments 

551 for function L{pygeodesy.haversine}. 

552 

553 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, 

554 B{C{seed}}, B{C{name}} and other exceptions. 

555 ''' 

556 Hausdorff.__init__(self, points, **seed_name__radius_wrap) 

557 self._func = _formy.haversine 

558 self._func_ = _formy.haversine_ 

559 

560 if _FOR_DOCS: 

561 directed = Hausdorff.directed 

562 symmetric = Hausdorff.symmetric 

563 

564 

565class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny 

566 if _FOR_DOCS: 

567 __doc__ = HausdorffFlatLocal.__doc__ 

568 __init__ = HausdorffFlatLocal.__init__ 

569 directed = HausdorffFlatLocal.directed 

570 distance = HausdorffFlatLocal.distance 

571 symmetric = HausdorffFlatLocal.symmetric 

572 

573 

574class HausdorffKarney(Hausdorff): 

575 '''Compute the C{Hausdorff} distance with I{Karney}'s U{geographiclib 

576 <https://PyPI.org/project/geographiclib>} U{geodesic.Geodesic 

577 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}C{.Inverse} 

578 method. 

579 ''' 

580 _units = _unitsBase._Str_degrees 

581 

582 def __init__(self, point1s, datum=None, **seed_name__wrap): 

583 '''New L{HausdorffKarney} calculator. 

584 

585 @kwarg datum: Datum to override the default C{Datums.WGS84} and 

586 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid}, 

587 L{Ellipsoid2} or L{a_f2Tuple}). 

588 @kwarg seed_name__wrap: Optional C{B{seed}=None} and C{B{name}=NN} 

589 and keyword arguments for method C{Inverse1} of 

590 class L{geodesicw.Geodesic}. 

591 

592 @raise ImportError: Package U{geographiclib 

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

594 

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

596 

597 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, 

598 B{C{seed}}, B{C{name}} and other exceptions. 

599 ''' 

600 Hausdorff.__init__(self, point1s, **seed_name__wrap) 

601 self._datum_setter(datum) 

602 self._func = self.datum.ellipsoid.geodesic.Inverse1 

603 

604 

605class HausdorffThomas(_HausdorffMeterRadians): 

606 '''Compute the C{Hausdorff} distance with function L{pygeodesy.thomas_}. 

607 ''' 

608 def __init__(self, point1s, **seed_name__datum_wrap): 

609 '''New L{HausdorffThomas} calculator. 

610 

611 @kwarg seed_name__datum_wrap: Optional C{B{seed}=None} 

612 and C{B{name}=NN} and keyword arguments 

613 for function L{pygeodesy.thomas}. 

614 

615 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, 

616 B{C{seed}}, B{C{name}} and other exceptions. 

617 ''' 

618 Hausdorff.__init__(self, point1s, **seed_name__datum_wrap) 

619 self._func = _formy.thomas 

620 self._func_ = _formy.thomas_ 

621 

622 if _FOR_DOCS: 

623 directed = Hausdorff.directed 

624 symmetric = Hausdorff.symmetric 

625 

626 

627class HausdorffVincentys(_HausdorffMeterRadians): 

628 '''Compute the C{Hausdorff} distance with function L{pygeodesy.vincentys_}. 

629 

630 @note: See note at function L{pygeodesy.vincentys_}. 

631 ''' 

632 _wrap = False 

633 

634 def __init__(self, point1s, **seed_name__radius_wrap): 

635 '''New L{HausdorffVincentys} calculator. 

636 

637 @kwarg seed_name__radius_wrap: Optional C{B{seed}=None} 

638 and C{B{name}=NN} and keyword arguments 

639 for function L{pygeodesy.vincentys}. 

640 

641 @see: L{Hausdorff.__init__} for details about B{C{point1s}}, 

642 B{C{seed}}, B{C{name}} and other exceptions. 

643 ''' 

644 Hausdorff.__init__(self, point1s, **seed_name__radius_wrap) 

645 self._func = _formy.vincentys 

646 self._func_ = _formy.vincentys_ 

647 

648 if _FOR_DOCS: 

649 directed = Hausdorff.directed 

650 symmetric = Hausdorff.symmetric 

651 

652 

653def _hausdorff_(ps1, ps2, both, early, seed, units, distance, point): 

654 '''(INTERNAL) Core of function L{hausdorff_} and methods C{directed} 

655 and C{symmetric} of classes C{hausdorff.Hausdorff...}. 

656 ''' 

657 # shuffling the points generally increases the 

658 # chance of an early break in the inner j loop 

659 rr = randomrangenerator(seed) if seed else range 

660 

661 hd = NINF 

662 hi = hj = m = mn = 0 

663 md = _0_0 

664 

665 # forward or forward and backward 

666 for fb in range(2 if both else 1): 

667 n = len(ps2) 

668 for i in rr(len(ps1)): 

669 p1 = point(ps1[i]) 

670 dh, dj = INF, 0 

671 for j in rr(n): 

672 p2 = point(ps2[j]) 

673 d = distance(p1, p2) 

674 if early and d < hd: 

675 break # early 

676 elif d < dh: 

677 dh, dj = d, j 

678 else: # no early break 

679 if hd < dh: 

680 hd = dh 

681 if fb: 

682 hi, hj = dj, i 

683 else: 

684 hi, hj = i, dj 

685 md += dh 

686 mn += 1 

687 m += 1 

688 # swap model and target 

689 ps1, ps2 = ps2, ps1 

690 

691 md = None if mn < m else (md / float(m)) 

692 return Hausdorff6Tuple(hd, hi, hj, m, md, units) 

693 

694 

695def _point(p): 

696 '''Default B{C{point}} callable for function L{hausdorff_}. 

697 

698 @arg p: The original C{model} or C{target} point (C{any}). 

699 

700 @return: The point, suitable for the L{hausdorff_} 

701 B{C{distance}} callable. 

702 ''' 

703 return p 

704 

705 

706def hausdorff_(model, target, both=False, early=True, seed=None, units=NN, 

707 distance=None, point=_point): 

708 '''Compute the C{directed} or C{symmetric} U{Hausdorff 

709 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance between 2 sets of points 

710 with or without U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} 

711 and U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}. 

712 

713 @arg model: First set of points (C{LatLon}[], C{Numpy2LatLon}[], 

714 C{Tuple2LatLon}[] or C{other}[]). 

715 @arg target: Second set of points (C{LatLon}[], C{Numpy2LatLon}[], 

716 C{Tuple2LatLon}[] or C{other}[]). 

717 @kwarg both: Return the C{directed} (forward only) or the C{symmetric} 

718 (combined forward and reverse) C{Hausdorff} distance (C{bool}). 

719 @kwarg early: Enable or disable U{early breaking<https://Publik.TUWien.ac.AT/ 

720 files/PubDat_247739.pdf>} (C{bool}). 

721 @kwarg seed: Random sampling seed (C{any}) or C{None}, C{0} or C{False} for no 

722 U{random sampling<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}. 

723 @kwarg units: Optional, the distance units (C{Unit} or C{str}). 

724 @kwarg distance: Callable returning the distance between a B{C{model}} 

725 and B{C{target}} point (signature C{(point1, point2)}). 

726 @kwarg point: Callable returning the B{C{model}} or B{C{target}} point 

727 suitable for B{C{distance}} (signature C{(point)}). 

728 

729 @return: A L{Hausdorff6Tuple}C{(hd, i, j, mn, md, units)}. 

730 

731 @raise HausdorffError: Insufficient number of B{C{model}} or B{C{target}} points. 

732 

733 @raise TypeError: If B{C{distance}} or B{C{point}} is not callable. 

734 ''' 

735 _xcallable(distance=distance, point=point) 

736 

737 _, ps1 = _points2(model, closed=False, Error=HausdorffError) # PYCHOK non-sequence 

738 _, ps2 = _points2(target, closed=False, Error=HausdorffError) # PYCHOK non-sequence 

739 return _hausdorff_(ps1, ps2, both, early, seed, units, distance, point) 

740 

741 

742class Hausdorff6Tuple(_NamedTuple): 

743 '''6-Tuple C{(hd, i, j, mn, md, units)} with the U{Hausdorff 

744 <https://WikiPedia.org/wiki/Hausdorff_distance>} distance C{hd}, 

745 indices C{i} and C{j}, the total count C{mn}, the C{I{mean} 

746 Hausdorff} distance C{md} and the class or name of both distance 

747 C{units}. 

748 

749 For C{directed Hausdorff} distances, count C{mn} is the number 

750 of model points considered. For C{symmetric Hausdorff} distances 

751 count C{mn} twice that. 

752 

753 Indices C{i} and C{j} are the C{model} respectively C{target} 

754 point with the C{hd} distance. 

755 

756 Mean distance C{md} is C{None} if an C{early break} occurred and 

757 U{early breaking<https://Publik.TUWien.ac.AT/files/PubDat_247739.pdf>} 

758 was enabled by keyword argument C{early=True}. 

759 ''' 

760 _Names_ = ('hd', _i_, _j_, 'mn', 'md', _units_) 

761 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass) 

762 

763 def toUnits(self, **Error_name): # PYCHOK expected 

764 '''Overloaded C{_NamedTuple.toUnits} for C{hd} and C{md} units. 

765 ''' 

766 u = list(Hausdorff6Tuple._Units_) 

767 u[0] = U = _unitsBase._xUnit(self.units, Float) # PYCHOK expected 

768 u[4] = _Pass if self.md is None else U # PYCHOK expected 

769 return _NamedTuple.toUnits(self.reUnit(*u), **Error_name) # PYCHOK self 

770 

771 

772def randomrangenerator(seed): 

773 '''Return a C{seed}ed random range function generator. 

774 

775 @arg seed: Initial, internal L{Random} state (C{hashable} 

776 or C{None}). 

777 

778 @note: L{Random} with C{B{seed} is None} seeds from the 

779 current time or from a platform-specific randomness 

780 source, if available. 

781 

782 @return: A function to generate random ranges. 

783 

784 @example: 

785 

786 >>> rrange = randomrangenerator('R') 

787 >>> for r in rrange(n): 

788 >>> ... # r is random in 0..n-1 

789 ''' 

790 R = Random(seed) 

791 

792 def _range(n, *stop_step): 

793 '''Like standard L{range}C{start, stop=..., step=...)}, 

794 except the returned values are in random order. 

795 

796 @note: Especially C{range(n)} behaves like standard 

797 L{Random.sample}C{(range(n), n)} but avoids 

798 creating a tuple with the entire C{population} 

799 and a list containing all sample values (for 

800 large C{n}). 

801 ''' 

802 if stop_step: 

803 s = range(n, *stop_step) 

804 

805 elif n > 32: 

806 r = R.randrange # Random._randbelow 

807 s = set() 

808 for _ in range(n - 32): 

809 i = r(n) 

810 while i in s: 

811 i = r(n) 

812 s.add(i) 

813 yield i 

814 s = set(range(n)) - s # [i for i in range(n) if i not in s] 

815 else: 

816 s = range(n) 

817 

818 s = list(s) 

819 R.shuffle(s) 

820 while s: 

821 yield s.pop(0) 

822 

823 return _range 

824 

825# **) MIT License 

826# 

827# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

828# 

829# Permission is hereby granted, free of charge, to any person obtaining a 

830# copy of this software and associated documentation files (the "Software"), 

831# to deal in the Software without restriction, including without limitation 

832# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

833# and/or sell copies of the Software, and to permit persons to whom the 

834# Software is furnished to do so, subject to the following conditions: 

835# 

836# The above copyright notice and this permission notice shall be included 

837# in all copies or substantial portions of the Software. 

838# 

839# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

840# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

841# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

842# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

843# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

844# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

845# OTHER DEALINGS IN THE SOFTWARE.