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 _IsnotError, PointsError, _xattr, _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 _Named, _NamedTuple, notOverloaded, _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_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__ = '23.09.22'

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

104 _kwds = {} # func_ options

105 _model = ()

106 _seed = None

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

108

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

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

111

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

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

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

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

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

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

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

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

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

121 retrievable with property C{kwds}.

122

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

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

125 or B{C{units}}.

126 '''

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

128 if seed:

129 self.seed = seed

130 if name:

131 self.name = name

132 if units: # and not self.units:

133 self.units = units

134 if kwds:

135 self._kwds = kwds

136

137 @Property_RO

138 def adjust(self):

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

140 '''

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

142

143 @Property_RO

144 def datum(self):

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

146 '''

147 return self._datum

148

149 def _datum_setter(self, datum):

150 '''(INTERNAL) Set the datum.

151 '''

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

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

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

155

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

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

158

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

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

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

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

163

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

165

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

167 an invalid B{C{point2}}.

168

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

170 '''

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

172

173 def distance(self, point1, point2):

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

175 subject to the supplied optional keyword arguments, see

176 property C{kwds}.

177 '''

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

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

180

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

182 _, ps2 = self._points2(point2s)

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

184 self.units, distance, self.point)

185

186 @property_RO

187 def kwds(self):

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

189 '''

190 return self._kwds

191

192 def point(self, point):

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

194 '''

195 return point # pass thru

196

197 def _points2(self, points):

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

199 '''

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

201

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

203 def seed(self):

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

205 '''

206 return self._seed

207

208 @seed.setter # PYCHOK setter!

209 def seed(self, seed):

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

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

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

213

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

215 '''

216 if seed:

217 try:

218 Random(seed)

219 except (TypeError, ValueError) as x:

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

221 self._seed = seed

222 else:

223 self._seed = None

224

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

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

227

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

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

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

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

232

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

234

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

236 an invalid B{C{point2}}.

237

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

239 '''

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

241

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

243 def units(self):

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

245 '''

246 return self._units

247

248 @units.setter # PYCHOK setter!

249 def units(self, units):

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

251

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

253 '''

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

255

256 @Property_RO

257 def wrap(self):

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

259 '''

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

261

262

263 class HausdorffDegrees(Hausdorff):

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

265 points in C{degrees}.

266 '''

267 _units = _Str_degrees

268

269 if _FOR_DOCS:

270 __init__ = Hausdorff.__init__

271 directed = Hausdorff.directed

272 symmetric = Hausdorff.symmetric

273

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

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

276 I{Must be overloaded}.'''

277 notOverloaded(self, point1, point2)

278

279

280 class HausdorffRadians(Hausdorff):

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

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

283 '''

284 _units = _Str_radians

285

286 if _FOR_DOCS:

287 __init__ = Hausdorff.__init__

288 directed = Hausdorff.directed

289 symmetric = Hausdorff.symmetric

290

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

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

293 I{Must be overloaded}.'''

294 notOverloaded(self, point1, point2)

295

296 def point(self, point):

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

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

299

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

301 '''

302 try:

303 return point.philam

304 except AttributeError:

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

306

307

308 class _HausdorffMeterRadians(Hausdorff):

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

310 the optional keyword arguments supplied at instantiation

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

312 '''

313 _units = _Str_meter

314 _units_ = _Str_radians

315

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

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

318 the distance function and units from the optional

319 keyword arguments given at this instantiation, see

320 property C{kwds}.

321

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

323 '''

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

325

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

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

328 the distance function and units from the optional

329 keyword arguments given at this instantiation, see

330 property C{kwds}.

331

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

333 '''

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

335

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

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

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

339

340

341 class HausdorffCosineAndoyerLambert(_HausdorffMeterRadians):

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

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

344 '''

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

346 '''New L{HausdorffCosineAndoyerLambert} calculator.

347

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

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

350

351 @kwarg datum_wrap: Optional keyword arguments for function

352 L{pygeodesy.cosineAndoyerLambert}.

353 '''

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

355 **datum_wrap)

356 self._func = _formy.cosineAndoyerLambert

357 self._func_ = _formy.cosineAndoyerLambert_

358

359 if _FOR_DOCS:

360 directed = Hausdorff.directed

361 symmetric = Hausdorff.symmetric

362

363

364 class HausdorffCosineForsytheAndoyerLambert(_HausdorffMeterRadians):

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

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

367 '''

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

369 '''New L{HausdorffCosineForsytheAndoyerLambert} calculator.

370

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

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

373

374 @kwarg datum_wrap: Optional keyword arguments for function

375 L{pygeodesy.cosineAndoyerLambert}.

376 '''

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

378 **datum_wrap)

379 self._func = _formy.cosineForsytheAndoyerLambert

