Coverage for pygeodesy/hausdorff.py: 95%

243 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-11-12 16:17 -0500

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 as _points # 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, 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__ = '24.06.15' 

89 

90 

91class HausdorffError(PointsError): 

92 '''Hausdorff issue. 

93 ''' 

94 pass 

95 

96 

97class Hausdorff(_Named): 

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

99 be overloaded. 

100 ''' 

101 _datum = _WGS84 

102 _func = None # formy function/property 

103 _kwds = {} # func_ options 

104 _model = () 

105 _seed = None 

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

107 

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

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

110 

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

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

113 C{other}[]). 

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

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

116 PubDat_247739.pdf>}. 

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

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

119 and keyword arguments for the distance function, retrievable 

120 with property C{kwds}. 

121 

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

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

124 ''' 

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

126 if name: 

127 self.name = name 

128 

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

130 if seed: 

131 self.seed = seed 

132 if units: # and not self.units: 

133 self.units = units 

134 if kwds: 

135 self._kwds = kwds 

136 

137 @Property_RO 

138 def adjust(self): 

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

140 ''' 

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

142 

143 @Property_RO 

144 def datum(self): 

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

146 ''' 

147 return self._datum 

148 

149 def _datum_setter(self, datum): 

150 '''(INTERNAL) Set the datum. 

151 ''' 

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

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

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

155 

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

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

158 

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

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

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

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

163 

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

165 

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

167 an invalid B{C{point2}}. 

168 

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

170 ''' 

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

172 

173 def distance(self, point1, point2): 

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

175 subject to the supplied optional keyword arguments, see 

176 property C{kwds}. 

177 ''' 

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

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

180 

181 @Property 

182 def _func(self): 

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

184 self._notOverloaded(**self.kwds) 

185 

186 @_func.setter_ # PYCHOK setter_underscore! 

187 def _func(self, func): 

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

189 

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

191 _, ps2 = self._points2(point2s) 

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

193 self.units, distance, self.point) 

194 

195 @property_RO 

196 def kwds(self): 

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

198 ''' 

199 return self._kwds 

200 

201 def point(self, point): 

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

203 ''' 

204 return point # pass thru 

205 

206 def _points2(self, points): 

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

208 ''' 

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

210 

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

212 def seed(self): 

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

214 ''' 

215 return self._seed 

216 

217 @seed.setter # PYCHOK setter! 

218 def seed(self, seed): 

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

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

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

222 

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

224 ''' 

225 if seed: 

226 try: 

227 Random(seed) 

228 except (TypeError, ValueError) as x: 

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

230 self._seed = seed 

231 else: 

232 self._seed = None 

233 

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

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

236 

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

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

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

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

241 

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

243 

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

245 an invalid B{C{point2}}. 

246 

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

248 ''' 

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

250 

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

252 def units(self): 

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

254 ''' 

255 return self._units 

256 

257 @units.setter # PYCHOK setter! 

258 def units(self, units): 

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

260 

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

262 ''' 

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

264 

265 @Property_RO 

266 def wrap(self): 

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

268 ''' 

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

270 

271 

272class HausdorffDegrees(Hausdorff): 

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

274 points in C{degrees}. 

275 ''' 

276 _units = _unitsBase._Str_degrees 

277 

278 if _FOR_DOCS: 

279 __init__ = Hausdorff.__init__ 

280 directed = Hausdorff.directed 

281 symmetric = Hausdorff.symmetric 

282 

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

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

285 self._notOverloaded(point1, point2) 

286 

287 

288class HausdorffRadians(Hausdorff): 

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

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

291 ''' 

292 _units = _unitsBase._Str_radians 

293 

294 if _FOR_DOCS: 

295 __init__ = Hausdorff.__init__ 

296 directed = Hausdorff.directed 

297 symmetric = Hausdorff.symmetric 

298 

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

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

301 self._notOverloaded(point1, point2) 

302 

303 def point(self, point): 

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

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

306 

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

308 ''' 

