Coverage for pygeodesy/hausdorff.py: 95%

1

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

3

4 u'''Hausdorff distances.

5

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

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

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

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

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

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

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

13

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

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

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

17

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

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

20 points.

21

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

23

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

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

26

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

28

29 respectively

30

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

32

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

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

35

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

37

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

39

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

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

42 instance, more details in the documentation thereof.

43

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

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

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

47

48 >>> from pygeodesy import Hausdorff, hypot_

49 >>>

50 >>> class H3D(Hausdorff):

51 >>> """Custom Hausdorff example.

52 >>> """

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

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

55 >>>

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

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

58

59 Transcribed from the original SciPy U{Directed Hausdorff Code

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

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

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

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

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

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

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

67'''

68

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

70 from pygeodesy.constants import INF, NINF, _0_0

71 from pygeodesy.datums import _ellipsoidal_datum, _WGS84, _isin

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

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

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

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

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

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

78 from pygeodesy.namedTuples import PhiLam2Tuple

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

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

81 from pygeodesy.units import Float, Number_

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

83

84# from math import radians # from .points

85 from random import Random

86

87 __all__ = _ALL_LAZY.hausdorff

88 __version__ = '25.05.26'

89

90 _formy = _MODS.into(formy=__name__)

91

92

93 class HausdorffError(PointsError):

94 '''Hausdorff issue.

95 '''

96 pass

97

98

99 class Hausdorff(_Named):

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

101 be overloaded.

102 '''

103 _datum = _WGS84

104# _func = None # formy function/property

105 _kwds = {} # func_ options

106 _model = ()

107 _seed = None

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

109

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

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

112

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

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

115 C{other}[]).

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

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

118 PubDat_247739.pdf>}.

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

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

121 and keyword arguments for the distance function, retrievable

122 with property C{kwds}.

123

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

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

126 '''

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

128 if name:

129 self.name = name

130

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

132 if seed:

133 self.seed = seed

134 if units: # and not self.units:

135 self.units = units

136 if kwds:

137 self._kwds = kwds

138

139 @Property_RO

140 def adjust(self):

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

142 '''

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

144

145 @Property_RO

146 def datum(self):

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

148 '''

149 return self._datum

150

151 def _datum_setter(self, datum):

152 '''(INTERNAL) Set the datum.

153 '''

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

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

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

157

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

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

160

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

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

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

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

165

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

167

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

169 an invalid B{C{point2}}.

170

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

172 '''

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

174

175 def distance(self, point1, point2):

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

177 subject to the supplied optional keyword arguments, see

178 property C{kwds}.

179 '''

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

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

182

183 @Property

184 def _func(self):

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

186 self._notOverloaded(**self.kwds)

187

188 @_func.setter_ # PYCHOK setter_underscore!

189 def _func(self, func):

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

191

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

193 _, ps2 = self._points2(point2s)

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

195 self.units, distance, self.point)

196

197 @property_RO

198 def kwds(self):

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

200 '''

201 return self._kwds

202

203 def point(self, point):

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

205 '''

206 return point # pass thru

207

208 def _points2(self, points):

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

210 '''

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

212

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

214 def seed(self):

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

216 '''

217 return self._seed

218

219 @seed.setter # PYCHOK setter!

220 def seed(self, seed):

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

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

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

224

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

226 '''

227 if seed:

228 try:

229 Random(seed)

230 except (TypeError, ValueError) as x:

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

232 self._seed = seed

233 else:

234 self._seed = None

235

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

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

238

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

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

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

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

243

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

245

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

247 an invalid B{C{point2}}.

248

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

250 '''

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

252

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

254 def units(self):

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

256 '''

257 return self._units

258

259 @units.setter # PYCHOK setter!

260 def units(self, units):

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

262

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

264 '''

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

266

267 @Property_RO

268 def wrap(self):

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

270 '''

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

272

273

274 class HausdorffDegrees(Hausdorff):

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

276 points in C{degrees}.

277 '''

278 _units = _unitsBase._Str_degrees

279

280 if _FOR_DOCS:

281 __init__ = Hausdorff.__init__

282 directed = Hausdorff.directed

283 symmetric = Hausdorff.symmetric

284

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

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

287 self._notOverloaded(point1, point2)

288

289

290 class HausdorffRadians(Hausdorff):

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

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

293 '''

294 _units = _unitsBase._Str_radians

295

296 if _FOR_DOCS:

297 __init__ = Hausdorff.__init__

298 directed = Hausdorff.directed

299 symmetric = Hausdorff.symmetric

300

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

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

303 self._notOverloaded(point1, point2)

304

305 def point(self, point):

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

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

308

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

310 '''

