pygeodesy 24.3.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 (115) hide show
  1. PyGeodesy-24.3.24.dist-info/METADATA +272 -0
  2. PyGeodesy-24.3.24.dist-info/RECORD +115 -0
  3. PyGeodesy-24.3.24.dist-info/WHEEL +6 -0
  4. PyGeodesy-24.3.24.dist-info/top_level.txt +1 -0
  5. pygeodesy/LICENSE +21 -0
  6. pygeodesy/__init__.py +615 -0
  7. pygeodesy/__main__.py +103 -0
  8. pygeodesy/albers.py +867 -0
  9. pygeodesy/auxilats/_CX_4.py +218 -0
  10. pygeodesy/auxilats/_CX_6.py +314 -0
  11. pygeodesy/auxilats/_CX_8.py +475 -0
  12. pygeodesy/auxilats/__init__.py +54 -0
  13. pygeodesy/auxilats/__main__.py +86 -0
  14. pygeodesy/auxilats/auxAngle.py +548 -0
  15. pygeodesy/auxilats/auxDLat.py +302 -0
  16. pygeodesy/auxilats/auxDST.py +296 -0
  17. pygeodesy/auxilats/auxLat.py +848 -0
  18. pygeodesy/auxilats/auxily.py +272 -0
  19. pygeodesy/azimuthal.py +1150 -0
  20. pygeodesy/basics.py +892 -0
  21. pygeodesy/booleans.py +2031 -0
  22. pygeodesy/cartesianBase.py +1062 -0
  23. pygeodesy/clipy.py +704 -0
  24. pygeodesy/constants.py +516 -0
  25. pygeodesy/css.py +660 -0
  26. pygeodesy/datums.py +752 -0
  27. pygeodesy/deprecated/__init__.py +61 -0
  28. pygeodesy/deprecated/bases.py +40 -0
  29. pygeodesy/deprecated/classes.py +262 -0
  30. pygeodesy/deprecated/consterns.py +54 -0
  31. pygeodesy/deprecated/datum.py +40 -0
  32. pygeodesy/deprecated/functions.py +375 -0
  33. pygeodesy/deprecated/nvector.py +48 -0
  34. pygeodesy/deprecated/rhumbBase.py +32 -0
  35. pygeodesy/deprecated/rhumbaux.py +33 -0
  36. pygeodesy/deprecated/rhumbsolve.py +33 -0
  37. pygeodesy/deprecated/rhumbx.py +33 -0
  38. pygeodesy/dms.py +986 -0
  39. pygeodesy/ecef.py +1348 -0
  40. pygeodesy/elevations.py +279 -0
  41. pygeodesy/ellipsoidalBase.py +1224 -0
  42. pygeodesy/ellipsoidalBaseDI.py +913 -0
  43. pygeodesy/ellipsoidalExact.py +343 -0
  44. pygeodesy/ellipsoidalGeodSolve.py +343 -0
  45. pygeodesy/ellipsoidalKarney.py +403 -0
  46. pygeodesy/ellipsoidalNvector.py +685 -0
  47. pygeodesy/ellipsoidalVincenty.py +590 -0
  48. pygeodesy/ellipsoids.py +2476 -0
  49. pygeodesy/elliptic.py +1198 -0
  50. pygeodesy/epsg.py +243 -0
  51. pygeodesy/errors.py +804 -0
  52. pygeodesy/etm.py +1190 -0
  53. pygeodesy/fmath.py +1013 -0
  54. pygeodesy/formy.py +1818 -0
  55. pygeodesy/frechet.py +865 -0
  56. pygeodesy/fstats.py +760 -0
  57. pygeodesy/fsums.py +1898 -0
  58. pygeodesy/gars.py +358 -0
  59. pygeodesy/geodesicw.py +581 -0
  60. pygeodesy/geodesicx/_C4_24.py +1699 -0
  61. pygeodesy/geodesicx/_C4_27.py +2395 -0
  62. pygeodesy/geodesicx/_C4_30.py +3301 -0
  63. pygeodesy/geodesicx/__init__.py +48 -0
  64. pygeodesy/geodesicx/__main__.py +91 -0
  65. pygeodesy/geodesicx/gx.py +1382 -0
  66. pygeodesy/geodesicx/gxarea.py +535 -0
  67. pygeodesy/geodesicx/gxbases.py +154 -0
  68. pygeodesy/geodesicx/gxline.py +669 -0
  69. pygeodesy/geodsolve.py +426 -0
  70. pygeodesy/geohash.py +914 -0
  71. pygeodesy/geoids.py +1884 -0
  72. pygeodesy/hausdorff.py +892 -0
  73. pygeodesy/heights.py +1155 -0
  74. pygeodesy/interns.py +687 -0
  75. pygeodesy/iters.py +545 -0
  76. pygeodesy/karney.py +919 -0
  77. pygeodesy/ktm.py +633 -0
  78. pygeodesy/latlonBase.py +1766 -0
  79. pygeodesy/lazily.py +960 -0
  80. pygeodesy/lcc.py +684 -0
  81. pygeodesy/ltp.py +1107 -0
  82. pygeodesy/ltpTuples.py +1563 -0
  83. pygeodesy/mgrs.py +721 -0
  84. pygeodesy/named.py +1324 -0
  85. pygeodesy/namedTuples.py +683 -0
  86. pygeodesy/nvectorBase.py +695 -0
  87. pygeodesy/osgr.py +781 -0
  88. pygeodesy/points.py +1686 -0
  89. pygeodesy/props.py +628 -0
  90. pygeodesy/resections.py +1048 -0
  91. pygeodesy/rhumb/__init__.py +46 -0
  92. pygeodesy/rhumb/aux_.py +397 -0
  93. pygeodesy/rhumb/bases.py +1148 -0
  94. pygeodesy/rhumb/ekx.py +563 -0
  95. pygeodesy/rhumb/solve.py +572 -0
  96. pygeodesy/simplify.py +647 -0
  97. pygeodesy/solveBase.py +472 -0
  98. pygeodesy/sphericalBase.py +724 -0
  99. pygeodesy/sphericalNvector.py +1264 -0
  100. pygeodesy/sphericalTrigonometry.py +1447 -0
  101. pygeodesy/streprs.py +627 -0
  102. pygeodesy/trf.py +2079 -0
  103. pygeodesy/triaxials.py +1484 -0
  104. pygeodesy/units.py +969 -0
  105. pygeodesy/unitsBase.py +349 -0
  106. pygeodesy/ups.py +538 -0
  107. pygeodesy/utily.py +1231 -0
  108. pygeodesy/utm.py +762 -0
  109. pygeodesy/utmups.py +318 -0
  110. pygeodesy/utmupsBase.py +517 -0
  111. pygeodesy/vector2d.py +785 -0
  112. pygeodesy/vector3d.py +968 -0
  113. pygeodesy/vector3dBase.py +1049 -0
  114. pygeodesy/webmercator.py +383 -0
  115. pygeodesy/wgrs.py +439 -0