309 try: 

310 return point.philam 

311 except AttributeError: 

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

313 

314 

315class _HausdorffMeterRadians(Hausdorff): 

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

317 the optional keyword arguments supplied at instantiation 

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

319 ''' 

320 _units = _unitsBase._Str_meter 

321 _units_ = _unitsBase._Str_radians 

322 

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

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

325 the distance function and units from the optional 

326 keyword arguments given at this instantiation, see 

327 property C{kwds}. 

328 

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

330 ''' 

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

332 

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

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

335 the distance function and units from the optional 

336 keyword arguments given at this instantiation, see 

337 property C{kwds}. 

338 

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

340 ''' 

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

342 

343 @Property 

344 def _func_(self): 

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

346 self._notOverloaded(**self.kwds) 

347 

348 @_func_.setter_ # PYCHOK setter_underscore! 

349 def _func_(self, func): 

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

351 

352 

353class HausdorffCosineAndoyerLambert(_HausdorffMeterRadians): 

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

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

356 ''' 

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

358 '''New L{HausdorffCosineAndoyerLambert} calculator. 

359 

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

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

362 function L{pygeodesy.cosineAndoyerLambert}. 

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

368 self._func = _formy.cosineAndoyerLambert 

369 self._func_ = _formy.cosineAndoyerLambert_ 

370 

371 if _FOR_DOCS: 

372 directed = Hausdorff.directed 

373 symmetric = Hausdorff.symmetric 

374 

375 

376class HausdorffCosineForsytheAndoyerLambert(_HausdorffMeterRadians): 

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

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

379 ''' 

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

381 '''New L{HausdorffCosineForsytheAndoyerLambert} calculator. 

382 

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

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

385 L{pygeodesy.cosineForsytheAndoyerLambert}. 

386 

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

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

389 ''' 

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

391 self._func = _formy.cosineForsytheAndoyerLambert 

392 self._func_ = _formy.cosineForsytheAndoyerLambert_ 

393 

394 if _FOR_DOCS: 

395 directed = Hausdorff.directed 

396 symmetric = Hausdorff.symmetric 

397 

398 

399class HausdorffCosineLaw(_HausdorffMeterRadians): 

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

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

402 

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

404 ''' 

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

406 '''New L{HausdorffCosineLaw} calculator. 

407 

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

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

410 function L{pygeodesy.cosineLaw}. 

411 

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

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

414 ''' 

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

416 self._func = _formy.cosineLaw 

417 self._func_ = _formy.cosineLaw_ 

418 

419 if _FOR_DOCS: 

420 directed = Hausdorff.directed 

421 symmetric = Hausdorff.symmetric 

422 

423 

424class HausdorffDistanceTo(Hausdorff): 

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

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

427 ''' 

428 _units = _unitsBase._Str_meter 

429 

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

431 '''New L{HausdorffDistanceTo} calculator. 

432 

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

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

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

436 

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

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

439 

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

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

442 spherical C{LatLon} class. 

443 ''' 

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

445 

446 if _FOR_DOCS: 

447 directed = Hausdorff.directed 

448 symmetric = Hausdorff.symmetric 

449 

450 def distance(self, p1, p2): 

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

452 ''' 

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

454 

455 def _points2(self, points): 

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

457 ''' 

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

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

460 

461 

462class HausdorffEquirectangular(Hausdorff): 

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

464 in C{radians squared} like function L{pygeodesy.equirectangular}. 

465 ''' 

466 _units = _unitsBase._Str_degrees2 

467 

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

469 '''New L{HausdorffEquirectangular} calculator. 

470 

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

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

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

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

475 

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

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

478 ''' 

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

480 limit=0)) 

481 self._func = _formy._equirectangular # helper 

482 

483 if _FOR_DOCS: 

484 directed = Hausdorff.directed 

485 symmetric = Hausdorff.symmetric 

486 

487 

488class HausdorffEuclidean(_HausdorffMeterRadians): 

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

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

491 ''' 

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

