pygeodesy 25.11.5__py2.py3-none-any.whl → 25.12.31__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 (125) hide show
  1. pygeodesy/__init__.py +46 -25
  2. pygeodesy/__main__.py +1 -1
  3. pygeodesy/albers.py +1 -1
  4. pygeodesy/angles.py +960 -0
  5. pygeodesy/auxilats/_CX_4.py +1 -1
  6. pygeodesy/auxilats/_CX_6.py +1 -1
  7. pygeodesy/auxilats/_CX_8.py +1 -1
  8. pygeodesy/auxilats/_CX_Rs.py +1 -1
  9. pygeodesy/auxilats/__init__.py +2 -2
  10. pygeodesy/auxilats/__main__.py +1 -1
  11. pygeodesy/auxilats/auxAngle.py +7 -8
  12. pygeodesy/auxilats/auxDLat.py +1 -1
  13. pygeodesy/auxilats/auxDST.py +1 -1
  14. pygeodesy/auxilats/auxLat.py +1 -1
  15. pygeodesy/auxilats/auxily.py +1 -1
  16. pygeodesy/azimuthal.py +6 -5
  17. pygeodesy/basics.py +14 -10
  18. pygeodesy/booleans.py +1 -1
  19. pygeodesy/cartesianBase.py +7 -7
  20. pygeodesy/clipy.py +1 -1
  21. pygeodesy/constants.py +29 -24
  22. pygeodesy/css.py +1 -1
  23. pygeodesy/datums.py +1 -1
  24. pygeodesy/deprecated/__init__.py +1 -1
  25. pygeodesy/deprecated/bases.py +1 -1
  26. pygeodesy/deprecated/classes.py +14 -7
  27. pygeodesy/deprecated/consterns.py +1 -1
  28. pygeodesy/deprecated/datum.py +1 -1
  29. pygeodesy/deprecated/functions.py +1 -1
  30. pygeodesy/deprecated/nvector.py +1 -1
  31. pygeodesy/deprecated/rhumbBase.py +1 -1
  32. pygeodesy/deprecated/rhumbaux.py +1 -1
  33. pygeodesy/deprecated/rhumbsolve.py +1 -1
  34. pygeodesy/deprecated/rhumbx.py +1 -1
  35. pygeodesy/dms.py +1 -1
  36. pygeodesy/ecef.py +1 -1
  37. pygeodesy/ecefLocals.py +1 -1
  38. pygeodesy/elevations.py +1 -1
  39. pygeodesy/ellipsoidalBase.py +1 -1
  40. pygeodesy/ellipsoidalBaseDI.py +1 -1
  41. pygeodesy/ellipsoidalExact.py +1 -1
  42. pygeodesy/ellipsoidalGeodSolve.py +1 -1
  43. pygeodesy/ellipsoidalKarney.py +1 -1
  44. pygeodesy/ellipsoidalNvector.py +1 -1
  45. pygeodesy/ellipsoidalVincenty.py +1 -1
  46. pygeodesy/ellipsoids.py +30 -17
  47. pygeodesy/elliptic.py +1 -1
  48. pygeodesy/epsg.py +1 -1
  49. pygeodesy/errors.py +8 -4
  50. pygeodesy/etm.py +1 -1
  51. pygeodesy/fmath.py +19 -14
  52. pygeodesy/formy.py +251 -10
  53. pygeodesy/frechet.py +1 -1
  54. pygeodesy/fstats.py +1 -1
  55. pygeodesy/fsums.py +41 -29
  56. pygeodesy/gars.py +1 -1
  57. pygeodesy/geod3solve.py +489 -0
  58. pygeodesy/geodesici.py +9 -8
  59. pygeodesy/geodesicw.py +1 -1
  60. pygeodesy/geodesicx/_C4_24.py +1 -1
  61. pygeodesy/geodesicx/_C4_27.py +1 -1
  62. pygeodesy/geodesicx/_C4_30.py +1 -1
  63. pygeodesy/geodesicx/__init__.py +2 -2
  64. pygeodesy/geodesicx/__main__.py +1 -1
  65. pygeodesy/geodesicx/gx.py +1 -1
  66. pygeodesy/geodesicx/gxarea.py +54 -24
  67. pygeodesy/geodesicx/gxbases.py +1 -1
  68. pygeodesy/geodesicx/gxline.py +1 -1
  69. pygeodesy/geodsolve.py +73 -104
  70. pygeodesy/geohash.py +1 -1
  71. pygeodesy/geoids.py +1 -1
  72. pygeodesy/hausdorff.py +1 -1
  73. pygeodesy/heights.py +1 -1
  74. pygeodesy/internals.py +1 -1
  75. pygeodesy/interns.py +3 -3
  76. pygeodesy/iters.py +1 -1
  77. pygeodesy/karney.py +152 -151
  78. pygeodesy/ktm.py +1 -1
  79. pygeodesy/latlonBase.py +1 -1
  80. pygeodesy/lazily.py +24 -13
  81. pygeodesy/lcc.py +1 -1
  82. pygeodesy/ltp.py +1 -1
  83. pygeodesy/ltpTuples.py +1 -1
  84. pygeodesy/mgrs.py +3 -3
  85. pygeodesy/named.py +15 -10
  86. pygeodesy/namedTuples.py +1 -1
  87. pygeodesy/nvectorBase.py +1 -1
  88. pygeodesy/osgr.py +1 -1
  89. pygeodesy/points.py +1 -1
  90. pygeodesy/props.py +6 -4
  91. pygeodesy/resections.py +1 -1
  92. pygeodesy/rhumb/__init__.py +8 -6
  93. pygeodesy/rhumb/aux_.py +1 -1
  94. pygeodesy/rhumb/bases.py +1 -1
  95. pygeodesy/rhumb/ekx.py +1 -1
  96. pygeodesy/rhumb/solve.py +91 -84
  97. pygeodesy/simplify.py +1 -1
  98. pygeodesy/solveBase.py +72 -49
  99. pygeodesy/sphericalBase.py +1 -1
  100. pygeodesy/sphericalNvector.py +1 -1
  101. pygeodesy/sphericalTrigonometry.py +1 -1
  102. pygeodesy/streprs.py +6 -4
  103. pygeodesy/trf.py +2 -4
  104. pygeodesy/triaxials/__init__.py +70 -0
  105. pygeodesy/triaxials/bases.py +966 -0
  106. pygeodesy/triaxials/conformal3.py +617 -0
  107. pygeodesy/triaxials/triaxial3.py +968 -0
  108. pygeodesy/{triaxials.py → triaxials/triaxial5.py} +353 -781
  109. pygeodesy/units.py +1 -1
  110. pygeodesy/unitsBase.py +1 -1
  111. pygeodesy/ups.py +2 -3
  112. pygeodesy/utily.py +17 -14
  113. pygeodesy/utm.py +1 -1
  114. pygeodesy/utmups.py +1 -1
  115. pygeodesy/utmupsBase.py +1 -1
  116. pygeodesy/vector2d.py +1 -1
  117. pygeodesy/vector3d.py +1 -1
  118. pygeodesy/vector3dBase.py +1 -1
  119. pygeodesy/webmercator.py +1 -1
  120. pygeodesy/wgrs.py +1 -1
  121. {pygeodesy-25.11.5.dist-info → pygeodesy-25.12.31.dist-info}/METADATA +28 -21
  122. pygeodesy-25.12.31.dist-info/RECORD +125 -0
  123. pygeodesy-25.11.5.dist-info/RECORD +0 -119
  124. {pygeodesy-25.11.5.dist-info → pygeodesy-25.12.31.dist-info}/WHEEL +0 -0
  125. {pygeodesy-25.11.5.dist-info → pygeodesy-25.12.31.dist-info}/top_level.txt +0 -0