pygeodesy/vector2d.py ADDED
@@ -0,0 +1,785 @@
1
+
2
+ # -*- coding: utf-8 -*-
3
+
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}.
6
+ '''
7
+
8
+ from pygeodesy.basics import len2, map2, _xnumpy
9
+ from pygeodesy.constants import EPS, EPS0, EPS02, EPS4, INF, INT0, \
10
+ _EPS4e8, isnear0, _0_0, _0_25, _0_5, _N_0_5, \
11
+ _1_0, _1_0_1T, _N_1_0, _2_0, _N_2_0, _4_0
12
+ from pygeodesy.errors import _and, _AssertionError, IntersectionError, NumPyError, \
13
+ PointsError, TriangleError, _xError, _xkwds
14
+ from pygeodesy.fmath import fabs, fdot, hypot, hypot2_, sqrt
15
+ from pygeodesy.fsums import Fsum, fsumf_, fsum1f_
16
+ from pygeodesy.interns import NN, _a_, _and_, _b_, _c_, _center_, _coincident_, \
17
+ _colinear_, _COMMASPACE_, _concentric_, _few_, \
18
+ _intersection_, _invalid_, _near_, _no_, _of_, \
19
+ _radius_, _rIn_, _s_, _SPACE_, _too_, _with_
20
+ # from pygeodesy.lazily import _ALL_LAZY # from .named
21
+ from pygeodesy.named import _ALL_LAZY, _NamedTuple, _Pass, Property_RO
22
+ from pygeodesy.namedTuples import LatLon3Tuple, Vector2Tuple
23
+ # from pygeodesy.props import Property_RO # from .named
24
+ from pygeodesy.streprs import Fmt, unstr
25
+ 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
28
+
29
+ from contextlib import contextmanager
30
+ # from math import fabs, sqrt # from .fmath
31
+
32
+ __all__ = _ALL_LAZY.vector2d
33
+ __version__ = '23.11.17'
34
+
35
+ _cA_ = 'cA'
36
+ _cB_ = 'cB'
37
+ _cC_ = 'cC'
38
+ _deltas_ = 'deltas'
39
+ _outer_ = 'outer'
40
+ _raise_ = 'raise' # PYCHOK used!
41
+ _rank_ = 'rank'
42
+ _residuals_ = 'residuals'
43
+ _Type_ = 'Type'
44
+
45
+
46
+ class Circin6Tuple(_NamedTuple):
47
+ '''6-Tuple C{(radius, center, deltas, cA, cB, cC)} with the C{radius}, the
48
+ trilaterated C{center} and contact points of the I{inscribed} aka I{In-
49
+ circle} of a triangle. The C{center} is I{un}ambiguous if C{deltas} is
50
+ C{None}, otherwise C{center} is the mean and C{deltas} the differences of
51
+ the L{pygeodesy.trilaterate3d2} results. Contact points C{cA}, C{cB} and
52
+ C{cC} are the points of tangency, aka the corners of the U{Contact Triangle
53
+ <https://MathWorld.Wolfram.com/ContactTriangle.html>}.
54
+ '''
55
+ _Names_ = (_radius_, _center_, _deltas_, _cA_, _cB_, _cC_)
56
+ _Units_ = ( Radius, _Pass, _Pass, _Pass, _Pass, _Pass)
57
+
58
+
59
+ class Circum3Tuple(_NamedTuple): # in .latlonBase
60
+ '''3-Tuple C{(radius, center, deltas)} with the C{circumradius} and trilaterated
61
+ C{circumcenter} of the C{circumcircle} through 3 points (aka {Meeus}' Type II
62
+ circle) or the C{radius} and C{center} of the smallest I{Meeus}' Type I circle.
63
+ The C{center} is I{un}ambiguous if C{deltas} is C{None}, otherwise C{center}
64
+ is the mean and C{deltas} the differences of the L{pygeodesy.trilaterate3d2}
65
+ results.
66
+ '''
67
+ _Names_ = (_radius_, _center_, _deltas_)
68
+ _Units_ = ( Radius, _Pass, _Pass)
69
+
70
+
71
+ class Circum4Tuple(_NamedTuple):
72
+ '''4-Tuple C{(radius, center, rank, residuals)} with C{radius} and C{center}
73
+ of a sphere I{least-squares} fitted through given points and the C{rank}
74
+ and C{residuals} -if any- from U{numpy.linalg.lstsq
75
+ <https://NumPy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html>}.
76
+ '''
77
+ _Names_ = (_radius_, _center_, _rank_, _residuals_)
78
+ _Units_ = ( Radius, _Pass, Int, _Pass)
79
+
80
+
81
+ class Meeus2Tuple(_NamedTuple):
82
+ '''2-Tuple C{(radius, Type)} with C{radius} and I{Meeus}' C{Type} of the smallest
83
+ circle I{containing} 3 points. C{Type} is C{None} for a I{Meeus}' Type II
84
+ C{circumcircle} passing through all 3 points. Otherwise C{Type} is the center
85
+ of a I{Meeus}' Type I circle with 2 points on (a diameter of) and 1 point
86
+ inside the circle.
87
+ '''
88
+ _Names_ = (_radius_, _Type_)
89
+ _Units_ = ( Radius, _Pass)
90
+
91
+
92
+ class Radii11Tuple(_NamedTuple):
93
+ '''11-Tuple C{(rA, rB, rC, cR, rIn, riS, roS, a, b, c, s)} with the C{Tangent}
94
+ circle radii C{rA}, C{rB} and C{rC}, the C{circumradius} C{cR}, the C{Incircle}
95
+ radius C{rIn} aka C{inradius}, the inner and outer I{Soddy} circle radii C{riS}
96
+ and C{roS}, the sides C{a}, C{b} and C{c} and semi-perimeter C{s} of a triangle,
97
+ all in C{meter} conventionally.
98
+
99
+ @note: C{Circumradius} C{cR} and outer I{Soddy} radius C{roS} may be C{INF}.
100
+ '''
101
+ _Names_ = ('rA', 'rB', 'rC', 'cR', _rIn_, 'riS', 'roS', _a_, _b_, _c_, _s_)
102
+ _Units_ = ( Meter,) * len(_Names_)
103
+
104
+
105
+ class Soddy4Tuple(_NamedTuple):
106
+ '''4-Tuple C{(radius, center, deltas, outer)} with C{radius} and (trilaterated)
107
+ C{center} of the I{inner} I{Soddy} circle and the radius of the C{outer}
108
+ I{Soddy} circle. The C{center} is I{un}ambiguous if C{deltas} is C{None},
109
+ otherwise C{center} is the mean and C{deltas} the differences of the
110
+ L{pygeodesy.trilaterate3d2} results.
111
+
112
+ @note: The outer I{Soddy} radius C{outer} may be C{INF}.
113
+ '''
114
+ _Names_ = (_radius_, _center_, _deltas_, _outer_)
115
+ _Units_ = ( Radius, _Pass, _Pass, Radius)
116
+
117
+
118
+ def circin6(point1, point2, point3, eps=EPS4, useZ=True):
119
+ '''Return the radius and center of the I{inscribed} aka I{Incircle} of
120
+ a (2- or 3-D) triangle.
121
+
122
+ @arg point1: First point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
123
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
124
+ @arg point2: Second point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
125
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
126
+ @arg point3: Third point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
127
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
128
+ @kwarg eps: Tolerance for function L{pygeodesy.trilaterate3d2} if
129
+ C{B{useZ} is True} else L{pygeodesy.trilaterate2d2}.
130
+ @kwarg useZ: If C{True}, use the Z components, otherwise force C{z=INT0} (C{bool}).
131
+
132
+ @return: L{Circin6Tuple}C{(radius, center, deltas, cA, cB, cC)}. The
133
+ C{center} and contact points C{cA}, C{cB} and C{cC}, each an
134
+ instance of B{C{point1}}'s (sub-)class, are co-planar with
135
+ the three given points.
136
+
137
+ @raise ImportError: Package C{numpy} not found, not installed or older
138
+ than version 1.10 and C{B{useZ} is True}.
139
+
140
+ @raise IntersectionError: Near-coincident or -colinear points or
141
+ a trilateration or C{numpy} issue.
142
+
143
+ @raise TypeError: Invalid B{C{point1}}, B{C{point2}} or B{C{point3}}.
144
+
145
+ @see: Functions L{radii11} and L{circum3}, U{Contact Triangle
146
+ <https://MathWorld.Wolfram.com/ContactTriangle.html>} and
147
+ U{Incircle<https://MathWorld.Wolfram.com/Incircle.html>}.
148
+ '''
149
+ try:
150
+ return _circin6(point1, point2, point3, eps=eps, useZ=useZ)
151
+ except (AssertionError, TypeError, ValueError) as x:
152
+ raise _xError(x, point1=point1, point2=point2, point3=point3)
153
+
154
+
155
+ def _circin6(point1, point2, point3, eps=EPS4, useZ=True, dLL3=False, **Vector_kwds):
156
+ # (INTERNAL) Radius, center, deltas, 3 contact points
157
+
158
+ def _fraction(r, a):
159
+ return (r / a) if a > EPS0 else _0_5
160
+
161
+ def _contact2(a, p2, r2, p3, r3, V, V_kwds):
162
+ c = p2.intermediateTo(p3, _fraction(r2, a)) if r2 > r3 else \
163
+ p3.intermediateTo(p2, _fraction(r3, a))
164
+ C = V(c.x, c.y, c.z, **V_kwds)
165
+ return c, C
166
+
167
+ t, p1, p2, p3 = _radii11ABC(point1, point2, point3, useZ=useZ)
168
+ V, r1, r2, r3 = point1.classof, t.rA, t.rB, t.rC
169
+
170
+ c1, cA = _contact2(t.a, p2, r2, p3, r3, V, _xkwds(Vector_kwds, name=_cA_))
171
+ c2, cB = _contact2(t.b, p3, r3, p1, r1, V, _xkwds(Vector_kwds, name=_cB_))
172
+ c3, cC = _contact2(t.c, p1, r1, p2, r2, V, _xkwds(Vector_kwds, name=_cC_))
173
+
174
+ r = t.rIn
175
+ c, d = _tricenter3d2(c1, r, c2, r, c3, r, eps=eps, useZ=useZ, dLL3=dLL3,
176
+ **_xkwds(Vector_kwds, Vector=V, name=circin6.__name__))
177
+ return Circin6Tuple(r, c, d, cA, cB, cC)
178
+
179
+
180
+ def circum3(point1, point2, point3, circum=True, eps=EPS4, useZ=True):
181
+ '''Return the radius and center of the smallest circle I{through} or
182
+ I{containing} three (2- or 3-D) points.
183
+
184
+ @arg point1: First point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple} or
185
+ C{Vector4Tuple}).
186
+ @arg point2: Second point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple} or
187
+ C{Vector4Tuple}).
188
+ @arg point3: Third point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple} or
189
+ C{Vector4Tuple}).
190
+ @kwarg circum: If C{True} return the C{circumradius} and C{circumcenter}
191
+ always, ignoring the I{Meeus}' Type I case (C{bool}).
192
+ @kwarg eps: Tolerance for function L{pygeodesy.trilaterate3d2} if C{B{useZ}
193
+ is True} else L{pygeodesy.trilaterate2d2}.
194
+ @kwarg useZ: If C{True}, use the Z components, otherwise force C{z=INT0} (C{bool}).
195
+
196
+ @return: A L{Circum3Tuple}C{(radius, center, deltas)}. The C{center}, an
197
+ instance of B{C{point1}}'s (sub-)class, is co-planar with the three
198
+ given points.
199
+
200
+ @raise ImportError: Package C{numpy} not found, not installed or older
201
+ than version 1.10 and C{B{useZ} is True}.
202
+
203
+ @raise IntersectionError: Near-coincident or -colinear points or
204
+ a trilateration or C{numpy} issue.
205
+
206
+ @raise TypeError: Invalid B{C{point1}}, B{C{point2}} or B{C{point3}}.
207
+
208
+ @see: Functions L{pygeodesy.circum4_} and L{pygeodesy.meeus2} and Meeus, J.
209
+ U{I{Astronomical Algorithms}<http://www.Agopax.IT/Libri_astronomia/pdf/
210
+ Astronomical%20Algorithms.pdf>}, 2nd Ed. 1998, page 127ff, U{circumradius
211
+ <https://MathWorld.Wolfram.com/Circumradius.html>} and U{circumcircle
212
+ <https://MathWorld.Wolfram.com/Circumcircle.html>}.
213
+ '''
214
+ try:
215
+ p1 = _otherV3d(useZ=useZ, point1=point1)
216
+ return _circum3(p1, point2, point3, circum=circum, eps=eps, useZ=useZ,
217
+ clas=point1.classof)
218
+ except (AssertionError, TypeError, ValueError) as x:
219
+ raise _xError(x, point1=point1, point2=point2, point3=point3, circum=circum)
220
+
221
+
222
+ def _circum3(p1, point2, point3, circum=True, eps=EPS4, useZ=True, dLL3=False,
223
+ clas=Vector3d, **clas_kwds): # in .latlonBase
224
+ # (INTERNAL) Radius, center, deltas
225
+ r, d, p2, p3 = _meeus4(p1, point2, point3, circum=circum, useZ=useZ,
226
+ clas=clas, **clas_kwds)
227
+ if d is None: # Meeus' Type II or circum=True
228
+ kwds = _xkwds(clas_kwds, eps=eps, Vector=clas, name=circum3.__name__)
229
+ c, d = _tricenter3d2(p1, r, p2, r, p3, r, useZ=useZ, dLL3=dLL3, **kwds)
230
+ else: # Meeus' Type I
231
+ c, d = d, None
232
+ return Circum3Tuple(r, c, d)
233
+
234
+
235
+ def circum4_(*points, **useZ_Vector_and_kwds):
236
+ '''Best-fit a sphere through three or more (3-D) points.
237
+
238
+ @arg points: The points (each a C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
239
+ 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.
245
+
246
+ @return: L{Circum4Tuple}C{(radius, center, rank, residuals)} with C{center} an
247
+ instance of C{B{points}[0]}' (sub-)class or B{C{Vector}} if specified.
248
+
249
+ @raise ImportError: Package C{numpy} not found, not installed or older than
250
+ version 1.10.
251
+
252
+ @raise NumPyError: Some C{numpy} issue.
253
+
254
+ @raise PointsError: Too few B{C{points}}.
255
+
256
+ @raise TypeError: One of the B{C{points}} is invalid.
257
+
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>}.
263
+ '''
264
+ def _useZ_kwds(useZ=True, **kwds):
265
+ return useZ, kwds
266
+
267
+ n, ps = len2(points)
268
+ if n < 3:
269
+ raise PointsError(points=n, txt=_too_(_few_))
270
+ useZ, kwds = _useZ_kwds(**useZ_Vector_and_kwds)
271
+
272
+ A, b = [], []
273
+ for i, p in enumerate(ps):
274
+ v = _otherV3d(useZ=useZ, i=i, points=p)
275
+ A.append(v.times(_2_0).xyz + _1_0_1T)
276
+ b.append(v.length2)
277
+
278
+ with _numpy(circum4_, n=n) as _np:
279
+ A = _np.array(A).reshape((n, 4))
280
+ 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
284
+
285
+ n = circum4_.__name__
286
+ c = Vector3d(*C[:3], name=n)
287
+ r = Radius(sqrt(fsumf_(C[3], *c.x2y2z2)), name=n)
288
+
289
+ c = _nVc(c, **_xkwds(kwds, clas=ps[0].classof, name=n))
290
+ return Circum4Tuple(r, c, rk, R)
291
+
292
+
293
+ def _iscolinearWith(p, point1, point2, eps=EPS, useZ=True):
294
+ # (INTERNAL) Check colinear, see L{iscolinearWith} above,
295
+ # separated to allow callers to embellish any exceptions
296
+ p1 = _otherV3d(useZ=useZ, point1=point1)
297
+ p2 = _otherV3d(useZ=useZ, point2=point2)
298
+ n, _ = _nearestOn2(p, p1, p2, within=False, eps=eps)
299
+ return n is p1 or n.minus(p).length2 < eps
300
+
301
+
302
+ def meeus2(point1, point2, point3, circum=False, useZ=True):
303
+ '''Return the radius and I{Meeus}' Type of the smallest circle I{through}
304
+ or I{containing} three (2- or 3-D) points.
305
+
306
+ @arg point1: First point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
307
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
308
+ @arg point2: Second point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
309
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
310
+ @arg point3: Third point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
311
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
312
+ @kwarg circum: If C{True} return the C{circumradius} and C{circumcenter}
313
+ always, overriding I{Meeus}' Type II case (C{bool}).
314
+ @kwarg useZ: If C{True}, use the Z components, otherwise force C{z=INT0} (C{bool}).
315
+
316
+ @return: L{Meeus2Tuple}C{(radius, Type)}, with C{Type} the C{circumcenter}
317
+ iff C{B{circum}=True}.
318
+
319
+ @raise IntersectionError: Near-coincident or -colinear points, iff C{B{circum}=True}.
320
+
321
+ @raise TypeError: Invalid B{C{point1}}, B{C{point2}} or B{C{point3}}.
322
+
323
+ @see: Functions L{pygeodesy.circum3} and L{pygeodesy.circum4_} and Meeus, J.
324
+ U{I{Astronomical Algorithms}<http://www.Agopax.IT/Libri_astronomia/pdf/
325
+ Astronomical%20Algorithms.pdf>}, 2nd Ed. 1998, page 127ff, U{circumradius
326
+ <https://MathWorld.Wolfram.com/Circumradius.html>} and U{circumcircle
327
+ <https://MathWorld.Wolfram.com/Circumcircle.html>}.
328
+ '''
329
+ try:
330
+ A = _otherV3d(useZ=useZ, point1=point1)
331
+ return _meeus2(A, point2, point3, circum, useZ=useZ, clas=point1.classof)
332
+ except (TypeError, ValueError) as x:
333
+ raise _xError(x, point1=point1, point2=point2, point3=point3, circum=circum)
334
+
335
+
336
+ def _meeus2(A, point2, point3, circum, useZ=True, **clas_and_kwds): # in .vector3d
337
+ # (INTERNAL) Radius and center or Meeus' Type
338
+ f = _circum3 if circum else _meeus4
339
+ t = f(A, point2, point3, circum=circum, useZ=useZ, **clas_and_kwds)[:2]
340
+ return Meeus2Tuple(t)
341
+
342
+
343
+ def _meeus4(A, point2, point3, circum=False, useZ=True, clas=None, **clas_kwds):
344
+ # (INTERNAL) Radius and Meeus' Type
345
+ B = p2 = _otherV3d(useZ=useZ, point2=point2)
346
+ C = p3 = _otherV3d(useZ=useZ, point3=point3)
347
+
348
+ a = B.minus(C).length2
349
+ b = C.minus(A).length2
350
+ c = A.minus(B).length2
351
+ if a < b:
352
+ a, b, A, B = b, a, B, A
353
+ if a < c:
354
+ a, c, A, C = c, a, C, A
355
+
356
+ if a > EPS02 and (circum or a < (b + c)): # circumradius
357
+ b = sqrt(b / a)
358
+ c = sqrt(c / a)
359
+ R = Fsum(_1_0, b, c) * Fsum(_1_0, b, -c) * Fsum(_1_0, -b, c) * Fsum(_N_1_0, b, c)
360
+ r = R.fover(a)
361
+ if r < EPS02:
362
+ raise IntersectionError(_coincident_ if b < EPS0 or c < EPS0 else (
363
+ _colinear_ if _iscolinearWith(A, B, C) else _invalid_))
364
+ r = b * c / sqrt(r)
365
+ t = None # Meeus' Type II
366
+ else: # obtuse or right angle at A
367
+ r = sqrt(a * _0_25) if a > EPS02 else INT0
368
+ t = B.plus(C).times(_0_5) # Meeus' Type I
369
+ if clas is not None:
370
+ t = clas(t.x, t.y, t.z, **_xkwds(clas_kwds, name=meeus2.__name__))
371
+ return r, t, p2, p3
372
+
373
+
374
+ class _numpy(object): # see also .formy._idllmn6, .geodesicw._wargs, .latlonBase._toCartesian3
375
+ '''(INTERNAL) Partial C{NumPy} wrapper.
376
+ '''
377
+ @contextmanager # <https://www.Python.org/dev/peps/pep-0343/> Examples
378
+ def __call__(self, where, *args, **kwds):
379
+ '''(INTERNAL) Yield self with any errors raised as L{NumPyError}
380
+ embellished with all B{C{args}} and B{C{kwds}}.
381
+ '''
382
+ np = self.np
383
+ try: # <https://NumPy.org/doc/stable/reference/generated/numpy.seterr.html>
384
+ e = np.seterr(all=_raise_) # throw FloatingPointError for numpy errors
385
+ yield self
386
+ except Exception as x: # mostly FloatingPointError?
387
+ t = unstr(where, *args, **kwds)
388
+ raise NumPyError(t, cause=x) # _xError2?
389
+ finally: # restore numpy error handling
390
+ np.seterr(**e)
391
+
392
+ @Property_RO
393
+ def array(self):
394
+ return self.np.array
395
+
396
+ @Property_RO
397
+ def least_squares4(self):
398
+ '''Linear least-squares function.
399
+ '''
400
+ return self.np.linalg.lstsq
401
+
402
+ @Property_RO
403
+ def np(self):
404
+ '''Import numpy 1.10+ once.
405
+ '''
406
+ return _xnumpy(self.__class__, 1, 10)
407
+
408
+ def null_space2(self, A, rcond=None):
409
+ '''Return the C{null_space} and C{rank} of matrix B{C{A}}.
410
+
411
+ @see: U{Source<https://docs.SciPy.org/doc/scipy/reference/generated/scipy.linalg.null_space.html>}
412
+ U{SciPY Cookbook<https://SciPy-Cookbook.ReadTheDocs.io/items/RankNullspace.html>}, U{here
413
+ <https://NumPy.org/doc/stable/reference/generated/numpy.linalg.svd.html>}, U{here
414
+ <https://StackOverflow.com/questions/19820921>}, U{here
415
+ <https://StackOverflow.com/questions/2992947>} and U{here
416
+ <https://StackOverflow.com/questions/5889142>}.
417
+ '''
418
+ def _Error(txt=self.null_space2.__name__, **kwds):
419
+ return _AssertionError(txt=txt, **kwds)
420
+
421
+ np = self.np
422
+ A = np.array(A)
423
+ m = max(A.shape)
424
+ if m != 4: # for this usage
425
+ raise _Error(shape=m)
426
+ # if needed, square A, pad with zeros
427
+ A = np.resize(A, m * m).reshape(m, m)
428
+ # try: # no np.linalg.null_space <https://docs.SciPy.org/doc/>
429
+ # Z = scipy.linalg.null_space(A) # XXX no scipy.linalg?
430
+ # return Z, ...
431
+ # except AttributeError:
432
+ # pass
433
+ U, S, V = np.linalg.svd(A)
434
+ s = max(EPS, rcond) if rcond else (EPS * max(U.shape[0], V.shape[1]))
435
+ t = max(EPS, float(np.max(S) * s)) # abs_tol, rel_tol * largest singular
436
+ r = int(np.sum(S > t)) # rank
437
+ if r == 3: # get null_space
438
+ Z = np.transpose(V[r:])
439
+ s = map2(int, Z.shape)
440
+ if s != (m, 1): # bad null_space shape
441
+ raise _Error(shape=s, m=m)
442
+ D = A.dot(Z) # near-zeros-vector
443
+ n = float(np.linalg.norm(D, INF)) # INF = max(fabs(D)), 2 = hypot_(*D)
444
+ if n > t: # largest exceed tol
445
+ raise _Error(dot=tuple(D.ravel()), norm=n, tol=t)
446
+ else: # coincident, colinear, concentric centers, ambiguous, etc.
447
+ Z = None
448
+ # del A, S, U, V # release numpy
449
+ return Z, r
450
+
451
+ @Property_RO
452
+ def pseudo_inverse(self):
453
+ '''Moore-Penrose pseudo-inverse function.
454
+ '''
455
+ return self.np.linalg.pinv
456
+
457
+ def real_roots(self, *coeffs):
458
+ '''Compute the real, non-complex roots of a polynomial.
459
+ '''
460
+ np = self.np
461
+ rs = np.polynomial.polynomial.polyroots(coeffs)
462
+ return tuple(float(r) for r in rs if not np.iscomplex(r))
463
+
464
+ _numpy = _numpy() # PYCHOK singleton
465
+
466
+
467
+ def radii11(point1, point2, point3, useZ=True):
468
+ '''Return the radii of the C{In-}, I{Soddy} and C{Tangent} circles of a
469
+ (2- or 3-D) triangle.
470
+
471
+ @arg point1: First point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
472
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
473
+ @arg point2: Second point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
474
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
475
+ @arg point3: Third point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
476
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
477
+ @kwarg useZ: If C{True}, use the Z components, otherwise force C{z=INT0} (C{bool}).
478
+
479
+ @return: L{Radii11Tuple}C{(rA, rB, rC, cR, rIn, riS, roS, a, b, c, s)}.
480
+
481
+ @raise TriangleError: Near-coincident or -colinear points.
482
+
483
+ @raise TypeError: Invalid B{C{point1}}, B{C{point2}} or B{C{point3}}.
484
+
485
+ @see: U{Circumradius<https://MathWorld.Wolfram.com/Circumradius.html>},
486
+ U{Incircle<https://MathWorld.Wolfram.com/Incircle.html>}, U{Soddy
487
+ Circles<https://MathWorld.Wolfram.com/SoddyCircles.html>} and
488
+ U{Tangent Circles<https://MathWorld.Wolfram.com/TangentCircles.html>}.
489
+ '''
490
+ try:
491
+ return _radii11ABC(point1, point2, point3, useZ=useZ)[0]
492
+ except (TypeError, ValueError) as x:
493
+ raise _xError(x, point1=point1, point2=point2, point3=point3)
494
+
495
+
496
+ def _radii11ABC(point1, point2, point3, useZ=True):
497
+ # (INTERNAL) Tangent, Circum, Incircle, Soddy radii, sides and semi-perimeter
498
+ A = _otherV3d(useZ=useZ, point1=point1, NN_OK=False)
499
+ B = _otherV3d(useZ=useZ, point2=point2, NN_OK=False)
500
+ C = _otherV3d(useZ=useZ, point3=point3, NN_OK=False)
501
+
502
+ a = B.minus(C).length
503
+ b = C.minus(A).length
504
+ c = A.minus(B).length
505
+
506
+ S = Fsum(a, b, c) * _0_5
507
+ s = float(S) # semi-perimeter
508
+ if s > EPS0:
509
+ rs = float(S - a), float(S - b), float(S - c)
510
+ r3, r2, r1 = sorted(rs) # r3 <= r2 <= r1
511
+ if r3 > EPS0: # and r2 > EPS0 and r1 > EPS0
512
+ r3_r1 = r3 / r1
513
+ r3_r2 = r3 / r2
514
+ # t = r1 * r2 * r3 * (r1 + r2 + r3)
515
+ # = r1**2 * r2 * r3 * (1 + r2 / r1 + r3 / r1)
516
+ # = (r1 * r2)**2 * (r3 / r2) * (1 + r2 / r1 + r3 / r1)
517
+ t = r3_r2 * fsum1f_(_1_0, r2 / r1, r3_r1) # * (r1 * r2)**2
518
+ if t > EPS02:
519
+ t = sqrt(t) * _2_0 # * r1 * r2
520
+ # d = r1 * r2 + r2 * r3 + r3 * r1
521
+ # = r1 * (r2 + r2 * r3 / r1 + r3)
522
+ # = r1 * r2 * (1 + r3 / r1 + r3 / r2)
523
+ d = fsum1f_(_1_0, r3_r1, r3_r2) # * r1 * r2
524
+ # si/o = r1 * r2 * r3 / (r1 * r2 * (d +/- t))
525
+ # = r3 / (d +/- t)
526
+ si = r3 / (d + t)
527
+ so = (r3 / (d - t)) if d > t else INF
528
+ # ci = sqrt(r1 * r2 * r3 / s)
529
+ # = r1 * sqrt(r2 * r3 / r1 / s)
530
+ ci = r1 * sqrt(r2 * r3_r1 / s)
531
+ # co = a * b * c / (4 * ci * s)
532
+ t = ci * s * _4_0
533
+ co = (a * b * c / t) if t > EPS0 else INF
534
+ r1, r2, r3 = rs # original order
535
+ t = Radii11Tuple(r1, r2, r3, co, ci, si, so, a, b, c, s)
536
+ return t, A, B, C
537
+
538
+ raise TriangleError(_near_(_coincident_) if min(a, b, c) < EPS0 else (
539
+ _colinear_ if _iscolinearWith(A, B, C) else _invalid_))
540
+
541
+
542
+ def soddy4(point1, point2, point3, eps=EPS4, useZ=True):
543
+ '''Return the radius and center of the C{inner} I{Soddy} circle of a
544
+ (2- or 3-D) triangle.
545
+
546
+ @arg point1: First point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
547
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
548
+ @arg point2: Second point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
549
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
550
+ @arg point3: Third point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple},
551
+ C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}).
552
+ @kwarg eps: Tolerance for function L{pygeodesy.trilaterate3d2} if
553
+ C{B{useZ} is True} otherwise L{pygeodesy.trilaterate2d2}.
554
+ @kwarg useZ: If C{True}, use the Z components, otherwise force C{z=INT0} (C{bool}).
555
+
556
+ @return: L{Soddy4Tuple}C{(radius, center, deltas, outer)}. The C{center},
557
+ an instance of B{C{point1}}'s (sub-)class, is co-planar with the
558
+ three given points. The C{outer} I{Soddy} radius may be C{INF}.
559
+
560
+ @raise ImportError: Package C{numpy} not found, not installed or older
561
+ than version 1.10 and C{B{useZ} is True}.
562
+
563
+ @raise IntersectionError: Near-coincident or -colinear points or
564
+ a trilateration or C{numpy} issue.
565
+
566
+ @raise TypeError: Invalid B{C{point1}}, B{C{point2}} or B{C{point3}}.
567
+
568
+ @see: Functions L{radii11} and L{circum3} and U{Soddy Circles
569
+ <https://MathWorld.Wolfram.com/SoddyCircles.html>}.
570
+ '''
571
+ t, p1, p2, p3 = _radii11ABC(point1, point2, point3, useZ=useZ)
572
+
573
+ r = t.riS
574
+ c, d = _tricenter3d2(p1, t.rA + r,
575
+ p2, t.rB + r,
576
+ p3, t.rC + r, eps=eps, useZ=useZ,
577
+ Vector=point1.classof, name=soddy4.__name__)
578
+ return Soddy4Tuple(r, c, d, t.roS)
579
+
580
+
581
+ def _tricenter3d2(p1, r1, p2, r2, p3, r3, eps=EPS4, useZ=True, dLL3=False, **kwds):
582
+ # (INTERNAL) Trilaterate and disambiguate the 3-D center
583
+ d, kwds = None, _xkwds(kwds, eps=eps, coin=True)
584
+ if useZ and p1.z != p2.z != p3.z: # ignore z if all match
585
+ a, b = _trilaterate3d2(p1, r1, p2, r2, p3, r3, **kwds)
586
+ if a is b: # no unambiguity
587
+ c = a # == b
588
+ else:
589
+ c = a.plus(b).times(_0_5) # mean
590
+ if not a.isconjugateTo(b, minum=0, eps=eps):
591
+ if dLL3: # deltas as (lat, lon, height)
592
+ a = a.toLatLon()
593
+ b = b.toLatLon()
594
+ d = LatLon3Tuple(b.lat - a.lat,
595
+ b.lon - a.lon,
596
+ b.height - a.height, name=_deltas_)
597
+ else:
598
+ d = b.minus(a) # vectorial deltas
599
+ else:
600
+ if useZ: # pass z to Vector if given
601
+ kwds = _xkwds(kwds, z=p1.z)
602
+ c = _trilaterate2d2(p1.x, p1.y, r1,
603
+ p2.x, p2.y, r2,
604
+ p3.x, p3.y, r3, **kwds)
605
+ return c, d
606
+
607
+
608
+ def _trilaterate2d2(x1, y1, radius1, x2, y2, radius2, x3, y3, radius3,
609
+ coin=False, eps=None,
610
+ Vector=None, **Vector_kwds):
611
+ # (INTERNAL) Trilaterate three circles, see L{pygeodesy.trilaterate2d2}
612
+
613
+ def _abct4(x1, y1, r1, x2, y2, r2):
614
+ a = x2 - x1
615
+ b = y2 - y1
616
+ t = _tri3near2far(r1, r2, hypot(a, b), coin)
617
+ c = _0_0 if t else (hypot2_(r1, x2, y2) - hypot2_(r2, x1, y1))
618
+ return a, b, c, t
619
+
620
+ def _astr(**kwds): # kwds as (name=value, ...) strings
621
+ return Fmt.PAREN(_COMMASPACE_(*(Fmt.EQUALg(*t) for t in kwds.items())))
622
+
623
+ r1 = Radius_(radius1=radius1)
624
+ r2 = Radius_(radius2=radius2)
625
+ r3 = Radius_(radius3=radius3)
626
+
627
+ a, b, c, t = _abct4(x1, y1, r1, x2, y2, r2)
628
+ if t:
629
+ raise IntersectionError(_and(_astr(x1=x1, y1=y1, radius1=r1),
630
+ _astr(x2=x2, y2=y2, radius2=r2)), txt=t)
631
+
632
+ d, e, f, t = _abct4(x2, y2, r2, x3, y3, r3)
633
+ if t:
634
+ raise IntersectionError(_and(_astr(x2=x2, y2=y2, radius2=r2),
635
+ _astr(x3=x3, y3=y3, radius3=r3)), txt=t)
636
+
637
+ _, _, _, t = _abct4(x3, y3, r3, x1, y1, r1)
638
+ if t:
639
+ raise IntersectionError(_and(_astr(x3=x3, y3=y3, radius3=r3),
640
+ _astr(x1=x1, y1=y1, radius1=r1)), txt=t)
641
+
642
+ q = (a * e - b * d) * _2_0
643
+ if isnear0(q):
644
+ t = _no_(_intersection_)
645
+ raise IntersectionError(_and(_astr(x1=x1, y1=y1, radius1=r1),
646
+ _astr(x2=x2, y2=y2, radius2=r2),
647
+ _astr(x3=x3, y3=y3, radius3=r3)), txt=t)
648
+ t = Vector2Tuple((c * e - b * f) / q,
649
+ (a * f - c * d) / q, name=trilaterate2d2.__name__)
650
+
651
+ if eps and eps > 0: # check distances to center vs radius
652
+ for x, y, r in ((x1, y1, r1), (x2, y2, r2), (x3, y3, r3)):
653
+ d = hypot(x - t.x, y - t.y)
654
+ e = fabs(d - r)
655
+ if e > eps:
656
+ t = _and(Float(delta=e).toRepr(), r.toRepr(),
657
+ Float(distance=d).toRepr(), t.toRepr())
658
+ raise IntersectionError(t, txt=Fmt.exceeds_eps(eps))
659
+
660
+ if Vector is not None:
661
+ t = Vector(t.x, t.y, **_xkwds(Vector_kwds, name=t.name))
662
+ return t
663
+
664
+
665
+ def _trilaterate3d2(c1, r1, c2, r2, c3, r3, eps=EPS, coin=False,
666
+ **clas_Vector_and_kwds):
667
+ # (INTERNAL) Intersect three spheres or circles, see function
668
+ # L{pygeodesy.trilaterate3d2}, separated to allow callers to
669
+ # embellish exceptions, like C{FloatingPointError}s from C{numpy}
670
+
671
+ def _F3d2(F):
672
+ # map numpy 4-vector to floats tuple and Vector3d
673
+ T = map2(float, F)
674
+ return T, Vector3d(*T[1:])
675
+
676
+ def _N3(t01, x, z):
677
+ # compute x, y and z and return as B{C{clas}} or B{C{Vector}}
678
+ v = x.plus(z.times(t01))
679
+ n = trilaterate3d2.__name__
680
+ return _nVc(v, **_xkwds(clas_Vector_and_kwds, name=n))
681
+
682
+ c2 = _otherV3d(center2=c2, NN_OK=False)
683
+ c3 = _otherV3d(center3=c3, NN_OK=False)
684
+ rs = (r1, Radius_(radius2=r2, low=EPS),
685
+ Radius_(radius3=r3, low=EPS))
686
+
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)]
689
+ with _numpy(trilaterate3d2, A=A, eps=eps) as _np:
690
+ Z, _ = _np.null_space2(A, eps)
691
+ if Z is not None:
692
+ Z, z = _F3d2(Z) # [4 x 1]
693
+ z2 = z.length2
694
+ A = _np.pseudo_inverse(A) # [4 x 3]
695
+ bs = [c.length2 for c in (c1, c2, c3)]
696
+ # perturbe radii and vector b slightly by eps and eps * 4
697
+ for p in _tri5perturbs(eps, min(rs)):
698
+ 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)
703
+ if t:
704
+ v = _N3(t[0], x, z)
705
+ if len(t) < 2: # one intersection
706
+ t = v, v
707
+ elif fabs(t[0] - t[1]) < eps: # abutting
708
+ t = v, v
709
+ else: # "lowest" intersection first (to avoid test failures)
710
+ u = _N3(t[1], x, z)
711
+ t = (u, v) if u.x < v.x else (v, u)
712
+ return t
713
+
714
+ # coincident, concentric, colinear, too distant, no intersection:
715
+ # create the explanation and and throw an IntersectionError
716
+
717
+ def _no_intersection(coin):
718
+ t = _no_(_intersection_)
719
+ if coin:
720
+ def _reprs(*crs):
721
+ return _and(*map(repr, crs))
722
+
723
+ r = repr(r1) if r1 == r2 == r3 else _reprs(r1, r2, r3)
724
+ t = _SPACE_(t, _of_, _reprs(c1, c2, c3), _with_, _radius_, r)
725
+ elif Z is None:
726
+ t = _COMMASPACE_(t, _no_(_numpy.null_space2.__name__))
727
+ return t
728
+
729
+ t = _tri4near2far(c1, r1, c2, r2, coin) or \
730
+ _tri4near2far(c1, r1, c3, r3, coin) or \
731
+ _tri4near2far(c2, r2, c3, r3, coin) or (
732
+ _colinear_ if _iscolinearWith(c1, c2, c3, eps=eps) else
733
+ _no_intersection(coin))
734
+ raise IntersectionError(t, txt=None)
735
+
736
+
737
+ def _tri3near2far(r1, r2, h, coin):
738
+ # check for near-coincident/-concentric or too distant spheres/circles
739
+ return _too_(Fmt.distant(h)) if h > (r1 + r2) else (_near_(
740
+ _coincident_ if coin else _concentric_) if h < fabs(r1 - r2) else NN)
741
+
742
+
743
+ def _tri4near2far(c1, r1, c2, r2, coin):
744
+ # check for near-coincident/-concentric or too distant spheres/circles
745
+ t = _tri3near2far(r1, r2, c1.minus(c2).length, coin)
746
+ return _SPACE_(c1.name, _and_, c2.name, t) if t else NN
747
+
748
+
749
+ def _tri5perturbs(eps, r):
750
+ # perturb the radii to handle this corner case
751
+ # <https://GitHub.com/mrJean1/PyGeodesy/issues/49>
752
+ yield _0_0
753
+ if eps and eps > 0:
754
+ p = max(eps, EPS)
755
+ yield p
756
+ m = min(p, r)
757
+ yield -m
758
+ q = max(eps * _4_0, _EPS4e8)
759
+ if q > p:
760
+ yield q
761
+ q = min(q, r)
762
+ if q > m:
763
+ yield -q
764
+
765
+ # **) MIT License
766
+ #
767
+ # Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
768
+ #
769
+ # Permission is hereby granted, free of charge, to any person obtaining a
770
+ # copy of this software and associated documentation files (the "Software"),
771
+ # to deal in the Software without restriction, including without limitation
772
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
773
+ # and/or sell copies of the Software, and to permit persons to whom the
774
+ # Software is furnished to do so, subject to the following conditions:
775
+ #
776
+ # The above copyright notice and this permission notice shall be included
777
+ # in all copies or substantial portions of the Software.
778
+ #
779
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
780
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
781
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
782
+ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
783
+ # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
784
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
785
+ # OTHER DEALINGS IN THE SOFTWARE.