pygeodesy 24.7.24__py2.py3-none-any.whl → 24.8.24__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {PyGeodesy-24.7.24.dist-info → PyGeodesy-24.8.24.dist-info}/METADATA +20 -19
  2. {PyGeodesy-24.7.24.dist-info → PyGeodesy-24.8.24.dist-info}/RECORD +57 -57
  3. {PyGeodesy-24.7.24.dist-info → PyGeodesy-24.8.24.dist-info}/WHEEL +1 -1
  4. pygeodesy/__init__.py +26 -27
  5. pygeodesy/auxilats/auxAngle.py +2 -2
  6. pygeodesy/auxilats/auxDST.py +3 -3
  7. pygeodesy/azimuthal.py +4 -4
  8. pygeodesy/basics.py +3 -3
  9. pygeodesy/cartesianBase.py +6 -6
  10. pygeodesy/constants.py +11 -11
  11. pygeodesy/css.py +5 -5
  12. pygeodesy/ellipsoidalBase.py +18 -15
  13. pygeodesy/ellipsoidalExact.py +2 -2
  14. pygeodesy/ellipsoidalGeodSolve.py +2 -2
  15. pygeodesy/ellipsoidalKarney.py +2 -2
  16. pygeodesy/ellipsoidalNvector.py +2 -2
  17. pygeodesy/ellipsoidalVincenty.py +7 -6
  18. pygeodesy/ellipsoids.py +3 -3
  19. pygeodesy/epsg.py +3 -3
  20. pygeodesy/fmath.py +2 -1
  21. pygeodesy/formy.py +2 -2
  22. pygeodesy/fsums.py +4 -4
  23. pygeodesy/gars.py +66 -58
  24. pygeodesy/geodesici.py +4 -10
  25. pygeodesy/geodesicx/gx.py +3 -3
  26. pygeodesy/geodesicx/gxarea.py +3 -3
  27. pygeodesy/geodsolve.py +3 -3
  28. pygeodesy/geohash.py +491 -267
  29. pygeodesy/geoids.py +298 -316
  30. pygeodesy/heights.py +176 -194
  31. pygeodesy/internals.py +39 -6
  32. pygeodesy/interns.py +2 -3
  33. pygeodesy/karney.py +2 -2
  34. pygeodesy/latlonBase.py +14 -8
  35. pygeodesy/lazily.py +22 -21
  36. pygeodesy/ltp.py +6 -7
  37. pygeodesy/ltpTuples.py +12 -6
  38. pygeodesy/named.py +5 -4
  39. pygeodesy/namedTuples.py +14 -1
  40. pygeodesy/osgr.py +7 -7
  41. pygeodesy/points.py +2 -2
  42. pygeodesy/resections.py +7 -7
  43. pygeodesy/rhumb/solve.py +3 -3
  44. pygeodesy/simplify.py +10 -10
  45. pygeodesy/sphericalBase.py +3 -3
  46. pygeodesy/sphericalTrigonometry.py +2 -2
  47. pygeodesy/streprs.py +3 -3
  48. pygeodesy/triaxials.py +210 -204
  49. pygeodesy/units.py +36 -19
  50. pygeodesy/unitsBase.py +4 -4
  51. pygeodesy/utmupsBase.py +3 -3
  52. pygeodesy/vector2d.py +158 -51
  53. pygeodesy/vector3d.py +13 -52
  54. pygeodesy/vector3dBase.py +81 -63
  55. pygeodesy/webmercator.py +3 -3
  56. pygeodesy/wgrs.py +109 -101
  57. {PyGeodesy-24.7.24.dist-info → PyGeodesy-24.8.24.dist-info}/top_level.txt +0 -0
pygeodesy/units.py CHANGED
@@ -11,11 +11,11 @@ from pygeodesy.constants import EPS, EPS1, PI, PI2, PI_2, _umod_360, _0_0, \
11
11
  _0_001, _0_5, INT0 # PYCHOK for .mgrs, .namedTuples
12
12
  from pygeodesy.dms import F__F, F__F_, S_NUL, S_SEP, parseDMS, parseRad, _toDMS
13
13
  from pygeodesy.errors import _AssertionError, TRFError, UnitError, _xattr, _xcallable
