pygeodesy 25.12.12__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.
pygeodesy/__init__.py CHANGED
@@ -8,8 +8,8 @@ geodetic (lat-/longitude), geocentric (U{ECEF<https://WikiPedia.org/wiki/ECEF>}
8
8
  <https://GeographicLib.SourceForge.io/1.44/triaxial.html>} coordinates.
9
9
 
10
10
  Transcoded in part from U{JavaScript originals<https://GitHub.com/ChrisVeness/geodesy>} by I{Chris Veness (C)
11
- 2005-2024} and from several U{C++ classes<https://GeographicLib.SourceForge.io/C++/doc/annotated.html>} by I{Charles
12
- F. F. Karney (C) 2008-2024} and published under the same U{MIT License<https://OpenSource.org/licenses/MIT>}**.
11
+ 2005-2025} and from several U{C++ classes<https://GeographicLib.SourceForge.io/C++/doc/annotated.html>} by I{Charles
12
+ F. F. Karney (C) 2008-2025} and published under the same U{MIT License<https://OpenSource.org/licenses/MIT>}**.
13
13
 
14
14
  There are four modules for ellipsoidal earth models, C{ellipsoidalExact}, C{-Karney}, C{-Vincenty} and C{-Nvector}
15
15
  and two for spherical ones, C{sphericalTrigonometry} and C{-Nvector}. Each module provides a geodetic B{C{LatLon}}
@@ -101,9 +101,15 @@ and L{GnomonicGeodSolve} depend on I{Karney}'s C++ utility U{GeodSolve
101
101
  <https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} to be executable and set with
102
102
  env variable C{PYGEODESY_GEODSOLVE} or with property L{Ellipsoid.geodsolve}.
103
103
 
104
+ Triaxial geodesic classes L{Geodesic3Solve} and L{GeodesicLine3Solve} in module L{geod3solve} need
105
+ I{Karney}'s C++ utility U{Geod3Solve<https://GeographicLib.SourceForge.io/C++/doc/Geod3Solve.1.html>}
106
+ to be executable and set with env variable C{PYGEODESY_GEOD3SOLVE} or with property
107
+ L{Geodesic3Solve.Geod3Solve<geod3solve._Geodesic3SolveBase.Geod3Solve>}.
108
+
104
109
  Class L{Intersectool} in module L{geodesici} needs I{Karney}'s C++ utility U{IntersectTool
105
110
  <https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>} to be executable and set with
106
- env variable C{PYGEODESY_INTERSECTTOOL}.
111
+ env variable C{PYGEODESY_INTERSECTTOOL} or with property L{Intersectool.IntersectTool
112
+ <geodesici.Intersectool.IntersectTool>}.
107
113
 
108
114
  To compare C{MGRS} results from modules L{mgrs} and C{testMgrs} with I{Karney}'s C++ utility
109
115
  U{GeoConvert<https://GeographicLib.SourceForge.io/C++/doc/GeoConvert.1.html>}, the latter must
@@ -128,22 +134,24 @@ Tests
128
134
  The tests ran with Python 3.14.2 (with U{geographiclib<https://PyPI.org/project/geographiclib>} 2.1),
129
135
  Python 3.13.9 (with U{geographiclib<https://PyPI.org/project/geographiclib>} 2.1),
130
136
  U{numpy<https://PyPI.org/project/numpy>} 2.3.3, U{scipy<https://PyPI.org/project/scipy>} 1.16.2,
131
- U{GeoConvert<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.5 and
132
- U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.5),
137
+ U{GeoConvert<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7 and
138
+ U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7),
133
139
  Python 3.12.7 (with U{geographiclib<https://PyPI.org/project/geographiclib>} 2.0,
134
140
  U{numpy<https://PyPI.org/project/numpy>} 2.1.0, U{scipy<https://PyPI.org/project/scipy>} 1.14.1,
135
- U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.5,
136
- U{IntersectTool<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.5 and
137
- U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.5),
141
+ U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7,
142
+ U{GeodS3olve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7,
143
+ U{IntersectTool<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7 and
144
+ U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7),
138
145
  Python 3.11.5 (with U{geographiclib<https://PyPI.org/project/geographiclib>} 2.0,
139
146
  U{numpy<https://PyPI.org/project/numpy>} 1.24.2 and U{scipy<https://PyPI.org/project/scipy>} 1.10.1),
140
147
  and with Python 2.7.18 (with U{geographiclib<https://PyPI.org/project/geographiclib>} 1.50,
141
148
  U{numpy<https://PyPI.org/project/numpy>} 1.16.6, U{scipy<https://PyPI.org/project/scipy>} 1.2.2,
142
- U{GeoConvert<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.5,
143
- U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.5,
144
- U{IntersectTool<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.5 and
145
- U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.5), all in 64-bit on
146
- macOS 26.1 Tahoe.
149
+ U{GeoConvert<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7,
150
+ U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7,
151
+ U{Geod3Solve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7,
152
+ U{IntersectTool<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7 and
153
+ U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/utilities.html>} 2.7), all in 64-bit on
154
+ macOS 26.2 Tahoe.
147
155
 
148
156
  All tests ran with and without C{lazy import} for Python 3 and with command line option C{-W default} and
149
157
  env variable C{PYGEODESY_WARNINGS=on} for all Python versions. The results of those tests are included in
@@ -153,11 +161,11 @@ Test coverage has been measured with U{coverage<https://PyPI.org/project/coverag
153
161
  3.14.2, 3.13.9 and 3.12.7. The complete coverage report in HTML and a PDF summary are included in the
154
162
  distribution files.
155
163
 
156
- Python 3.14.2, 3.13.9, 3.12.7 and 3.11.5 run on Apple M4 Si (C{arm64}), I{natively}. Python 2.7.18 runs
164
+ Python 3.14.2, 3.13.9, 3.12.7 and 3.11.5 run on Apple Si M4 (C{arm64}), I{natively}. Python 2.7.18 runs
157
165
  on Intel (C{x86_64}) or Intel I{emulation} ("C{arm64_x86_64}", see function L{machine<pygeodesy.machine>}).
158
166
 
159
- The tests also ran with Python 3.13.9 (and U{geographiclib<https://PyPI.org/project/geographiclib>} 2.1) on
160
- U{Debian 12<https://Cirrus-CI.com/github/mrJean1/PyGeodesy/master>} in 64-bit only, with Python 3.12.8 (and
167
+ The tests also ran with Python 3.14.2 (and U{geographiclib<https://PyPI.org/project/geographiclib>} 2.1) on
168
+ U{Debian 12<https://Cirrus-CI.com/github/mrJean1/PyGeodesy/master>} in 64-bit only, with Python 3.13.9 (and
161
169
  U{geographiclib<https://PyPI.org/project/geographiclib>} 2.0) on U{Windows 2019Server
162
170
  <https://CI.AppVeyor.com/project/mrJean1/pygeodesy>} in 64-bit only and with Python 2.7.18 (and U{geographiclib
163
171
  <https://PyPI.org/project/geographiclib>} 1.52) on U{Windows 10<https://CI.AppVeyor.com/project/mrJean1/pygeodesy>}
@@ -190,7 +198,7 @@ All Python source code has been statically U{checked<https://GitHub.com/ActiveSt
190
198
  546532_PyChecker_postprocessor>} with U{Ruff<https://GitHub.com/astral-sh/ruff>} using Python 3.13.9 and with
191
199
  U{PyChecker<https://PyPI.org/project/pychecker>}, U{PyFlakes<https://PyPI.org/project/pyflakes>}, U{PyCodeStyle
192
200
  <https://PyPI.org/project/pycodestyle>} (formerly Pep8) and U{McCabe<https://PyPI.org/project/mccabe>} using Python
193
- 2.7.18, both in 64-bit on macOS 26.1 Tahoe.
201
+ 2.7.18, both in 64-bit on macOS 26.2 Tahoe.
194
202
 
195
203
  For a summary of all I{Karney}-based functionality in C{pygeodesy}, see module U{karney
196
204
  <https://mrJean1.GitHub.io/PyGeodesy/docs/pygeodesy.karney-module.html>}.
@@ -210,7 +218,7 @@ The following environment variables are observed by C{pygeodesy}:
210
218
  - C{PYGEODESY_FSUM_RESIDUAL} - see module L{fsums<pygeodesy.fsums>} and method L{RESIDUAL<pygeodesy.Fsum.RESIDUAL>}.
211
219
  - C{PYGEODESY_GEOCONVERT} - see module L{mgrs<pygeodesy.mgrs>}.
212
220
  - C{PYGEODESY_GEODSOLVE} - see module L{geodsolve<pygeodesy.geodsolve>}.
213
- - C{PYGEODESY_GEOD3SOLVE} - see module L{geodsolve<pygeodesy.geod3solve>}.
221
+ - C{PYGEODESY_GEOD3SOLVE} - see module L{geod3solve<pygeodesy.geod3solve>}.
214
222
  - C{PYGEODESY_INTERSECTTOOL} - see module L{geodesici<pygeodesy.geodesici>}.
215
223
  - C{PYGEODESY_LAZY_IMPORT} - see module L{lazily<pygeodesy.lazily>} and variable L{isLazy<pygeodesy.isLazy>}.
216
224
  - C{PYGEODESY_NOTIMPLEMENTED} - C{__special__} methods return C{NotImplemented} if set to "std".
@@ -618,7 +626,7 @@ else:
618
626
 
619
627
  from pygeodesy.internals import _version2, _DOT_ # noqa: E402
620
628
  # from pygeodesy.interns import _DOT_ # from .internals
621
- __version__ = '25.12.12'
629
+ __version__ = '25.12.31'
622
630
  # see setup.py for similar logic
623
631
  version = _DOT_(*_version2(__version__, n=3))
624
632
 
pygeodesy/constants.py CHANGED
@@ -332,6 +332,8 @@ R_VM = _Radius(R_VM=6366707.0194937) # PYCHOK aViation/naVigation earth radius
332
332
 
333
333
  _INF_NAN_NINF = {INF, NAN, NINF, _inf, _nan}
334
334
  _pos_self = _1_0.__pos__() is _1_0 # PYCHOK in .fsums, .vector3dBase
335
+ _1_3rd = 1 / 3 # in .fmath
336
+ _2_3rd = 2 / 3 # in .fmath, .formy
335
337
 
336
338
 
337
339
  def _0_0s(n):
pygeodesy/ellipsoids.py CHANGED
@@ -67,10 +67,10 @@ from __future__ import division as _; del _ # noqa: E702 ;
67
67
  # from pygeodesy.albers import AlbersEqualAreaCylindrical # _MODS
68
68
  from pygeodesy.basics import copysign0, isbool, _isin, isint, typename
69
69
  from pygeodesy.constants import EPS, EPS_2, EPS0, EPS02, EPS1, INF, NINF, \
70
- _over, PI_2, PI_3, PI4, R_M, R_MA, R_FM, _EPSqrt, \
70
+ _EPSqrt, PI_2, PI_3, PI2, PI4, R_M, R_MA, R_FM, \
71
71
  _EPStol as _TOL, _floatuple as _T, _isfinite, \
72
- _0_0s, _0_0, _0_5, _1_0, _1_EPS, _2_0, _4_0, _90_0, \
73
- _0_25, _3_0 # PYCHOK used!
72
+ _over, _0_0s, _0_0, _0_5, _1_0, _1_EPS, _2_0, \
73
+ _4_0, _90_0, _0_25, _3_0 # PYCHOK used!
74
74
  from pygeodesy.errors import _AssertionError, IntersectionError, _ValueError, _xattr, _xkwds_not
75
75
  from pygeodesy.fmath import cbrt, cbrt2, fdot, Fhorner, fpowers, hypot, hypot_, \
76
76
  hypot1, hypot2, sqrt3, Fsum
@@ -96,7 +96,7 @@ from pygeodesy.utily import atan1, atan1d, atan2b, degrees90, m2radians, radians
96
96
  from math import asinh, atan, atanh, cos, degrees, exp, fabs, radians, sin, sinh, sqrt, tan # as _tan
97
97
 
98
98
  __all__ = _ALL_LAZY.ellipsoids
99
- __version__ = '25.11.26'
99
+ __version__ = '25.12.14'
100
100
 
101
101
  _f_0_0 = Float(f =_0_0) # zero flattening
102
102
  _f__0_0 = Float(f_=_0_0) # zero inverse flattening
@@ -111,19 +111,19 @@ def _aux(lat, inverse, auxLat, clip=90):
111
111
  return Lat(lat, clip=clip, name=_lat_ if inverse else typename(auxLat))
112
112
 
113
113
 
114
- def _s2_c2(phi):
114
+ def _sin2cos2(rad):
115
115
  '''(INTERNAL) Return 2-tuple C{(sin(B{phi})**2, cos(B{phi})**2)}.
116
116
  '''
117
- if phi:
118
- s2 = sin(phi)**2
117
+ if rad:
118
+ s2 = sin(rad)**2
119
119
  if s2 > EPS:
120
120
  c2 = _1_0 - s2
121
121
  if c2 > EPS:
122
122
  if c2 < EPS1:
123
123
  return s2, c2
124
124
  else:
125
- return _1_0, _0_0 # phi == PI_2
126
- return _0_0, _1_0 # phi == 0
125
+ return _1_0, _0_0 # rad == PI_2
126
+ return _0_0, _1_0 # rad == 0
127
127
 
128
128
 
129
129
  class a_f2Tuple(_NamedTuple):
@@ -871,6 +871,12 @@ class Ellipsoid(_NamedEnumItem):
871
871
 
872
872
  equatoradius = a # Requatorial
873
873
 
874
+ @Property_RO
875
+ def equatorimeter(self):
876
+ '''Get the ellipsoid's I{equatorial} perimeter (C{meter}).
877
+ '''
878
+ return Meter(equatorimeter=self.a * PI2)
879
+
874
880
  def e2s(self, s):
875
881
  '''Compute norm M{sqrt(1 - e2 * s**2)}.
876
882
 
@@ -1381,6 +1387,12 @@ class Ellipsoid(_NamedEnumItem):
1381
1387
 
1382
1388
  polaradius = b # Rpolar
1383
1389
 
1390
+ @property_RO
1391
+ def polarimeter(self):
1392
+ '''Get the ellipsoid's I{polar}, meridional perimeter (C{meter}).
1393
+ '''
1394
+ return Meter(polarimeter=self.L * _4_0)
1395
+
1384
1396
  # Q = A # I{meridian arc unit} C{Q}, the mean, meridional length I{per radian}
1385
1397
 
1386
1398
  @deprecated_Property_RO
@@ -1484,7 +1496,7 @@ class Ellipsoid(_NamedEnumItem):
1484
1496
  r, p = self.a, Phid(lat)
1485
1497
  if p and self.f:
1486
1498
  if fabs(p) < PI_2:
1487
- s2, c2 = _s2_c2(p)
1499
+ s2, c2 = _sin2cos2(p)
1488
1500
  # R == sqrt((a2**2 * c2 + b2**2 * s2) / (a2 * c2 + b2 * s2))
1489
1501
  # == sqrt(a2**2 * (c2 + (b2 / a2)**2 * s2) / (a2 * (c2 + b2 / a2 * s2)))
1490
1502
  # == sqrt(a2 * (c2 + (b2 / a2)**2 * s2) / (c2 + (b2 / a2) * s2))
@@ -1709,7 +1721,7 @@ class Ellipsoid(_NamedEnumItem):
1709
1721
  '''(INTERNAL) Helper for C{rocAzimuth} and C{rocBearing}.
1710
1722
  '''
1711
1723
  if self.f:
1712
- s2, c2 = _s2_c2(radians(deg))
1724
+ s2, c2 = _sin2cos2(radians(deg))
1713
1725
  m, n = self.roc2_(Phid(lat))
1714
1726
  if n < m: # == n / (c2 * n / m + s2)
1715
1727
  c2 *= n / m
@@ -1751,7 +1763,7 @@ class Ellipsoid(_NamedEnumItem):
1751
1763
  # ... requires 1 or 2 sqrt
1752
1764
  g = self.b
1753
1765
  if self.f:
1754
- s2, c2 = _s2_c2(Phid(lat))
1766
+ s2, c2 = _sin2cos2(Phid(lat))
1755
1767
  g = _over(g, c2 + self.b2_a2 * s2)
1756
1768
  return Radius(rocGauss=g)
1757
1769
 
pygeodesy/fmath.py CHANGED
@@ -10,8 +10,8 @@ from __future__ import division as _; del _ # noqa: E702 ;
10
10
  from pygeodesy.basics import _copysign, copysign0, isbool, isint, isodd, \
11
11
  isscalar, len2, map1, _xiterable, typename
12
12
  from pygeodesy.constants import EPS0, EPS02, EPS1, NAN, PI, PI_2, PI_4, \
13
- _0_0, _0_125, _0_25, _0_5, _1_0, _1_5, \
14
- _copysign_0_0, isfinite, remainder
13
+ _0_0, _0_125, _0_25, _1_3rd, _0_5, _2_3rd, \
14
+ _1_0, _1_5, _copysign_0_0, isfinite, remainder
15
15
  from pygeodesy.errors import _IsnotError, LenError, _TypeError, _ValueError, \
16
16
  _xError, _xkwds, _xkwds_pop2, _xsError
17
17
  from pygeodesy.fsums import _2float, Fsum, fsum, _isFsum_2Tuple, Fmt, unstr
@@ -25,13 +25,11 @@ from math import fabs, sqrt # pow
25
25
  import operator as _operator # in .datums, .trf, .utm
26
26
 
27
27
  __all__ = _ALL_LAZY.fmath
28
- __version__ = '25.12.02'
28
+ __version__ = '25.12.23'
29
29
 
30
30
  # sqrt(2) - 1 <https://WikiPedia.org/wiki/Square_root_of_2>
31
31
  _0_4142 = 0.41421356237309504880 # ~ 3_730_904_090_310_553 / 9_007_199_254_740_992
32
- _1_3rd = _1_0 / 3
33
- _1_6th = _1_0 / 6
34
- _2_3rd = _1_3rd * 2
32
+ _1_6th = 1 / 6
35
33
  _h_lt_b_ = 'abs(h) < abs(b)'
36
34
 
37
35
 
@@ -1049,7 +1047,7 @@ def sqrt3(x):
1049
1047
 
1050
1048
 
1051
1049
  def sqrt_a(h, b):
1052
- '''Compute C{I{a}} side of a right-angled triangle from
1050
+ '''Compute the C{I{a}} side of a right-angled triangle from
1053
1051
  C{sqrt(B{h}**2 - B{b}**2)}.
1054
1052
 
1055
1053
  @arg h: Hypotenuse or outer annulus radius (C{scalar}).
pygeodesy/formy.py CHANGED
@@ -8,24 +8,28 @@ 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
- _90_0, _180_0, _360_0, MANT_DIG as _DIG53
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
34
  # from pygeodesy.triaxials.triaxial5 import _hartzell3 # _MODS
31
35
  from pygeodesy.units import _isDegrees, _isHeight, _isRadius, Bearing, Degrees_, \
@@ -42,14 +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.12.12'
49
+ __version__ = '25.12.31'
46
50
 
47
51
  _RADIANS2 = radians(_1_0)**2 # degree to radians-squared
48
52
  _ratio_ = 'ratio'
49
- _TOL53 = sqrt(pow(_0_5, _DIG53)) # elliperim
50
53
  _xline_ = 'xline'
51
54
 
52
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
+
53
284
  def angle2chord(rad, radius=R_M):
54
285
  '''Get the chord length of a (central) angle or I{angular} distance.
55
286
 
@@ -369,104 +600,12 @@ def _dS(fun_, radius, wrap, *lls, **adjust):
369
600
 
370
601
 
371
602
  def elliperim(a, b):
372
- '''Compute the perimeter of an ellipse with semi-axes C{a} and C{b} using U{SciPy's
373
- ellipe<https://www.JohnDCook.com/perimeter_ellipse.html>} function or the U{AGM
374
- <https://PaulBourke.net/geometry/ellipsecirc>} (Arithmetic Geometric Mean) method.
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.
375
605
 
376
606
  @return: The perimeter (C{scalar}, same units as C{a} and C{b}).
377
607
  '''
378
- if a < b:
379
- a, b = b, a
380
- if 0 < b < a:
381
- try:
382
- from scipy.special import ellipe
383
- a *= float(ellipe(_1_0 - (b / a)**2)) * _4_0
384
- except (AttributeError, ImportError):
385
- # relative accuracy is about _TOL53**2
386
- if (b * _DIG53) > (a * _TOL53):
387
- c = a + b
388
- d = a - b
389
- m, s = -1, [c**2]
390
- _s = s.append
391
- while d > (b * _TOL53) and len(s) < 32: # 4..5 trips
392
- b = sqrt(a * b)
393
- a = c * _0_5
394
- c = a + b
395
- d = a - b
396
- m *= 2
397
- _s(m * d**2)
398
- a = fsumf_(*s) * PI / c
399
- else: # near flat
400
- a *= _4_0
401
- elif b < 0:
402
- raise _ValueError(unstr(elliperim, a, b))
403
- else: # circle or flat
404
- a *= PI2 if b else _4_0
405
- return a
406
-
407
-
408
- # def elliperimR2(a, b):
409
- # '''Compute the perimeter of an ellipse with semi-axes C{a} and C{b} using
410
- # Ramanujan's U{2nd approximation<https://PaulBourke.net/geometry/ellipsecirc>}.
411
- # '''
412
- # if a < 0 or b < 0:
413
- # raise ValueError(unstr(elliperimR2, a, b))
414
- # p = a + b
415
- # if p:
416
- # t = ((a - b) / p)**2 * _3_0
417
- # t = t / (_10_0 + sqrt(_4_0 - t)) + _1_0
418
- # p *= t * PI
419
- # return p
420
-
421
-
422
- # def elliperim4arc3(a, b):
423
- # '''Compute the perimeter of an ellipse with semi-axes C{a} and C{b} using
424
- # the U{4 arc approximation<https://PaulBourke.net/geometry/ellipsecirc>}.
425
- #
426
- # @return: 3-Tuple C{(p, ra, rb)} with perimeter C{p}, arc radius C{ra}
427
- # at the major and arc radius C{rb} at the minor semi-axes.
428
- # '''
429
- # _r = a < b
430
- # if _r:
431
- # a, b = b, a
432
- # if 0 < b < a:
433
- # h = hypot(a, b)
434
- # L = (h - b) * _0_5
435
- # p = atan2(b, a)
436
- # s, c = sincos2(p)
437
- # ra = L / c
438
- # rb = (h - L) / s
439
- # p = rb * p + ra * (PI_2 - p)
440
- # elif b < 0:
441
- # raise ValueError(unstr(elliperim4arc3, a, b))
442
- # elif b == a:
443
- # ra = rb = a
444
- # p = a * PI_2
445
- # else: # b == 0
446
- # ra, rb = _0_0, a
447
- # p = a
448
- # p *= _4_0
449
- # if _r:
450
- # ra, rb = rb, ra
451
- # return p, ra, rb
452
-
453
-
454
- # def elliperimGKS(a, b):
455
- # '''Compute the perimeter of an ellipse with semi-axes C{a} and C{b} using the U{Gauss-Kummer
456
- # Series<https://www.JohnDCook.com/blog/2023/05/28/approximate-ellipse-perimeter/>}.
457
- # '''
458
- # if a < b:
459
- # a, b = b, a
460
- # if b < 0:
461
- # raise ValueError(unstr(elliperimGKS, a, b))
462
- # if b:
463
- # p = a + b
464
- # h = (a - b) / p
465
- # h *= h
466
- # p *= (1 + h * (1 / 4 + h * (1 / 64 + h * (1 / 256 + h * (25 / 16384 + h * (49 / 65536)))))) * PI
467
- # else:
468
- # p = _4_0 * a
469
- # return p
608
+ return Elliperim.e2k(a, b, Elliperim.E2k)
470
609
 
471
610
 
472
611
  def _ellipsoidal(earth, where):