pygeodesy/formy.py CHANGED
@@ -8,26 +8,30 @@ from __future__ import division as _; del _ # noqa: E702 ;
8
8
 
9
9
  from pygeodesy.basics import _copysign, _isin # _args_kwds_count2
10
10
  # from pygeodesy.cartesianBase import CartesianBase # _MODS
11
- from pygeodesy.constants import EPS, EPS0, EPS1, PI, PI2, PI3, PI_2, R_M, \
11
+ from pygeodesy.constants import EPS, EPS0, EPS1, EPS_2, PI, PI2, PI3, PI_2, R_M, \
12
12
  _0_0s, float0_, isnon0, remainder, _umod_PI2, \
13
13
  _0_0, _0_125, _0_25, _0_5, _1_0, _2_0, _4_0, \
14
14
  _90_0, _180_0, _360_0
15
+ from pygeodesy.constants import _3_0, _10_0, MANT_DIG as _DIG53 # PYCHOK used!
15
16
  from pygeodesy.datums import Datum, Ellipsoid, _ellipsoidal_datum, \
16
17
  _mean_radius, _spherical_datum, _WGS84, _EWGS84
17
18
  # from pygeodesy.ellipsoids import Ellipsoid, _EWGS84 # from .datums
19
+ # from pygeodesy.elliptic import Elliptic # _MODS
18
20
  from pygeodesy.errors import IntersectionError, LimitError, limiterrors, \
19
21
  _TypeError, _ValueError, _xattr, _xError, \
20
22
  _xcallable, _xkwds, _xkwds_pop2