14
- from pygeodesy.interns import NN, _band_, _bearing_, _COMMASPACE_, _degrees_, \
15
- _degrees2_, _distance_, _E_, _easting_, _epoch_, _EW_, \
16
- _feet_, _height_, _lam_, _lat_, _LatLon_, _lon_, \
17
- _meter_, _meter2_, _N_, _negative_, _northing_, _NS_, \
18
- _NSEW_, _number_, _PERCENT_, _phi_, _precision_, \
14
+ from pygeodesy.interns import NN, _azimuth_, _band_, _bearing_, _COMMASPACE_, \
15
+ _degrees_, _degrees2_, _distance_, _E_, _easting_, \
16
+ _epoch_, _EW_, _feet_, _height_, _lam_, _lat_, _LatLon_, \
17
+ _lon_, _meter_, _meter2_, _N_, _negative_, _northing_, \
18
+ _NS_, _NSEW_, _number_, _PERCENT_, _phi_, _precision_, \
19
19
  _radians_, _radians2_, _radius_, _S_, _scalar_, \
20
20
  _units_, _W_, _zone_, _std_ # PYCHOK used!
21
21
  from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS, _getenv
@@ -24,10 +24,10 @@ from pygeodesy.props import Property_RO
24
24
  # from pygeodesy.streprs import Fmt, fstr # from .unitsBase
25
25
  from pygeodesy.unitsBase import Float, Int, _NamedUnit, Radius, Str, Fmt, fstr
26
26
 
27
- from math import degrees, radians
27
+ from math import degrees, isnan, radians
28
28
 
29
29
  __all__ = _ALL_LAZY.units
30
- __version__ = '24.06.29'
30
+ __version__ = '24.08.13'
31
31
 
32
32
 
33
33
  class Float_(Float):
@@ -193,7 +193,7 @@ class Degrees(Float):
193
193
  def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
194
194
  '''Return a representation of this C{Degrees}.
195
195
 
196
- @kwarg std: If C{True} return the standard C{repr}, otherwise
196
+ @kwarg std: If C{True}, return the standard C{repr}, otherwise
197
197
  the named representation (C{bool}).
198
198
 
199
199
  @see: Methods L{Degrees.toStr}, L{Float.toRepr} and function
@@ -290,7 +290,7 @@ class Radians(Float):
290
290
  def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
291
291
  '''Return a representation of this C{Radians}.
292
292
 
293
- @kwarg std: If C{True} return the standard C{repr}, otherwise
293
+ @kwarg std: If C{True}, return the standard C{repr}, otherwise
294
294
  the named representation (C{bool}).
295
295
 
296
296
  @see: Methods L{Radians.toStr}, L{Float.toRepr} and function
@@ -339,18 +339,35 @@ class Radians2(Float_):
339
339
  return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg)
340
340
 
341
341
 
342
+ def _Degrees_new(cls, **arg_name_suffix_clip_Error_name_arg):
343
+ d = Degrees.__new__(cls, **arg_name_suffix_clip_Error_name_arg)
344
+ b = _umod_360(d) # 0 <= b < 360
345
+ return d if b == d else Degrees.__new__(cls, arg=b, name=d.name)
346
+
347
+
348
+ class Azimuth(Degrees):
349
+ '''Named C{float} representing an azimuth in compass C{degrees} from (true) North.
350
+ '''
351
+ _ddd_ = 1
352
+ _suf_ = _W_, S_NUL, _E_ # no zero suffix
353
+
354
+ def __new__(cls, arg=None, name=_azimuth_, **clip_Error_name_arg):
355
+ '''New, named L{Azimuth} with optional suffix 'E' for clockwise or 'W' for
356
+ anti-clockwise, see L{Degrees}.
357
+ '''
358
+ return _Degrees_new(cls, arg=arg, name=name, suffix=_EW_, **clip_Error_name_arg)
359
+
360
+
342
361
  class Bearing(Degrees):
343
362
  '''Named C{float} representing a bearing in compass C{degrees} from (true) North.
344
363
  '''
345
364
  _ddd_ = 1
346
365
  _suf_ = _N_ * 3 # always suffix N
347
366
 
348
- def __new__(cls, arg=None, name=_bearing_, clip=0, **Error_name_arg):
367
+ def __new__(cls, arg=None, name=_bearing_, **clip_Error_name_arg):
349
368
  '''New, named L{Bearing}, see L{Degrees}.
350
369
  '''
351
- d = Degrees.__new__(cls, arg=arg, name=name, suffix=_N_, clip=clip, **Error_name_arg)
352
- b = _umod_360(d) # 0 <= b < 360
353
- return d if b == d else Degrees.__new__(cls, arg=b, name=d.name)
370
+ return _Degrees_new(cls, arg=arg, name=name, suffix=_N_, **clip_Error_name_arg)
354
371
 
355
372
 
356
373
  class Bearing_(Radians):
@@ -782,7 +799,7 @@ class Zone(Int):
782
799
 
783
800
 