380 self._func_ = _formy.cosineForsytheAndoyerLambert_

381

382 if _FOR_DOCS:

383 directed = Hausdorff.directed

384 symmetric = Hausdorff.symmetric

385

386

387 class HausdorffCosineLaw(_HausdorffMeterRadians):

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

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

390

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

392 '''

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

394 '''New L{HausdorffCosineLaw} calculator.

395

396 @kwarg radius_wrap: Optional keyword arguments for function

397 L{pygeodesy.cosineLaw}.

398

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

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

401 '''

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

403 **radius_wrap)

404 self._func = _formy.cosineLaw

405 self._func_ = _formy.cosineLaw_

406

407 if _FOR_DOCS:

408 directed = Hausdorff.directed

409 symmetric = Hausdorff.symmetric

410

411

412 class HausdorffDistanceTo(Hausdorff):

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

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

415 '''

416 _units = _Str_meter

417

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

419 '''New L{HausdorffDistanceTo} calculator.

420

421 @kwarg distanceTo_kwds: Optional keyword arguments for each

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

423 method.

424

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

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

427

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

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

430 spherical C{LatLon} class.

431 '''

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

433 **distanceTo_kwds)

434

435 if _FOR_DOCS:

436 directed = Hausdorff.directed

437 symmetric = Hausdorff.symmetric

438

439 def distance(self, p1, p2):

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

441 '''

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

443

444 def _points2(self, points):

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

446 '''

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

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

449

450

451 class HausdorffEquirectangular(Hausdorff):

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

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

454 '''

455 _units = _Str_degrees2

456

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

458 '''New L{HausdorffEquirectangular} calculator.

459

460 @kwarg adjust_limit_wrap: Optional keyword arguments for function

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

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

463

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

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

466 '''

467 adjust_limit_wrap = _xkwds(adjust_limit_wrap, limit=0)

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

469 **adjust_limit_wrap)

470 self._func = _formy._equirectangular # helper

471

472 if _FOR_DOCS:

473 directed = Hausdorff.directed

474 symmetric = Hausdorff.symmetric

475

476

477 class HausdorffEuclidean(_HausdorffMeterRadians):

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

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

480 '''

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

482 '''New L{HausdorffEuclidean} calculator.

483

484 @kwarg adjust_radius_wrap: Optional keyword arguments for

485 function L{pygeodesy.euclidean}.

486

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

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

489 '''

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

491 **adjust_wrap)

492 self._func = _formy.euclidean

493 self._func_ = _formy.euclidean_

494

495 if _FOR_DOCS:

496 directed = Hausdorff.directed

497 symmetric = Hausdorff.symmetric

498

499

500 class HausdorffExact(Hausdorff):

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

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

503 '''

504 _units = _Str_degrees

505

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

507 '''New L{HausdorffKarney} calculator.

508

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

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

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

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

513 of class L{geodesicx.GeodesicExact}.

514

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

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

517

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

519 '''

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

521 **wrap)

522 self._datum_setter(datum)

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

524

525 if _FOR_DOCS:

526 directed = Hausdorff.directed

527 symmetric = Hausdorff.symmetric

528

529

530 class HausdorffFlatLocal(_HausdorffMeterRadians):

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

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

533 '''

534 _units = _Str_radians2

535

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

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

538

539 @kwarg datum_scaled_wrap: Optional keyword arguments for

540 function L{pygeodesy.flatLocal}.

541

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

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

544

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

546 '''

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

548 **datum_scaled_wrap)

549 self._func = _formy.flatLocal

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

551

552 if _FOR_DOCS:

553 directed = Hausdorff.directed

554 symmetric = Hausdorff.symmetric

555

556

557 class HausdorffFlatPolar(_HausdorffMeterRadians):

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

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

560 '''

561 _wrap = False

562

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

564 '''New L{HausdorffFlatPolar} calculator.

565

566 @kwarg radius_wrap: Optional keyword arguments for function

567 L{pygeodesy.flatPolar}.

568

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

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

571 '''

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

573 **radius_wrap)

574 self._func = _formy.flatPolar

575 self._func_ = _formy.flatPolar_

576

577 if _FOR_DOCS:

578 directed = Hausdorff.directed

579 symmetric = Hausdorff.symmetric

580

581

582 class HausdorffHaversine(_HausdorffMeterRadians):

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

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

585

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

587 '''

588 _wrap = False

589

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

591 '''New L{HausdorffHaversine} calculator.

592

593 @kwarg radius_wrap: Optional keyword arguments for function

594 L{pygeodesy.haversine}.

595

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

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

