Coverage for pygeodesy/hausdorff.py: 96%

233 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-10-04 12:08 -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 _IsnotError, PointsError, _xattr, _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 _Named, _NamedTuple, notOverloaded, _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__ = '23.09.22' 

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 

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, name=NN, units=NN, **kwds): 

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

111 

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

113 C{template} (C{LatLon}[], C{Numpy2LatLon}[], 

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

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

116 or C{False} for no U{random sampling<https:// 

117 Publik.TUWien.ac.AT/files/PubDat_247739.pdf>}. 

118 @kwarg name: Optional name for this interpolator (C{str}). 

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

120 @kwarg kwds: Optional keyword argument for distance function, 

121 retrievable with property C{kwds}. 

122 

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

124 or an invalid B{C{point1}}, B{C{seed}} 

125 or B{C{units}}. 

126 ''' 

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

128 if seed: 

129 self.seed = seed 

130 if name: 

131 self.name = name 

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 def _hausdorff_(self, point2s, both, early, distance): 

182 _, ps2 = self._points2(point2s) 

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

184 self.units, distance, self.point) 

185 

186 @property_RO 

187 def kwds(self): 

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

189 ''' 

190 return self._kwds 

191 

192 def point(self, point): 

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

194 ''' 

195 return point # pass thru 

196 

197 def _points2(self, points): 

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

199 ''' 

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

201 

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

203 def seed(self): 

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

205 ''' 

206 return self._seed 

207 

208 @seed.setter # PYCHOK setter! 

209 def seed(self, seed): 

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

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

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

213 

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

215 ''' 

216 if seed: 

217 try: 

218 Random(seed) 

219 except (TypeError, ValueError) as x: 

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

221 self._seed = seed 

222 else: 

223 self._seed = None 

224 

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

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

227 

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

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

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

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

232 

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

234 

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

236 an invalid B{C{point2}}. 

237 

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

239 ''' 

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

241 

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

243 def units(self): 

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

245 ''' 

246 return self._units 

247 

248 @units.setter # PYCHOK setter! 

249 def units(self, units): 

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

251 

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

253 ''' 

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

255 

256 @Property_RO 

257 def wrap(self): 

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

259 ''' 

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

261 

262 

263class HausdorffDegrees(Hausdorff): 

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

265 points in C{degrees}. 

266 ''' 

267 _units = _Str_degrees 

268 

269 if _FOR_DOCS: 

270 __init__ = Hausdorff.__init__ 

271 directed = Hausdorff.directed 

272 symmetric = Hausdorff.symmetric 

273 

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

275 '''Return the distance in C{degrees} between B{C{point1}} and B{C{point2}}. 

276 I{Must be overloaded}.''' 

277 notOverloaded(self, point1, point2) 

278 

279 

280class HausdorffRadians(Hausdorff): 

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

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

283 ''' 

284 _units = _Str_radians 

285 

286 if _FOR_DOCS: 

287 __init__ = Hausdorff.__init__ 

288 directed = Hausdorff.directed 

289 symmetric = Hausdorff.symmetric 

290 

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

292 '''Return the distance in C{radians} between B{C{point1}} and B{C{point2}}. 

293 I{Must be overloaded}.''' 

294 notOverloaded(self, point1, point2) 

295 

296 def point(self, point): 

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

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

299 

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

301 ''' 

302 try: 

303 return point.philam 

304 except AttributeError: 

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

306 

307 

308class _HausdorffMeterRadians(Hausdorff): 

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

310 the optional keyword arguments supplied at instantiation 

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

312 ''' 

313 _units = _Str_meter 

314 _units_ = _Str_radians 

315 

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

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

318 the distance function and units from the optional 

319 keyword arguments given at this instantiation, see 

320 property C{kwds}. 

321 

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

323 ''' 

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

325 

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

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

328 the distance function and units from the optional 

329 keyword arguments given at this instantiation, see 

330 property C{kwds}. 

331 

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

333 ''' 

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

335 

336 def _func_(self, *args, **kwds): # PYCHOK no cover 

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

338 notOverloaded(self, *args, **kwds) 

339 

340 

341class HausdorffCosineAndoyerLambert(_HausdorffMeterRadians): 

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

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

344 ''' 

345 def __init__(self, point1s, seed=None, name=NN, **datum_wrap): 

346 '''New L{HausdorffCosineAndoyerLambert} calculator. 

347 

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

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

350 

351 @kwarg datum_wrap: Optional keyword arguments for function 

352 L{pygeodesy.cosineAndoyerLambert}. 

353 ''' 

354 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

355 **datum_wrap) 

356 self._func = _formy.cosineAndoyerLambert 

357 self._func_ = _formy.cosineAndoyerLambert_ 

358 

359 if _FOR_DOCS: 

360 directed = Hausdorff.directed 

361 symmetric = Hausdorff.symmetric 

362 

363 

364class HausdorffCosineForsytheAndoyerLambert(_HausdorffMeterRadians): 

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

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

367 ''' 

368 def __init__(self, point1s, seed=None, name=NN, **datum_wrap): 

369 '''New L{HausdorffCosineForsytheAndoyerLambert} calculator. 

370 

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

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

373 

374 @kwarg datum_wrap: Optional keyword arguments for function 

375 L{pygeodesy.cosineAndoyerLambert}. 

376 ''' 

377 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

378 **datum_wrap) 