493 '''New L{HausdorffEuclidean} calculator. 

494 

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

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

497 function L{pygeodesy.euclidean}. 

498 

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

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

501 ''' 

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

503 self._func = _formy.euclidean 

504 self._func_ = _formy.euclidean_ 

505 

506 if _FOR_DOCS: 

507 directed = Hausdorff.directed 

508 symmetric = Hausdorff.symmetric 

509 

510 

511class HausdorffExact(Hausdorff): 

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

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

514 ''' 

515 _units = _unitsBase._Str_degrees 

516 

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

518 '''New L{HausdorffKarney} calculator. 

519 

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

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

522 or L{a_f2Tuple}). 

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

524 keyword argument for method C{Inverse1} of class 

525 L{geodesicx.GeodesicExact}. 

526 

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

528 

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

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

531 ''' 

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

533 self._datum_setter(datum) 

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

535 

536 if _FOR_DOCS: 

537 directed = Hausdorff.directed 

538 symmetric = Hausdorff.symmetric 

539 

540 

541class HausdorffFlatLocal(_HausdorffMeterRadians): 

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

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

544 ''' 

545 _units = _unitsBase._Str_radians2 

546 

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

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

549 

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

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

552 L{pygeodesy.flatLocal}. 

553 

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

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

556 

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

558 ''' 

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

560 self._func = _formy.flatLocal 

561 self._func_ = self.datum.ellipsoid._hubeny_2 

562 

563 if _FOR_DOCS: 

564 directed = Hausdorff.directed 

565 symmetric = Hausdorff.symmetric 

566 

567 

568class HausdorffFlatPolar(_HausdorffMeterRadians): 

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

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

571 ''' 

572 _wrap = False 

573 

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

575 '''New L{HausdorffFlatPolar} calculator. 

576 

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

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

579 for function L{pygeodesy.flatPolar}. 

580 

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

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

583 ''' 

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

585 self._func = _formy.flatPolar 

586 self._func_ = _formy.flatPolar_ 

587 

588 if _FOR_DOCS: 

589 directed = Hausdorff.directed 

590 symmetric = Hausdorff.symmetric 

591 

592 

593class HausdorffHaversine(_HausdorffMeterRadians): 

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

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

596 

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

598 ''' 

599 _wrap = False 

600 

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

602 '''New L{HausdorffHaversine} calculator. 

603 

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

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

606 for function L{pygeodesy.haversine}. 

607 

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

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

610 ''' 

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

612 self._func = _formy.haversine 

613 self._func_ = _formy.haversine_ 

614 

615 if _FOR_DOCS: 

616 directed = Hausdorff.directed 

617 symmetric = Hausdorff.symmetric 

618 

619 

620class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny 

621 if _FOR_DOCS: 

622 __doc__ = HausdorffFlatLocal.__doc__ 

623 __init__ = HausdorffFlatLocal.__init__ 

624 directed = HausdorffFlatLocal.directed 

625 distance = HausdorffFlatLocal.distance 

626 symmetric = HausdorffFlatLocal.symmetric 

627 

628 

629class HausdorffKarney(Hausdorff): 

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

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

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

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

634 Inverse method. 

635 ''' 

636 _units = _unitsBase._Str_degrees 

637 

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

639 '''New L{HausdorffKarney} calculator. 

640 

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

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

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

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

645 and keyword arguments for method C{Inverse1} of 

646 class L{geodesicw.Geodesic}. 

647 

648 @raise ImportError: Package U{geographiclib 

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

650 

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

652 

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

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

655 ''' 

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

657 self._datum_setter(datum) 

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

659 

660 

661class HausdorffThomas(_HausdorffMeterRadians): 

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

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

664 ''' 

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

666 '''New L{HausdorffThomas} calculator. 

