Coverage for pygeodesy/vector3dBase.py: 91%
288 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-09-09 13:03 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-09-09 13:03 -0400
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) Private, 3-D vector base class C{Vector3dBase}.
6A pure Python implementation of vector-based functions by I{(C) Chris Veness
72011-2024} published under the same MIT Licence**, see U{Vector-based geodesy
8<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>}.
9'''
11from pygeodesy.basics import _copysign, islistuple, isscalar, \
12 map1, map2, _signOf, _zip
13from pygeodesy.constants import EPS, EPS0, INT0, PI, PI2, \
14 _1_0, isnear0, isnear1, isneg0, \
15 _copysignINF, _float0, _pos_self
16from pygeodesy.errors import CrossError, VectorError, _xcallable, _xError
17from pygeodesy.fmath import euclid_, fdot, fdot_, hypot_, hypot2_ # _MODS.fmath.fma
18from pygeodesy.interns import _coincident_, _colinear_, _COMMASPACE_, _xyz_
19from pygeodesy.lazily import _ALL_LAZY, _ALL_DOCS, _ALL_MODS as _MODS
20from pygeodesy.named import _NamedBase, _NotImplemented, _xother3
21# from pygeodesy.namedTuples import Vector3Tuple # _MODS
22# from pygeodesy.nvectorBase import _N_Vector # _MODS
23from pygeodesy.props import deprecated_method, Property, Property_RO, \
24 property_doc_, property_RO, _update_all
25from pygeodesy.streprs import Fmt, strs, unstr
26from pygeodesy.units import Float, Scalar
27from pygeodesy.utily import atan2, sincos2, fabs
29from math import ceil as _ceil, floor as _floor, trunc as _trunc
31__all__ = _ALL_LAZY.vector3dBase
32__version__ = '25.08.31'
35class Vector3dBase(_NamedBase): # sync __methods__ with .fsums.Fsum
36 '''(INTERNAL) Generic 3-D vector base class.
37 '''
38 _crosserrors = True # un/set by .errors.crosserrors
40 _ll = None # original latlon, '_fromll'
41# _x = INT0 # X component
42# _y = INT0 # Y component
43# _z = INT0 # Z component
45 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, **name):
46 '''New L{Vector3d} or C{Vector3dBase} instance.
48 The vector may be normalised or use x, y, z for position and
49 distance from earth centre or height relative to the surface
50 of the earth' sphere or ellipsoid.
52 @arg x_xyz: X component of vector (C{scalar}) or a (3-D) vector
53 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
54 L{Vector3Tuple}, L{Vector4Tuple} or a C{tuple} or
55 C{list} of 3+ C{scalar} items).
56 @kwarg y: Y component of vector (C{scalar}), ignored if B{C{x_xyz}}
57 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
58 @kwarg z: Z component of vector (C{scalar}), ignored if B{C{x_xyz}}
59 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
60 @kwarg ll: Optional latlon reference (C{LatLon}).
61 @kwarg name: Optional C{B{name}=NN} (C{str}).
63 @raise VectorError: Invalid B{C{x_xyz}}, B{C{y}} or B{C{z}}.
64 '''
65 self._x, \
66 self._y, \
67 self._z = _xyz3(type(self), x_xyz, y, z) if isscalar(x_xyz) else \
68 _xyz3(type(self), x_xyz)
69 if ll:
70 self._ll = ll
71 if name:
72 self.name = name
74 def __abs__(self):
75 '''Return the norm of this vector.
77 @return: Norm, unit length (C{float});
78 '''
79 return self.length
81 def __add__(self, other):
82 '''Add this to an other vector (L{Vector3d}).
84 @return: Vectorial sum (L{Vector3d}).
86 @raise TypeError: Incompatible B{C{other}} C{type}.
87 '''
88 return self.plus(other)
90 def __bool__(self): # PYCHOK PyChecker
91 '''Is this vector non-zero?
93 @see: Method C{bools}.
94 '''
95 return bool(self.x or self.y or self.z)
97 def __ceil__(self): # PYCHOK no cover
98 '''Return a vector with the C{ceil} of these components.
100 @return: Ceil-ed (L{Vector3d}).
101 '''
102 return self._mapped(_ceil)
104 def __cmp__(self, other): # Python 2-
105 '''Compare this and an other vector (L{Vector3d}).
107 @return: -1, 0 or +1 (C{int}).
109 @raise TypeError: Incompatible B{C{other}} C{type}.
110 '''
111 return _signOf(self.length, self._other_cmp(other))
113 cmp = __cmp__
115 def __divmod__(self, other): # PYCHOK no cover
116 '''Not implemented.'''
117 return _NotImplemented(self, other)
119 def __eq__(self, other):
120 '''Is this vector equal to an other vector?
122 @arg other: The other vector (L{Vector3d}).
124 @return: C{True} if equal, C{False} otherwise.
126 @raise TypeError: Incompatible B{C{other}} C{type}.
127 '''
128 return self.isequalTo(other, eps=EPS0)
130 def __float__(self): # PYCHOK no cover
131 '''Not implemented, see method C{float}.'''
132 return _NotImplemented(self) # must return C{float}
134 def __floor__(self): # PYCHOK no cover
135 '''Return a vector with the C{floor} of these components.
137 @return: Floor-ed (L{Vector3d}).
138 '''
139 return self._mapped(_floor)
141 def __floordiv__(self, other): # PYCHOK no cover
142 '''Not implemented.'''
143 return _NotImplemented(self, other)
145 def __ge__(self, other):
146 '''Is this vector longer than or equal to an other vector?
148 @arg other: The other vector (L{Vector3d}).
150 @return: C{True} if so, C{False} otherwise.
152 @raise TypeError: Incompatible B{C{other}} C{type}.
153 '''
154 return self.length >= self._other_cmp(other)
156# def __getitem__(self, key):
157# '''Return C{item} at index or slice C{[B{key}]}.
158# '''
159# return self.xyz[key]
161 def __gt__(self, other):
162 '''Is this vector longer than an other vector?
164 @arg other: The other vector (L{Vector3d}).
166 @return: C{True} if so, C{False} otherwise.
168 @raise TypeError: Incompatible B{C{other}} C{type}.
169 '''
170 return self.length > self._other_cmp(other)
172 def __hash__(self): # PYCHOK no cover
173 '''Return this instance' C{hash}.
174 '''
175 return hash(self.xyz) # XXX id(self)?
177 def __iadd__(self, other):
178 '''Add this and an other vector I{in-place}, C{this += B{other}}.
180 @arg other: The other vector (L{Vector3d}).
182 @raise TypeError: Incompatible B{C{other}} C{type}.
183 '''
184 return self._xyz(self.plus(other))
186 def __ifloordiv__(self, other): # PYCHOK no cover
187 '''Not implemented.'''
188 return _NotImplemented(self, other)
190 def __imatmul__(self, other): # PYCHOK Python 3.5+
191 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}.
193 @arg other: The other vector (L{Vector3d}).
195 @raise TypeError: Incompatible B{C{other}} C{type}.
197 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 397+, 2022 p. 578+.
198 '''
199 return self._xyz(self.cross(other))
201 def __imod__(self, other): # PYCHOK no cover
202 '''Not implemented.'''
203 return _NotImplemented(self, other)
205 def __imul__(self, scalar):
206 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}.
208 @arg scalar: Factor (C{scalar}).
210 @raise TypeError: Non-scalar B{C{scalar}}.
211 '''
212 return self._xyz(self.times(scalar))
214 def __int__(self): # PYCHOK no cover
215 '''Not implemented, see method C{ints}.'''
216 return _NotImplemented(self) # must return C{int}
218 def __ipow__(self, other, *mod): # PYCHOK no cover
219 '''Not implemented.'''
220 return _NotImplemented(self, other, *mod)
222 def __isub__(self, other):
223 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.
225 @arg other: The other vector (L{Vector3d}).
227 @raise TypeError: Incompatible B{C{other}} C{type}.
228 '''
229 return self._xyz(self.minus(other))
231# def __iter__(self):
232# '''Return an C{iter}ator over this vector's components.
233# '''
234# return iter(self.xyz3)
236 def __itruediv__(self, scalar):
237 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.
239 @arg scalar: The divisor (C{scalar}).
241 @raise TypeError: Non-scalar B{C{scalar}}.
242 '''
243 return self._xyz(self.dividedBy(scalar))
245 def __le__(self, other): # Python 3+
246 '''Is this vector shorter than or equal to an other vector?
248 @arg other: The other vector (L{Vector3d}).
250 @return: C{True} if so, C{False} otherwise.
252 @raise TypeError: Incompatible B{C{other}} C{type}.
253 '''
254 return self.length <= self._other_cmp(other)
256# def __len__(self):
257# '''Return C{3}, always.
258# '''
259# return len(self.xyz)
261 def __lt__(self, other): # Python 3+
262 '''Is this vector shorter than an other vector?
264 @arg other: The other vector (L{Vector3d}).
266 @return: C{True} if so, C{False} otherwise.
268 @raise TypeError: Incompatible B{C{other}} C{type}.
269 '''
270 return self.length < self._other_cmp(other)
272 def __matmul__(self, other): # PYCHOK Python 3.5+
273 '''Compute the cross product of this and an other vector, C{this @ B{other}}.
275 @arg other: The other vector (L{Vector3d}).
277 @return: Cross product (L{Vector3d}).
279 @raise TypeError: Incompatible B{C{other}} C{type}.
280 '''
281 return self.cross(other)
283 def __mod__(self, other): # PYCHOK no cover
284 '''Not implemented.'''
285 return _NotImplemented(self, other)
287 def __mul__(self, scalar):
288 '''Multiply this vector by a scalar, C{this * B{scalar}}.
290 @arg scalar: Factor (C{scalar}).
292 @return: Product (L{Vector3d}).
293 '''
294 return self.times(scalar)
296 def __ne__(self, other):
297 '''Is this vector not equal to an other vector?
299 @arg other: The other vector (L{Vector3d}).
301 @return: C{True} if so, C{False} otherwise.
303 @raise TypeError: Incompatible B{C{other}} C{type}.
304 '''
305 return not self.isequalTo(other, eps=EPS0)
307 def __neg__(self):
308 '''Return the opposite of this vector.
310 @return: A negated copy (L{Vector3d})
311 '''
312 return self.negate()
314 def __pos__(self): # PYCHOK no cover
315 '''Return this vector I{as-is} or a copy.
317 @return: This instance (L{Vector3d})
318 '''
319 return self if _pos_self else self.copy()
321 def __pow__(self, other, *mod): # PYCHOK no cover
322 '''Not implemented.'''
323 return _NotImplemented(self, other, *mod)
325 __radd__ = __add__ # PYCHOK no cover
327 def __rdivmod__ (self, other): # PYCHOK no cover
328 '''Not implemented.'''
329 return _NotImplemented(self, other)
331# def __repr__(self):
332# '''Return the default C{repr(this)}.
333# '''
334# return self.toRepr()
336 def __rfloordiv__(self, other): # PYCHOK no cover
337 '''Not implemented.'''
338 return _NotImplemented(self, other)
340 def __rmatmul__(self, other): # PYCHOK Python 3.5+
341 '''Compute the cross product of an other and this vector, C{B{other} @ this}.
343 @arg other: The other vector (L{Vector3d}).
345 @return: Cross product (L{Vector3d}).
347 @raise TypeError: Incompatible B{C{other}} C{type}.
348 '''
349 return self.others(other).cross(self)
351 def __rmod__(self, other): # PYCHOK no cover
352 '''Not implemented.'''
353 return _NotImplemented(self, other)
355 __rmul__ = __mul__
357 def __round__(self, *ndigits): # PYCHOK no cover
358 '''Return a vector with these components C{rounded}.
360 @arg ndigits: Optional number of digits (C{int}).
362 @return: Rounded (L{Vector3d}).
363 '''
364 # <https://docs.Python.org/3.12/reference/datamodel.html?#object.__round__>
365 return self.classof(*(round(_, *ndigits) for _ in self.xyz3))
367 def __rpow__(self, other, *mod): # PYCHOK no cover
368 '''Not implemented.'''
369 return _NotImplemented(self, other, *mod)
371 def __rsub__(self, other): # PYCHOK no cover
372 '''Subtract this vector from an other vector, C{B{other} - this}.
374 @arg other: The other vector (L{Vector3d}).
376 @return: Difference (L{Vector3d}).
378 @raise TypeError: Incompatible B{C{other}} C{type}.
379 '''
380 return self.others(other).minus(self)
382 def __rtruediv__(self, scalar): # PYCHOK no cover
383 '''Not implemented.'''
384 return _NotImplemented(self, scalar)
386# def __str__(self):
387# '''Return the default C{str(self)}.
388# '''
389# return self.toStr()
391 def __sub__(self, other):
392 '''Subtract an other vector from this vector, C{this - B{other}}.
394 @arg other: The other vector (L{Vector3d}).
396 @return: Difference (L{Vector3d}).
398 @raise TypeError: Incompatible B{C{other}} C{type}.
399 '''
400 return self.minus(other)
402 def __truediv__(self, scalar):
403 '''Divide this vector by a scalar, C{this / B{scalar}}.
405 @arg scalar: The divisor (C{scalar}).
407 @return: Quotient (L{Vector3d}).
409 @raise TypeError: Non-scalar B{C{scalar}}.
410 '''
411 return self.dividedBy(scalar)
413 def __trunc__(self): # PYCHOK no cover
414 '''Return a vector with the C{trunc} of these components.
416 @return: Trunc-ed (L{Vector3d}).
417 '''
418 return self._mapped(_trunc)
420 if _MODS.sys_version_info2 < (3, 0): # PYCHOK no cover
421 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
422 __div__ = __truediv__
423 __idiv__ = __itruediv__
424 __long__ = __int__
425 __nonzero__ = __bool__
426 __rdiv__ = __rtruediv__
428 def angleTo(self, other, vSign=None, wrap=False):
429 '''Compute the angle between this and an other vector.
431 @arg other: The other vector (L{Vector3d}).
432 @kwarg vSign: Optional vector, if supplied (and out of the
433 plane of this and the other), angle is signed
434 positive if this->other is clockwise looking
435 along vSign or negative in opposite direction,
436 otherwise angle is unsigned.
437 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).
439 @return: Angle (C{radians}).
441 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.
442 '''
443 x = self.cross(other)
444 s = x.length
445 # use vSign as reference to set sign of s
446 if s and vSign and x.dot(vSign) < 0:
447 s = -s
449 a = atan2(s, self.dot(other))
450 if wrap and fabs(a) > PI:
451 a -= _copysign(PI2, a)
452 return a
454 def apply(self, fun2, other_x, *y_z, **fun2_kwds):
455 '''Apply a 2-argument function pairwise to the components
456 of this and an other vector.
458 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),
459 return a C{scalar} or L{INT0} result.
460 @arg other_x: Other X component (C{scalar}) or a vector
461 with X, Y and Z components (C{Cartesian},
462 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
463 L{Vector3Tuple} or L{Vector4Tuple}).
464 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).
465 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.
467 @return: New, applied vector (L{Vector3d}).
469 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
470 '''
471 _xcallable(fun2=fun2)
472 if fun2_kwds:
473 def _f2(a, b):
474 return fun2(a, b, **fun2_kwds)
475 else:
476 _f2 = fun2
478 xyz = _xyz3(self.apply, other_x, *y_z)
479 xyz = (_f2(a, b) for a, b in _zip(self.xyz3, xyz)) # strict=True
480 return self.classof(*xyz)
482 def bools(self):
483 '''Return the vector with C{bool} components.
484 '''
485 return self._mapped(bool)
487 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN
488 '''Compute the cross product of this and an other vector.
490 @arg other: The other vector (L{Vector3d}).
491 @kwarg raiser: Optional, L{CrossError} label if raised (C{str}, non-L{NN}).
492 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as C{x}, C{y} and
493 C{z}.
495 @return: Cross product (L{Vector3d}).
497 @raise CrossError: Zero or near-zero cross product and if B{C{raiser}} and
498 L{crosserrors<pygeodesy.crosserrors>} are both C{True}.
500 @raise TypeError: Incompatible B{C{other}} C{type}.
501 '''
502 X, Y, Z = self.others(other).xyz3
503 x, y, z = self.xyz3
504 xyz = (fdot_(y, Z, -z, Y),
505 fdot_(z, X, -x, Z),
506 fdot_(x, Y, -y, X))
508 if raiser and self.crosserrors and eps0 > 0 \
509 and max(map(fabs, xyz)) < eps0:
510 r = other._fromll or other
511 s = self._fromll or self
512 t = self.isequalTo(other, eps=eps0)
513 t = _coincident_ if t else _colinear_
514 raise CrossError(raiser, s, other=r, txt=t)
516 return self.classof(*xyz) # name__=self.cross
518 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')
519 def crosserrors(self):
520 '''Get L{CrossError} exceptions (C{bool}).
521 '''
522 return self._crosserrors
524 @crosserrors.setter # PYCHOK setter!
525 def crosserrors(self, raiser):
526 '''Raise or ignore L{CrossError} exceptions (C{bool}).
527 '''
528 self._crosserrors = bool(raiser)
530 def dividedBy(self, divisor):
531 '''Divide this vector by a scalar.
533 @arg divisor: The divisor (C{scalar}).
535 @return: New, scaled vector (L{Vector3d}).
537 @raise TypeError: Non-scalar B{C{divisor}}.
539 @raise VectorError: Invalid or zero B{C{divisor}}.
540 '''
541 d = Scalar(divisor=divisor)
542 try:
543 return self._times(_1_0 / d)
544 except (ValueError, ZeroDivisionError) as x:
545 raise VectorError(divisor=divisor, cause=x)
547 def dot(self, other):
548 '''Compute the dot (scalar) product of this and an other vector.
550 @arg other: The other vector (L{Vector3d}).
552 @return: Dot product (C{float}).
554 @raise TypeError: Incompatible B{C{other}} C{type}.
555 '''
556 return self.length2 if other is self else fdot(
557 self.xyz3, *self.others(other).xyz3)
559 @deprecated_method
560 def equals(self, other, units=False): # PYCHOK no cover
561 '''DEPRECATED, use method C{isequalTo}.
562 '''
563 return self.isequalTo(other, units=units)
565 @Property_RO
566 def euclid(self):
567 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).
569 @see: Properties C{length} and C{length2} and function
570 L{pygeodesy.euclid_}.
571 '''
572 return Float(euclid=euclid_(*self.xyz3))
574 def equirectangular(self, other):
575 '''I{Approximate} the difference between this and an other vector.
577 @arg other: Vector to subtract (C{Vector3dBase}).
579 @return: The length I{squared} of the difference (C{Float}).
581 @raise TypeError: Incompatible B{C{other}} C{type}.
583 @see: Property C{length2}.
584 '''
585 d = self.minus(other)
586 return Float(equirectangular=hypot2_(*d.xyz3))
588 def fabs(self):
589 '''Return the vector with C{fabs} components.
590 '''
591 return self._mapped(fabs)
593 def floats(self):
594 '''Return the vector with C{float} components.
595 '''
596 return self._mapped(_float0)
598 @Property
599 def _fromll(self):
600 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.
601 '''
602 return self._ll
604 @_fromll.setter # PYCHOK setter!
605 def _fromll(self, ll):
606 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.
607 '''
608 self._ll = ll or None
610 @property_RO
611 def homogeneous(self):
612 '''Get this vector's homogeneous representation (L{Vector3d}).
613 '''
614 x, y, z = self.xyz3
615 if z:
616 x = x / z # /= chokes PyChecker
617 y = y / z
618# z = _1_0
619 else:
620 if isneg0(z):
621 x = -x
622 y = -y
623 x = _copysignINF(x)
624 y = _copysignINF(y)
625# z = NAN
626 return self.classof(x, y, _1_0)
628 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False
629 '''Locate the vector at a given fraction between (or along) this
630 and an other vector.
632 @arg other: The other vector (L{Vector3d}).
633 @arg fraction: Fraction between both vectors (C{scalar},
634 0.0 for this and 1.0 for the other vector).
636 @return: Intermediate vector (L{Vector3d}).
638 @raise TypeError: Incompatible B{C{other}} C{type}.
639 '''
640 f = Scalar(fraction=fraction)
641 if isnear0(f): # PYCHOK no cover
642 r = self
643 else:
644 r = self.others(other)
645 if not isnear1(f): # self * (1 - f) + r * f
646 r = self.plus(r.minus(self)._times(f))
647 return r
649 def ints(self):
650 '''Return the vector with C{int} components.
651 '''
652 return self._mapped(int)
654 def isconjugateTo(self, other, minum=1, eps=EPS):
655 '''Determine whether this and an other vector are conjugates.
657 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},
658 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).
659 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).
660 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),
661 same units as C{x}, C{y}, and C{z}.
663 @return: C{True} if both vector's components either match
664 or at least C{B{minum}} have opposite signs.
666 @raise TypeError: Incompatible B{C{other}} C{type}.
668 @see: Method C{isequalTo}.
669 '''
670 self.others(other)
671 n = 0
672 for a, b in zip(self.xyz3, other.xyz3):
673 if fabs(a + b) < eps and ((a < 0 and b > 0) or
674 (a > 0 and b < 0)):
675 n += 1 # conjugate
676 elif fabs(a - b) > eps:
677 return False # unequal
678 return bool(n >= minum)
680 def isequalTo(self, other, units=False, eps=EPS):
681 '''Check if this and an other vector are equal or equivalent.
683 @arg other: The other vector (L{Vector3d}).
684 @kwarg units: Optionally, compare the normalized, unit
685 version of both vectors.
686 @kwarg eps: Tolerance for equality (C{scalar}), same units as
687 C{x}, C{y}, and C{z}.
689 @return: C{True} if vectors are identical, C{False} otherwise.
691 @raise TypeError: Incompatible B{C{other}} C{type}.
693 @see: Method C{isconjugateTo}.
694 '''
695 if units:
696 self.others(other)
697 d = self.unit().minus(other.unit())
698 else:
699 d = self.minus(other)
700 return max(map(fabs, d.xyz3)) < eps
702 @Property_RO
703 def length(self): # __dict__ value overwritten by Property_RO C{_united}
704 '''Get the length (norm, magnitude) of this vector (C{Float}).
706 @see: Properties L{length2} and L{euclid}.
707 '''
708 return Float(length=hypot_(self.x, self.y, self.z))
710 @Property_RO
711 def length2(self): # __dict__ value overwritten by Property_RO C{_united}
712 '''Get the length I{squared} of this vector (C{Float}).
714 @see: Property L{length} and method C{equirectangular}.
715 '''
716 return Float(length2=hypot2_(self.x, self.y, self.z))
718 def _mapped(self, func):
719 '''(INTERNAL) Apply C{func} to all components.
720 '''
721 return self.classof(*map2(func, self.xyz3))
723 def minus(self, other):
724 '''Subtract an other vector from this vector.
726 @arg other: The other vector (L{Vector3d}).
728 @return: New vector difference (L{Vector3d}).
730 @raise TypeError: Incompatible B{C{other}} C{type}.
731 '''
732 return self._minus(*self.others(other).xyz3)
734 def _minus(self, x, y, z):
735 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.
736 '''
737 return self.classof(self.x - x, self.y - y, self.z - z)
739 def minus_(self, other_x, *y_z):
740 '''Subtract separate X, Y and Z components from this vector.
742 @arg other_x: X component (C{scalar}) or a vector's
743 X, Y, and Z components (C{Cartesian},
744 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
745 L{Vector3Tuple}, L{Vector4Tuple}).
746 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
747 ignored if B{C{other_x}} is not C{scalar}.
749 @return: New, vectiorial vector (L{Vector3d}).
751 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
752 '''
753 return self._minus(*_xyz3(self.minus_, other_x, *y_z))
755 def negate(self):
756 '''Return the opposite of this vector.
758 @return: A negated copy (L{Vector3d})
759 '''
760 return self.classof(-self.x, -self.y, -self.z)
762 @Property_RO
763 def _N_vector(self):
764 '''(INTERNAL) Get the (C{nvectorBase._N_Vector})
765 '''
766 _N = _MODS.nvectorBase._N_Vector
767 return _N(*self.xyz3, name=self.name)
769 def _other_cmp(self, other):
770 '''(INTERNAL) Return the value for comparison.
771 '''
772 return other if isscalar(other) else self.others(other).length
774 def others(self, *other, **name_other_up):
775 '''Refined class comparison.
777 @arg other: The other vector (L{Vector3d}).
778 @kwarg name_other_up: Overriding C{name=other} and C{up=1}
779 keyword arguments.
781 @return: The B{C{other}} if compatible.
783 @raise TypeError: Incompatible B{C{other}} C{type}.
784 '''
785 other, name, up = _xother3(self, other, **name_other_up)
786 if not isinstance(other, Vector3dBase):
787 _NamedBase.others(self, other, name=name, up=up + 1)
788 return other
790 def plus(self, other):
791 '''Add this vector and an other vector.
793 @arg other: The other vector (L{Vector3d}).
795 @return: Vectorial sum (L{Vector3d}).
797 @raise TypeError: Incompatible B{C{other}} C{type}.
798 '''
799 return self._plus(*self.others(other).xyz3)
801 sum = plus # alternate name
803 def _plus(self, x, y, z):
804 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.
805 '''
806 return self.classof(self.x + x, self.y + y, self.z + z)
808 def plus_(self, other_x, *y_z):
809 '''Sum of this vector and separate X, Y and Z components.
811 @arg other_x: X component (C{scalar}) or a vector's
812 X, Y, and Z components (C{Cartesian},
813 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
814 L{Vector3Tuple}, L{Vector4Tuple}).
815 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
816 ignored if B{C{other_x}} is not C{scalar}.
818 @return: New, vectiorial vector (L{Vector3d}).
820 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
821 '''
822 return self._plus(*_xyz3(self.plus_, other_x, *y_z))
824 def rotate(self, axis, theta, fma=False):
825 '''Rotate this vector around an axis by a specified angle.
827 @arg axis: The axis being rotated around (L{Vector3d}).
828 @arg theta: The angle of rotation (C{radians}).
829 @kwarg fma: If C{True}, use fused-multiply-add (C{bool}).
831 @return: New, rotated vector (L{Vector3d}).
833 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/
834 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and
835 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/
836 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.
837 '''
838 s, c = sincos2(theta) # rotation angle
839 d = _1_0 - c
840 if d or s:
841 p = self.unit().xyz # point being rotated
842 r = self.others(axis=axis).unit() # axis being rotated around
844 ax, ay, az = r.xyz3 # quaternion-derived rotation matrix
845 bx, by, bz = r.times(d).xyz3
846 sx, sy, sz = r.times(s).xyz3
848 if fma:
849 _fma = _MODS.fmath.fma
850 else:
851 def _fma(a, b, c):
852 return a * b + c
854 x = fdot(p, _fma(ax, bx, c), _fma(ax, by, -sz), _fma(ax, bz, sy))
855 y = fdot(p, _fma(ay, bx, sz), _fma(ay, by, c), _fma(ay, bz, -sx))
856 z = fdot(p, _fma(az, bx, -sy), _fma(az, by, sx), _fma(az, bz, c))
857 else: # unrotated
858 x, y, z = self.xyz3
859 return self.classof(x, y, z)
861 @deprecated_method
862 def rotateAround(self, axis, theta):
863 '''DEPRECATED, use method C{rotate}.'''
864 return self.rotate(axis, theta) # PYCHOK no cover
866 def times(self, factor):
867 '''Multiply this vector by a scalar.
869 @arg factor: Scale factor (C{scalar}).
871 @return: New, scaled vector (L{Vector3d}).
873 @raise TypeError: Non-scalar B{C{factor}}.
874 '''
875 return self._times(Scalar(factor=factor))
877 def _times(self, s):
878 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.
879 '''
880 return self.classof(self.x * s, self.y * s, self.z * s)
882 def times_(self, other_x, *y_z):
883 '''Multiply this vector's components by separate X, Y and Z factors.
885 @arg other_x: X scale factor (C{scalar}) or a vector's
886 X, Y, and Z components as scale factors
887 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
888 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
889 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),
890 ignored if B{C{other_x}} is not C{scalar}.
892 @return: New, scaled vector (L{Vector3d}).
894 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
895 '''
896 x, y, z = _xyz3(self.times_, other_x, *y_z)
897 return self.classof(self.x * x, self.y * y, self.z * z)
899# @deprecated_method
900# def to2ab(self): # PYCHOK no cover
901# '''DEPRECATED, use property C{Nvector.philam}.
902#
903# @return: A L{PhiLam2Tuple}C{(phi, lam)}.
904# '''
905# return _MODS.nvectorBase.n_xyz2philam(self.x, self.y, self.z)
907# @deprecated_method
908# def to2ll(self): # PYCHOK no cover
909# '''DEPRECATED, use property C{Nvector.latlon}.
910#
911# @return: A L{LatLon2Tuple}C{(lat, lon)}.
912# '''
913# return _MODS.nvectorBase.n_xyz2latlon(self.x, self.y, self.z)
915 @deprecated_method
916 def to3xyz(self): # PYCHOK no cover
917 '''DEPRECATED, use property L{xyz}.
918 '''
919 return self.xyz
921 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected
922 '''Return a string representation of this vector.
924 @kwarg prec: Number of decimal places (C{int}).
925 @kwarg fmt: Enclosing format to use (C{str}).
926 @kwarg sep: Separator between components (C{str}).
928 @return: Vector as "(x, y, z)" (C{str}).
929 '''
930 t = sep.join(strs(self.xyz3, prec=prec))
931 return (fmt % (t,)) if fmt else t
933 def unit(self, ll=None):
934 '''Normalize this vector to unit length.
936 @kwarg ll: Optional, original location (C{LatLon}).
938 @return: Normalized vector (L{Vector3d}).
939 '''
940 u = self._united
941 if ll:
942 u._fromll = ll
943 return u
945 @Property_RO
946 def _united(self): # __dict__ value overwritten below
947 '''(INTERNAL) Get normalized vector (L{Vector3d}).
948 '''
949 n = self.length
950 if n > EPS0 and fabs(n - _1_0) > EPS0:
951 u = self._xnamed(self.dividedBy(n))
952 u._update(False, length=_1_0, length2=_1_0, _united=u)
953 else:
954 u = self.copy()
955 u._update(False, _united=u)
956 if self._fromll:
957 u._fromll = self._fromll
958 return u
960 @Property
961 def x(self):
962 '''Get the X component (C{float}).
963 '''
964 return self._x
966 @x.setter # PYCHOK setter!
967 def x(self, x):
968 '''Set the X component, if different (C{float}).
969 '''
970 x = Float(x=x)
971 if self._x != x:
972 _update_all(self, needed=3)
973 self._x = x
975 @Property
976 def xyz(self):
977 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).
978 '''
979 return _MODS.namedTuples.Vector3Tuple(*self.xyz3, name=self.name)
981 @xyz.setter # PYCHOK setter!
982 def xyz(self, xyz):
983 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},
984 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}
985 or a C{tuple} or C{list} of 3+ C{scalar} items).
986 '''
987 self._xyz(xyz)
989 def _xyz(self, x_xyz, *y_z):
990 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.
991 '''
992 _update_all(self, needed=3)
993 self._x, self._y, self._z = _xyz3(_xyz_, x_xyz, *y_z)
994 return self
996 @property_RO
997 def xyz3(self):
998 '''Get the X, Y and Z components as C{3-tuple}.
999 '''
1000 return self.x, self.y, self.z
1002 @property_RO
1003 def x2y2z2(self):
1004 '''Get the X, Y and Z components I{squared} (3-tuple C{(x**2, y**2, z**2)}).
1005 '''
1006 return self.x**2, self.y**2, self.z**2
1008 @Property
1009 def y(self):
1010 '''Get the Y component (C{float}).
1011 '''
1012 return self._y
1014 @y.setter # PYCHOK setter!
1015 def y(self, y):
1016 '''Set the Y component, if different (C{float}).
1017 '''
1018 y = Float(y=y)
1019 if self._y != y:
1020 _update_all(self, needed=3)
1021 self._y = y
1023 @Property
1024 def z(self):
1025 '''Get the Z component (C{float}).
1026 '''
1027 return self._z
1029 @z.setter # PYCHOK setter!
1030 def z(self, z):
1031 '''Set the Z component, if different (C{float}).
1032 '''
1033 z = Float(z=z)
1034 if self._z != z:
1035 _update_all(self, needed=3)
1036 self._z = z
1039def _xyz3(where, x_xyz, *y_z): # in .cartesianBase._rtp3
1040 '''(INTERNAL) Get , Y and Z as 3-tuple C{(x, y, z)}.
1041 '''
1042 try:
1043 xyz3 = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for Vector*Tuple
1044 map2(_float0, x_xyz[:3]) if islistuple(x_xyz, minum=3) else
1045 x_xyz.xyz) # .xyz3
1046 except (AttributeError, TypeError, ValueError) as x:
1047 raise _xError(x, unstr(where, x_xyz, *y_z))
1048 return xyz3
1051__all__ += _ALL_DOCS(Vector3dBase)
1053# **) MIT License
1054#
1055# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1056#
1057# Permission is hereby granted, free of charge, to any person obtaining a
1058# copy of this software and associated documentation files (the "Software"),
1059# to deal in the Software without restriction, including without limitation
1060# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1061# and/or sell copies of the Software, and to permit persons to whom the
1062# Software is furnished to do so, subject to the following conditions:
1063#
1064# The above copyright notice and this permission notice shall be included
1065# in all copies or substantial portions of the Software.
1066#
1067# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1068# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1069# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1070# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1071# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1072# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1073# OTHER DEALINGS IN THE SOFTWARE.