21
- from pygeodesy.fmath import euclid, fdot_, fprod, hypot, hypot2, sqrt0
22
- from pygeodesy.fsums import fsumf_, Fmt, unstr
23
+ from pygeodesy.fmath import euclid, fdot_, fhorner, fprod, hypot, hypot2, sqrt0
24
+ from pygeodesy.fsums import fsum, fsumf_, Fmt, unstr
23
25
  # from pygeodesy.internals import typename # from .named
24
- from pygeodesy.interns import _delta_, _distant_, _inside_, _SPACE_, _too_
25
- from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS
26
- from pygeodesy.named import _name__, _name2__, _NamedTuple, _xnamed, typename
26
+ from pygeodesy.interns import _delta_, _distant_, _DOT_, _inside_, _SPACE_, _too_
27
+ from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _FOR_DOCS
28
+ from pygeodesy.named import callername, _name__, _name2__, _NamedTuple, \
29
+ _xnamed, typename
27
30
  from pygeodesy.namedTuples import Bearing2Tuple, Distance4Tuple, LatLon2Tuple, \
28
31
  Intersection3Tuple, PhiLam2Tuple
32
+ from pygeodesy.props import property_ROnce
29
33
  # from pygeodesy.streprs import Fmt, unstr # from .fsums
30
- # from pygeodesy.triaxials import _hartzell3 # _MODS
34
+ # from pygeodesy.triaxials.triaxial5 import _hartzell3 # _MODS
31
35
  from pygeodesy.units import _isDegrees, _isHeight, _isRadius, Bearing, Degrees_, \
32
36
  Distance, Distance_, Height, Lamd, Lat, Lon, Meter_, \
33
37
  Phid, Radians, Radians_, Radius, Radius_, Scalar, _100km
@@ -42,13 +46,241 @@ from contextlib import contextmanager
42
46
  from math import atan, cos, degrees, fabs, radians, sin, sqrt # pow
43
47
 
44
48
  __all__ = _ALL_LAZY.formy
45
- __version__ = '25.05.12'
49
+ __version__ = '25.12.31'
46
50
 
47
51
  _RADIANS2 = radians(_1_0)**2 # degree to radians-squared
48
52
  _ratio_ = 'ratio'
49
53
  _xline_ = 'xline'
50
54
 
51
55
 