379 self._func = _formy.cosineForsytheAndoyerLambert 

380 self._func_ = _formy.cosineForsytheAndoyerLambert_ 

381 

382 if _FOR_DOCS: 

383 directed = Hausdorff.directed 

384 symmetric = Hausdorff.symmetric 

385 

386 

387class HausdorffCosineLaw(_HausdorffMeterRadians): 

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

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

390 

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

392 ''' 

393 def __init__(self, point1s, seed=None, name=NN, **radius_wrap): 

394 '''New L{HausdorffCosineLaw} calculator. 

395 

396 @kwarg radius_wrap: Optional keyword arguments for function 

397 L{pygeodesy.cosineLaw}. 

398 

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

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

401 ''' 

402 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

403 **radius_wrap) 

404 self._func = _formy.cosineLaw 

405 self._func_ = _formy.cosineLaw_ 

406 

407 if _FOR_DOCS: 

408 directed = Hausdorff.directed 

409 symmetric = Hausdorff.symmetric 

410 

411 

412class HausdorffDistanceTo(Hausdorff): 

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

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

415 ''' 

416 _units = _Str_meter 

417 

418 def __init__(self, point1s, seed=None, name=NN, **distanceTo_kwds): 

419 '''New L{HausdorffDistanceTo} calculator. 

420 

421 @kwarg distanceTo_kwds: Optional keyword arguments for each 

422 B{C{point1s}}' C{LatLon.distanceTo} 

423 method. 

424 

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

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

427 

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

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

430 spherical C{LatLon} class. 

431 ''' 

432 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

433 **distanceTo_kwds) 

434 

435 if _FOR_DOCS: 

436 directed = Hausdorff.directed 

437 symmetric = Hausdorff.symmetric 

438 

439 def distance(self, p1, p2): 

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

441 ''' 

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

443 

444 def _points2(self, points): 

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

446 ''' 

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

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

449 

450 

451class HausdorffEquirectangular(Hausdorff): 

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

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

454 ''' 

455 _units = _Str_degrees2 

456 

457 def __init__(self, point1s, seed=None, name=NN, **adjust_limit_wrap): 

458 '''New L{HausdorffEquirectangular} calculator. 

459 

460 @kwarg adjust_limit_wrap: Optional keyword arguments for function 

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

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

463 

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

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

466 ''' 

467 adjust_limit_wrap = _xkwds(adjust_limit_wrap, limit=0) 

468 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

469 **adjust_limit_wrap) 

470 self._func = _formy._equirectangular # helper 

471 

472 if _FOR_DOCS: 

473 directed = Hausdorff.directed 

474 symmetric = Hausdorff.symmetric 

475 

476 

477class HausdorffEuclidean(_HausdorffMeterRadians): 

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

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

480 ''' 

481 def __init__(self, point1s, seed=None, name=NN, **adjust_wrap): 

482 '''New L{HausdorffEuclidean} calculator. 

483 

484 @kwarg adjust_radius_wrap: Optional keyword arguments for 

485 function L{pygeodesy.euclidean}. 

486 

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

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

489 ''' 

490 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

491 **adjust_wrap) 

492 self._func = _formy.euclidean 

493 self._func_ = _formy.euclidean_ 

494 

495 if _FOR_DOCS: 

496 directed = Hausdorff.directed 

497 symmetric = Hausdorff.symmetric 

