Coverage for pygeodesy/hausdorff.py: 95%

235 statements  

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

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 

69# from pygeodesy.basics import _isin # from .datums 

70from pygeodesy.constants import INF, NINF, _0_0 

71from pygeodesy.datums import _ellipsoidal_datum, _WGS84, _isin 

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

73# from pygeodesy import formy as _formy # _MODS.into 

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

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

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

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

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

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

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

81from pygeodesy.units import Float, Number_ 

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

83 

84# from math import radians # from .points 

85from random import Random 

86 

87__all__ = _ALL_LAZY.hausdorff 

88__version__ = '25.04.21' 

89 

90_formy = _MODS.into(formy=__name__) 

91 

92 

93class HausdorffError(PointsError): 

94 '''Hausdorff issue. 

95 ''' 

96 pass 

97 

98 

99class Hausdorff(_Named): 

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

101 be overloaded. 

102 ''' 

103 _datum = _WGS84 

104 _func = None # formy function/property 

105 _kwds = {} # func_ options 

106 _model = () 

107 _seed = None 

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

109 

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

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

112 

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

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

115 C{other}[]). 

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

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

118 PubDat_247739.pdf>}. 

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

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

121 and keyword arguments for the distance function, retrievable 

122 with property C{kwds}. 

123 

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

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

126 ''' 

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

128 if name: 

129 self.name = name 

130 

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

132 if seed: 

133 self.seed = seed 

134 if units: # and not self.units: 

135 self.units = units 

136 if kwds: 

137 self._kwds = kwds 

138 

139 @Property_RO 

140 def adjust(self): 

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

142 ''' 

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

144 

145 @Property_RO 

146 def datum(self): 

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

148 ''' 

149 return self._datum 

150 

151 def _datum_setter(self, datum): 

152 '''(INTERNAL) Set the datum. 

153 ''' 

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

155 if not _isin(d, None, self._datum): # PYCHOK no cover 

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

157 

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

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

160 

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

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

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

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

165 

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

167 

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

169 an invalid B{C{point2}}. 

170 

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

172 ''' 

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

174 

175 def distance(self, point1, point2): 

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

177 subject to the supplied optional keyword arguments, see 

178 property C{kwds}. 

179 ''' 

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

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

182 

183 @Property 

184 def _func(self): 

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

186 self._notOverloaded(**self.kwds) 

187 

188 @_func.setter_ # PYCHOK setter_underscore! 

189 def _func(self, func): 

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

191 

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

193 _, ps2 = self._points2(point2s) 

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

195 self.units, distance, self.point) 

196 

197 @property_RO 

198 def kwds(self): 

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

200 ''' 

201 return self._kwds 

202 

203 def point(self, point): 

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

205 ''' 

206 return point # pass thru 

207 

208 def _points2(self, points): 

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

210 ''' 

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

212 

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

214 def seed(self): 

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

216 ''' 

217 return self._seed 

218 

219 @seed.setter # PYCHOK setter! 

220 def seed(self, seed): 

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

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

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

224 

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

226 ''' 

227 if seed: 

228 try: 

229 Random(seed) 

230 except (TypeError, ValueError) as x: 

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

232 self._seed = seed 

233 else: 

234 self._seed = None 

235 

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

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

238 

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

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

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

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

243 

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

245 

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

247 an invalid B{C{point2}}. 

248 

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

250 ''' 

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

252 

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

254 def units(self): 

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

256 ''' 

257 return self._units 

258 

259 @units.setter # PYCHOK setter! 

260 def units(self, units): 

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

262 

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

264 ''' 

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

266 

267 @Property_RO 

268 def wrap(self): 

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

270 ''' 

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

272 

273 

274class HausdorffDegrees(Hausdorff): 

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

276 points in C{degrees}. 

277 ''' 

278 _units = _unitsBase._Str_degrees 

279 

280 if _FOR_DOCS: 

281 __init__ = Hausdorff.__init__ 

282 directed = Hausdorff.directed 

283 symmetric = Hausdorff.symmetric 

284 

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

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

287 self._notOverloaded(point1, point2) 

288 

289 

290class HausdorffRadians(Hausdorff): 

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

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

293 ''' 

294 _units = _unitsBase._Str_radians 

295 

296 if _FOR_DOCS: 

297 __init__ = Hausdorff.__init__ 

298 directed = Hausdorff.directed 

299 symmetric = Hausdorff.symmetric 

300 

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

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

303 self._notOverloaded(point1, point2) 

304 

305 def point(self, point): 

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

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

308 

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

310 ''' 

311 try: 

312 return point.philam 

313 except AttributeError: 

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

315 

316 

317class _HausdorffMeterRadians(Hausdorff): 

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

319 the optional keyword arguments supplied at instantiation 

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

321 ''' 

322 _units = _unitsBase._Str_meter 

323 _units_ = _unitsBase._Str_radians 

324 

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

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

327 the distance function and units from the optional 

328 keyword arguments given at this instantiation, see 

329 property C{kwds}. 

330 

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

332 ''' 

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