311 try:

312 return point.philam

313 except AttributeError:

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

315

316

317 class _HausdorffMeterRadians(Hausdorff):

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

319 the optional keyword arguments supplied at instantiation

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

321 '''

322 _units = _unitsBase._Str_meter

323 _units_ = _unitsBase._Str_radians

324

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

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

327 the distance function and units from the optional

328 keyword arguments given at this instantiation, see

329 property C{kwds}.

330

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

332 '''

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

334

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

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

337 the distance function and units from the optional

338 keyword arguments given at this instantiation, see

339 property C{kwds}.

340

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

342 '''

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

344

345 @Property

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

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

348 self._notOverloaded(**self.kwds)

349

350 @_func_.setter_ # PYCHOK setter_underscore!

351 def _func_(self, func):

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

353

354

355 class HausdorffCosineLaw(_HausdorffMeterRadians):

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

357

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

359 '''

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

361 '''New L{HausdorffCosineLaw} calculator.

362

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

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

365 L{pygeodesy.cosineLaw}.

366

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

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

369 '''

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

371 self._func = _formy.cosineLaw

372 self._func_ = _formy.cosineLaw_

373

374 if _FOR_DOCS:

375 directed = Hausdorff.directed

376 symmetric = Hausdorff.symmetric

377

378

379 class HausdorffDistanceTo(Hausdorff):

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

381 '''

382 _units = _unitsBase._Str_meter

383

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

385 '''New L{HausdorffDistanceTo} calculator.

386

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

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

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

390

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

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

393

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

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

396 spherical C{LatLon} class.

397 '''

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

399

400 if _FOR_DOCS:

401 directed = Hausdorff.directed

402 symmetric = Hausdorff.symmetric

403

404 def distance(self, p1, p2):

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

406 '''

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

408

409 def _points2(self, points):

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

411 '''

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

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

414

415

416 class HausdorffEquirectangular(Hausdorff):

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

418 '''

419 _units = _unitsBase._Str_degrees2

420

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

422 '''New L{HausdorffEquirectangular} calculator.

423

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

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

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

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

428

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

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

431 '''

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

433 limit=0))

434 self._func = _formy._equirectangular # helper

435

436 if _FOR_DOCS:

437 directed = Hausdorff.directed

438 symmetric = Hausdorff.symmetric

439

440

441 class HausdorffEuclidean(_HausdorffMeterRadians):

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

443 '''

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

445 '''New L{HausdorffEuclidean} calculator.

446

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

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

449 function L{pygeodesy.euclidean}.

450

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

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

453 '''

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

455 self._func = _formy.euclidean

456 self._func_ = _formy.euclidean_

457

458 if _FOR_DOCS:

459 directed = Hausdorff.directed

460 symmetric = Hausdorff.symmetric

461

462

463 class HausdorffExact(Hausdorff):

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

465 '''

466 _units = _unitsBase._Str_degrees

467

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

469 '''New L{HausdorffKarney} calculator.

470

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

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

473 or L{a_f2Tuple}).

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

475 keyword argument for method C{Inverse1} of class

476 L{geodesicx.GeodesicExact}.

477

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

479

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

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

482 '''

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

484 self._datum_setter(datum)

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

486

487 if _FOR_DOCS:

488 directed = Hausdorff.directed

489 symmetric = Hausdorff.symmetric

490

491

492 class HausdorffFlatLocal(_HausdorffMeterRadians):

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

494 '''

495 _units = _unitsBase._Str_radians2

496

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

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

499

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

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

502 L{pygeodesy.flatLocal}.

503

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

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

506

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

508 '''

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

510 self._func = _formy.flatLocal

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

512

513 if _FOR_DOCS:

514 directed = Hausdorff.directed

515 symmetric = Hausdorff.symmetric

516

517

518 class HausdorffFlatPolar(_HausdorffMeterRadians):

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

520 '''

521 _wrap = False

522

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

524 '''New L{HausdorffFlatPolar} calculator.

525

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

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

528 for function L{pygeodesy.flatPolar}.

529

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

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

532 '''

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

534 self._func = _formy.flatPolar

535 self._func_ = _formy.flatPolar_

536

537 if _FOR_DOCS:

538 directed = Hausdorff.directed

539 symmetric = Hausdorff.symmetric

540

541

542 class HausdorffHaversine(_HausdorffMeterRadians):

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

544

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

546 '''

547 _wrap = False

548

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

550 '''New L{HausdorffHaversine} calculator.

551

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

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

554 for function L{pygeodesy.haversine}.

555

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

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

558 '''

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

560 self._func = _formy.haversine

561 self._func_ = _formy.haversine_