498 

499 

500class HausdorffExact(Hausdorff): 

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

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

503 ''' 

504 _units = _Str_degrees 

505 

506 def __init__(self, point1s, seed=None, name=NN, datum=None, **wrap): 

507 '''New L{HausdorffKarney} calculator. 

508 

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

510 first B{C{point1s}}' datum (L{Datum}, L{Ellipsoid}, 

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

512 @kwarg wrap: Optional keyword argument for method C{Inverse1} 

513 of class L{geodesicx.GeodesicExact}. 

514 

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

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

517 

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

519 ''' 

520 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

521 **wrap) 

522 self._datum_setter(datum) 

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

524 

525 if _FOR_DOCS: 

526 directed = Hausdorff.directed 

527 symmetric = Hausdorff.symmetric 

528 

529 

530class HausdorffFlatLocal(_HausdorffMeterRadians): 

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

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

533 ''' 

534 _units = _Str_radians2 

535 

536 def __init__(self, point1s, seed=None, name=NN, **datum_scaled_wrap): 

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

538 

539 @kwarg datum_scaled_wrap: Optional keyword arguments for 

540 function L{pygeodesy.flatLocal}. 

541 

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

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

544 

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

546 ''' 

547 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

548 **datum_scaled_wrap) 

549 self._func = _formy.flatLocal 

550 self._func_ = self.datum.ellipsoid._hubeny_2 

551 

552 if _FOR_DOCS: 

553 directed = Hausdorff.directed 

554 symmetric = Hausdorff.symmetric 

555 

556 

557class HausdorffFlatPolar(_HausdorffMeterRadians): 

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

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

560 ''' 

561 _wrap = False 

562 

563 def __init__(self, points, seed=None, name=NN, **radius_wrap): 

564 '''New L{HausdorffFlatPolar} calculator. 

565 

566 @kwarg radius_wrap: Optional keyword arguments for function 

567 L{pygeodesy.flatPolar}. 

568 

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

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

571 ''' 

572 Hausdorff.__init__(self, points, seed=seed, name=name, 

573 **radius_wrap) 

574 self._func = _formy.flatPolar 

575 self._func_ = _formy.flatPolar_ 

576 

577 if _FOR_DOCS: 

578 directed = Hausdorff.directed 

579 symmetric = Hausdorff.symmetric 

580 

581 

582class HausdorffHaversine(_HausdorffMeterRadians): 

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

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

585 

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

587 ''' 

588 _wrap = False 

589 

590 def __init__(self, points, seed=None, name=NN, **radius_wrap): 

591 '''New L{HausdorffHaversine} calculator. 

592 

593 @kwarg radius_wrap: Optional keyword arguments for function 

594 L{pygeodesy.haversine}. 

595 

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

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

598 ''' 

599 Hausdorff.__init__(self, points, seed=seed, name=name, 

600 **radius_wrap) 

601 self._func = _formy.haversine 

602 self._func_ = _formy.haversine_ 

603 

604 if _FOR_DOCS: 

605 directed = Hausdorff.directed 

606 symmetric = Hausdorff.symmetric 

607 

608 

609class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny 

610 if _FOR_DOCS: 

611 __doc__ = HausdorffFlatLocal.__doc__ 

612 __init__ = HausdorffFlatLocal.__init__ 

613 directed = HausdorffFlatLocal.directed 

614 distance = HausdorffFlatLocal.distance 

615 symmetric = HausdorffFlatLocal.symmetric 

616 

617 

618class HausdorffKarney(Hausdorff): 

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

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

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

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

623 Inverse method. 

624 ''' 

625 _units = _Str_degrees 

626 

627 def __init__(self, point1s, datum=None, seed=None, name=NN, **wrap): 

628 '''New L{HausdorffKarney} calculator. 

629 

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

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

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

633 @kwarg wrap: Optional keyword argument for method C{Inverse1} 

634 of class L{geodesicw.Geodesic}. 

635 

636 @raise ImportError: Package U{geographiclib 

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

638 

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

640 

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

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

643 ''' 

644 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

645 **wrap) 

646 self._datum_setter(datum) 

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

648 

649 

650class HausdorffThomas(_HausdorffMeterRadians): 

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

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

653 ''' 

654 def __init__(self, point1s, seed=None, name=NN, **datum_wrap): 

655 '''New L{HausdorffThomas} calculator. 

656 