334 

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

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

337 the distance function and units from the optional 

338 keyword arguments given at this instantiation, see 

339 property C{kwds}. 

340 

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

342 ''' 

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

344 

345 @Property 

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

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

348 self._notOverloaded(**self.kwds) 

349 

350 @_func_.setter_ # PYCHOK setter_underscore! 

351 def _func_(self, func): 

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

353 

354 

355class HausdorffCosineLaw(_HausdorffMeterRadians): 

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

357 

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

359 ''' 

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

361 '''New L{HausdorffCosineLaw} calculator. 

362 

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

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

365 L{pygeodesy.cosineLaw}. 

366 

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

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

369 ''' 

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

371 self._func = _formy.cosineLaw 

372 self._func_ = _formy.cosineLaw_ 

373 

374 if _FOR_DOCS: 

375 directed = Hausdorff.directed 

376 symmetric = Hausdorff.symmetric 

377 

378 

379class HausdorffDistanceTo(Hausdorff): 

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

381 ''' 

382 _units = _unitsBase._Str_meter 

383 

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

385 '''New L{HausdorffDistanceTo} calculator. 

386 

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

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

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

390 

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

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

393 

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

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

396 spherical C{LatLon} class. 

397 ''' 

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

399 

400 if _FOR_DOCS: 

401 directed = Hausdorff.directed 

402 symmetric = Hausdorff.symmetric 

403 

404 def distance(self, p1, p2): 

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

406 ''' 

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

408 

409 def _points2(self, points): 

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

411 ''' 

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

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

414 

415 

416class HausdorffEquirectangular(Hausdorff): 

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

418 ''' 

419 _units = _unitsBase._Str_degrees2 

420 

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

422 '''New L{HausdorffEquirectangular} calculator. 

423 

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

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

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

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

428 

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

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

431 ''' 

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

433 limit=0)) 

434 self._func = _formy._equirectangular # helper 

435 

436 if _FOR_DOCS: 

437 directed = Hausdorff.directed 

438 symmetric = Hausdorff.symmetric 

439 

440 

441class HausdorffEuclidean(_HausdorffMeterRadians): 

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

443 ''' 

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

445 '''New L{HausdorffEuclidean} calculator. 

446 

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

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

449 function L{pygeodesy.euclidean}. 

450 

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

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

453 ''' 

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

455 self._func = _formy.euclidean 

456 self._func_ = _formy.euclidean_ 

457 

458 if _FOR_DOCS: 

459 directed = Hausdorff.directed 

460 symmetric = Hausdorff.symmetric 

461 

462 

463class HausdorffExact(Hausdorff): 

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

465 ''' 

466 _units = _unitsBase._Str_degrees 

467 

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

469 '''New L{HausdorffKarney} calculator. 

470 

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

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

473 or L{a_f2Tuple}). 

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

475 keyword argument for method C{Inverse1} of class 

476 L{geodesicx.GeodesicExact}. 

477 

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

479 

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

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

482 ''' 

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

484 self._datum_setter(datum) 

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

486 

487 if _FOR_DOCS: 

488 directed = Hausdorff.directed 

489 symmetric = Hausdorff.symmetric 

490 

491 

492class HausdorffFlatLocal(_HausdorffMeterRadians): 

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

494 ''' 

495 _units = _unitsBase._Str_radians2 

496 

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

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

499 

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

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

502 L{pygeodesy.flatLocal}. 

503 

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

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

506 

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

508 ''' 

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

510 self._func = _formy.flatLocal 

511 self._func_ = self.datum.ellipsoid._hubeny_2 

512 

513 if _FOR_DOCS: 

514 directed = Hausdorff.directed 

515 symmetric = Hausdorff.symmetric 

516 

517 

518class HausdorffFlatPolar(_HausdorffMeterRadians): 

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

520 ''' 

521 _wrap = False 

522 

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

524 '''New L{HausdorffFlatPolar} calculator. 

525 

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

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

528 for function L{pygeodesy.flatPolar}. 

529 

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

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

532 ''' 

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

534 self._func = _formy.flatPolar 

535 self._func_ = _formy.flatPolar_ 

536 

537 if _FOR_DOCS: 

538 directed = Hausdorff.directed 

539 symmetric = Hausdorff.symmetric 

540 

541 

542class HausdorffHaversine(_HausdorffMeterRadians): 

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

544 

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

546 ''' 

547 _wrap = False 

548 

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

550 '''New L{HausdorffHaversine} calculator. 

551 

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

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

554 for function L{pygeodesy.haversine}. 

555 

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

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

558 ''' 

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

560 self._func = _formy.haversine 

561 self._func_ = _formy.haversine_ 

562 

563 if _FOR_DOCS: 

564 directed = Hausdorff.directed 

565 symmetric = Hausdorff.symmetric 

566 

567 

568class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny 