598 '''

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

600 **radius_wrap)

601 self._func = _formy.haversine

602 self._func_ = _formy.haversine_

603

604 if _FOR_DOCS:

605 directed = Hausdorff.directed

606 symmetric = Hausdorff.symmetric

607

608

609 class HausdorffHubeny(HausdorffFlatLocal): # for Karl Hubeny

610 if _FOR_DOCS:

611 __doc__ = HausdorffFlatLocal.__doc__

612 __init__ = HausdorffFlatLocal.__init__

613 directed = HausdorffFlatLocal.directed

614 distance = HausdorffFlatLocal.distance

615 symmetric = HausdorffFlatLocal.symmetric

616

617

618 class HausdorffKarney(Hausdorff):

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

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

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

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

623 Inverse method.

624 '''

625 _units = _Str_degrees

626

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

628 '''New L{HausdorffKarney} calculator.

629

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

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

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

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

634 of class L{geodesicw.Geodesic}.

635

636 @raise ImportError: Package U{geographiclib

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

638

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

640

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

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

643 '''

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

645 **wrap)

646 self._datum_setter(datum)

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

648

649

650 class HausdorffThomas(_HausdorffMeterRadians):

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

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

653 '''

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

655 '''New L{HausdorffThomas} calculator.

656

657 @kwarg datum_wrap: Optional keyword argument for function

658 L{pygeodesy.thomas}.

659

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

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

662 '''

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

664 **datum_wrap)

665 self._func = _formy.thomas

666 self._func_ = _formy.thomas_

667

668 if _FOR_DOCS:

669 directed = Hausdorff.directed

670 symmetric = Hausdorff.symmetric

671

672

673 class HausdorffVincentys(_HausdorffMeterRadians):

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

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

676

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

678 '''

679 _wrap = False

680

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

682 '''New L{HausdorffVincentys} calculator.

683

684 @kwarg radius_wrap: Optional keyword arguments for function

685 L{pygeodesy.vincentys}.

686

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

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

689 '''

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

691 **radius_wrap)

692 self._func = _formy.vincentys

693 self._func_ = _formy.vincentys_

694

695 if _FOR_DOCS:

696 directed = Hausdorff.directed

697 symmetric = Hausdorff.symmetric

698

699

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

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

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

703 '''

704 # shuffling the points generally increases the

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

706 rr = randomrangenerator(seed) if seed else range

707

708 hd = NINF

709 hi = hj = m = mn = 0

710 md = _0_0

711

712 # forward or forward and backward

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

714 n = len(ps2)

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

716 p1 = point(ps1[i])

717 dh, dj = INF, 0

718 for j in rr(n):

719 p2 = point(ps2[j])

720 d = distance(p1, p2)

721 if early and d < hd:

722 break # early

723 elif d < dh:

724 dh, dj = d, j

725 else: # no early break

726 if hd < dh:

727 hd = dh

728 if fb:

729 hi, hj = dj, i

730 else:

731 hi, hj = i, dj

732 md += dh

733 mn += 1

734 m += 1

735 # swap model and target

736 ps1, ps2 = ps2, ps1

737

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

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

740

741

742 def _point(p):

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

744

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

746

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

748 B{C{distance}} callable.

749 '''

750 return p

751

752

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

754 distance=None, point=_point):

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

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

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

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

759

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

775

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

777

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

779

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

781 '''

782 if not callable(distance):

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

784 if not callable(point):

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

786

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

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

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

790

791

792 class Hausdorff6Tuple(_NamedTuple):

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

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

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

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

797 C{units}.

798

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

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

801 count C{mn} twice that.

802

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

804 point with the C{hd} distance.

805

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

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

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

809 '''

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

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

812

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

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

815 '''

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

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

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

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

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

821

822

823 def randomrangenerator(seed):

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

825

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

827 or C{None}).

828

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

830 current time or from a platform-specific randomness

831 source, if available.

832

833 @return: A function to generate random ranges.

834

835 @example:

836

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

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

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

840 '''

841 R = Random(seed)

842

843 def _range(n, *stop_step):

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

845 except the returned values are in random order.

846

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

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

849 creating a tuple with the entire C{population}

850 and a list containing all sample values (for

851 large C{n}).

852 '''

853 if stop_step:

854 s = range(n, *stop_step)

855

856 elif n > 32:

857 r = R.randrange # Random._randbelow

858 s = set()

859 for _ in range(n - 32):

860 i = r(n)

861 while i in s:

862 i = r(n)

863 s.add(i)

864 yield i

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

866 else:

867 s = range(n)

868

869 s = list(s)

870 R.shuffle(s)

871 while s:

872 yield s.pop(0)

873

874 return _range

875

876# **) MIT License

877#

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

879#

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

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

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

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

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

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

886#

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

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

889#

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

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

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

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

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

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

896# OTHER DEALINGS IN THE SOFTWARE.