56
+ class Elliperim(object):
57
+ '''Singleton with various methods to compute the perimeter of an ellipse.
58
+ '''
59
+ _TOL53 = sqrt(EPS_2) # sqrt(pow(_0_5, _DIG53))
60
+ _TOL53_53 = _TOL53 / _DIG53 # "flat" b/a tolerance, 1.9e-10
61
+ # assert _DIG53 == 53
62
+
63
+ def AGM(self, a, b, maxit=_DIG53):
64
+ '''Return the perimeter of an ellipse with semi-axes C{a} and C{b} using the U{AGM
65
+ <https://PaulBourke.net/geometry/ellipsecirc>} (Arithmetic-Geometric Mean) method.
66
+
67
+ @kwarg maxit: Number of iterations (C{int}).
68
+
69
+ @raise ValueError: No convergence for B{C{maxit}} iterations.
70
+ '''
71
+ _, p, a, b = self._pab4(a, b)
72
+ if p is None:
73
+ c_ = []
74
+ ts = self._AGMs(a, b, max(maxit, _DIG53), c_)
75
+ p = fsum(ts, nonfinites=True)
76
+ p *= PI / c_[0]
77
+ return p
78
+
79
+ def _AGMs(self, a, b, maxit, c_):
80
+ '''(INTERNAL) Yield the C{AGM} terms and final C{c}.
81
+ '''
82
+ c = a + b
83
+ yield c**2
84
+ m = -1
85
+ t = self._TOL53
86
+ for _ in range(maxit): # 4..5 trips
87
+ b = sqrt(a * b)
88
+ a = c * _0_5
89
+ c = a + b
90
+ d = a - b
91
+ m *= 2
92
+ yield d**2 * m
93
+ if d <= (b * t):
94
+ break
95
+ else:
96
+ raise self._Error(maxit, d, b * t)
97
+ c_.append(c) # kludge
98
+
99
+ def Arc43(self, a, b):
100
+ '''Return the perimeter (and arcs) of an ellipse with semi-axes C{a} and C{b}
101
+ with the U{4-Arc<https://PaulBourke.net/geometry/ellipsecirc>} approximation.
102
+
103
+ @return: 3-Tuple C{(p, Ra, Rb)} with perimeter C{p}, arc radius C{Ra} at the
104
+ major and arc radius C{Rb} at the minor semi-axis.
105
+ '''
106
+ _r, p, a, b = self._pab4(a, b)
107
+ if p is None:
108
+ h = hypot(a, b)
109
+ p = atan2(b, a)
110
+ s, c = sincos2(p)
111
+ L = (h - b) * _0_5
112
+ Ra = L / c
113
+ Rb = (h - L) / s
114
+ p = Rb * p + Ra * (PI_2 - p)
115
+ p *= _4_0
116
+ else: # circle or flat
117
+ Ra, Rb = a, b
118
+ return (p, Rb, Ra) if _r else (p, Ra, Rb)
119
+
120
+ # def CR(self, a, b):
121
+ # '''Return the perimeter of an ellipse with semi-axes C{a} and C{b} using U{Rackauckas'
122
+ # <https://www.ChrisRackauckas.com/assets/Papers/ChrisRackauckas-The_Circumference_of_an_Ellipse.pdf>}
123
+ # approximation, also U{here<https://ExtremeLearning.com.AU/a-formula-for-the-perimeter-of-an-ellipse>}.
124
+ # '''
125
+ # _, p, a, b = self._pab4(a, b)
126
+ # if p is None:
127
+ # p = a + b
128
+ # h = ((a - b) / p)**2
129
+ # p *= (fhorner(h, 135168, -85760, -5568, 3867) /
130
+ # fhorner(h, 135168, -119552, 22208, 345)) * PI
131
+ # return p
132
+
133
+ def E2k(self, a, b):
134
+ '''Return the perimeter of an ellipse with semi-axes C{a} and C{b} from the complete
135
+ elliptic integral of the 2nd kind L{E(k)<pygeodesy.elliptic.Elliptic.cE>}.
136
+ '''
137
+ return self._ellip2k(a, b, self._ellipE)
138
+
139
+ def e2k(self, a, b, E_alt=None):
140
+ '''Return the perimeter of an ellipse with semi-axes C{a} and C{b} using U{SciPy's
141
+ ellipe<https://www.JohnDCook.com/perimeter_ellipse.html>} function or method
142
+ C{E_alt}, otherwise C{None}.
143
+
144
+ @kwarg E_alt: An other C{Elliperim}C{(a, b)} method to use in case C{SciPy's
145
+ ellipe} is not available.
146
+ '''
147
+ p = self._ellipe
148
+ if p is not None: # i.e. callable
149
+ p = self._ellip2k(a, b, p)
150
+ elif callable(E_alt): # and E_alt is not Elliperim.e2k
151
+ p = E_alt(a, b)
152
+ return p
153
+
154
+ def _ellipE(self, k):
155
+ '''(INTERNAL) Get the complete C{elliptic} integeral C{E(k)}.
156
+ '''
157
+ return _MODS.elliptic.Elliptic(k).cE
158
+
159
+ @property_ROnce
160
+ def _ellipe(self):
161
+ '''(INTERNAL) Wrap function C{scipy.special.ellipe}, I{once}.
162
+ '''
163
+ try:
164
+ from scipy.special import ellipe
165
+
166
+ def _ellipe(k):
167
+ return float(ellipe(k))
168
+
169
+ except (AttributeError, ImportError):
170
+ _ellipe = None
171
+ return _ellipe # overwrite property_ROnce
172
+
173
+ def _ellip2k(self, a, b, _ellip):
174
+ '''(INTERNAL) Helper for methods C{E2k} and C{e2k}.
175
+ '''
176
+ _, p, a, b = self._pab4(a, b)
177
+ if p is None: # see .ellipsoids.Ellipsoid.L
178
+ k = _1_0 - (b / a)**2
179
+ p = _ellip(k) * a * _4_0
180
+ return p
181
+
182
+ def _Error(self, maxit, d, t):
183
+ return _ValueError(maxit=maxit, txt=Fmt.no_convergence(d, t))
184
+
185
+ def GK(self, a, b):
186
+ '''Return the perimeter of an ellipse with semi-axes C{a} and C{b} using the U{Gauss-Kummer
187
+ <https://www.JohnDCook.com/blog/2023/05/28/approximate-ellipse-perimeter>} series, and
188
+ U{here<https://www.MathsIsFun.com/geometry/ellipse-perimeter.html>}, C{B{b / a} > 0.75}.
189
+ '''
190
+ _, p, a, b = self._pab4(a, b)
191
+ if p is None:
192
+ p = a + b
193
+ h = (a - b) / p
194
+ p *= fhorner(h**2, *self._GKs) * PI
195
+ return p
196
+
197
+ @property_ROnce
198
+ def _GKs(self):
199
+ '''(INTERNAL) Compute the Gauss-Kummer coefficients, I{once}.
200
+ '''
201
+ return (1, 1 / 4, 1 / 64, 1 / 256, 25 / 16384, 49 / 65536,
202
+ 441 / 1048576, 1089 / 4194304) # overwrite property_ROnce
203
+
204
+ def HG(self, a, b, maxit=_DIG53):
205
+ '''Return the perimeter of an ellipse with semi-axes C{a} and C{b} using the U{HG
206
+ <https://web.Tecnico.ULisboa.PT/~mcasquilho/compute/com/,ellips/PerimeterOfEllipse.pdf>}
207
+ (HyperGeometric Gauss-Kummer) series.
208
+
209
+ @kwarg maxit: Number of iterations (C{int}), sufficient for C{B{b / a} > 0.125}.
210
+
211
+ @raise ValueError: No convergence for B{C{maxit}} iterations.
212
+ '''
213
+ _, p, a, b = self._pab4(a, b)
214
+ if p is None:
215
+ p = a + b
216
+ h = (a - b) / p
217
+ ts = self._HGs(h, max(maxit, _DIG53))
218
+ p *= fsum(ts, nonfinites=True) * PI
219
+ return p
220
+
221
+ def _HGs(self, h, maxit):
222
+ '''(INTERNAL) Yield the C{HG} terms.
223
+ '''
224
+ t = s_ = -1
225
+ s = _1_0
226
+ yield s
227
+ for u in range(-1, maxit * 2, 2):
228
+ t *= u / (u + 3) * h
229
+ t2 = t**2
230
+ s += t2
231
+ yield t2
232
+ if s == s_:
233
+ break
234
+ s_ = s
235
+ else:
236
+ s -= s_ - t2
237
+ raise self._Error(maxit, t2, s)
238
+
239
+ # def LS(self, a, b):
240
+ # '''Return the perimeter of an ellipse with semi-axes C{a} and C{b} using the U{Linderholm-Segal
241
+ # <https://www.JohnDCook.com/blog/2021/03/24/perimeter-of-an-ellipse>} formula, aka C{3/2 norm}.
242
+ # '''
243
+ # _, p, a, b = self._pab4(a, b)
244
+ # if p is None:
245
+ # p = pow(a, _1_5) + pow(b, _1_5)
246
+ # p = pow(p * _0_5, _2_3rd) * PI2
247
+ # return p
248
+
249
+ def _pab4(self, a, b):
250
+ _r = a < b
251
+ if _r:
252
+ a, b = b, a
253
+ if a > b:
254
+ if b > (a * self._TOL53_53):
255
+ p = None
256
+ elif b < 0:
257
+ t = callername() # underOK=True
258
+ t = _DOT_(typename(self), t)
259
+ raise _ValueError(unstr(t, a, b))
260
+ else: # "flat"
261
+ p = a * _4_0
262
+ else: # circle
263
+ p = a * PI2
264
+ return _r, p, a, b
265
+
266
+ def R2(self, a, b):
267
+ '''Return the perimeter of an ellipse with semi-axes C{a} and C{b} using U{Ramanujan's
268
+ 2nd<https://PaulBourke.net/geometry/ellipsecirc>} approximation, C{B{b / a} > 0.9}.
269
+ '''
270
+ _, p, a, b = self._pab4(a, b)
271
+ if p is None:
272
+ p = a + b
273
+ h = (a - b) / p
274
+ h *= _3_0 * h
275
+ h /= sqrt(_4_0 - h) + _10_0 # /= chokes PyChecker?
276
+ p *= (h + _1_0) * PI
277
+ return p
278
+
279
+ if not _FOR_DOCS: # PYCHOK force epydoc
280
+ Elliperim = Elliperim() # singleton
281
+ del _FOR_DOCS
282
+
283
+
52
284
  def angle2chord(rad, radius=R_M):
