Coverage for pygeodesy/hausdorff.py: 96%

244 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-25 12:04 -0400

1 

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

3 

4u'''Hausdorff distances. 

5 

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

7L{HausdorffCosineAndoyerLambert}, L{HausdorffCosineForsytheAndoyerLambert}, 

8L{HausdorffCosineLaw}, L{HausdorffDistanceTo}, L{HausdorffEquirectangular}, 

9L{HausdorffEuclidean}, L{HausdorffFlatLocal}, L{HausdorffFlatPolar}, 

10L{HausdorffHaversine}, L{HausdorffHubeny}, L{HausdorffKarney}, 

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

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

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

14 

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

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

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

18 

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

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

21points. 

22 

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

24 

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

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

27 

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

29 

30respectively 

31 

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

33 

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

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

36 

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

38 

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

40 

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

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

43instance, more details in the documentation thereof. 

44 

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

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

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

48 

49 >>> from pygeodesy import Hausdorff, hypot_ 

50 >>> 

51 >>> class H3D(Hausdorff): 

52 >>> """Custom Hausdorff example. 

53 >>> """ 

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

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

56 >>> 

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

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

59 

60Transcribed from the original SciPy U{Directed Hausdorff Code 

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

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

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

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

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

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

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

68''' 

69 

70from pygeodesy.constants import INF, NINF, _0_0 

71from pygeodesy.datums import _ellipsoidal_datum, _WGS84 

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

73import pygeodesy.formy as _formy 

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

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

76from pygeodesy.lazily import _ALL_LAZY, _FOR_DOCS 

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

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

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

80from pygeodesy.props import Property_RO, property_doc_, property_RO 

81from pygeodesy.units import Float, Number_, _xUnit, _xUnits 

82from pygeodesy.unitsBase import _Str_degrees, _Str_degrees2, _Str_meter, _Str_NN, \ 

83 _Str_radians, _Str_radians2 

84 

85# from math import radians # from .points 

86from random import Random 

87 

88__all__ = _ALL_LAZY.hausdorff 

89__version__ = '24.05.24' 

90 

91 

92class HausdorffError(PointsError): 

93 '''Hausdorff issue. 

94 ''' 

95 pass 

96 

97 

98class Hausdorff(_Named): 

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

100 be overloaded. 

101 ''' 

102 _datum = _WGS84 

103 _func = None # formy function/property 

104 _kwds = {} # func_ options 

105 _model = () 

106 _seed = None 

107 _units = _Str_NN # XXX Str to _Pass and for backward compatibility 

108 

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

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

111 

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

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

114 C{other}[]). 

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

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

117 PubDat_247739.pdf>}. 

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

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

120 and keyword arguments for the distance function, retrievable 

121 with property C{kwds}. 

122 

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

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

125 ''' 

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

127 if name: 

128 self.name = name 

129 

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

131 if seed: 

132 self.seed = seed 

133 if units: # and not self.units: 

134 self.units = units 

135 if kwds: 

136 self._kwds = kwds 

137 

138 @Property_RO 

139 def adjust(self): 

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

141 ''' 

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

143 

144 @Property_RO 

145 def datum(self): 

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

147 ''' 

148 return self._datum 

149 

150 def _datum_setter(self, datum): 

151 '''(INTERNAL) Set the datum. 

152 ''' 

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

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

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

156 

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

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

159 

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

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

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

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

164 

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

166 

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

168 an invalid B{C{point2}}. 

169 

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

171 ''' 

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

173 

174 def distance(self, point1, point2): 

175 '''Return the distance between B{C{point1}} and B{C{point2s}}, 

176 subject to the supplied optional keyword arguments, see 

177 property C{kwds}. 

178 ''' 

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

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

181 

182 @property 

183 def _func(self): 

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

185 return _formy._Propy(self, 0, _func=None) 

186 

187 @_func.setter # PYCHOK setter! 

188 def _func(self, func): 

189 _formy._Propy(self, 4, _func=func) 

190 

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

192 _, ps2 = self._points2(point2s) 

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

194 self.units, distance, self.point) 

195 

196 @property_RO 

197 def kwds(self): 

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

199 ''' 

