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 # 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, points2 as _points2, PhiLam2Tuple, radians

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

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

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

83 _Str_radians, _Str_radians2

84

85# from math import radians # from .points

86 from random import Random

87

88__all__ = _ALL_LAZY.hausdorff

89__version__ = '24.05.26'

90

91

92 class HausdorffError(PointsError):

93 '''Hausdorff issue.

94 '''

95 pass

96

97

98 class Hausdorff(_Named):

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

100 be overloaded.

101 '''

102 _datum = _WGS84

103 _func = None # formy function/property

104 _kwds = {} # func_ options

105 _model = ()

106 _seed = None

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

108

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

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

111

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

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

114 C{other}[]).

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

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

117 PubDat_247739.pdf>}.

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

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

120 and keyword arguments for the distance function, retrievable

121 with property C{kwds}.

122

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

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

125 '''

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

127 if name:

128 self.name = name

129

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

131 if seed:

132 self.seed = seed

133 if units: # and not self.units:

134 self.units = units

135 if kwds:

136 self._kwds = kwds

137

138 @Property_RO

139 def adjust(self):

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

141 '''

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

143

144 @Property_RO

145 def datum(self):

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

147 '''

148 return self._datum

149

150 def _datum_setter(self, datum):

151 '''(INTERNAL) Set the datum.

152 '''

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

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

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

156

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

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

159

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

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

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

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

164

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

166

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

168 an invalid B{C{point2}}.

169

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

171 '''

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

173

174 def distance(self, point1, point2):

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

176 subject to the supplied optional keyword arguments, see

177 property C{kwds}.

178 '''

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

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

181

182 @Property

183 def _func(self):

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

185 self._notOverloaded(**self.kwds)

186

187 @_func.setter_ # PYCHOK setter_underscore!

188 def _func(self, func):

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

190

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

192 _, ps2 = self._points2(point2s)

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

194 self.units, distance, self.point)

195

196 @property_RO

197 def kwds(self):

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

199 '''

200 return self._kwds

201

202 def point(self, point):

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

204 '''

205 return point # pass thru

206

207 def _points2(self, points):

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

209 '''

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

211

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

213 def seed(self):

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

215 '''

216 return self._seed

217

218 @seed.setter # PYCHOK setter!

219 def seed(self, seed):

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

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

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

223

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

225 '''

226 if seed:

227 try:

228 Random(seed)

229 except (TypeError, ValueError) as x:

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

231 self._seed = seed

232 else:

233 self._seed = None

234

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

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

237

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

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

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

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

242

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

244

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

246 an invalid B{C{point2}}.

247

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

249 '''

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

251

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

253 def units(self):

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

255 '''

256 return self._units

257

258 @units.setter # PYCHOK setter!

259 def units(self, units):

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

261

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

263 '''

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

265

266 @Property_RO

267 def wrap(self):

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

269 '''

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

271

272

273 class HausdorffDegrees(Hausdorff):

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

275 points in C{degrees}.

276 '''

277 _units = _Str_degrees

278

279 if _FOR_DOCS:

280 __init__ = Hausdorff.__init__

281 directed = Hausdorff.directed

282 symmetric = Hausdorff.symmetric

283

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

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

286 self._notOverloaded(point1, point2)

287

288

289 class HausdorffRadians(Hausdorff):

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

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

292 '''

293 _units = _Str_radians

294

295 if _FOR_DOCS:

296 __init__ = Hausdorff.__init__

297 directed = Hausdorff.directed

298 symmetric = Hausdorff.symmetric

299

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

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

302 self._notOverloaded(point1, point2)

303

304 def point(self, point):

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

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

307

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

309 '''

310 try:

311 return point.philam

312 except AttributeError:

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

314

315

316 class _HausdorffMeterRadians(Hausdorff):

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

318 the optional keyword arguments supplied at instantiation

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

320 '''

321 _units = _Str_meter

322 _units_ = _Str_radians

323

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

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

326 the distance function and units from the optional

327 keyword arguments given at this instantiation, see

328 property C{kwds}.

329

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

331 '''

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

333

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

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

336 the distance function and units from the optional

337 keyword arguments given at this instantiation, see

338 property C{kwds}.

339

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

341 '''

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

343

344 @Property

345 def _func_(self):

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

347 self._notOverloaded(**self.kwds)

348

349 @_func_.setter_ # PYCHOK setter_underscore!

350 def _func_(self, func):

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

352

353

354 class HausdorffCosineAndoyerLambert(_HausdorffMeterRadians):

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

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

357 '''

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

359 '''New L{HausdorffCosineAndoyerLambert} calculator.

360

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

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

363 function L{pygeodesy.cosineAndoyerLambert}.

364

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

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

367 '''

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