53
285
  '''Get the chord length of a (central) angle or I{angular} distance.
54
286
 
@@ -367,6 +599,15 @@ def _dS(fun_, radius, wrap, *lls, **adjust):
367
599
  return r * radius
368
600
 
369
601
 
602
+ def elliperim(a, b):
603
+ '''Compute the perimeter of an ellipse with semi-axes C{a} and C{b}
604
+ using the C{Elliperim.e2k} or C{Elliperim.AGM} method.
605
+
606
+ @return: The perimeter (C{scalar}, same units as C{a} and C{b}).
607
+ '''
608
+ return Elliperim.e2k(a, b, Elliperim.E2k)
609
+
610
+
370
611
  def _ellipsoidal(earth, where):
371
612
  '''(INTERNAL) Helper for distances.
372
613
  '''
@@ -926,7 +1167,7 @@ def hartzell(pov, los=False, earth=_WGS84, **name_LatLon_and_kwds):
926
1167
  n, kwds = _name2__(name_LatLon_and_kwds, name__=hartzell)
927
1168
  try:
928
1169
  D = _spherical_datum(earth, name__=hartzell)
929
- r, h, i = _MODS.triaxials._hartzell3(pov, los, D.ellipsoid._triaxial)
1170
+ r, h, i = _MODS.triaxials.triaxial5._hartzell3(pov, los, D.ellipsoid._triaxial)
930
1171
 
931
1172
  C = _MODS.cartesianBase.CartesianBase
932
1173
  if kwds:
@@ -1663,7 +1904,7 @@ def vincentys_(phi2, phi1, lam21):
1663
1904
 
1664
1905
  # **) MIT License
1665
1906
  #
1666
- # Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1907
+ # Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.
1667
1908
  #
1668
1909
  # Permission is hereby granted, free of charge, to any person obtaining a
1669
1910
  # copy of this software and associated documentation files (the "Software"),
pygeodesy/frechet.py CHANGED
@@ -906,7 +906,7 @@ if __name__ == _DMAIN_:
906
906
 
907
907
  # **) MIT License
908
908
  #
909
- # Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
909
+ # Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.
910
910
  #
911
911
  # Permission is hereby granted, free of charge, to any person obtaining a
912
912
  # copy of this software and associated documentation files (the "Software"),
pygeodesy/fstats.py CHANGED
@@ -804,7 +804,7 @@ __all__ += _ALL_DOCS(_FstatsBase, _FstatsNamed)
804
804
 
805
805
  # **) MIT License
806
806
  #
807
- # Copyright (C) 2021-2025 -- mrJean1 at Gmail -- All Rights Reserved.
807
+ # Copyright (C) 2021-2026 -- mrJean1 at Gmail -- All Rights Reserved.
808
808
  #
809
809
  # Permission is hereby granted, free of charge, to any person obtaining a
810
810
  # copy of this software and associated documentation files (the "Software"),
pygeodesy/fsums.py CHANGED
@@ -5,16 +5,16 @@ u'''Class L{Fsum} for precision floating point summation similar to
5
5
  Python's C{math.fsum}, but enhanced with I{precision running} summation