569 if _FOR_DOCS: 

570 __doc__ = HausdorffFlatLocal.__doc__ 

571 __init__ = HausdorffFlatLocal.__init__ 

572 directed = HausdorffFlatLocal.directed 

573 distance = HausdorffFlatLocal.distance 

574 symmetric = HausdorffFlatLocal.symmetric 

575 

576 

577class HausdorffKarney(Hausdorff): 

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

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

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

581 method. 

582 ''' 

583 _units = _unitsBase._Str_degrees 

584 

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

586 '''New L{HausdorffKarney} calculator. 

587 

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

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

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

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

592 and keyword arguments for method C{Inverse1} of 

593 class L{geodesicw.Geodesic}. 

594 

595 @raise ImportError: Package U{geographiclib 

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

597 

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

599 

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

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

602 ''' 

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

604 self._datum_setter(datum) 

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

606 

607 

608class HausdorffThomas(_HausdorffMeterRadians): 

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

610 ''' 

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

612 '''New L{HausdorffThomas} calculator. 

613 

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

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

616 for function L{pygeodesy.thomas}. 

617 

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

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

620 ''' 

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

622 self._func = _formy.thomas 

623 self._func_ = _formy.thomas_ 

624 

625 if _FOR_DOCS: 

626 directed = Hausdorff.directed 

627 symmetric = Hausdorff.symmetric 

628 

629 

630class HausdorffVincentys(_HausdorffMeterRadians): 

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

632 

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

634 ''' 

635 _wrap = False 

636 

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

638 '''New L{HausdorffVincentys} calculator. 

639 

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

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

642 for function L{pygeodesy.vincentys}. 

643 

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

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

646 ''' 

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

648 self._func = _formy.vincentys 

649 self._func_ = _formy.vincentys_ 

650 

651 if _FOR_DOCS: 

652 directed = Hausdorff.directed 

653 symmetric = Hausdorff.symmetric 

654 

655 

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

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

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

659 ''' 

660 # shuffling the points generally increases the 

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

662 rr = randomrangenerator(seed) if seed else range 

663 

664 hd = NINF 

665 mn = m = hi = hj = 0 

666 md = _0_0 

667 

668 # forward or forward and backward 

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

670 ji, n = False, len(ps2) 

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

672 p1 = point(ps1[i]) 

673 dh, dj = INF, 0 

674 for j in rr(n): 

675 p2 = point(ps2[j]) 

676 d = distance(p1, p2) 

677 if early and d < hd: 

678 break # early 

679 elif d < dh: 

680 dh, dj = d, j 

681 else: # no early break 

682 if hd < dh: 

683 hd = dh 

684 hi = i 

685 hj = dj 

686 ji = fb 

687 md += dh 

688 mn += 1 

689 m += 1 

690 if ji: # swap indices 

691 hi, hj = hj, hi 

692 # swap model and target 

693 ps1, ps2 = ps2, ps1 

694 

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

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

697 

698 

699def _point(p): 

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

701 

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

703 

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

705 B{C{distance}} callable. 

706 ''' 

707 return p 

708 

709 

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

711 distance=None, point=_point): 

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

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

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

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

716 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

732 

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

734 

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

736 

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

738 ''' 

739 _xcallable(distance=distance, point=point) 

740 

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

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

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

744 

745 

746class Hausdorff6Tuple(_NamedTuple): 

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

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

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

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

751 C{units}. 

752 

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

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

755 count C{mn} twice that. 

756 

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

758 point with the C{hd} distance. 

759 

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

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

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

763 ''' 

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

765 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass) 

766 

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

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

769 ''' 

770 u = list(Hausdorff6Tuple._Units_) 

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

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

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

774 

775 

776def randomrangenerator(seed): 

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

778 

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

780 or C{None}). 

781 

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

783 current time or from a platform-specific randomness 

784 source, if available. 

785 

786 @return: A function to generate random ranges. 

787 

788 @example: 

789 

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

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

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

793 ''' 

794 R = Random(seed) 

795 

796 def _range(n, *stop_step): 

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

798 except the returned values are in random order. 

799 

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

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

802 creating a tuple with the entire C{population} 

803 and a list containing all sample values (for 

804 large C{n}). 

805 ''' 

806 if stop_step: 

807 s = range(n, *stop_step) 

808 

809 elif n > 32: 

810 r = R.randrange # Random._randbelow 

811 s = set() 

812 for _ in range(n - 32): 

813 i = r(n) 

814 while i in s: 

815 i = r(n) 

816 s.add(i) 

817 yield i 

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

819 else: 

820 s = range(n) 

821 

822 s = list(s) 

823 R.shuffle(s) 

824 while s: 

825 yield s.pop(0) 

826 

827 return _range 

828 

829# **) MIT License 

830# 

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

832# 

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

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

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

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

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

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

839# 

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

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

842# 

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

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

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

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

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

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

849# OTHER DEALINGS IN THE SOFTWARE.