200 return self._kwds 

201 

202 def point(self, point): 

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

204 ''' 

205 return point # pass thru 

206 

207 def _points2(self, points): 

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

209 ''' 

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

211 

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

213 def seed(self): 

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

215 ''' 

216 return self._seed 

217 

218 @seed.setter # PYCHOK setter! 

219 def seed(self, seed): 

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

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

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

223 

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

225 ''' 

226 if seed: 

227 try: 

228 Random(seed) 

229 except (TypeError, ValueError) as x: 

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

231 self._seed = seed 

232 else: 

233 self._seed = None 

234 

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

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

237 

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

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

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

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

242 

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

244 

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

246 an invalid B{C{point2}}. 

247 

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

249 ''' 

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

251 

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

253 def units(self): 

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

255 ''' 

256 return self._units 

257 

258 @units.setter # PYCHOK setter! 

259 def units(self, units): 

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

261 

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

263 ''' 

264 self._units = _xUnits(units, Base=Float) 

265 

266 @Property_RO 

267 def wrap(self): 

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

269 ''' 

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

271 

272 

273class HausdorffDegrees(Hausdorff): 

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

275 points in C{degrees}. 

276 ''' 

277 _units = _Str_degrees 

278 

279 if _FOR_DOCS: 

280 __init__ = Hausdorff.__init__ 

281 directed = Hausdorff.directed 

282 symmetric = Hausdorff.symmetric 

283 

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

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

286 self._notOverloaded(point1, point2) 

287 

288 

289class HausdorffRadians(Hausdorff): 

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

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

292 ''' 

293 _units = _Str_radians 

294 

295 if _FOR_DOCS: 

296 __init__ = Hausdorff.__init__ 

297 directed = Hausdorff.directed 

298 symmetric = Hausdorff.symmetric 

299 

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

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

302 self._notOverloaded(point1, point2) 

303 

304 def point(self, point): 

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

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

307 

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

309 ''' 

310 try: 

311 return point.philam 

312 except AttributeError: 

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

314 

315 

316class _HausdorffMeterRadians(Hausdorff): 

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

318 the optional keyword arguments supplied at instantiation 

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

320 ''' 

321 _units = _Str_meter 

322 _units_ = _Str_radians 

323 

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

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

326 the distance function and units from the optional 

327 keyword arguments given at this instantiation, see 

328 property C{kwds}. 

329 

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

331 ''' 

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

333 

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

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

336 the distance function and units from the optional 

337 keyword arguments given at this instantiation, see 

338 property C{kwds}. 

339 

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

341 ''' 

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

343 

344 @property 

345 def _func_(self): 

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

347 return _formy._Propy(self, 0, _func_=None) 

348 

349 @_func_.setter # PYCHOK setter! 

350 def _func_(self, func): 

351 _formy._Propy(self, 3,_func_=func) 

352 

353 

354class HausdorffCosineAndoyerLambert(_HausdorffMeterRadians): 

355 '''Compute the C{Hausdorff} distance based on the I{angular} distance 

356 in C{radians} from function L{pygeodesy.cosineAndoyerLambert}. 

357 ''' 

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

359 '''New L{HausdorffCosineAndoyerLambert} calculator. 

360 

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

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

363 function L{pygeodesy.cosineAndoyerLambert}. 

364 

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

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

367 ''' 

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

369 self._func = _formy.cosineAndoyerLambert 

370 self._func_ = _formy.cosineAndoyerLambert_ 

371 

372 if _FOR_DOCS: 

373 directed = Hausdorff.directed 

374 symmetric = Hausdorff.symmetric 

375 

376 

377class HausdorffCosineForsytheAndoyerLambert(_HausdorffMeterRadians): 

378 '''Compute the C{Hausdorff} distance based on the I{angular} distance 

379 in C{radians} from function L{pygeodesy.cosineForsytheAndoyerLambert}. 

380 ''' 

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