562

563 if _FOR_DOCS:

564 directed = Hausdorff.directed

565 symmetric = Hausdorff.symmetric

566

567

568 class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny

569 if _FOR_DOCS:

570 __doc__ = HausdorffFlatLocal.__doc__

571 __init__ = HausdorffFlatLocal.__init__

572 directed = HausdorffFlatLocal.directed

573 distance = HausdorffFlatLocal.distance

574 symmetric = HausdorffFlatLocal.symmetric

575

576

577 class HausdorffKarney(Hausdorff):

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

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

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

581 method.

582 '''

583 _units = _unitsBase._Str_degrees

584

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

586 '''New L{HausdorffKarney} calculator.

587

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

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

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

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

592 and keyword arguments for method C{Inverse1} of

593 class L{geodesicw.Geodesic}.

594

595 @raise ImportError: Package U{geographiclib

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

597

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

599

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

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

602 '''

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

604 self._datum_setter(datum)

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

606

607

608 class HausdorffThomas(_HausdorffMeterRadians):

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

610 '''

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

612 '''New L{HausdorffThomas} calculator.

613

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

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

616 for function L{pygeodesy.thomas}.

617

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

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

620 '''

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

622 self._func = _formy.thomas

623 self._func_ = _formy.thomas_

624

625 if _FOR_DOCS:

626 directed = Hausdorff.directed

627 symmetric = Hausdorff.symmetric

628

629

630 class HausdorffVincentys(_HausdorffMeterRadians):

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

632

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

634 '''

635 _wrap = False

636

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

638 '''New L{HausdorffVincentys} calculator.

639

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

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

642 for function L{pygeodesy.vincentys}.

643

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

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

646 '''

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

648 self._func = _formy.vincentys

649 self._func_ = _formy.vincentys_

650

651 if _FOR_DOCS:

652 directed = Hausdorff.directed

653 symmetric = Hausdorff.symmetric

654

655

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

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

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

659 '''

660 # shuffling the points generally increases the

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

662 rr = randomrangenerator(seed) if seed else range

663

664 hd = NINF

665 mn = m = hi = hj = 0

666 md = _0_0

667

668 # forward or forward and backward

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

670 ji, n = False, len(ps2)

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

672 p1 = point(ps1[i])

673 dh, dj = INF, 0

674 for j in rr(n):

675 p2 = point(ps2[j])

676 d = distance(p1, p2)

677 if early and d < hd:

678 break # early

679 elif d < dh:

680 dh, dj = d, j

681 else: # no early break

682 if hd < dh:

683 hd = dh

684 hi = i

685 hj = dj

686 ji = fb

687 md += dh

688 mn += 1

689 m += 1

690 if ji: # swap indices

691 hi, hj = hj, hi

692 # swap model and target

693 ps1, ps2 = ps2, ps1

694

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

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

697

698

699 def _point(p):

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

701

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

703

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

705 B{C{distance}} callable.

706 '''

707 return p

708

709

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

711 distance=None, point=_point):

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

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

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

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

716

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

732

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

734

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

736

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

738 '''

739 _xcallable(distance=distance, point=point)

740

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

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

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

744

745

746 class Hausdorff6Tuple(_NamedTuple):

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

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

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

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

751 C{units}.

752

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

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

755 count C{mn} twice that.

756

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

758 point with the C{hd} distance.

759

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

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

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

763 '''

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

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

766

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

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

769 '''

770 u = list(Hausdorff6Tuple._Units_)

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

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

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

774

775

776 def randomrangenerator(seed):

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

778

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

780 or C{None}).

781

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

783 current time or from a platform-specific randomness

784 source, if available.

785

786 @return: A function to generate random ranges.

787

788 @example:

789

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

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

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

793 '''

794 R = Random(seed)

795

796 def _range(n, *stop_step):

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

798 except the returned values are in random order.

799

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

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

802 creating a tuple with the entire C{population}

803 and a list containing all sample values (for

804 large C{n}).

805 '''

806 if stop_step:

807 s = range(n, *stop_step)

808

809 elif n > 32:

810 r = R.randrange # Random._randbelow

811 s = set()

812 for _ in range(n - 32):

813 i = r(n)

814 while i in s:

815 i = r(n)

816 s.add(i)

817 yield i

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

819 else:

820 s = range(n)

821

822 s = list(s)

823 R.shuffle(s)

824 while s:

825 yield s.pop(0)

826

827 return _range

828

829# **) MIT License

830#

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

832#

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

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

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

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

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

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

839#

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

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

842#

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

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

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

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

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

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

849# OTHER DEALINGS IN THE SOFTWARE.