6
6
  plus optionally, accurate I{TwoProduct} multiplication.
7
7
 
8
- Accurate multiplication is based on the C{math.fma} function from
9
- Python 3.13 and newer or an equivalent C{fma} implementation for
10
- Python 3.12 and older. To enable accurate multiplication, set env
11
- variable C{PYGEODESY_FSUM_F2PRODUCT} to C{"std"} or any non-empty
12
- string or invoke function C{pygeodesy.f2product(True)} or set. With
13
- C{"std"} the C{fma} implemention follows the C{math.fma} function,
14
- otherwise the C{PyGeodesy 24.09.09} release.
8
+ Accurate multiplication is based on the C{math.fma} function from Python
9
+ 3.13 and newer or an equivalent C{fma} implementation for Python 3.12 and
10
+ older. Set env variable C{PYGEODESY_FSUM_F2PRODUCT} to C{"std"} or any
11
+ non-empty string or invoke function C{pygeodesy.f2product(True)} to enable
12
+ accurate multiplication. With C{"std"} the C{fma} implemention follows
13
+ the C{math.fma} function, otherwise the implementation of the C{PyGeodesy
14
+ 24.09.09} release.
15
15
 
16
16
  Generally, an L{Fsum} instance is considered a C{float} plus a small or
17
- zero C{residue} aka C{residual} value, see property L{Fsum.residual}.
17
+ zero C{residue} aka C{residual}, see property L{Fsum.residual}.
18
18
 
19
19
  Set env variable C{PYGEODESY_FSUM_RESIDUAL} to a C{float} string greater
20
20
  than C{"0.0"} as the threshold to throw a L{ResidualError} for a division,
@@ -28,7 +28,7 @@ L{Fsum.fint2} and L{Fsum.is_integer}. Also, L{Fsum} methods L{Fsum.pow},
28
28
  L{Fsum.__ipow__}, L{Fsum.__pow__} and L{Fsum.__rpow__} return a (very long)
29
29
  C{int} if invoked with optional argument C{mod} set to C{None}. The
30
30
  C{residual} of an C{integer} L{Fsum} is between C{-1.0} and C{+1.0} and
31
- will be C{INT0} if that is considered to be I{exact}.
31
+ will be C{INT0} if that L{Fsum} is an I{exact float} or I{exact integer}.
32
32
 
33
33
  Set env variable C{PYGEODESY_FSUM_NONFINITES} to C{"std"} or use function
34
34
  C{pygeodesy.nonfiniterrors(False)} to allow I{non-finite} C{float}s like
@@ -62,7 +62,7 @@ from math import fabs, isinf, isnan, \
62
62
  ceil as _ceil, floor as _floor # PYCHOK used! .ltp
63
63
 
64
64
  __all__ = _ALL_LAZY.fsums
65
- __version__ = '25.06.03'
65
+ __version__ = '25.12.24'
66
66
 