369 self._func = _formy.cosineAndoyerLambert

370 self._func_ = _formy.cosineAndoyerLambert_

371

372 if _FOR_DOCS:

373 directed = Hausdorff.directed

374 symmetric = Hausdorff.symmetric

375

376

377 class HausdorffCosineForsytheAndoyerLambert(_HausdorffMeterRadians):

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

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

380 '''

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

382 '''New L{HausdorffCosineForsytheAndoyerLambert} calculator.

383

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

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

386 L{pygeodesy.cosineForsytheAndoyerLambert}.

387

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

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

390 '''

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

392 self._func = _formy.cosineForsytheAndoyerLambert

393 self._func_ = _formy.cosineForsytheAndoyerLambert_

394

395 if _FOR_DOCS:

396 directed = Hausdorff.directed

397 symmetric = Hausdorff.symmetric

398

399

400 class HausdorffCosineLaw(_HausdorffMeterRadians):

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

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

403

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

405 '''

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

407 '''New L{HausdorffCosineLaw} calculator.

408

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

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

411 function L{pygeodesy.cosineLaw}.

412

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

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

415 '''

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

417 self._func = _formy.cosineLaw

418 self._func_ = _formy.cosineLaw_

419

420 if _FOR_DOCS:

421 directed = Hausdorff.directed

422 symmetric = Hausdorff.symmetric

423

424

425 class HausdorffDistanceTo(Hausdorff):

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

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

428 '''

429 _units = _Str_meter

430

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

432 '''New L{HausdorffDistanceTo} calculator.

433

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

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

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

437

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

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

440

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

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

443 spherical C{LatLon} class.

444 '''

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

446

447 if _FOR_DOCS:

448 directed = Hausdorff.directed

449 symmetric = Hausdorff.symmetric

450

451 def distance(self, p1, p2):

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

453 '''

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

455

456 def _points2(self, points):

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

458 '''

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

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

461

462

463 class HausdorffEquirectangular(Hausdorff):

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

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

466 '''

467 _units = _Str_degrees2

468

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

470 '''New L{HausdorffEquirectangular} calculator.

471

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

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

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

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

476

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

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

479 '''

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

481 limit=0))

482 self._func = _formy._equirectangular # helper

483

484 if _FOR_DOCS:

485 directed = Hausdorff.directed

486 symmetric = Hausdorff.symmetric

487

488

489 class HausdorffEuclidean(_HausdorffMeterRadians):

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

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

492 '''

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

494 '''New L{HausdorffEuclidean} calculator.

495

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

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

498 function L{pygeodesy.euclidean}.

499

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

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

502 '''

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

504 self._func = _formy.euclidean

505 self._func_ = _formy.euclidean_

506

507 if _FOR_DOCS:

508 directed = Hausdorff.directed

509 symmetric = Hausdorff.symmetric

510

511

512 class HausdorffExact(Hausdorff):

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

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

515 '''

516 _units = _Str_degrees

517

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

519 '''New L{HausdorffKarney} calculator.

520

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

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

523 or L{a_f2Tuple}).

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

525 keyword argument for method C{Inverse1} of class

526 L{geodesicx.GeodesicExact}.

527

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

529

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

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

532 '''

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

534 self._datum_setter(datum)

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

536

537 if _FOR_DOCS:

538 directed = Hausdorff.directed

539 symmetric = Hausdorff.symmetric

540

541

542 class HausdorffFlatLocal(_HausdorffMeterRadians):

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

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

545 '''

546 _units = _Str_radians2

547

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

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

550

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

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

553 L{pygeodesy.flatLocal}.

554

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

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

557

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

559 '''

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

561 self._func = _formy.flatLocal

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

563

564 if _FOR_DOCS:

565 directed = Hausdorff.directed

566 symmetric = Hausdorff.symmetric

567

568

569 class HausdorffFlatPolar(_HausdorffMeterRadians):

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

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

572 '''

573 _wrap = False

574

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

576 '''New L{HausdorffFlatPolar} calculator.

577

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

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

580 for function L{pygeodesy.flatPolar}.

581

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

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

584 '''

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

586 self._func = _formy.flatPolar

587 self._func_ = _formy.flatPolar_

588

589 if _FOR_DOCS:

590 directed = Hausdorff.directed

591 symmetric = Hausdorff.symmetric

592

593

594 class HausdorffHaversine(_HausdorffMeterRadians):

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

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

597

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

599 '''

600 _wrap = False

601

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

603 '''New L{HausdorffHaversine} calculator.

604

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

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

607 for function L{pygeodesy.haversine}.

608

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

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

611 '''

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