784
801
  _ScalarU = Float, Float_, Scalar, Scalar_
785
- _Degrees = (Bearing, Bearing_, Degrees, Degrees_) + _ScalarU
802
+ _Degrees = (Azimuth, Bearing, Bearing_, Degrees, Degrees_) + _ScalarU
786
803
  _Meters = (Distance, Distance_, Meter, Meter_) + _ScalarU
787
804
  _Radians = (Radians, Radians_) + _ScalarU # PYCHOK unused
788
805
  _Radii = _Meters + (Radius, Radius_)
@@ -794,7 +811,7 @@ def _isDegrees(obj):
794
811
 
795
812
 
796
813
  def _isHeight(obj):
797
- # Check for valid heigth types.
814
+ # Check for valid height types.
798
815
  return isinstance(obj, _Meters) or _isScalar(obj)
799
816
 
800
817
 
@@ -823,13 +840,13 @@ def _toUnit(Unit, arg, name=NN, **Error):
823
840
 
824
841
 
825
842
  def _xlimits(arg, low, high, g=False):
826
- '''(INTERNAL) Check C{low <= unit <= high}.
843
+ '''(INTERNAL) Check C{low <= arg <= high}.
827
844
  '''
828
- if (low is not None) and arg < low:
845
+ if (low is not None) and (arg < low or isnan(arg)):
829
846
  if g:
830
847
  low = Fmt.g(low, prec=6, ints=isinstance(arg, Epoch))
831
848
  t = Fmt.limit(below=low)
832
- elif (high is not None) and arg > high:
849
+ elif (high is not None) and (arg > high or isnan(arg)):
833
850
  if g:
834
851
  high = Fmt.g(high, prec=6, ints=isinstance(arg, Epoch))
835
852
  t = Fmt.limit(above=high)
@@ -847,7 +864,7 @@ def _std_repr(*Classes):
847
864
  if _getenv(env, _std_).lower() != _std_:
848
865
  C._std_repr = False
849
866
 
850
- _std_repr(Bearing, Bool, Degrees, Float, Int, Meter, Radians, Str) # PYCHOK expected
867
+ _std_repr(Azimuth, Bearing, Bool, Degrees, Float, Int, Meter, Radians, Str) # PYCHOK expected
851
868
  del _std_repr
852
869
 
853
870
  __all__ += _ALL_DOCS(_NamedUnit)
pygeodesy/unitsBase.py CHANGED
@@ -15,7 +15,7 @@ from pygeodesy.named import modulename, _Named, property_doc_
15
15
  from pygeodesy.streprs import Fmt, fstr
16
16
 
17
17
  __all__ = _ALL_LAZY.unitsBase
18
- __version__ = '24.06.15'
18
+ __version__ = '24.08.13'
19
19
 
20
20
 
21
21
  class _NamedUnit(_Named):
@@ -156,7 +156,7 @@ class Float(float, _NamedUnit):
156
156
  def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
157
157
  '''Return a representation of this C{Float}.
158
158
 
159
- @kwarg std: If C{True} return the standard C{repr},
159
+ @kwarg std: If C{True}, return the standard C{repr},
160
160
  otherwise the named representation (C{bool}).
161
161
 
162
162
  @see: Function L{fstr<pygeodesy.streprs.fstr>} and methods
@@ -216,7 +216,7 @@ class Int(int, _NamedUnit):
216
216
  def toRepr(self, std=False, **unused): # PYCHOK **unused
217
217
  '''Return a representation of this C{Int}.
218
218
 
219
- @kwarg std: If C{True} return the standard C{repr},
219
+ @kwarg std: If C{True}, return the standard C{repr},
220
220
  otherwise the named representation (C{bool}).
221
221
 
222
222
  @see: Method L{Int.__repr__} for more documentation.
@@ -298,7 +298,7 @@ class Str(str, _NamedUnit):
298
298
  def toRepr(self, std=False, **unused): # PYCHOK **unused
299
299
  '''Return a representation of this C{Str}.
300
300
 
301
- @kwarg std: If C{True} return the standard C{repr},
301
+ @kwarg std: If C{True}, return the standard C{repr},
302
302
  otherwise the named representation (C{bool}).
303
303
 
304
304
  @see: Method L{Str.__repr__} for more documentation.
pygeodesy/utmupsBase.py CHANGED
@@ -27,7 +27,7 @@ from pygeodesy.units import Band, Easting, Northing, Scalar, Zone
27
27
  from pygeodesy.utily import _Wrap, wrap360
28
28
 
29
29
  __all__ = _ALL_LAZY.utmupsBase