382 '''New L{HausdorffCosineForsytheAndoyerLambert} calculator. 

383 

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

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

386 L{pygeodesy.cosineForsytheAndoyerLambert}. 

387 

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

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

390 ''' 

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

392 self._func = _formy.cosineForsytheAndoyerLambert 

393 self._func_ = _formy.cosineForsytheAndoyerLambert_ 

394 

395 if _FOR_DOCS: 

396 directed = Hausdorff.directed 

397 symmetric = Hausdorff.symmetric 

398 

399 

400class HausdorffCosineLaw(_HausdorffMeterRadians): 

401 '''Compute the C{Hausdorff} distance based on the I{angular} 

402 distance in C{radians} from function L{pygeodesy.cosineLaw_}. 

403 

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

405 ''' 

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

407 '''New L{HausdorffCosineLaw} calculator. 

408 

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

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

411 function L{pygeodesy.cosineLaw}. 

412 

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

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

415 ''' 

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

417 self._func = _formy.cosineLaw 

418 self._func_ = _formy.cosineLaw_ 

419 

420 if _FOR_DOCS: 

421 directed = Hausdorff.directed 

422 symmetric = Hausdorff.symmetric 

423 

424 

425class HausdorffDistanceTo(Hausdorff): 

426 '''Compute the C{Hausdorff} distance based on the distance from the 

427 points' C{LatLon.distanceTo} method, conventionally in C{meter}. 

428 ''' 

429 _units = _Str_meter 

430 

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

432 '''New L{HausdorffDistanceTo} calculator. 

433 

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

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

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

437 

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

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

440 

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

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

443 spherical C{LatLon} class. 

444 ''' 

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

446 

447 if _FOR_DOCS: 

448 directed = Hausdorff.directed 

449 symmetric = Hausdorff.symmetric 

450 

451 def distance(self, p1, p2): 

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

453 ''' 

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

455 

456 def _points2(self, points): 

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

458 ''' 

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

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

461 

462 

463class HausdorffEquirectangular(Hausdorff): 

464 '''Compute the C{Hausdorff} distance based on the C{equirectangular} distance 

465 in C{radians squared} like function L{pygeodesy.equirectangular_}. 

466 ''' 

467 _units = _Str_degrees2 

468 

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

470 '''New L{HausdorffEquirectangular} calculator. 

471 

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

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

474 L{pygeodesy.equirectangular_} I{with default} 

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

476 

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

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

479 ''' 

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

481 limit=0)) 

482 self._func = _formy._equirectangular # helper 

483 

484 if _FOR_DOCS: 

485 directed = Hausdorff.directed 

486 symmetric = Hausdorff.symmetric 

487 

488 

489class HausdorffEuclidean(_HausdorffMeterRadians): 

490 '''Compute the C{Hausdorff} distance based on the C{Euclidean} 

491 distance in C{radians} from function L{pygeodesy.euclidean_}. 

492 ''' 

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

494 '''New L{HausdorffEuclidean} calculator. 

495 

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

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

498 function L{pygeodesy.euclidean}. 

499 

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

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

502 ''' 

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

504 self._func = _formy.euclidean 

505 self._func_ = _formy.euclidean_ 

506 

507 if _FOR_DOCS: 

508 directed = Hausdorff.directed 

509 symmetric = Hausdorff.symmetric 

510 

511 

512class HausdorffExact(Hausdorff): 

513 '''Compute the C{Hausdorff} distance based on the I{angular} 

514 distance in C{degrees} from method L{GeodesicExact}C{.Inverse}. 

515 ''' 

516 _units = _Str_degrees 

517 

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

519 '''New L{HausdorffKarney} calculator. 

520 

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

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

523 or L{a_f2Tuple}). 

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

525 keyword argument for method C{Inverse1} of class 

526 L{geodesicx.GeodesicExact}. 

527 

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

529 

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

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

532 ''' 

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

534 self._datum_setter(datum) 

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