613 self._func = _formy.haversine

614 self._func_ = _formy.haversine_

615

616 if _FOR_DOCS:

617 directed = Hausdorff.directed

618 symmetric = Hausdorff.symmetric

619

620

621 class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny

622 if _FOR_DOCS:

623 __doc__ = HausdorffFlatLocal.__doc__

624 __init__ = HausdorffFlatLocal.__init__

625 directed = HausdorffFlatLocal.directed

626 distance = HausdorffFlatLocal.distance

627 symmetric = HausdorffFlatLocal.symmetric

628

629

630 class HausdorffKarney(Hausdorff):

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

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

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

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

635 Inverse method.

636 '''

637 _units = _Str_degrees

638

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

640 '''New L{HausdorffKarney} calculator.

641

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

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

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

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

646 and keyword arguments for method C{Inverse1} of

647 class L{geodesicw.Geodesic}.

648

649 @raise ImportError: Package U{geographiclib

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

651

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

653

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

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

656 '''

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

658 self._datum_setter(datum)

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

660

661

662 class HausdorffThomas(_HausdorffMeterRadians):

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

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

665 '''

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

667 '''New L{HausdorffThomas} calculator.

668

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

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

671 for function L{pygeodesy.thomas}.

672

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

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

675 '''

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

677 self._func = _formy.thomas

678 self._func_ = _formy.thomas_

679

680 if _FOR_DOCS:

681 directed = Hausdorff.directed

682 symmetric = Hausdorff.symmetric

683

684

685 class HausdorffVincentys(_HausdorffMeterRadians):

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

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

688

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

690 '''

691 _wrap = False

692

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

694 '''New L{HausdorffVincentys} calculator.

695

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

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

698 for function L{pygeodesy.vincentys}.

699

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

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

702 '''

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

704 self._func = _formy.vincentys

705 self._func_ = _formy.vincentys_

706

707 if _FOR_DOCS:

708 directed = Hausdorff.directed

709 symmetric = Hausdorff.symmetric

710

711

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

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

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

715 '''

716 # shuffling the points generally increases the

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

718 rr = randomrangenerator(seed) if seed else range

719

720 hd = NINF

721 hi = hj = m = mn = 0

722 md = _0_0

723

724 # forward or forward and backward

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

726 n = len(ps2)

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

728 p1 = point(ps1[i])

729 dh, dj = INF, 0

730 for j in rr(n):

731 p2 = point(ps2[j])

732 d = distance(p1, p2)

733 if early and d < hd:

734 break # early

735 elif d < dh:

736 dh, dj = d, j

737 else: # no early break

738 if hd < dh:

739 hd = dh

740 if fb:

741 hi, hj = dj, i

742 else:

743 hi, hj = i, dj

744 md += dh

745 mn += 1

746 m += 1

747 # swap model and target

748 ps1, ps2 = ps2, ps1

749

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

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

752

753

754 def _point(p):

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

756

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

758

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

760 B{C{distance}} callable.

761 '''

762 return p

763

764

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

766 distance=None, point=_point):

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

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

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

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

771

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

787

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

789

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

791

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

793 '''

794 _xcallable(distance=distance, point=point)

795

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

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

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

799

800

801 class Hausdorff6Tuple(_NamedTuple):

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

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

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

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

806 C{units}.

807

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

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

810 count C{mn} twice that.

811

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

813 point with the C{hd} distance.

814

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

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

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

818 '''

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

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

821

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

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

824 '''

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

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

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

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

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

830

831

832 def randomrangenerator(seed):

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

834

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

836 or C{None}).

837

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

839 current time or from a platform-specific randomness

840 source, if available.

841

842 @return: A function to generate random ranges.

843

844 @example:

845

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

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

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

849 '''

850 R = Random(seed)

851

852 def _range(n, *stop_step):

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

854 except the returned values are in random order.

855

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

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

858 creating a tuple with the entire C{population}

859 and a list containing all sample values (for

860 large C{n}).

861 '''

862 if stop_step:

863 s = range(n, *stop_step)

864

865 elif n > 32:

866 r = R.randrange # Random._randbelow

867 s = set()

868 for _ in range(n - 32):

869 i = r(n)

870 while i in s:

871 i = r(n)

872 s.add(i)

873 yield i

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

875 else:

876 s = range(n)

877

878 s = list(s)

879 R.shuffle(s)

880 while s:

881 yield s.pop(0)

882

883 return _range

884

885# **) MIT License

886#

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

888#

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

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

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

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

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

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

895#

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

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

898#

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

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

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

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

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

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

905# OTHER DEALINGS IN THE SOFTWARE.