67
67
  from pygeodesy.interns import (
68
68
  _PLUS_ as _add_op_, # in .auxilats.auxAngle
@@ -321,8 +321,8 @@ def nonfiniterrors(raiser=None):
321
321
  '''
322
322
  d = Fsum._isfine
323
323
  if raiser is not None:
324
- Fsum._isfine = {} if bool(raiser) else Fsum._nonfinites_isfine_kwds[True]
325
- return (False if d is Fsum._nonfinites_isfine_kwds[True] else
324
+ Fsum._isfine = {} if bool(raiser) else _nonfinites_isfine_kwds[True]
325
+ return (False if d is _nonfinites_isfine_kwds[True] else
326
326
  _xkwds_get1(d, _isfine=_isfinite) is _isfinite) if d else True
327
327
 
328
328
 
@@ -370,7 +370,7 @@ def _Psum(ps, **name_f2product_nonfinites_RESIDUAL):
370
370
  return F
371
371
 
372
372
 
373
- def _Psum_(*ps, **name_f2product_nonfinites_RESIDUAL): # in .fmath
373
+ def _Psum_(*ps, **name_f2product_nonfinites_RESIDUAL):
374
374
  '''(INTERNAL) Return an C{Fsum} from I{known scalar} C{ps}.
375
375
  '''
376
376
  return _Psum(ps, **name_f2product_nonfinites_RESIDUAL)
@@ -486,7 +486,7 @@ class Fsum(_Named): # sync __methods__ with .vector3dBase.Vector3dBase, .fstats
486
486
 
487
487
  @note: Handling of I{non-finites} as C{inf}, C{INF}, C{NINF}, C{nan} and C{NAN} is
488
488
  determined by function L{nonfiniterrors<fsums.nonfiniterrors>} for the default
489
- and by method L{nonfinites<Fsum.nonfinites>} for individual C{Fsum} instances,
489
+ or by method L{nonfinites<Fsum.nonfinites>} for individual C{Fsum} instances,
490
490
  overruling the default. For backward compatibility, I{non-finites} raise
491
491
  exceptions by default.
492
492
 
@@ -829,7 +829,7 @@ class Fsum(_Named): # sync __methods__ with .vector3dBase.Vector3dBase, .fstats
829
829
  return self._cmp_0(other, _lt_op_ + _fset_op_) <= 0
830
830
 
831
831
  def __len__(self):
832
- '''Return the number of values accumulated (C{int}).
832
+ '''Return the number of (non-zero) values accumulated (C{int}).
833
833
  '''
834
834
  return self._n
835
835
 
@@ -1447,7 +1447,7 @@ class Fsum(_Named): # sync __methods__ with .vector3dBase.Vector3dBase, .fstats
1447
1447
  @arg other2: Addend (C{scalar}, an L{Fsum} or L{Fsum2Tuple}).
1448
1448
  @kwarg nonfinites: Use C{B{nonfinites}=True} or C{False}, to
1449
1449
  override L{nonfinites<Fsum.nonfinites>} and
1450
- L{nonfiniterrors} default (C{bool}).
1450
+ the L{nonfiniterrors} default (C{bool}).
1451
1451
  '''
1452
1452
  op = typename(self.fma)
1453
1453
  _fs = self._ps_other
@@ -1479,7 +1479,7 @@ class Fsum(_Named): # sync __methods__ with .vector3dBase.Vector3dBase, .fstats
1479
1479
  an L{Fsum} or L{Fsum2Tuple}), all positional.
1480
1480
  @kwarg nonfinites: Use C{B{nonfinites}=True} or C{False}, to
1481
1481
  override L{nonfinites<Fsum.nonfinites>} and
1482
- L{nonfiniterrors} default (C{bool}).
1482
+ the L{nonfiniterrors} default (C{bool}).
1483
1483
 
1484
1484
  @note: Equivalent to L{fdot_<pygeodesy.fmath.fdot_>}C{(*xys,
1485
1485
  start=self)}.
@@ -1615,8 +1615,7 @@ class Fsum(_Named): # sync __methods__ with .vector3dBase.Vector3dBase, .fstats
1615
1615
  '''
1616
1616
  if two: # delattrof(self, _f2product=None)
1617
1617
  t = _xkwds_pop(self.__dict__, _f2product=None)
1618
- if two[0] is not None:
1619
- self._f2product = bool(two[0])
1618
+ self._optionals(f2product=two[0])
1620
1619
  else: # getattrof(self, _f2product=None)
1621
1620
  t = _xkwds_get(self.__dict__, _f2product=None)
1622
1621
  return t
@@ -2070,25 +2069,21 @@ class Fsum(_Named): # sync __methods__ with .vector3dBase.Vector3dBase, .fstats
2070
2069
  @see: Function L{nonfiniterrors<fsums.nonfiniterrors>}.
2071
2070
 
2072
2071
  @note: Use property L{nonfinitesOK<Fsum.nonfinitesOK>} to determine
2073
- whether I{non-finites} are C{OK} for this L{Fsum} and by the
2072
+ whether I{non-finites} are C{OK} for this L{Fsum} or by the
2074
2073
  L{nonfiniterrors} default.
2075
2074
  '''
2076
- _ks = Fsum._nonfinites_isfine_kwds
2077
2075
  if OK: # delattrof(self, _isfine=None)
2078
2076
  k = _xkwds_pop(self.__dict__, _isfine=None)
2079
- if OK[0] is not None:
2080
- self._isfine = _ks[bool(OK[0])]
2077
+ self._optionals(nonfinites=OK[0])
2081
2078
  self._update()
2082
2079
  else: # getattrof(self, _isfine=None)
2083
2080
  k = _xkwds_get(self.__dict__, _isfine=None)
2081
+ _ks = _nonfinites_isfine_kwds
2084
2082
  # dict(map(reversed, _ks.items())).get(k, None)
2085
2083
  # raises a TypeError: unhashable type: 'dict'
2086
2084
  return True if k is _ks[True] else (
2087
2085
  False if k is _ks[False] else None)
2088
2086
 
2089
- _nonfinites_isfine_kwds = {True: dict(_isfine=_isOK),
2090
- False: dict(_isfine=_isfinite)}
2091
-
2092
2087
  @property_RO
2093
2088
  def nonfinitesOK(self):
2094
2089
  '''Are I{non-finites} C{OK} for this L{Fsum} or by default? (C{bool}).
@@ -2111,9 +2106,9 @@ class Fsum(_Named): # sync __methods__ with .vector3dBase.Vector3dBase, .fstats
2111
2106
  '''(INTERNAL) Re/set options from keyword arguments.
2112
2107
  '''
2113
2108
  if f2product is not None:
2114
- self.f2product(f2product)
2109
+ self._f2product = bool(f2product)
2115
2110
  if nonfinites is not None:
2116
- self.nonfinites(nonfinites)
2111
+ self._isfine = _nonfinites_isfine_kwds[bool(nonfinites)]
2117
2112
  if name_RESIDUAL: # MUST be last
2118
2113
  n, kwds = _name2__(**name_RESIDUAL)
2119
2114
  if kwds:
@@ -2535,6 +2530,8 @@ class Fsum(_Named): # sync __methods__ with .vector3dBase.Vector3dBase, .fstats
2535
2530
 
2536
2531
  _ROs = _allPropertiesOf_n(3, Fsum, Property_RO) # PYCHOK see Fsum._update
2537
2532
 
2533
+ _nonfinites_isfine_kwds = {True: dict(_isfine=_isOK),
2534
+ False: dict(_isfine=_isfinite)}
2538
2535
  if _NONFINITES == _std_: # PYCHOK no cover
2539
2536
  _ = nonfiniterrors(False)
2540
2537
 
@@ -2685,6 +2682,21 @@ class Fsum2Tuple(_NamedTuple): # in .fstats
2685
2682
  _Fsum_2Tuple_types = Fsum, Fsum2Tuple # PYCHOK lines
2686
2683
 
2687
2684
 
2685
+ class _Ksum(Fsum):
2686
+ '''(INTERNAL) For C{.karney._sum3}, specifically and only.
2687
+ '''
2688
+ _isfine = _nonfinites_isfine_kwds[True]
2689
+
2690
+ def __init__(self, s, t, *xs):
2691
+ ps = [t, s] if t else [s]
2692
+ self._ps = self._ps_acc(ps, xs, up=False)
2693
+
2694
+ @property_RO
2695
+ def _s_t_n3(self):
2696
+ s, t = self._fprs2
2697
+ return s, t, self._n
2698
+
2699
+
2688
2700
  class ResidualError(_ValueError):
2689
2701
  '''Error raised for a division, power or root operation of
2690
2702
  an L{Fsum} instance with a C{residual} I{ratio} exceeding
@@ -2863,7 +2875,7 @@ if __name__ == _DMAIN_:
2863
2875
 
2864
2876
  # **) MIT License
2865
2877
  #
2866
- # Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
2878
+ # Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.
2867
2879
  #
2868
2880
  # Permission is hereby granted, free of charge, to any person obtaining a
2869
2881
  # copy of this software and associated documentation files (the "Software"),
pygeodesy/gars.py CHANGED
@@ -344,7 +344,7 @@ __all__ += _ALL_DOCS(decode3, # functions
344
344
 
345
345
  # **) MIT License
346
346
  #
347
- # Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
347
+ # Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.
348
348
  #
349
349
  # Permission is hereby granted, free of charge, to any person obtaining a
350
350
  # copy of this software and associated documentation files (the "Software"),