536 

537 if _FOR_DOCS: 

538 directed = Hausdorff.directed 

539 symmetric = Hausdorff.symmetric 

540 

541 

542class HausdorffFlatLocal(_HausdorffMeterRadians): 

543 '''Compute the C{Hausdorff} distance based on the I{angular} distance in 

544 C{radians squared} like function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny_}. 

545 ''' 

546 _units = _Str_radians2 

547 

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

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

550 

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

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

553 L{pygeodesy.flatLocal}. 

554 

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

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

557 

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

559 ''' 

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

561 self._func = _formy.flatLocal 

562 self._func_ = self.datum.ellipsoid._hubeny_2 

563 

564 if _FOR_DOCS: 

565 directed = Hausdorff.directed 

566 symmetric = Hausdorff.symmetric 

567 

568 

569class HausdorffFlatPolar(_HausdorffMeterRadians): 

570 '''Compute the C{Hausdorff} distance based on the I{angular} 

571 distance in C{radians} from function L{pygeodesy.flatPolar_}. 

572 ''' 

573 _wrap = False 

574 

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

576 '''New L{HausdorffFlatPolar} calculator. 

577 

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

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

580 for function L{pygeodesy.flatPolar}. 

581 

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

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

584 ''' 

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

586 self._func = _formy.flatPolar 

587 self._func_ = _formy.flatPolar_ 

588 

589 if _FOR_DOCS: 

590 directed = Hausdorff.directed 

591 symmetric = Hausdorff.symmetric 

592 

593 

594class HausdorffHaversine(_HausdorffMeterRadians): 

595 '''Compute the C{Hausdorff} distance based on the I{angular} 

596 distance in C{radians} from function L{pygeodesy.haversine_}. 

597 

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

599 ''' 

600 _wrap = False 

601 

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

603 '''New L{HausdorffHaversine} calculator. 

604 

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

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

607 for function L{pygeodesy.haversine}. 

608 

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

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

611 ''' 

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

613 self._func = _formy.haversine 

614 self._func_ = _formy.haversine_ 

615 

616 if _FOR_DOCS: 

617 directed = Hausdorff.directed 

618 symmetric = Hausdorff.symmetric 

619 

620 

621class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny 

622 if _FOR_DOCS: 

623 __doc__ = HausdorffFlatLocal.__doc__ 

624 __init__ = HausdorffFlatLocal.__init__ 

625 directed = HausdorffFlatLocal.directed 

626 distance = HausdorffFlatLocal.distance 

627 symmetric = HausdorffFlatLocal.symmetric 

628 

629 

630class HausdorffKarney(Hausdorff): 

631 '''Compute the C{Hausdorff} distance based on the I{angular} 

632 distance in C{degrees} from I{Karney}'s U{geographiclib 

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

634 <https://GeographicLib.SourceForge.io/Python/doc/code.html>} 

635 Inverse method. 

636 ''' 

637 _units = _Str_degrees 

638 

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

640 '''New L{HausdorffKarney} calculator. 

641 

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

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

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

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

646 and keyword arguments for method C{Inverse1} of 

647 class L{geodesicw.Geodesic}. 

648 

