1

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

3

4 u'''Hausdorff distances.

5

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

7 L{HausdorffCosineAndoyerLambert}, L{HausdorffCosineForsytheAndoyerLambert},

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

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

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

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

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

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

14

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

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

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

18

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

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

21 points.

22

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

24

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

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

27

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

29

30 respectively

31

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

33

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

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

36

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

38

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

40

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

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

43 instance, more details in the documentation thereof.

44

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

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

47 C{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

60 Transcribed from the original SciPy U{Directed Hausdorff Code

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

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

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

64 breaking} 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

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

68'''

69

70 from pygeodesy.constants import INF, NINF, _0_0

71 from pygeodesy.datums import _ellipsoidal_datum, _WGS84

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

73 import pygeodesy.formy as _formy

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

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

76 from pygeodesy.lazily import _ALL_LAZY, _FOR_DOCS

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

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

79 from pygeodesy.points import _distanceTo, PhiLam2Tuple, 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__ = '24.06.15'

89

90

91 class HausdorffError(PointsError):

92 '''Hausdorff issue.

93 '''

94 pass

95

96

97 class 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

272 class 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

288 class 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

315 class _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

353 class 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

376 class 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

399 class 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

424 class 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

462 class 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

488 class 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

511 class 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

541 class 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

568 class 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

593 class 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

620 class 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

629 class 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

661 class 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

684 class 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

711 def _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

753 def _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

764 def 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

800 class 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

830 def 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.