30
- __version__ = '24.06.12'
30
+ __version__ = '24.08.13'
31
31
 
32
32
  _UPS_BANDS = _A_, _B_, _Y_, _Z_ # UPS polar bands SE, SW, NE, NW
33
33
  # _UTM_BANDS = _MODS.utm._Bands
@@ -157,8 +157,8 @@ class UtmUpsBase(_NamedBase):
157
157
  def eastingnorthing2(self, falsed=True):
158
158
  '''Return easting and northing, falsed or unfalsed.
159
159
 
160
- @kwarg falsed: If C{True} return easting and northing falsed
161
- (C{bool}), otherwise unfalsed.
160
+ @kwarg falsed: If C{True}, return easting and northing falsed,
161
+ otherwise unfalsed (C{bool}).
162
162
 
163
163
  @return: An L{EasNor2Tuple}C{(easting, northing)} in C{meter}.
164
164
  '''
pygeodesy/vector2d.py CHANGED
@@ -2,7 +2,8 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  u'''2- or 3-D vectorial functions L{circin6}, L{circum3}, L{circum4_},
5
- L{iscolinearWith}, L{meeus2}, L{nearestOn}, L{radii11} and L{soddy4}.
5
+ L{iscolinearWith}, L{meeus2}, L{nearestOn}, L{radii11}, L{soddy4} and
6
+ L{trilaterate2d2}.
6
7
  '''
7
8
 
8
9
  from pygeodesy.basics import len2, map2, _xnumpy
@@ -23,14 +24,14 @@ from pygeodesy.namedTuples import LatLon3Tuple, Vector2Tuple
23
24
  # from pygeodesy.props import Property_RO # from .named
24
25
  from pygeodesy.streprs import Fmt, unstr
25
26
  from pygeodesy.units import Float, Int, Meter, Radius, Radius_
26
- from pygeodesy.vector3d import iscolinearWith, nearestOn, _nearestOn2, _nVc, _otherV3d, \
27
- trilaterate2d2, trilaterate3d2, Vector3d # PYCHOK unused
27
+ from pygeodesy.vector3d import iscolinearWith, nearestOn, _nearestOn2, _nVc, \
28
+ _otherV3d, trilaterate3d2, Vector3d # PYCHOK unused
28
29
 
29
30
  from contextlib import contextmanager
30
31
  # from math import fabs, sqrt # from .fmath
31
32
 
32
33
  __all__ = _ALL_LAZY.vector2d
33
- __version__ = '24.05.17'
34
+ __version__ = '24.08.19'
34
35
 
35
36
  _cA_ = 'cA'
36
37
  _cB_ = 'cB'
@@ -115,6 +116,16 @@ class Soddy4Tuple(_NamedTuple):
115
116
  _Units_ = ( Radius, _Pass, _Pass, Radius)
116
117
 
117
118
 
119
+ class Triaxum5Tuple(_NamedTuple):
120
+ '''5-Tuple C{(a, b, c, rank, residuals)} with the (unordered) triaxial radii
121
+ C{a}, C{b} and C{c} of an ellipsoid I{least-squares} fitted through given
122
+ points and the C{rank} and C{residuals} -if any- from U{numpy.linalg.lstsq
123
+ <https://NumPy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html>}.
124
+ '''
125
+ _Names_ = (_a_, _b_, _c_, _rank_, _residuals_)
126
+ _Units_ = ( Radius, Radius, Radius, Int, _Pass)
127
+
128
+
118
129
  def circin6(point1, point2, point3, eps=EPS4, useZ=True):
119
130
  '''Return the radius and center of the I{inscribed} aka I{Incircle} of
120
131
  a (2- or 3-D) triangle.
@@ -187,7 +198,7 @@ def circum3(point1, point2, point3, circum=True, eps=EPS4, useZ=True):
187
198
  C{Vector4Tuple}).
188
199
  @arg point3: Third point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple} or
189
200
  C{Vector4Tuple}).
190
- @kwarg circum: If C{True} return the C{circumradius} and C{circumcenter}
201
+ @kwarg circum: If C{True}, return the C{circumradius} and C{circumcenter}
191
202
  always, ignoring the I{Meeus}' Type I case (C{bool}).
192
203
  @kwarg eps: Tolerance for function L{pygeodesy.trilaterate3d2} if C{B{useZ}
193
204
  is True} else L{pygeodesy.trilaterate2d2}.
@@ -232,16 +243,16 @@ def _circum3(p1, point2, point3, circum=True, eps=EPS4, useZ=True, dLL3=False,
232
243
  return Circum3Tuple(r, c, d)
233
244
 
234
245
 
235
- def circum4_(*points, **useZ_Vector_and_kwds):
246
+ def circum4(points, useZ=True, **Vector_and_kwds):
236
247
  '''Best-fit a sphere through three or more (3-D) points.