649 @raise ImportError: Package U{geographiclib 

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

651 

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

653 

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

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

656 ''' 

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

658 self._datum_setter(datum) 

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

660 

661 

662class HausdorffThomas(_HausdorffMeterRadians): 

663 '''Compute the C{Hausdorff} distance based on the I{angular} 

664 distance in C{radians} from function L{pygeodesy.thomas_}. 

665 ''' 

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

667 '''New L{HausdorffThomas} calculator. 

668 

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

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

671 for function L{pygeodesy.thomas}. 

672 

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

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

675 ''' 

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

677 self._func = _formy.thomas 

678 self._func_ = _formy.thomas_ 

679 

680 if _FOR_DOCS: 

681 directed = Hausdorff.directed 

682 symmetric = Hausdorff.symmetric 

683 

684 

685class HausdorffVincentys(_HausdorffMeterRadians): 

686 '''Compute the C{Hausdorff} distance based on the I{angular} 

687 distance in C{radians} from function L{pygeodesy.vincentys_}. 

688 

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

690 ''' 

691 _wrap = False 

692 

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

694 '''New L{HausdorffVincentys} calculator. 

695 

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

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

698 for function L{pygeodesy.vincentys}. 

699 

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

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

702 ''' 

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

704 self._func = _formy.vincentys 

705 self._func_ = _formy.vincentys_ 

706 

707 if _FOR_DOCS: 

708 directed = Hausdorff.directed 

709 symmetric = Hausdorff.symmetric 

710 

711 

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

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

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

715 ''' 

716 # shuffling the points generally increases the 

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

718 rr = randomrangenerator(seed) if seed else range 

719 

720 hd = NINF 

721 hi = hj = m = mn = 0 

722 md = _0_0 

723 

724 # forward or forward and backward 

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

726 n = len(ps2) 

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

728 p1 = point(ps1[i]) 

729 dh, dj = INF, 0 

730 for j in rr(n): 

731 p2 = point(ps2[j]) 

732 d = distance(p1, p2) 

733 if early and d < hd: 

734 break # early 

735 elif d < dh: 

736 dh, dj = d, j 

737 else: # no early break 

738 if hd < dh: 

739 hd = dh 

740 if fb: 

741 hi, hj = dj, i 

742 else: 

743 hi, hj = i, dj 

744 md += dh 

745 mn += 1 

746 m += 1 

747 # swap model and target 

748 ps1, ps2 = ps2, ps1 

749 

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

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

752 

753 

754def _point(p): 

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

756 

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

758 

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

760 B{C{distance}} callable. 

761 ''' 

762 return p 

763 

764 

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

766 distance=None, point=_point): 

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

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

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

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

771 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

787 

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

789 

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

791 

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

793 ''' 

794 _xcallable(distance=distance, point=point) 

795 

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

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

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

799 

800 

801class Hausdorff6Tuple(_NamedTuple): 

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

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

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

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

806 C{units}. 

807 

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

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

810 count C{mn} twice that. 

811 

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

813 point with the C{hd} distance. 

814 

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

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

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

818 ''' 

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

820 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass) 

821 

822 def toUnits(self, **Error): # PYCHOK expected 

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

824 ''' 

825 U = _xUnit(self.units, Float) # PYCHOK expected 

826 M = _Pass if self.md is None else U # PYCHOK expected 

827 self._Units_ = (U,) + Hausdorff6Tuple._Units_[1:4] \ 

828 + (M,) + Hausdorff6Tuple._Units_[5:] 

829 return _NamedTuple.toUnits(self, **Error) 

830 

831 

832def randomrangenerator(seed): 

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

834 

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

836 or C{None}). 

837 

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

839 current time or from a platform-specific randomness 

840 source, if available. 

841 

842 @return: A function to generate random ranges. 

843 

844 @example: 

845 

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

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

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

849 ''' 

850 R = Random(seed) 

851 

852 def _range(n, *stop_step): 

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

854 except the returned values are in random order. 

855 

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

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

858 creating a tuple with the entire C{population} 

859 and a list containing all sample values (for 

860 large C{n}). 

861 ''' 

862 if stop_step: 

863 s = range(n, *stop_step) 

864 

865 elif n > 32: 

866 r = R.randrange # Random._randbelow 

867 s = set() 

868 for _ in range(n - 32): 

869 i = r(n) 

870 while i in s: 

871 i = r(n) 

872 s.add(i) 

873 yield i 

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

875 else: 

876 s = range(n) 

877 

878 s = list(s) 

879 R.shuffle(s) 

880 while s: 

881 yield s.pop(0) 

882 

883 return _range 

884 

885# **) MIT License 

886# 

887# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

888# 

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

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

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

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

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

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

895# 

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

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

898# 

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

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

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

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

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

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

905# OTHER DEALINGS IN THE SOFTWARE.