667 

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

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

670 for function L{pygeodesy.thomas}. 

671 

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

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

674 ''' 

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

676 self._func = _formy.thomas 

677 self._func_ = _formy.thomas_ 

678 

679 if _FOR_DOCS: 

680 directed = Hausdorff.directed 

681 symmetric = Hausdorff.symmetric 

682 

683 

684class HausdorffVincentys(_HausdorffMeterRadians): 

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

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

687 

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

689 ''' 

690 _wrap = False 

691 

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

693 '''New L{HausdorffVincentys} calculator. 

694 

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

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

697 for function L{pygeodesy.vincentys}. 

698 

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

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

701 ''' 

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

703 self._func = _formy.vincentys 

704 self._func_ = _formy.vincentys_ 

705 

706 if _FOR_DOCS: 

707 directed = Hausdorff.directed 

708 symmetric = Hausdorff.symmetric 

709 

710 

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

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

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

714 ''' 

715 # shuffling the points generally increases the 

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

717 rr = randomrangenerator(seed) if seed else range 

718 

719 hd = NINF 

720 hi = hj = m = mn = 0 

721 md = _0_0 

722 

723 # forward or forward and backward 

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

725 n = len(ps2) 

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

727 p1 = point(ps1[i]) 

728 dh, dj = INF, 0 

729 for j in rr(n): 

730 p2 = point(ps2[j]) 

731 d = distance(p1, p2) 

732 if early and d < hd: 

733 break # early 

734 elif d < dh: 

735 dh, dj = d, j 

736 else: # no early break 

737 if hd < dh: 

738 hd = dh 

739 if fb: 

740 hi, hj = dj, i 

741 else: 

742 hi, hj = i, dj 

743 md += dh 

744 mn += 1 

745 m += 1 

746 # swap model and target 

747 ps1, ps2 = ps2, ps1 

748 

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

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

751 

752 

753def _point(p): 

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

755 

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

757 

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

759 B{C{distance}} callable. 

760 ''' 

761 return p 

762 

763 

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

765 distance=None, point=_point): 

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

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

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

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

770 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

786 

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

788 

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

790 

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

792 ''' 

793 _xcallable(distance=distance, point=point) 

794 

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

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

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

798 

799 

800class Hausdorff6Tuple(_NamedTuple): 

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

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

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

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

805 C{units}. 

806 

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

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

809 count C{mn} twice that. 

810 

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

812 point with the C{hd} distance. 

813 

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

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

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

817 ''' 

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

819 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass) 

820 

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

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

823 ''' 

824 u = list(Hausdorff6Tuple._Units_) 

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

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

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

828 

829 

830def randomrangenerator(seed): 

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

832 

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

834 or C{None}). 

835 

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

837 current time or from a platform-specific randomness 

838 source, if available. 

839 

840 @return: A function to generate random ranges. 

841 

842 @example: 

843 

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

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

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

847 ''' 

848 R = Random(seed) 

849 

850 def _range(n, *stop_step): 

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

852 except the returned values are in random order. 

853 

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

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

856 creating a tuple with the entire C{population} 

857 and a list containing all sample values (for 

858 large C{n}). 

859 ''' 

860 if stop_step: 

861 s = range(n, *stop_step) 

862 

863 elif n > 32: 

864 r = R.randrange # Random._randbelow 

865 s = set() 

866 for _ in range(n - 32): 

867 i = r(n) 

868 while i in s: 

869 i = r(n) 

870 s.add(i) 

871 yield i 

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

873 else: 

874 s = range(n) 

875 

876 s = list(s) 

877 R.shuffle(s) 

878 while s: 

879 yield s.pop(0) 

880 

881 return _range 

882 

883# **) MIT License 

884# 

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

886# 

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

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

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

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

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

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

893# 

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

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

896# 

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

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

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

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

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

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

903# OTHER DEALINGS IN THE SOFTWARE.