237
248
 
238
- @arg points: The points (each a C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
249
+ @arg points: Iterable of points (each a C{Cartesian}, L{Vector3d}, C{Vector3Tuple}
239
250
  or C{Vector4Tuple}).
240
- @kwarg useZ_Vector_and_kwds: Keyword arguments C{B{useZ}=True} (C{bool})
241
- to use the Z components, otherwise force all C{z=INT0}, class
242
- C{B{Vector}=None} to return the center point with optionally,
243
- additional nB{C{Vector}} keyword arguments, otherwise the
244
- first B{C{points}}' (sub-)class is used.
251
+ @kwarg useZ: If C{True}, use the points' Z component, otherwise force C{z=INT0}
252
+ (C{bool}).
253
+ @kwarg Vector_and_kwds: Optional class C{B{Vector}=None} to return the center point
254
+ and optional, additional B{C{Vector}} keyword arguments, otherwise
255
+ the first B{C{points}}' (sub-)class is used.
245
256
 
246
257
  @return: L{Circum4Tuple}C{(radius, center, rank, residuals)} with C{center} an
247
258
  instance of C{B{points}[0]}' (sub-)class or B{C{Vector}} if specified.
@@ -255,40 +266,47 @@ def circum4_(*points, **useZ_Vector_and_kwds):
255
266
 
256
267
  @raise TypeError: One of the B{C{points}} is invalid.
257
268
 
258
- @see: Functions L{pygeodesy.circum3} and L{pygeodesy.meeus2}, Jekel, Charles F. U{I{Least
259
- Squares Sphere Fit}<https://Jekel.me/2015/Least-Squares-Sphere-Fit/>} Sep 13, 2015,
260
- U{Appendix A<https://hdl.handle.net/10019.1/98627>}, U{numpy.linalg.lstsq<https://
261
- NumPy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html>} and U{Eberly 6
262
- <https://www.sci.Utah.EDU/~balling/FEtools/doc_files/LeastSquaresFitting.pdf>}.
269
+ @see: Functions L{pygeodesy.circum3} and L{pygeodesy.meeus2}, I{Charles Jekel}'s
270
+ U{"Least Squares Sphere Fit"<https://Jekel.me/2015/Least-Squares-Sphere-Fit/>},
271
+ U{Appendix A<https://hdl.handle.net/10019.1/98627>}, U{numpy.linalg.lstsq
272
+ <https://NumPy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html>} and U{Eberly
273
+ 6<https://www.sci.Utah.EDU/~balling/FEtools/doc_files/LeastSquaresFitting.pdf>}.
263
274
  '''
264
- def _useZ_kwds(useZ=True, **kwds):
265
- return useZ, kwds
266
-
267
275
  n, ps = len2(points)
268
276
  if n < 3:
269
277
  raise PointsError(points=n, txt=_too_(_few_))
270
- useZ, kwds = _useZ_kwds(**useZ_Vector_and_kwds)
271
278
 
272
279
  A, b = [], []
273
280
  for i, p in enumerate(ps):
274
281
  v = _otherV3d(useZ=useZ, i=i, points=p)
275
- A.append(v.times(_2_0).xyz + _1_0_1T)
282
+ A.append(v.times(_2_0).xyz3 + _1_0_1T)
276
283
  b.append(v.length2)
277
284
 
278
- with _numpy(circum4_, n=n) as _np:
285
+ with _numpy(circum4, n=n) as _np:
279
286
  A = _np.array(A).reshape((n, 4))
280
287
  b = _np.array(b).reshape((n, 1))
281
- C, R, rk, _ = _np.least_squares4(A, b, rcond=None) # to silence warning
282
- C = map2(float, C)
283
- R = map2(float, R) # empty if rk < 4 or n <= 4
288
+ C, R, rk = _np.least_squares3(A, b)
284
289
 
285
- c = Vector3d(*C[:3], name__=circum4_) # .__name__
290
+ c = Vector3d(*C[:3], name__=circum4) # .__name__
286
291
  r = Radius(sqrt(fsumf_(C[3], *c.x2y2z2)), name=c.name)
287
292
 
288
- c = _nVc(c, **_xkwds(kwds, clas=ps[0].classof, name=c.name))
293
+ c = _nVc(c, **_xkwds(Vector_and_kwds, clas=ps[0].classof, name=c.name))
289
294
  return Circum4Tuple(r, c, rk, R)
290
295
 
291
296
 
297
+ def circum4_(*points, **useZ_Vector_and_kwds):
298
+ '''Best-fit a sphere through three or more (3-D) positional points.
299
+
300
+ @arg points: The points (each a C{Cartesian}, L{Vector3d}, C{Vector3Tuple}
301
+ or C{Vector4Tuple}), all positional.
302
+ @kwarg useZ_Vector_and_kwds: Keyword arguments C{B{useZ}=True} and
303
+ C{B{Vector}=None}, see function L{circum4}.
304
+
305
+ @see: Function L{circum4} for further details.
306
+ '''
307
+ return circum4(points, **useZ_Vector_and_kwds)
308
+
309
+
292
310
  def _iscolinearWith(p, point1, point2, eps=EPS, useZ=True):
293
311
  # (INTERNAL) Check colinear, see L{iscolinearWith} above,
294
312
  # separated to allow callers to embellish any exceptions
@@ -308,7 +326,7 @@ def meeus2(point1, point2, point3, circum=False, useZ=True):
308
326
  C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
309
327
  @arg point3: Third point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
310
328
  C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
311
- @kwarg circum: If C{True} return the C{circumradius} and C{circumcenter}
329
+ @kwarg circum: If C{True}, return the C{circumradius} and C{circumcenter}
312
330
  always, overriding I{Meeus}' Type II case (C{bool}).
313
331
  @kwarg useZ: If C{True}, use the Z components, otherwise force C{z=INT0} (C{bool}).
314
332
 
@@ -359,8 +377,9 @@ def _meeus4(A, point2, point3, circum=False, useZ=True, clas=None, **clas_kwds):
359
377
  _Fsumf_(_1_0, -b, c) * _Fsumf_(_N_1_0, b, c)
360
378
  r = R.fover(a)
361
379
  if r < EPS02:
362
- raise IntersectionError(_coincident_ if b < EPS0 or c < EPS0 else (
363
- _colinear_ if _iscolinearWith(A, B, C) else _invalid_))
380
+ t = _coincident_ if b < EPS0 or c < EPS0 else (
381
+ _colinear_ if _iscolinearWith(A, B, C) else _invalid_)
382
+ raise IntersectionError(t)
364
383
  r = b * c / sqrt(r)
365
384
  t = None # Meeus' Type II
366
385
  else: # obtuse or right angle at A
@@ -393,11 +412,13 @@ class _numpy(object): # see also .formy._idllmn6, .geodesicw._wargs, .latlonBas
393
412
  def array(self):
394
413
  return self.np.array
395
414
 
396
- @Property_RO
397
- def least_squares4(self):
415
+ def least_squares3(self, A, b):
398
416
  '''Linear least-squares function.
399
417
  '''
400
- return self.np.linalg.lstsq
418
+ C, R, rk, _ = self.np.linalg.lstsq(A, b, rcond=None) # to silence warning
419
+ C = map2(float, C)
420
+ R = map2(float, R) # empty if rk < 4 or n <= 4
421
+ return C, R, int(rk)
401
422
 
402
423
  @Property_RO
403
424
  def np(self):
@@ -578,6 +599,49 @@ def soddy4(point1, point2, point3, eps=EPS4, useZ=True):
578
599
  return Soddy4Tuple(r, c, d, t.roS)
579
600
 
580
601
 
602
+ def triaxum5(points, useZ=True):
603
+ '''Best-fit a triaxial ellipsoid through three or more (3-D) points.
604
+
605
+ @arg points: Iterable of points (each a C{Cartesian}, L{Vector3d}, C{Vector3Tuple}
606
+ or C{Vector4Tuple}).
607
+ @kwarg useZ: If C{True}, use the points' Z component, otherwise force C{z=INT0}
608
+ (C{bool}).
609
+
610
+ @return: L{Triaxum5Tuple}C{(a, b, c, rank, residuals)} with the unordered triaxial
611
+ radii C{a}, C{b} and C{c} in C{meter}, same units as the points' coordinates.
612
+
613
+ @raise ImportError: Package C{numpy} not found, not installed or older than version 1.10.
614
+
615
+ @raise NumPyError: Some C{numpy} issue.
616
+
617
+ @raise PointsError: Too few B{C{points}}.
618
+
619
+ @raise TypeError: One of the B{C{points}} is invalid.
620
+
621
+ @see: I{Charles Jekel}'s U{"Least Squares Ellipsoid Fit"<https://Jekel.me/2020/Least-Squares-Ellipsoid-Fit/>}
622
+ and U{numpy.linalg.lstsq<https://NumPy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html>}.
623
+ '''
624
+ n, ps = len2(points)
625
+ if n < 3:
626
+ raise PointsError(points=n, txt=_too_(_few_))
627
+
628
+ A = []
629
+ for i, p in enumerate(ps):
630
+ v = _otherV3d(useZ=useZ, i=i, points=p)
631
+ A.append(v.x2y2z2)
632
+
633
+ with _numpy(triaxum5, n=n) as _np:
634
+ A = _np.array(A)
635
+ b = _1_0_1T * n
636
+ T, R, rk = _np.least_squares3(A, b)
637
+
638
+ def _sqrt(x):
639
+ return sqrt(_1_0 / x) if x else _0_0
640
+
641
+ a, b, c = map(_sqrt, T)
642
+ return Triaxum5Tuple(a, b, c, rk, R)
643
+
644
+
581
645
  def _tricenter3d2(p1, r1, p2, r2, p3, r3, eps=EPS4, useZ=True, dLL3=False, **kwds):
582
646
  # (INTERNAL) Trilaterate and disambiguate the 3-D center
583
647
  d, kwds = None, _xkwds(kwds, eps=eps, coin=True)
@@ -605,6 +669,45 @@ def _tricenter3d2(p1, r1, p2, r2, p3, r3, eps=EPS4, useZ=True, dLL3=False, **kwd
605
669
  return c, d
606
670
 
607
671
 
672
+ def trilaterate2d2(x1, y1, radius1, x2, y2, radius2, x3, y3, radius3,
673
+ eps=None, **Vector_and_kwds):
674
+ '''Trilaterate three circles, each given as a (2-D) center and a radius.
675
+
676
+ @arg x1: Center C{x} coordinate of the 1st circle (C{scalar}).
677
+ @arg y1: Center C{y} coordinate of the 1st circle (C{scalar}).
678
+ @arg radius1: Radius of the 1st circle (C{scalar}).
679
+ @arg x2: Center C{x} coordinate of the 2nd circle (C{scalar}).
680
+ @arg y2: Center C{y} coordinate of the 2nd circle (C{scalar}).
681
+ @arg radius2: Radius of the 2nd circle (C{scalar}).
682
+ @arg x3: Center C{x} coordinate of the 3rd circle (C{scalar}).
683
+ @arg y3: Center C{y} coordinate of the 3rd circle (C{scalar}).
684
+ @arg radius3: Radius of the 3rd circle (C{scalar}).
685
+ @kwarg eps: Tolerance to check the trilaterated point I{delta} on
686
+ all 3 circles (C{scalar}) or C{None} for no checking.
687
+ @kwarg Vector_and_kwds: Optional class C{B{Vector}=None} to return
688
+ the trilateration and optional, additional
689
+ B{C{Vector}} keyword arguments).
690
+
691
+ @return: Trilaterated point as C{B{Vector}(x, y, **B{Vector_kwds})}
692
+ or L{Vector2Tuple}C{(x, y)} if C{B{Vector} is None}.
693
+
694
+ @raise IntersectionError: No intersection, near-concentric or -colinear
695
+ centers, trilateration failed some other way
696
+ or the trilaterated point is off one circle
697
+ by more than B{C{eps}}.
698
+
699
+ @raise UnitError: Invalid B{C{radius1}}, B{C{radius2}} or B{C{radius3}}.
700
+
701
+ @see: U{Issue #49<https://GitHub.com/mrJean1/PyGeodesy/issues/49>},
702
+ U{Find X location using 3 known (X,Y) location using trilateration
703
+ <https://math.StackExchange.com/questions/884807>} and function
704
+ L{pygeodesy.trilaterate3d2}.
705
+ '''
706
+ return _trilaterate2d2(x1, y1, radius1,
707
+ x2, y2, radius2,
708
+ x3, y3, radius3, eps=eps, **Vector_and_kwds)
709
+
710
+
608
711
  def _trilaterate2d2(x1, y1, radius1, x2, y2, radius2, x3, y3, radius3,
609
712
  coin=False, eps=None,
610
713
  Vector=None, **Vector_kwds):
@@ -662,16 +765,21 @@ def _trilaterate2d2(x1, y1, radius1, x2, y2, radius2, x3, y3, radius3,
662
765
  return t
663
766
 
664
767
 
665
- def _trilaterate3d2(c1, r1, c2, r2, c3, r3, eps=EPS4, coin=False,
768
+ def _trilaterate3d2(c1, r1, c2, r2, c3, r3, eps=EPS4, coin=False, # MCCABE 13
666
769
  **clas_Vector_and_kwds):
667
770
  # (INTERNAL) Intersect three spheres or circles, see function
668
771
  # L{pygeodesy.trilaterate3d2}, separated to allow callers to
669
772
  # embellish exceptions, like C{FloatingPointError}s from C{numpy}
670
773
 
671
- def _F3d2(F):
672
- # map numpy 4-vector to floats tuple and Vector3d
774
+ def _Arow4(c):
775
+ # make a row for matrix A (1, -2x, -2y, -2z)
776
+ return _1_0_1T + c.times(_N_2_0).xyz3
777
+
778
+ def _F4d3(F):
779
+ # map numpy 4-vector to floats and xyz3
673
780
  T = map2(float, F)
674
- return T, Vector3d(*T[1:])
781
+ t = T[1:]
782
+ return T, t, Vector3d(*t)
675
783
 
676
784
  def _N3(t01, x, z):
677
785
  # compute x, y and z and return as B{C{clas}} or B{C{Vector}}
@@ -684,22 +792,22 @@ def _trilaterate3d2(c1, r1, c2, r2, c3, r3, eps=EPS4, coin=False,
684
792
  rs = (r1, Radius_(radius2=r2, low=EPS),
685
793
  Radius_(radius3=r3, low=EPS))
686
794
 
687
- # get matrix A[3 x 4], its pseudo-inverse and null_space Z
688
- A = [(_1_0_1T + c.times(_N_2_0).xyz) for c in (c1, c2, c3)]
795
+ # get matrix A[3 x 4], its null_space Z and pseudo-inverse
796
+ A = [_Arow4(c) for c in (c1, c2, c3)]
689
797
  with _numpy(trilaterate3d2, A=A, eps=eps) as _np:
690
798
  Z, _ = _np.null_space2(A, eps)
691
799
  if Z is not None:
692
- Z, z = _F3d2(Z) # [4 x 1]
800
+ Z, _, z = _F4d3(Z) # [4 x 1]
693
801
  z2 = z.length2
694
802
  A = _np.pseudo_inverse(A) # [4 x 3]
695
803
  bs = [c.length2 for c in (c1, c2, c3)]
696
- # perturbe radii and vector b slightly by eps and eps * 4
804
+ # perturb radii slightly by eps and eps * 4
697
805
  for p in _tri5perturbs(eps, min(rs)):
698
806
  b = [((r + p)**2 - b) for r, b in zip(rs, bs)] # [3 x 1]
699
- X, x = _F3d2(A.dot(b))
700
- # quadratic polynomial, coefficients ordered (^0, ^1, ^2)
701
- t = _np.real_roots(fdot(X, _N_1_0, *x.xyz),
702
- fdot(Z, _N_0_5, *x.xyz) * _2_0, z2)
807
+ X, t, x = _F4d3(A.dot(b)) # [4 * 1]
808
+ # quadratic polynomial, coefficients order (^0, ^1, ^2)
809
+ t = _np.real_roots(fdot(X, _N_1_0, *t),
810
+ fdot(Z, _N_0_5, *t) * _2_0, z2)
703
811
  if t:
704
812
  v = _N3(t[0], x, z)
705
813
  if len(t) < 2: # one intersection
@@ -711,10 +819,7 @@ def _trilaterate3d2(c1, r1, c2, r2, c3, r3, eps=EPS4, coin=False,
711
819
  t = (u, v) if u.x < v.x else (v, u)
712
820
  return t
713
821
 
714
- # coincident, concentric, colinear, too distant, no intersection:
715
- # create the explanation and and throw an IntersectionError
716
-
717
- def _no_intersection(coin):
822
+ def _no_itersection(coin, Z):
718
823
  t = _no_(_intersection_)
719
824
  if coin:
720
825
  def _reprs(*crs):
@@ -726,11 +831,13 @@ def _trilaterate3d2(c1, r1, c2, r2, c3, r3, eps=EPS4, coin=False,
726
831
  t = _COMMASPACE_(t, _no_(_numpy.null_space2.__name__))
727
832
  return t
728
833
 
834
+ # coincident, concentric, colinear, too distant, no intersection:
835
+ # create the explanation and and throw an IntersectionError
729
836
  t = _tri4near2far(c1, r1, c2, r2, coin) or \
730
837
  _tri4near2far(c1, r1, c3, r3, coin) or \
731
838
  _tri4near2far(c2, r2, c3, r3, coin) or (
732
839
  _colinear_ if _iscolinearWith(c1, c2, c3, eps=eps) else
733
- _no_intersection(coin))
840
+ _no_itersection(coin, Z))
734
841
  raise IntersectionError(t, txt=None)
735
842
 
736
843