Coverage for pygeodesy/ellipses.py: 93%
404 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-25 15:01 -0400
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-25 15:01 -0400
2# -*- coding: utf-8 -*-
4u'''Class C{Ellipse} for 2-D ellipse attributes, like perimeter, area, etc.
5'''
6# make sure int/int division yields float quotient, see .basics
7from __future__ import division as _; del _ # noqa: E702 ;
9# from pygeodesy.basics import islistuple # _MODS
10from pygeodesy.constants import EPS, EPS_2, INT0, NEG0, PI, PI_2, PI3_2, PI2, \
11 _0_0, _1_0, _4_0, _isfinite, _over, _1_over # _N_1_0
12from pygeodesy.constants import _0_5, _3_0, _10_0, MANT_DIG as _DIG53 # PYCHOK used!
13# from pygeodesy.ellipsoids import Ellipsoid # _MODS
14from pygeodesy.errors import _ConvergenceError, _ValueError, _xkwds, _xkwds_pop2
15from pygeodesy.fmath import euclid, fhorner, fmean_, hypot
16from pygeodesy.fsums import _fsum # PYCHOK used!
17from pygeodesy.internals import typename, _DOT_, _UNDER_
18# from pygeodesy.interns import _DOT_, _UNDER_ # from .internals
19from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS
20from pygeodesy.named import _NamedBase, unstr
21from pygeodesy.props import Property_RO, property_RO, property_ROnce
22# from pygeodesy.streprs import unstr # from .named
23# from pygeodesy.triaxials import Triaxial_ , TriaxialError # _MODS
24from pygeodesy.units import Degrees, Meter, Meter2, Radians, Radius, Scalar
25from pygeodesy.utily import atan2, atan2b, atan2p, sincos2, sincos2d
26# from pygeodesy.vector3d import Vector3d # _MODS
28from math import degrees, fabs, radians, sqrt
29# import operator as _operator # from .fmath
31__all__ = _ALL_LAZY.ellipses
32__version__ = '26.03.25'
34_TOL53 = sqrt(EPS_2) # sqrt(pow(_0_5, _DIG53))
35_TOL53_53 = _TOL53 / _DIG53 # "flat" b/a tolerance, 1.9e-10
36# assert _DIG53 == 53
39class Ellipse(_NamedBase):
40 '''Class to compute various attributes of a 2-D ellipse.
41 '''
42# _ab3 = (a, b, a * b) # unordered
43 _flat = False
44 _maxit = _DIG53
45# _Pab4 = (r, P, a, b) # a >= b, ordered
46 _4_PI_ = _4_0 / PI - (14 / 11)
47 _tEPS = None
49 def __init__(self, a, b, **name):
50 '''New L{Ellipse} with semi-axes B{C{a}} and B{C{b}}.
52 The ellipse is C{oblate} if C{B{a} > B{b}}, C{prolate} if
53 C{B{a} < B{b}}, C{circular} if C{B{a} == B{b}} and C{"flat"}
54 if C{min(B{a}, B{b}) <<< max(B{a}, B{b})}.
56 @arg a: X semi-axis length (C{meter}, conventionally).
57 @arg b: Y semi-axis length (C{meter}, conventionally).
59 @raise ValueError: Invalid B{C{a}} or B{C{b}}.
61 @see: U{Ellipse<https://MathWorld.Wolfram.com/Ellipse.html>}.
62 '''
63 if name:
64 self.name = name
65 self._ab3 = a, b, (a * b) # unordered
67 r = a < b
68 if r: # prolate
69 a, b = b, a
70 if b < 0 or not _isfinite(a): # PYCHOK no cover
71 raise self._Error(None)
72 if a > b:
73 if _isFlat(a, b):
74 self._flat = True
75 P = a * _4_0
76 else: # pro-/oblate
77 P = None
78 else: # circular
79 P = a * PI2
80# b = a
81 self._Pab4 = r, P, a, b # ordered
83 @Property_RO
84 def a(self):
85 '''Get semi-axis C{B{a}} of this ellipse (C{meter}, conventionally).
86 '''
87 a, _, _ = self._ab3
88 return Meter(a=a)
90 @Property_RO
91 def apses2(self):
92 '''Get 2-tuple C{(apoapsis, periapsis)} with the U{apo-<https://MathWorld.Wolfram.com/Apoapsis.html>}
93 and U{periapsis<https://MathWorld.Wolfram.com/Periapsis.html>} of this ellipse, both C{meter}.
94 '''
95 _, _, a, p = self._Pab4
96 c = self.c
97 if c: # a != p
98 p = a - c
99 a += c
100 return a, p
102 def arc(self, deg2, deg1=0):
103 '''Compute the length of U{elliptic arc<https://www.JohnDCook.com/blog/2022/11/02/elliptic-arc-length/>}
104 C{(B{deg2} - B{deg1})}, both counter-clockwise from semi-axis B{C{a}} to B{C{b}} of the ellipse.
106 @arg deg2: End angle of the elliptic arc (C{degrees}).
107 @kwarg deg1: Start angle of the elliptic arc (C{degrees}).
109 @return: Arc length, signed (C{meter}, conventionally).
110 '''
111 return self.arc_(radians(deg2), (radians(deg1) if deg1 else _0_0))
113 def arc_(self, rad2, rad1=0):
114 '''Compute the length of U{elliptic arc<https://www.JohnDCook.com/blog/2022/11/02/elliptic-arc-length/>}
115 C{(B{rad2} - B{rad1})}, both counter-clockwise from semi-axis B{C{a}} to B{C{b}} of the ellipse.
117 @arg rad2: End angle of the elliptic arc (C{radians}).
118 @kwarg rad1: Start angle of the elliptic arc (C{radians}).
120 @return: Arc length, signed (C{meter}, conventionally).
121 '''
122 r, R, a, _ = self._Pab4
123 if R is None:
124 _e = self._ellipe or self._ellipE
125 k = self.e2
126 r = PI_2 if r else _0_0
127 R = _arc(_e, k, r + rad2)
128 r += rad1
129 if r:
130 R -= _arc(_e, k, r)
131 else:
132 a = (rad2 - rad1) / PI2
133 return Meter(arc=R * a)
135 @Property_RO
136 def area(self):
137 '''Get the area of this ellipse (C{meter**2}, conventionally).
138 '''
139 _, _, ab = self._ab3
140 return Meter2(area=ab * PI)
142 @Property_RO
143 def b(self):
144 '''Get semi-axis C{B{b}} of this ellipse (C{meter}, conventionally).
145 '''
146 _, b, _ = self._ab3
147 return Meter(b=b)
149 @Property_RO
150 def c(self):
151 '''Get the U{linear eccentricity<https://WikiPedia.org/wiki/Ellipse#Linear_eccentricity>}
152 C{c}, I{unsigned} (C{meter}, conventionally).
153 '''
154 return Meter(c=fabs(self.foci))
156 @Property_RO
157 def e(self):
158 '''Get the eccentricity (C{scalar, 0 <= B{e} <= 1}).
159 '''
160 e2 = self.e2
161 return Scalar(e=sqrt(e2) if 0 < e2 < 1 else e2)
163 @Property_RO
164 def e2(self):
165 '''Get the eccentricity I{squared} (C{scalar, 0 <= B{e2} <= 1}).
166 '''
167 # C{e2} is aka C{k}, Elliptic C{k2} and SciPy's C{m}
168 _, _, a, b = self._Pab4
169 e2 = ((_1_0 - (b / a)**2) if 0 < b else _1_0) if b < a else _0_0
170 return Scalar(e2=e2)
172 @Property_RO
173 def _Ek(self):
174 '''(INTERNAL) Get the C{Elliptic(k)} instance.
175 '''
176 return _MODS.elliptic._Ek(self.e2)
178 def _ellipE(self, k, phi=None): # PYCHOK k
179 '''(INTERNAL) Get the in-/complete integral of the 2nd kind.
180 '''
181 # assert k == self._Ek.k2
182 return self._Ek.cE if phi is None else self._Ek.fE(phi)
184 @property_ROnce
185 def _ellipe(self):
186 '''(INTERNAL) Wrap functions C{scipy.special.ellipe} and C{-.ellipeinc}, I{once}.
187 '''
188 try:
189 from scipy.special import ellipe, ellipeinc
191 def _ellipe(k, phi=None):
192 r = ellipe(k) if phi is None else ellipeinc(phi, k)
193 return float(r)
195 except (AttributeError, ImportError):
196 _ellipe = None
197 return _ellipe # overwrite property_ROnce
199 def _Error(self, where, **cause): # PYCHOK no cover
200 '''(INTERNAL) Build an L{EllipseError}.
201 '''
202 t = self.named3
203 u = unstr(t, a=self.a, b=self.b)
204 if where:
205 t = typename(where, where)
206 u = _DOT_(u, t)
207 return EllipseError(u, **cause)
209 @Property_RO
210 def foci(self):
211 '''Get the U{linear eccentricity<https://WikiPedia.org/wiki/Ellipse#Linear_eccentricity>},
212 I{signed} (C{meter}, conventionally), C{positive} if this ellipse is oblate, C{negative}
213 if prolate or C{0} if circular. See also property L{Ellipse.c}.
214 '''
215 c = float(self.e)
216 if c:
217 r, _, a, _ = self._Pab4
218 c *= a
219 if r: # prolate
220 c = -c
221 return Meter(foci=c) # signed
223 @property_ROnce
224 def _GKs(self):
225 '''(INTERNAL) Compute the coefficients for property C{.perimeterGK}, I{once}.
226 '''
227 # U{numerators<https://OEIS.org/A056981>}, U{denominators<https://OEIS.org/A056982>}
228 return (1, 1 / 4, 1 / 64, 1 / 256, 25 / 16384, 49 / 65536,
229 441 / 1048576, 1089 / 4194304) # overwrite property_ROnce
231 def hartzell4(self, x, y, los=False):
232 '''Compute the intersection of this ellipse with a Line-Of-Sight from Point-Of-View
233 C{(B{x}, B{y})} I{outside} this ellipse.
235 @kwarg los: Line-Of-Sight, I{direction} to the ellipse (L{Los}, L{Vector3d},
236 L{Vector2Tuple} or 2-tuple C{(dx, dy)}) or C{True} for the I{normal,
237 perpendicular, plumb} to this ellipse or C{False} or C{None} to
238 point to its center.
240 @return: L{Vector4Tuple}C{(x, y, z, h)} with coordinates C{x}, C{y} and C{z=0}
241 of the intersection and C{h} the distance to "Point-Of-View" C{(B{x},
242 B{y})} I{along the} B{C{los}}, all in C{meter}, conventionally.
244 @raise EllipseError: Invalid B{C{x}}, B{C{y}} or B{C{los}} or B{C{los}} points
245 outside or away from this ellipse.
247 @see: Function L{hartzell4<triaxials.triaxial5.hartzell4>} for further details.
248 '''
249 V3d = _MODS.vector3d.Vector3d
250 if los not in (True, False, None):
251 try:
252 los = V3d(los.x, los.y, 0)
253 except (AttributeError, TypeError):
254 if _MODS.basics.islistuple(los, minum=2):
255 los = V3d(*map(float, los[:2]))
256 return self._triaxialX(self.hartzell4, V3d(x, y, 0), los=los)
258 def height4(self, x, y, **normal_eps):
259 '''Compute the projection on and distance to this ellipse from a point C{(B{x}, B{y})}
260 in- or outside this ellipse.
262 @kwarg normal_eps: With default C{B{normal}=True} the projection is I{perpendicular,
263 plumb} to this ellipse, otherwise C{radially} to its center (C{bool}).
264 Tolerance C{B{eps}=EPS} for root finding and validation (C{scalar}),
265 use a negative value to skip validation.
267 @return: L{Vector4Tuple}C{(x, y, z, h)} with coordinates C{x}, C{y} and C{z=0}
268 of the projection on or the intersection with the ellipse and C{h} the
269 I{signed, normal distance} to the ellipse in C{meter}, conventionally.
270 Positive C{h} indicates, C{x} and/or C{y} are outside the ellipse,
271 negative C{h} means inside.
273 @raise EllipseError: Invalid B{C{x}}, B{C{y}} or B{C{eps}}, no convergence in
274 root finding or validation failed.
276 @see: Methods L{Ellipse.normal3d}, L{Ellipse.normal4} and function L{height4
277 <triaxials.triaxial5.height4>}.
278 '''
279 return self._triaxialX(self.height4, x, y, 0, **normal_eps)
281 def _HGKs(self, h, maxit):
282 '''(INTERNAL) Yield the terms for property C{.perimeterHGK}.
283 '''
284 s = t = _1_0
285 yield s
286 for u in range(-1, maxit * 2, 2):
287 t *= u / (u + 3) * h
288 t2 = t**2
289 yield t2
290 p = s
291 s += t2
292 if s == p: # 44 trips
293 break
294 else: # PYCHOK no cover
295 raise _ConvergenceError(maxit, s, p)
297 @property_RO
298 def isCircular(self):
299 '''Is this ellipse circular? (C{bool})
300 '''
301 return self.a == self.b
303 @property_RO
304 def isFlat(self):
305 '''Is this ellipse "flat", too pro-/oblate? (C{bool})
306 '''
307 return self._flat
309 @property_RO
310 def isOblate(self):
311 '''Is this ellipse oblate (foci on semi-axis C{a})? (C{bool})
312 '''
313 return self.a > self.b
315 @property_RO
316 def isProlate(self):
317 '''Is this ellipse prolate (foci on semi-axis C{b})? (C{bool})
318 '''
319 return self.a < self.b
321 @Property_RO
322 def lati(self):
323 '''Get the U{semi-latus rectum<https://WikiPedia.org/wiki/Ellipse#Semi-latus_rectum>},
324 I{signed} (C{meter}, conventionally), C{positive} if this ellipse is oblate or
325 circular, C{0} if "flat" and oblate, C{negative} if prolate or C{NEG0} if "flat"
326 and prolate. See also property L{Ellipse.p}.
327 '''
328 r, _, a, p = self._Pab4
329 if 0 < p < a:
330 p *= p / a
331 if r:
332 p = -p if p else NEG0
333 return Meter(lati=p) # signed
335 def normal3d(self, deg_x, y=None, **length):
336 '''Get a 3-D vector I{perpendicular to} this ellipse from point C{(B{x}, B{y})}
337 C{on} this ellipse or at C{B{deg} degrees} along this ellipse.
339 @kwarg length: Optional, signed C{B{length}=1} in out-/inward direction
340 (C{scalar}).
342 @return: A C{Vector3d(x_, y_, z_=0)} normalized to B{C{length}}, pointing
343 out- or inward for postive respectively negative B{C{length}}.
345 @raise EllipseError: Invalid B{C{x}} and/or B{C{y}}.
347 @see: Methods L{Ellipse.height4}, L{Ellipse.normal4}, L{Ellipse.sideOf} and
348 C{Triaxial_.normal3d}.
349 '''
350 return self._triaxialX(self.normal3d, *self._xy03(deg_x, y), **length)
352 def normal4(self, deg_x, y=None, **height_normal):
353 '''Compute a point at B{C{height}} above or below this ellipse point C{(B{x},
354 B{y})} C{on} this ellipse or at C{B{deg} degrees} along this ellipse.
356 @kwarg height_normal: The desired distance C{B{height}=0} in- or outside this
357 ellipse (C{meter}, conventionally) and C{B{normal}=True}, If
358 C{B{normal}=True}, the B{C{height}} is I{perpendicular, plumb}
359 to this ellipse, otherwise C{radially} to its center (C{bool}).
361 @return: L{Vector4Tuple}C{(x, y, z, h)} with coordinates C{x}, C{y} and C{z=0}
362 and C{h} the I{signed, normal distance} to the ellipse in C{meter},
363 conventionally. Positive C{h} indicates, C{x} and/or C{y} are outside
364 the ellipse, negative C{h} means inside.
366 @raise EllipseError: Invalid B{C{x}} and/or B{C{y}}.
368 @see: Methods L{Ellipse.height4}, L{Ellipse.normal3d}, L{Ellipse.sideOf} and
369 C{Triaxial_.normal4}.
370 '''
371 return self._triaxialX(self.normal4, *self._xy03(deg_x, y), **height_normal)
373 @Property_RO
374 def p(self):
375 '''Get the U{semi-latus rectum<https://WikiPedia.org/wiki/Ellipse#Semi-latus_rectum>}
376 C{p (aka B{𝓁}, script-small-l)}, I{unsigned} (C{meter}, conventionally).
377 '''
378 return Meter(p=fabs(self.lati))
380 @Property_RO
381 def perimeterAGM(self):
382 '''Compute the perimeter of this ellipse using the U{Arithmetic-Geometric Mean
383 <https://PaulBourke.net/geometry/ellipsecirc>} formula (C{meter}, conventionally).
384 '''
385 _, P, a, b = self._Pab4
386 if P is None:
387 t = _TOL53
388 m = -1
389 c = a + b
390 ds = [c**2]
391 _d = ds.append
392 for _ in range(self._maxit): # 4..5 trips
393 b = sqrt(a * b)
394 a = c * _0_5
395 c = a + b
396 d = a - b
397 m *= 2
398 _d(d**2 * m)
399 if d <= (b * t):
400 break
401 else: # PYCHOK no cover
402 raise _ConvergenceError(self._maxit, _over(d, b), t)
403 P = _over(_fsum(ds) * PI, c) # nonfinites=True
404 return Meter(perimeterAGM=P)
406 @Property_RO
407 def perimeter4Arc3(self):
408 '''Compute the perimeter (and arcs) of this ellipse using the U{4-Arc
409 <https://PaulBourke.net/geometry/ellipsecirc>} (aka 4-Center)
410 approximation as a 3-Tuple C{(P, Ra, Rb)} with perimeter C{P}, arc
411 radii C{Ra} and C{Rb} at the respective semi-axes (all in C{meter},
412 conventionally).
413 '''
414 r, P, a, b = self._Pab4
415 if P is None:
416 h = hypot(a, b)
417 t = atan2(b, a)
418 s, c = sincos2(t)
419 L = (h - (a - b)) * _0_5
420 a = _over(L, c)
421 b = _over(h - L, s)
422 P = (t * b + (PI_2 - t) * a) * _4_0
423 elif a > b: # flat
424 a, b = _0_0, _1_over(b) # INF
425# else: # circular
426# pass
427 if r:
428 a, b = b, a
429 return Meter(perimeter4Arc=P), Radius(Ra=a), Radius(Rb=b)
431# @Property_RO
432# def perimeterBPA(self):
433# '''Compute the perimeter of this ellipse using the U{Bronshtein Padé
434# Approximant <https://www.math.TTU.edu/~pearce/papers/schov.pdf>}
435# (C{meter}, conventionally).
436# '''
437# P, h = self._Ph2
438# if h:
439# h *= h
440# P *= _over(h**2 * _3_0 - _64_0, h * _16_0 - _64_0) * PI
441# return Meter(perimeterBPA=P)
443 @Property_RO
444 def perimeterCR(self):
445 '''Compute the perimeter of this ellipse using U{Rackauckas'
446 <https://www.ChrisRackauckas.com/assets/Papers/ChrisRackauckas-The_Circumference_of_an_Ellipse.pdf>}
447 approximation, also U{here<https://ExtremeLearning.com.AU/a-formula-for-the-perimeter-of-an-ellipse>} and
448 U{here<http://www.EByte.IT/library/docs/math05a/EllipsePerimeterApprox05.html>} (C{meter}, conventionally).
449 '''
450 P, h = self._Ph2
451 if h:
452 h *= h
453 P *= _over(fhorner(h, 135168, -85760, -5568, 3867),
454 fhorner(h, 135168, -119552, 22208, -345)) * PI
455 return Meter(perimeterCR=P)
457 @Property_RO
458 def perimeterGK(self):
459 '''Compute the perimeter of this ellipse using the U{Gauss-Kummer
460 <https://www.JohnDCook.com/blog/2023/05/28/approximate-ellipse-perimeter>}
461 series, C{B{b / a} > 0.75} (C{meter}, conventionally).
462 '''
463 P, h = self._Ph2
464 if h:
465 P *= fhorner(h**2, *self._GKs) * PI
466 return Meter(perimeterGK=P)
468 @Property_RO
469 def perimeterHGK(self):
470 '''Compute the perimeter of this ellipse using the U{Hypergeometric Gauss-Kummer
471 <https://web.Tecnico.ULisboa.PT/~mcasquilho/compute/com/,ellips/PerimeterOfEllipse.pdf>}
472 series (C{meter}, conventionally).
473 '''
474 P, h = self._Ph2
475 if h:
476 hs = self._HGKs(h, self._maxit)
477 P *= _fsum(hs) * PI # nonfinites=True
478 return Meter(perimeterHGK=P)
480# @Property_RO
481# def perimeterJWPA(self):
482# '''Compute the perimeter of this ellipse using the U{Jacobson-Waadeland
483# Padé Approximant <https://www.math.TTU.edu/~pearce/papers/schov.pdf>}
484# (C{meter}, conventionally).
485# '''
486# P, h = self._Ph2
487# if h:
488# h *= h
489# P *= _over(fhorner(h, 256, -48, -21),
490# fhorner(h, 256, -112, 3)) * PI
491# return Meter(perimeterJWPA=P)
493 @Property_RO
494 def perimeter2k(self):
495 '''Compute the perimeter of this ellipse using the complete integral
496 of the 2nd kind, C{Elliptic.cE} (C{meter}, conventionally).
497 '''
498 return Meter(perimeter2k=self._perimeter2k(self._ellipE))
500 @Property_RO
501 def perimeter2k_(self):
502 '''Compute the perimeter of this ellipse using U{SciPy's ellipe
503 <https://www.JohnDCook.com/perimeter_ellipse.html>} function
504 if available, otherwise use property C{perimeter2k} (C{meter},
505 conventionally).
506 '''
507 return Meter(perimeter2k_=self._perimeter2k(self._ellipe or self._ellipE))
509 def _perimeter2k(self, _ellip):
510 '''(INTERNAL) Helper for methods C{.PE2k} and C{.Pe2k}.
511 '''
512 _, P, a, _ = self._Pab4
513 if P is None: # see .ellipsoids.Ellipsoid.L
514 k = self.e2
515 P = _ellip(k) * a * _4_0
516 return P
518# @Property_RO
519# def perimeterLS(self):
520# '''Compute the perimeter of this ellipse using the U{Linderholm-Segal
521# <https://www.JohnDCook.com/blog/2021/03/24/perimeter-of-an-ellipse>}
522# formula, aka C{3/2 norm} (C{meter}, conventionally).
523# '''
524# _, P, a, b = self._Pab4
525# if P is None:
526# n = pow(a, _1_5) + pow(b, _1_5)
527# P = pow(n * _0_5, _2_3rd) * PI2
528# return Meter(perimeterLS=P)
530 @Property_RO
531 def perimeter2R(self):
532 '''Compute the perimeter of this ellipse using U{Ramanujan's 2nd
533 <https://PaulBourke.net/geometry/ellipsecirc>} approximation,
534 C{B{b / a} > 0.9} (C{meter}, conventionally).
535 '''
536 P, h = self._Ph2
537 if h:
538 P *= _2RC(h, _1_0)
539 return Meter(perimeter2R=P)
541 @Property_RO
542 def perimeter2RC(self):
543 '''Compute the perimeter of this ellipse using U{Cantrell Ramanujan's 2nd
544 <http://www.EByte.IT/library/docs/math05a/EllipsePerimeterApprox05.html>}
545 approximation, C{B{b / a} > 0.9} (C{meter}, conventionally).
546 '''
547 P, h = self._Ph2
548 if h:
549 P *= _2RC(h, pow(h, 24) * self._4_PI_ + _1_0)
550 return Meter(perimeter2RC=P)
552# @Property_RO
553# def perimeterSPA(self):
554# '''Compute the perimeter of this ellipse using the U{Selmer Padé Approximant
555# <http://www.EByte.IT/library/docs/math05a/EllipsePerimeterApprox05.html>}
556# (C{meter}, conventionally).
557# '''
558# P, h = self._Ph2
559# if h:
560# h *= h
561# P *= _over(h * _3_0 + _16_0, _16_0 - h) * PI
562# return Meter(perimeterSPA=P)
564 @Property_RO
565 def _Ph2(self):
566 _, P, a, b = self._Pab4
567 if P is None:
568 if b:
569 P = a + b
570 h = (a - b) / P
571 else:
572 P = a
573 h = _1_0
574 else:
575 h = None
576 return P, h
578 def point(self, deg_x, y=None):
579 '''Return the point I{on} this ellipse at C{B{deg}} or C{atan2d(B{y}, B{x})
580 degrees} along this ellipse.
582 @return: A 2-tuple C{(x, y)}.
583 '''
584 s, c = sincos2d(deg_x) if y is None else self._sc2(deg_x, y, eps=None)
585 return (self.a * c), (self.b * s)
587 def points(self, np, nq=4, ccw=False, ended=False, eps=EPS): # MCCABE 13
588 '''Yield up to C{np} points along this ellipse, each a 2-tuple C{(x, y)},
589 starting at semi-axis C{+a}, in (counter-)clockwise order and distributed
590 evenly along the minor semi-axis.
592 @arg np: Number of points to generate (C{int}).
593 @kwarg nq: Number of quarters to cover (C{int}, 1..4).
594 @kwarg ccw: Use C{B{ccw}=True} for counter-clockwise order (C{bool}).
595 @kwarg ended: If C{True}, include the last quadrant's end point (C{bool}).
596 @kwarg eps: Tolerance for duplicate points (C{meter}, conventionally).
598 @see: U{Directrix<https://MathWorld.Wolfram.com/ConicSectionDirectrix.html>}.
599 '''
600 a, b, _ = self._ab3
601 if min(a, b) > eps and not self.isFlat:
602 q = max(min(int(nq), 4), 1)
603 n = max( int(np) // q, 1)
604 if not ccw:
605 b = -b
606 ps = list(_q1ps(a, b, n, eps))
607 for p in ps: # 1st quadrant
608 yield p
609 p0 = ps.pop(0) # E
610 pq = _0_0, b # N/S
611 if q > 1:
612 yield pq
613 for x, y in reversed(ps): # 2nd
614 yield (-x), y
615 pq = (-a), _0_0 # W
616 if q > 2:
617 yield pq
618 for x, y in ps: # 3rd
619 yield (-x), (-y)
620 pq = _0_0, (-b) # S/N
621 if q > 3:
622 yield pq
623 for x, y in reversed(ps): # 4th
624 yield x, (-y)
625 pq = p0
626 if ended:
627 yield pq
628 else: # "flat"
629 p0 = a, b
630 yield p0
631 if max(a, b) > eps:
632 yield (-a), (-b)
633 if ended:
634 yield p0
636 def polar2d(self, deg_x, y=None):
637 '''For a point at C{B{deg}} or C{atan2d(B{y}, B{x}) degrees} along this
638 ellipse, return 2-tuple C{(radius, angle)} with the polar U{radius
639 <https://WikiPedia.org/wiki/Ellipse#Polar_form_relative_to_center>}
640 from the center (C{meter}, conventionally) and C{angle} in C{degrees}.
641 '''
642 s, c = sincos2d(deg_x) if y is None else self._sc2(deg_x, y, eps=None)
643 a, b, ab = self._ab3
644 r = (_over(ab, hypot(a * s, b * c)) if c else b) if s else a
645 return r, atan2b(s, c)
647 @Property_RO
648 def R1(self):
649 '''Get this ellipse' I{arithmetic mean} radius, C{(2 * a + b) / 3} (C{meter}, conventionally).
650 '''
651 _, _, a, r = self._Pab4
652 if r:
653 r = fmean_(a, a, r) if a > r else a
654 return Radius(R1=r or _0_0)
656 @Property_RO
657 def R2(self):
658 '''Get this ellipse' I{authalic} radius, C{sqrt(B{a} * B{b})} (C{meter}, conventionally).
659 '''
660 a, b, ab = self._ab3
661 return Radius(R2=(sqrt(ab) if a != b else float(a)) if ab else _0_0)
663 Rauthalic = Rgeometric = R2
665 def Roc(self, deg_x, y=None, eps=None):
666 '''Compute the U{radius of curvature<https://WikiPedia.org/wiki/Radius_of_curvature>}
667 at a point C{B{deg}} or C{atan2d(B{y}, B{x}) degrees} along this ellipse.
669 @see: Method L{Roc_<Ellipse.Roc_>} for ruther details.
670 '''
671 x = radians(deg_x) if y is None else deg_x
672 return self.Roc_(x, y, eps=eps)
674 def Roc_(self, rad_x, y=None, eps=None):
675 '''Compute the U{radius of curvature<https://WikiPedia.org/wiki/Radius_of_curvature>}
676 at a point C{B{rad}} or C{atan2(B{y}, B{x}) radians} along this ellipse.
678 @kwarg eps: See method C{sideOf}, use C{B{eps}=0} to permit any points.
680 @return: Curvature (C{meter}, conventionally).
682 @raise ValueError: Point C{(B{x}, B{y})} off this ellipse, unless C{B{eps}=0}.
683 '''
684 try:
685 a, b, ab = self._ab3
686 if b != a:
687 s, c = sincos2(rad_x) if y is None else self._sc2(rad_x, y, eps=eps)
688 r = _over(hypot(a * s, b * c)**3, ab)
689 else: # circular
690 r = float(a)
691 except Exception as X:
692 raise self._Error(self.Roc_, cause=X)
693 return Radius(Roc=r)
695 @Property_RO
696 def Rrectifying(self):
697 '''Get this ellipse' I{rectifying} radius, C{perimeter2k_ / PI2} (C{meter}, conventionally).
698 '''
699 return Radius(Rrectifying=self.perimeter2k_ / PI2)
701 def _sc2(self, x, y, eps=EPS):
702 '''(INTERNAL) Helper for methods C{.point}, C{.polar}, C{.Roc_} and C{.slope_}.
703 '''
704 if eps and eps > 0:
705 s = self._sideOf(x, y, eps)
706 if s:
707 raise _ValueError(x=x, y=y, eps=eps, sideOf=s)
708 h = hypot(x, y)
709 s = _over(y, h)
710 c = _over(x, h)
711 return s, c
713 def _sideOf(self, x, y, eps):
714 '''(INTERNAL) Helper for methods C{._sc2} and C{.sideOf}.
715 '''
716 a, b, ab = self._ab3
717 s = ab or max(a, b)
718 if s:
719 s = (hypot(x * b, y * a) - s) / s
720# s = max(_N_1_0, min(_1_0, s))
721 else: # dot
722 s = _1_0 if x or y else _0_0
723 return INT0 if fabs(s) < eps else s
725 def sideOf(self, x, y, eps=EPS):
726 '''Return a C{positive}, C{negative} or C{0} fraction if point C{(B{x}, B{y})}
727 is C{outside}, C{inside} respectively C{on} this ellipse.
728 '''
729 try:
730 return Scalar(sideOf=self._sideOf(x, y, eps))
731 except Exception as X:
732 raise self._Error(self.sideOf, x=x, y=y, cause=X)
734 def slope(self, deg_x, y=None, eps=None):
735 '''Compute the U{tangent slope<https://WikiPedia.org/wiki/Ellipse#Tangent_slope_as_parameter>}
736 at a point C{B{deg}} or C{atan2d(B{y}, B{x}) degrees} along this ellipse.
738 @return: Slope (C{degrees}), negative for C{0 <= B{deg} < 90}.
740 @see: Method L{slope_<Ellipse.slope_>} for further details.
741 '''
742 x = radians(deg_x) if y is None else deg_x
743 return Degrees(slope=degrees(self.slope_(x, y, eps=eps)))
745 def slope_(self, rad_x, y=None, eps=None):
746 '''Compute the U{tangent slope<https://WikiPedia.org/wiki/Ellipse#Tangent_slope_as_parameter>}
747 at a point C{B{rad}} or C{atan2(B{y}, B{x}) radians} along this ellipse.
749 @kwarg eps: See method C{sideOf}, use C{B{eps}=0} to permit any points.
751 @return: Slope (C{radians}), negative for C{0 <= B{rad} < PI/2}.
753 @raise ValueError: C{(B{x}, B{y})} off this ellipse, unless C{B{eps}=0}.
754 '''
755 # <https://UNacademy.com/content/jee/study-material/mathematics/equation-of-a-tangent-to-the-ellipse/>
756 s, c = sincos2(rad_x) if y is None else self._sc2(rad_x, y, eps=eps)
757 r = atan2p(-self.b * c, self.a * s)
758 if r >= PI3_2:
759 r -= PI2
760 return Radians(slope=r or _0_0) # no -0.0
762 def toEllipsoid(self, **Ellipsoid_and_kwds):
763 '''Return an L{Ellipsoid<pygeodesy.Ellipsoid>} from this ellipse'
764 C{a} and C{b} semi-axes.
766 @kwarg Ellipsoid_and_kwds: Optional C{B{Ellipsoid}=Ellipsoid} class
767 and additional C{Ellipsoid} keyword arguments.
768 '''
769 E, kwds = _xkwds_pop2(Ellipsoid_and_kwds, Ellipsoid=
770 _MODS.ellipsoids.Ellipsoid)
771 return E(self.a, b=self.b, **_xkwds(kwds, name=self.name))
773 def toStr(self, prec=8, terse=2, **sep_name): # PYCHOK signature
774 '''Return this ellipse as a text string.
776 @kwarg prec: Number of decimal digits, unstripped (C{int}).
777 @kwarg terse: Limit the number of items (C{int}, 0...9),
778 use C{B{terse}=0} or C{=None} for all.
779 @kwarg sep_name: Optional C{B{name}=NN} (C{str}) or C{None}
780 to exclude this ellipse' name and separator
781 C{B{sep}=", "} to join the items (C{str}).
783 @return: This C{Ellipse}' attributes (C{str}).
784 '''
785 E = Ellipse
786 t = E.a, E.b
787 if (terse or 0) != 2:
788 t += E.c, E.e, E.e2, E.p, E.area, E.perimeter2k, E.R2
789 if terse:
790 t = t[:terse]
791 return self._instr(prec=prec, props=t, **sep_name)
793 def toTriaxial_(self, c=EPS, **Triaxial_and_kwds): # like .Ellipse5Tuple.toTriaxial_
794 '''Return a L{Triaxial_<pygeodesy.Triaxial_>} from this ellipse' semi-axes.
796 @kwarg c: Near-zero, minor semi-axis (C{meter}, conventionally).
797 @kwarg Triaxial_and_kwds: Optional C{B{Triaxial}=Triaxial_} class and
798 additional C{Triaxial} keyword arguments.
799 '''
800 T, kwds = _xkwds_pop2(Triaxial_and_kwds, Triaxial=_MODS.triaxials.Triaxial_)
801 return T(self.a, b=self.b, c=c, **_xkwds(kwds, name=self.name or _UNDER_)) # 'NN'
803 def _triaxialX(self, method, *args, **kwds):
804 '''(INTERNAL) Invoke a triaxial method and map exceptions to L{EllipseError}s.
805 '''
806 try:
807 t = self._tEPS
808 if t is None:
809 self._tEPS = t = self.toTriaxial_(EPS)
810 _m = getattr(t, method.__name__)
811 return _m(*args, **kwds)
812 except Exception as x:
813 raise self._Error(method, Triaxial_=t, cause=x)
815 def _xy03(self, deg_x, y):
816 if y is None:
817 y, x = sincos2d(deg_x)
818 y *= self.b
819 x *= self.a
820 else:
821 x = float(deg_x)
822 y = float(y)
823 return x, y, 0
826class EllipseError(_ValueError):
827 '''Raised for any L{Ellipse} or C{ellipses} issue.
828 '''
829 pass # ...
832def _arc(_e, k, r):
833 # in C{Ellipse.arc_}
834 t, r = divmod(r, PI2)
835 R = _e(k, r) # phi=r
836 if t: # + t * perimeter
837 t *= _e(k) * _4_0
838 R += t
839 return R
842def _isFlat(a, b): # in .triaxials.bases
843 # is C{b <<< a}?
844 return b < (a * _TOL53_53)
847def _q1ps(a, b, n, eps):
848 # yield the 1st quadrant C{Ellipse.points}
849 if a > b: # oblate
850 def _yx2(i):
851 y = i / n
852 return y, sqrt(_1_0 - y**2)
854 elif a < b: # prolate
855 def _yx2(i): # PYCHOK redef
856 x = (n - i) / n
857 return sqrt(_1_0 - x**2), x
859 else: # circular
860 r = PI_2 / n
861 def _yx2(i): # PYCHOK redef
862 return sincos2(r * i)
864 p = a, _0_0 # == p0
865 yield p
866 for i in range(1, n):
867 y, x = _yx2(i)
868 y *= b
869 x *= a
870 if euclid(x, y, *p) > eps:
871 p = x, y
872 yield p
875def _2RC(h, r):
876 # in C{Ellipse.perimeter2R} and C{.perimeter2RC}
877 h *= _3_0 * h
878 r += h / (sqrt(_4_0 - h) + _10_0)
879 return r * PI
881# **) MIT License
882#
883# Copyright (C) 2026-2026 -- mrJean1 at Gmail -- All Rights Reserved.
884#
885# Permission is hereby granted, free of charge, to any person obtaining a
886# copy of this software and associated documentation files (the "Software"),
887# to deal in the Software without restriction, including without limitation
888# the rights to use, copy, modify, merge, publish, distribute, sublicense,
889# and/or sell copies of the Software, and to permit persons to whom the
890# Software is furnished to do so, subject to the following conditions:
891#
892# The above copyright notice and this permission notice shall be included
893# in all copies or substantial portions of the Software.
894#
895# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
896# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
897# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
898# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
899# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
900# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
901# OTHER DEALINGS IN THE SOFTWARE.