657 @kwarg datum_wrap: Optional keyword argument for function 

658 L{pygeodesy.thomas}. 

659 

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

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

662 ''' 

663 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

664 **datum_wrap) 

665 self._func = _formy.thomas 

666 self._func_ = _formy.thomas_ 

667 

668 if _FOR_DOCS: 

669 directed = Hausdorff.directed 

670 symmetric = Hausdorff.symmetric 

671 

672 

673class HausdorffVincentys(_HausdorffMeterRadians): 

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

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

676 

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

678 ''' 

679 _wrap = False 

680 

681 def __init__(self, point1s, seed=None, name=NN, **radius_wrap): 

682 '''New L{HausdorffVincentys} calculator. 

683 

684 @kwarg radius_wrap: Optional keyword arguments for function 

685 L{pygeodesy.vincentys}. 

686 

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

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

689 ''' 

690 Hausdorff.__init__(self, point1s, seed=seed, name=name, 

691 **radius_wrap) 

692 self._func = _formy.vincentys 

693 self._func_ = _formy.vincentys_ 

694 

695 if _FOR_DOCS: 

696 directed = Hausdorff.directed 

697 symmetric = Hausdorff.symmetric 

698 

699 

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

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

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

703 ''' 

704 # shuffling the points generally increases the 

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

706 rr = randomrangenerator(seed) if seed else range 

707 

708 hd = NINF 

709 hi = hj = m = mn = 0 

710 md = _0_0 

711 

712 # forward or forward and backward 

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

714 n = len(ps2) 

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

716 p1 = point(ps1[i]) 

717 dh, dj = INF, 0 

718 for j in rr(n): 

719 p2 = point(ps2[j]) 

720 d = distance(p1, p2) 

721 if early and d < hd: 

722 break # early 

723 elif d < dh: 

724 dh, dj = d, j 

725 else: # no early break 

726 if hd < dh: 

727 hd = dh 

728 if fb: 

729 hi, hj = dj, i 

730 else: 

731 hi, hj = i, dj 

732 md += dh 

733 mn += 1 

734 m += 1 

735 # swap model and target 

736 ps1, ps2 = ps2, ps1 

737 

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

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

740 

741 

742def _point(p): 

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

744 

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

746 

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

748 B{C{distance}} callable. 

749 ''' 

750 return p 

751 

752 

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

754 distance=None, point=_point): 

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

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

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

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

759 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

775 

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

777 

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

779 

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

781 ''' 

782 if not callable(distance): 

783 raise _IsnotError(callable.__name__, distance=distance) 

784 if not callable(point): 

785 raise _IsnotError(callable.__name__, point=point) 

786 

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

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

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

790 

791 

792class Hausdorff6Tuple(_NamedTuple): 

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

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

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

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

797 C{units}. 

798 

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

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

801 count C{mn} twice that. 

802 

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

804 point with the C{hd} distance. 

805 

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

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

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

809 ''' 

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

811 _Units_ = (_Pass, Number_, Number_, Number_, _Pass, _Pass) 

812 

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

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

815 ''' 

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

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

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

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

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

821 

822 

823def randomrangenerator(seed): 

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

825 

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

827 or C{None}). 

828 

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

830 current time or from a platform-specific randomness 

831 source, if available. 

832 

833 @return: A function to generate random ranges. 

834 

835 @example: 

836 

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

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

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

840 ''' 

841 R = Random(seed) 

842 

843 def _range(n, *stop_step): 

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

845 except the returned values are in random order. 

846 

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

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

849 creating a tuple with the entire C{population} 

850 and a list containing all sample values (for 

851 large C{n}). 

852 ''' 

853 if stop_step: 

854 s = range(n, *stop_step) 

855 

856 elif n > 32: 

857 r = R.randrange # Random._randbelow 

858 s = set() 

859 for _ in range(n - 32): 

860 i = r(n) 

861 while i in s: 

862 i = r(n) 

863 s.add(i) 

864 yield i 

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

866 else: 

867 s = range(n) 

868 

869 s = list(s) 

870 R.shuffle(s) 

871 while s: 

872 yield s.pop(0) 

873 

874 return _range 

875 

876# **) MIT License 

877# 

878# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved. 

879# 

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

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

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

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

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

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

886# 

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

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

889# 

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

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

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

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

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

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

896# OTHER DEALINGS IN THE SOFTWARE.