Coverage for pygeodesy/vector3dBase.py: 91%
287 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-29 12:40 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-29 12:40 -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, map1, map2, \
12 _signOf, _zip
13from pygeodesy.constants import EPS, EPS0, INT0, PI, PI2, _copysignINF, \
14 _float0, isnear0, isnear1, isneg0, \
15 _pos_self, _1_0
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
22from pygeodesy.props import deprecated_method, Property, Property_RO, \
23 property_doc_, property_RO, _update_all
24from pygeodesy.streprs import Fmt, strs, unstr
25from pygeodesy.units import Float, Scalar
26from pygeodesy.utily import atan2, sincos2, fabs
28from math import ceil as _ceil, floor as _floor, trunc as _trunc
30__all__ = _ALL_LAZY.vector3dBase
31__version__ = '24.11.24'
34class Vector3dBase(_NamedBase): # sync __methods__ with .fsums.Fsum
35 '''(INTERNAL) Generic 3-D vector base class.
36 '''
37 _crosserrors = True # un/set by .errors.crosserrors
39 _ll = None # original latlon, '_fromll'
40# _x = INT0 # X component
41# _y = INT0 # Y component
42# _z = INT0 # Z component
44 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, **name):
45 '''New L{Vector3d} or C{Vector3dBase} instance.
47 The vector may be normalised or use x, y, z for position and
48 distance from earth centre or height relative to the surface
49 of the earth' sphere or ellipsoid.
51 @arg x_xyz: X component of vector (C{scalar}) or a (3-D) vector
52 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
53 L{Vector3Tuple}, L{Vector4Tuple} or a C{tuple} or
54 C{list} of 3+ C{scalar} items).
55 @kwarg y: Y component of vector (C{scalar}), ignored if B{C{x_xyz}}
56 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
57 @kwarg z: Z component of vector (C{scalar}), ignored if B{C{x_xyz}}
58 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
59 @kwarg ll: Optional latlon reference (C{LatLon}).
60 @kwarg name: Optional C{B{name}=NN} (C{str}).
62 @raise VectorError: Invalid B{C{x_xyz}}, B{C{y}} or B{C{z}}.
63 '''
64 self._x, \
65 self._y, \
66 self._z = _xyz3(type(self), x_xyz, y, z) if isscalar(x_xyz) else \
67 _xyz3(type(self), x_xyz)
68 if ll:
69 self._ll = ll
70 if name:
71 self.name = name
73 def __abs__(self):
74 '''Return the norm of this vector.
76 @return: Norm, unit length (C{float});
77 '''
78 return self.length
80 def __add__(self, other):
81 '''Add this to an other vector (L{Vector3d}).
83 @return: Vectorial sum (L{Vector3d}).
85 @raise TypeError: Incompatible B{C{other}} C{type}.
86 '''
87 return self.plus(other)
89 def __bool__(self): # PYCHOK PyChecker
90 '''Is this vector non-zero?
92 @see: Method C{bools}.
93 '''
94 return bool(self.x or self.y or self.z)
96 def __ceil__(self): # PYCHOK no cover
97 '''Return a vector with the C{ceil} of these components.
99 @return: Ceil-ed (L{Vector3d}).
100 '''
101 return self._mapped(_ceil)
103 def __cmp__(self, other): # Python 2-
104 '''Compare this and an other vector (L{Vector3d}).
106 @return: -1, 0 or +1 (C{int}).
108 @raise TypeError: Incompatible B{C{other}} C{type}.
109 '''
110 return _signOf(self.length, self._other_cmp(other))
112 cmp = __cmp__
114 def __divmod__(self, other): # PYCHOK no cover
115 '''Not implemented.'''
116 return _NotImplemented(self, other)
118 def __eq__(self, other):
119 '''Is this vector equal to an other vector?
121 @arg other: The other vector (L{Vector3d}).
123 @return: C{True} if equal, C{False} otherwise.
125 @raise TypeError: Incompatible B{C{other}} C{type}.
126 '''
127 return self.isequalTo(other, eps=EPS0)
129 def __float__(self): # PYCHOK no cover
130 '''Not implemented, see method C{float}.'''
131 return _NotImplemented(self) # must return C{float}
133 def __floor__(self): # PYCHOK no cover
134 '''Return a vector with the C{floor} of these components.
136 @return: Floor-ed (L{Vector3d}).
137 '''
138 return self._mapped(_floor)
140 def __floordiv__(self, other): # PYCHOK no cover
141 '''Not implemented.'''
142 return _NotImplemented(self, other)
144 def __ge__(self, other):
145 '''Is this vector longer than or equal to an other vector?
147 @arg other: The other vector (L{Vector3d}).
149 @return: C{True} if so, C{False} otherwise.
151 @raise TypeError: Incompatible B{C{other}} C{type}.
152 '''
153 return self.length >= self._other_cmp(other)
155# def __getitem__(self, key):
156# '''Return C{item} at index or slice C{[B{key}]}.
157# '''
158# return self.xyz[key]
160 def __gt__(self, other):
161 '''Is this vector longer than an other vector?
163 @arg other: The other vector (L{Vector3d}).
165 @return: C{True} if so, C{False} otherwise.
167 @raise TypeError: Incompatible B{C{other}} C{type}.
168 '''
169 return self.length > self._other_cmp(other)
171 def __hash__(self): # PYCHOK no cover
172 '''Return this instance' C{hash}.
173 '''
174 return hash(self.xyz) # XXX id(self)?
176 def __iadd__(self, other):
177 '''Add this and an other vector I{in-place}, C{this += B{other}}.
179 @arg other: The other vector (L{Vector3d}).
181 @raise TypeError: Incompatible B{C{other}} C{type}.
182 '''
183 return self._xyz(self.plus(other))
185 def __ifloordiv__(self, other): # PYCHOK no cover
186 '''Not implemented.'''
187 return _NotImplemented(self, other)
189 def __imatmul__(self, other): # PYCHOK Python 3.5+
190 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}.
192 @arg other: The other vector (L{Vector3d}).
194 @raise TypeError: Incompatible B{C{other}} C{type}.
196 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 397+, 2022 p. 578+.
197 '''
198 return self._xyz(self.cross(other))
200 def __imod__(self, other): # PYCHOK no cover
201 '''Not implemented.'''
202 return _NotImplemented(self, other)
204 def __imul__(self, scalar):
205 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}.
207 @arg scalar: Factor (C{scalar}).
209 @raise TypeError: Non-scalar B{C{scalar}}.
210 '''
211 return self._xyz(self.times(scalar))
213 def __int__(self): # PYCHOK no cover
214 '''Not implemented, see method C{ints}.'''
215 return _NotImplemented(self) # must return C{int}
217 def __ipow__(self, other, *mod): # PYCHOK no cover
218 '''Not implemented.'''
219 return _NotImplemented(self, other, *mod)
221 def __isub__(self, other):
222 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.
224 @arg other: The other vector (L{Vector3d}).
226 @raise TypeError: Incompatible B{C{other}} C{type}.
227 '''
228 return self._xyz(self.minus(other))
230# def __iter__(self):
231# '''Return an C{iter}ator over this vector's components.
232# '''
233# return iter(self.xyz3)
235 def __itruediv__(self, scalar):
236 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.
238 @arg scalar: The divisor (C{scalar}).
240 @raise TypeError: Non-scalar B{C{scalar}}.
241 '''
242 return self._xyz(self.dividedBy(scalar))
244 def __le__(self, other): # Python 3+
245 '''Is this vector shorter than or equal to an other vector?
247 @arg other: The other vector (L{Vector3d}).
249 @return: C{True} if so, C{False} otherwise.
251 @raise TypeError: Incompatible B{C{other}} C{type}.
252 '''
253 return self.length <= self._other_cmp(other)
255# def __len__(self):
256# '''Return C{3}, always.
257# '''
258# return len(self.xyz)
260 def __lt__(self, other): # Python 3+
261 '''Is this vector shorter than an other vector?
263 @arg other: The other vector (L{Vector3d}).
265 @return: C{True} if so, C{False} otherwise.
267 @raise TypeError: Incompatible B{C{other}} C{type}.
268 '''
269 return self.length < self._other_cmp(other)
271 def __matmul__(self, other): # PYCHOK Python 3.5+
272 '''Compute the cross product of this and an other vector, C{this @ B{other}}.
274 @arg other: The other vector (L{Vector3d}).
276 @return: Cross product (L{Vector3d}).
278 @raise TypeError: Incompatible B{C{other}} C{type}.
279 '''
280 return self.cross(other)
282 def __mod__(self, other): # PYCHOK no cover
283 '''Not implemented.'''
284 return _NotImplemented(self, other)
286 def __mul__(self, scalar):
287 '''Multiply this vector by a scalar, C{this * B{scalar}}.
289 @arg scalar: Factor (C{scalar}).
291 @return: Product (L{Vector3d}).
292 '''
293 return self.times(scalar)
295 def __ne__(self, other):
296 '''Is this vector not equal to an other vector?
298 @arg other: The other vector (L{Vector3d}).
300 @return: C{True} if so, C{False} otherwise.
302 @raise TypeError: Incompatible B{C{other}} C{type}.
303 '''
304 return not self.isequalTo(other, eps=EPS0)
306 def __neg__(self):
307 '''Return the opposite of this vector.
309 @return: A negated copy (L{Vector3d})
310 '''
311 return self.negate()
313 def __pos__(self): # PYCHOK no cover
314 '''Return this vector I{as-is} or a copy.
316 @return: This instance (L{Vector3d})
317 '''
318 return self if _pos_self else self.copy()
320 def __pow__(self, other, *mod): # PYCHOK no cover
321 '''Not implemented.'''
322 return _NotImplemented(self, other, *mod)
324 __radd__ = __add__ # PYCHOK no cover
326 def __rdivmod__ (self, other): # PYCHOK no cover
327 '''Not implemented.'''
328 return _NotImplemented(self, other)
330# def __repr__(self):
331# '''Return the default C{repr(this)}.
332# '''
333# return self.toRepr()
335 def __rfloordiv__(self, other): # PYCHOK no cover
336 '''Not implemented.'''
337 return _NotImplemented(self, other)
339 def __rmatmul__(self, other): # PYCHOK Python 3.5+
340 '''Compute the cross product of an other and this vector, C{B{other} @ this}.
342 @arg other: The other vector (L{Vector3d}).
344 @return: Cross product (L{Vector3d}).
346 @raise TypeError: Incompatible B{C{other}} C{type}.
347 '''
348 return self.others(other).cross(self)
350 def __rmod__(self, other): # PYCHOK no cover
351 '''Not implemented.'''
352 return _NotImplemented(self, other)
354 __rmul__ = __mul__
356 def __round__(self, *ndigits): # PYCHOK no cover
357 '''Return a vector with these components C{rounded}.
359 @arg ndigits: Optional number of digits (C{int}).
361 @return: Rounded (L{Vector3d}).
362 '''
363 # <https://docs.Python.org/3.12/reference/datamodel.html?#object.__round__>
364 return self.classof(*(round(_, *ndigits) for _ in self.xyz3))
366 def __rpow__(self, other, *mod): # PYCHOK no cover
367 '''Not implemented.'''
368 return _NotImplemented(self, other, *mod)
370 def __rsub__(self, other): # PYCHOK no cover
371 '''Subtract this vector from an other vector, C{B{other} - this}.
373 @arg other: The other vector (L{Vector3d}).
375 @return: Difference (L{Vector3d}).
377 @raise TypeError: Incompatible B{C{other}} C{type}.
378 '''
379 return self.others(other).minus(self)
381 def __rtruediv__(self, scalar): # PYCHOK no cover
382 '''Not implemented.'''
383 return _NotImplemented(self, scalar)
385# def __str__(self):
386# '''Return the default C{str(self)}.
387# '''
388# return self.toStr()
390 def __sub__(self, other):
391 '''Subtract an other vector from this vector, C{this - B{other}}.
393 @arg other: The other vector (L{Vector3d}).
395 @return: Difference (L{Vector3d}).
397 @raise TypeError: Incompatible B{C{other}} C{type}.
398 '''
399 return self.minus(other)
401 def __truediv__(self, scalar):
402 '''Divide this vector by a scalar, C{this / B{scalar}}.
404 @arg scalar: The divisor (C{scalar}).
406 @return: Quotient (L{Vector3d}).
408 @raise TypeError: Non-scalar B{C{scalar}}.
409 '''
410 return self.dividedBy(scalar)
412 def __trunc__(self): # PYCHOK no cover
413 '''Return a vector with the C{trunc} of these components.
415 @return: Trunc-ed (L{Vector3d}).
416 '''
417 return self._mapped(_trunc)
419 if _MODS.sys_version_info2 < (3, 0): # PYCHOK no cover
420 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
421 __div__ = __truediv__
422 __idiv__ = __itruediv__
423 __long__ = __int__
424 __nonzero__ = __bool__
425 __rdiv__ = __rtruediv__
427 def angleTo(self, other, vSign=None, wrap=False):
428 '''Compute the angle between this and an other vector.
430 @arg other: The other vector (L{Vector3d}).
431 @kwarg vSign: Optional vector, if supplied (and out of the
432 plane of this and the other), angle is signed
433 positive if this->other is clockwise looking
434 along vSign or negative in opposite direction,
435 otherwise angle is unsigned.
436 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).
438 @return: Angle (C{radians}).
440 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.
441 '''
442 x = self.cross(other)
443 s = x.length
444 # use vSign as reference to set sign of s
445 if s and vSign and x.dot(vSign) < 0:
446 s = -s
448 a = atan2(s, self.dot(other))
449 if wrap and fabs(a) > PI:
450 a -= _copysign(PI2, a)
451 return a
453 def apply(self, fun2, other_x, *y_z, **fun2_kwds):
454 '''Apply a 2-argument function pairwise to the components
455 of this and an other vector.
457 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),
458 return a C{scalar} or L{INT0} result.
459 @arg other_x: Other X component (C{scalar}) or a vector
460 with X, Y and Z components (C{Cartesian},
461 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
462 L{Vector3Tuple} or L{Vector4Tuple}).
463 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).
464 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.
466 @return: New, applied vector (L{Vector3d}).
468 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
469 '''
470 _xcallable(fun2=fun2)
471 if fun2_kwds:
472 def _f2(a, b):
473 return fun2(a, b, **fun2_kwds)
474 else:
475 _f2 = fun2
477 xyz = _xyz3(self.apply, other_x, *y_z)
478 xyz = (_f2(a, b) for a, b in _zip(self.xyz3, xyz)) # strict=True
479 return self.classof(*xyz)
481 def bools(self):
482 '''Return the vector with C{bool} components.
483 '''
484 return self._mapped(bool)
486 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN
487 '''Compute the cross product of this and an other vector.
489 @arg other: The other vector (L{Vector3d}).
490 @kwarg raiser: Optional, L{CrossError} label if raised (C{str}, non-L{NN}).
491 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as C{x}, C{y} and
492 C{z}.
494 @return: Cross product (L{Vector3d}).
496 @raise CrossError: Zero or near-zero cross product and if B{C{raiser}} and
497 L{crosserrors<pygeodesy.crosserrors>} are both C{True}.
499 @raise TypeError: Incompatible B{C{other}} C{type}.
500 '''
501 X, Y, Z = self.others(other).xyz3
502 x, y, z = self.xyz3
503 xyz = (fdot_(y, Z, -z, Y),
504 fdot_(z, X, -x, Z),
505 fdot_(x, Y, -y, X))
507 if raiser and self.crosserrors and eps0 > 0 \
508 and max(map(fabs, xyz)) < eps0:
509 r = other._fromll or other
510 s = self._fromll or self
511 t = self.isequalTo(other, eps=eps0)
512 t = _coincident_ if t else _colinear_
513 raise CrossError(raiser, s, other=r, txt=t)
515 return self.classof(*xyz) # name__=self.cross
517 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')
518 def crosserrors(self):
519 '''Get L{CrossError} exceptions (C{bool}).
520 '''
521 return self._crosserrors
523 @crosserrors.setter # PYCHOK setter!
524 def crosserrors(self, raiser):
525 '''Raise or ignore L{CrossError} exceptions (C{bool}).
526 '''
527 self._crosserrors = bool(raiser)
529 def dividedBy(self, divisor):
530 '''Divide this vector by a scalar.
532 @arg divisor: The divisor (C{scalar}).
534 @return: New, scaled vector (L{Vector3d}).
536 @raise TypeError: Non-scalar B{C{divisor}}.
538 @raise VectorError: Invalid or zero B{C{divisor}}.
539 '''
540 d = Scalar(divisor=divisor)
541 try:
542 return self._times(_1_0 / d)
543 except (ValueError, ZeroDivisionError) as x:
544 raise VectorError(divisor=divisor, cause=x)
546 def dot(self, other):
547 '''Compute the dot (scalar) product of this and an other vector.
549 @arg other: The other vector (L{Vector3d}).
551 @return: Dot product (C{float}).
553 @raise TypeError: Incompatible B{C{other}} C{type}.
554 '''
555 return self.length2 if other is self else fdot(
556 self.xyz3, *self.others(other).xyz3)
558 @deprecated_method
559 def equals(self, other, units=False): # PYCHOK no cover
560 '''DEPRECATED, use method C{isequalTo}.
561 '''
562 return self.isequalTo(other, units=units)
564 @Property_RO
565 def euclid(self):
566 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).
568 @see: Properties C{length} and C{length2} and function
569 L{pygeodesy.euclid_}.
570 '''
571 return Float(euclid=euclid_(*self.xyz3))
573 def equirectangular(self, other):
574 '''I{Approximate} the difference between this and an other vector.
576 @arg other: Vector to subtract (C{Vector3dBase}).
578 @return: The length I{squared} of the difference (C{Float}).
580 @raise TypeError: Incompatible B{C{other}} C{type}.
582 @see: Property C{length2}.
583 '''
584 d = self.minus(other)
585 return Float(equirectangular=hypot2_(*d.xyz3))
587 def fabs(self):
588 '''Return the vector with C{fabs} components.
589 '''
590 return self._mapped(fabs)
592 def floats(self):
593 '''Return the vector with C{float} components.
594 '''
595 return self._mapped(_float0)
597 @Property
598 def _fromll(self):
599 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.
600 '''
601 return self._ll
603 @_fromll.setter # PYCHOK setter!
604 def _fromll(self, ll):
605 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.
606 '''
607 self._ll = ll or None
609 @property_RO
610 def homogeneous(self):
611 '''Get this vector's homogeneous representation (L{Vector3d}).
612 '''
613 x, y, z = self.xyz3
614 if z:
615 x = x / z # /= chokes PyChecker
616 y = y / z
617# z = _1_0
618 else:
619 if isneg0(z):
620 x = -x
621 y = -y
622 x = _copysignINF(x)
623 y = _copysignINF(y)
624# z = NAN
625 return self.classof(x, y, _1_0)
627 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False
628 '''Locate the vector at a given fraction between (or along) this
629 and an other vector.
631 @arg other: The other vector (L{Vector3d}).
632 @arg fraction: Fraction between both vectors (C{scalar},
633 0.0 for this and 1.0 for the other vector).
635 @return: Intermediate vector (L{Vector3d}).
637 @raise TypeError: Incompatible B{C{other}} C{type}.
638 '''
639 f = Scalar(fraction=fraction)
640 if isnear0(f): # PYCHOK no cover
641 r = self
642 else:
643 r = self.others(other)
644 if not isnear1(f): # self * (1 - f) + r * f
645 r = self.plus(r.minus(self)._times(f))
646 return r
648 def ints(self):
649 '''Return the vector with C{int} components.
650 '''
651 return self._mapped(int)
653 def isconjugateTo(self, other, minum=1, eps=EPS):
654 '''Determine whether this and an other vector are conjugates.
656 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},
657 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).
658 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).
659 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),
660 same units as C{x}, C{y}, and C{z}.
662 @return: C{True} if both vector's components either match
663 or at least C{B{minum}} have opposite signs.
665 @raise TypeError: Incompatible B{C{other}} C{type}.
667 @see: Method C{isequalTo}.
668 '''
669 self.others(other)
670 n = 0
671 for a, b in zip(self.xyz3, other.xyz3):
672 if fabs(a + b) < eps and ((a < 0 and b > 0) or
673 (a > 0 and b < 0)):
674 n += 1 # conjugate
675 elif fabs(a - b) > eps:
676 return False # unequal
677 return bool(n >= minum)
679 def isequalTo(self, other, units=False, eps=EPS):
680 '''Check if this and an other vector are equal or equivalent.
682 @arg other: The other vector (L{Vector3d}).
683 @kwarg units: Optionally, compare the normalized, unit
684 version of both vectors.
685 @kwarg eps: Tolerance for equality (C{scalar}), same units as
686 C{x}, C{y}, and C{z}.
688 @return: C{True} if vectors are identical, C{False} otherwise.
690 @raise TypeError: Incompatible B{C{other}} C{type}.
692 @see: Method C{isconjugateTo}.
693 '''
694 if units:
695 self.others(other)
696 d = self.unit().minus(other.unit())
697 else:
698 d = self.minus(other)
699 return max(map(fabs, d.xyz3)) < eps
701 @Property_RO
702 def length(self): # __dict__ value overwritten by Property_RO C{_united}
703 '''Get the length (norm, magnitude) of this vector (C{Float}).
705 @see: Properties L{length2} and L{euclid}.
706 '''
707 return Float(length=hypot_(self.x, self.y, self.z))
709 @Property_RO
710 def length2(self): # __dict__ value overwritten by Property_RO C{_united}
711 '''Get the length I{squared} of this vector (C{Float}).
713 @see: Property L{length} and method C{equirectangular}.
714 '''
715 return Float(length2=hypot2_(self.x, self.y, self.z))
717 def _mapped(self, func):
718 '''(INTERNAL) Apply C{func} to all components.
719 '''
720 return self.classof(*map2(func, self.xyz3))
722 def minus(self, other):
723 '''Subtract an other vector from this vector.
725 @arg other: The other vector (L{Vector3d}).
727 @return: New vector difference (L{Vector3d}).
729 @raise TypeError: Incompatible B{C{other}} C{type}.
730 '''
731 return self._minus(*self.others(other).xyz3)
733 def _minus(self, x, y, z):
734 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.
735 '''
736 return self.classof(self.x - x, self.y - y, self.z - z)
738 def minus_(self, other_x, *y_z):
739 '''Subtract separate X, Y and Z components from this vector.
741 @arg other_x: X component (C{scalar}) or a vector's
742 X, Y, and Z components (C{Cartesian},
743 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
744 L{Vector3Tuple}, L{Vector4Tuple}).
745 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
746 ignored if B{C{other_x}} is not C{scalar}.
748 @return: New, vectiorial vector (L{Vector3d}).
750 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
751 '''
752 return self._minus(*_xyz3(self.minus_, other_x, *y_z))
754 def negate(self):
755 '''Return the opposite of this vector.
757 @return: A negated copy (L{Vector3d})
758 '''
759 return self.classof(-self.x, -self.y, -self.z)
761 @Property_RO
762 def _N_vector(self):
763 '''(INTERNAL) Get the (C{nvectorBase._N_vector_})
764 '''
765 return _MODS.nvectorBase._N_vector_(*self.xyz3, name=self.name)
767 def _other_cmp(self, other):
768 '''(INTERNAL) Return the value for comparison.
769 '''
770 return other if isscalar(other) else self.others(other).length
772 def others(self, *other, **name_other_up):
773 '''Refined class comparison.
775 @arg other: The other vector (L{Vector3d}).
776 @kwarg name_other_up: Overriding C{name=other} and C{up=1}
777 keyword arguments.
779 @return: The B{C{other}} if compatible.
781 @raise TypeError: Incompatible B{C{other}} C{type}.
782 '''
783 other, name, up = _xother3(self, other, **name_other_up)
784 if not isinstance(other, Vector3dBase):
785 _NamedBase.others(self, other, name=name, up=up + 1)
786 return other
788 def plus(self, other):
789 '''Add this vector and an other vector.
791 @arg other: The other vector (L{Vector3d}).
793 @return: Vectorial sum (L{Vector3d}).
795 @raise TypeError: Incompatible B{C{other}} C{type}.
796 '''
797 return self._plus(*self.others(other).xyz3)
799 sum = plus # alternate name
801 def _plus(self, x, y, z):
802 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.
803 '''
804 return self.classof(self.x + x, self.y + y, self.z + z)
806 def plus_(self, other_x, *y_z):
807 '''Sum of this vector and separate X, Y and Z components.
809 @arg other_x: X component (C{scalar}) or a vector's
810 X, Y, and Z components (C{Cartesian},
811 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
812 L{Vector3Tuple}, L{Vector4Tuple}).
813 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
814 ignored if B{C{other_x}} is not C{scalar}.
816 @return: New, vectiorial vector (L{Vector3d}).
818 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
819 '''
820 return self._plus(*_xyz3(self.plus_, other_x, *y_z))
822 def rotate(self, axis, theta, fma=False):
823 '''Rotate this vector around an axis by a specified angle.
825 @arg axis: The axis being rotated around (L{Vector3d}).
826 @arg theta: The angle of rotation (C{radians}).
827 @kwarg fma: If C{True}, use fused-multiply-add (C{bool}).
829 @return: New, rotated vector (L{Vector3d}).
831 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/
832 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and
833 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/
834 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.
835 '''
836 s, c = sincos2(theta) # rotation angle
837 d = _1_0 - c
838 if d or s:
839 p = self.unit().xyz # point being rotated
840 r = self.others(axis=axis).unit() # axis being rotated around
842 ax, ay, az = r.xyz3 # quaternion-derived rotation matrix
843 bx, by, bz = r.times(d).xyz3
844 sx, sy, sz = r.times(s).xyz3
846 if fma:
847 _fma = _MODS.fmath.fma
848 else:
849 def _fma(a, b, c):
850 return a * b + c
852 x = fdot(p, _fma(ax, bx, c), _fma(ax, by, -sz), _fma(ax, bz, sy))
853 y = fdot(p, _fma(ay, bx, sz), _fma(ay, by, c), _fma(ay, bz, -sx))
854 z = fdot(p, _fma(az, bx, -sy), _fma(az, by, sx), _fma(az, bz, c))
855 else: # unrotated
856 x, y, z = self.xyz3
857 return self.classof(x, y, z)
859 @deprecated_method
860 def rotateAround(self, axis, theta):
861 '''DEPRECATED, use method C{rotate}.'''
862 return self.rotate(axis, theta) # PYCHOK no cover
864 def times(self, factor):
865 '''Multiply this vector by a scalar.
867 @arg factor: Scale factor (C{scalar}).
869 @return: New, scaled vector (L{Vector3d}).
871 @raise TypeError: Non-scalar B{C{factor}}.
872 '''
873 return self._times(Scalar(factor=factor))
875 def _times(self, s):
876 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.
877 '''
878 return self.classof(self.x * s, self.y * s, self.z * s)
880 def times_(self, other_x, *y_z):
881 '''Multiply this vector's components by separate X, Y and Z factors.
883 @arg other_x: X scale factor (C{scalar}) or a vector's
884 X, Y, and Z components as scale factors
885 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
886 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
887 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),
888 ignored if B{C{other_x}} is not C{scalar}.
890 @return: New, scaled vector (L{Vector3d}).
892 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
893 '''
894 x, y, z = _xyz3(self.times_, other_x, *y_z)
895 return self.classof(self.x * x, self.y * y, self.z * z)
897# @deprecated_method
898# def to2ab(self): # PYCHOK no cover
899# '''DEPRECATED, use property C{Nvector.philam}.
900#
901# @return: A L{PhiLam2Tuple}C{(phi, lam)}.
902# '''
903# return _MODS.nvectorBase.n_xyz2philam(self.x, self.y, self.z)
905# @deprecated_method
906# def to2ll(self): # PYCHOK no cover
907# '''DEPRECATED, use property C{Nvector.latlon}.
908#
909# @return: A L{LatLon2Tuple}C{(lat, lon)}.
910# '''
911# return _MODS.nvectorBase.n_xyz2latlon(self.x, self.y, self.z)
913 @deprecated_method
914 def to3xyz(self): # PYCHOK no cover
915 '''DEPRECATED, use property L{xyz}.
916 '''
917 return self.xyz
919 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected
920 '''Return a string representation of this vector.
922 @kwarg prec: Number of decimal places (C{int}).
923 @kwarg fmt: Enclosing format to use (C{str}).
924 @kwarg sep: Separator between components (C{str}).
926 @return: Vector as "(x, y, z)" (C{str}).
927 '''
928 t = sep.join(strs(self.xyz3, prec=prec))
929 return (fmt % (t,)) if fmt else t
931 def unit(self, ll=None):
932 '''Normalize this vector to unit length.
934 @kwarg ll: Optional, original location (C{LatLon}).
936 @return: Normalized vector (L{Vector3d}).
937 '''
938 u = self._united
939 if ll:
940 u._fromll = ll
941 return u
943 @Property_RO
944 def _united(self): # __dict__ value overwritten below
945 '''(INTERNAL) Get normalized vector (L{Vector3d}).
946 '''
947 n = self.length
948 if n > EPS0 and fabs(n - _1_0) > EPS0:
949 u = self._xnamed(self.dividedBy(n))
950 u._update(False, length=_1_0, length2=_1_0, _united=u)
951 else:
952 u = self.copy()
953 u._update(False, _united=u)
954 if self._fromll:
955 u._fromll = self._fromll
956 return u
958 @Property
959 def x(self):
960 '''Get the X component (C{float}).
961 '''
962 return self._x
964 @x.setter # PYCHOK setter!
965 def x(self, x):
966 '''Set the X component, if different (C{float}).
967 '''
968 x = Float(x=x)
969 if self._x != x:
970 _update_all(self, needed=3)
971 self._x = x
973 @Property
974 def xyz(self):
975 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).
976 '''
977 return _MODS.namedTuples.Vector3Tuple(*self.xyz3, name=self.name)
979 @xyz.setter # PYCHOK setter!
980 def xyz(self, xyz):
981 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},
982 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}
983 or a C{tuple} or C{list} of 3+ C{scalar} items).
984 '''
985 self._xyz(xyz)
987 def _xyz(self, x_xyz, *y_z):
988 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.
989 '''
990 _update_all(self, needed=3)
991 self._x, self._y, self._z = _xyz3(_xyz_, x_xyz, *y_z)
992 return self
994 @property_RO
995 def xyz3(self):
996 '''Get the X, Y and Z components as C{3-tuple}.
997 '''
998 return self.x, self.y, self.z
1000 @property_RO
1001 def x2y2z2(self):
1002 '''Get the X, Y and Z components I{squared} (3-tuple C{(x**2, y**2, z**2)}).
1003 '''
1004 return self.x**2, self.y**2, self.z**2
1006 @Property
1007 def y(self):
1008 '''Get the Y component (C{float}).
1009 '''
1010 return self._y
1012 @y.setter # PYCHOK setter!
1013 def y(self, y):
1014 '''Set the Y component, if different (C{float}).
1015 '''
1016 y = Float(y=y)
1017 if self._y != y:
1018 _update_all(self, needed=3)
1019 self._y = y
1021 @Property
1022 def z(self):
1023 '''Get the Z component (C{float}).
1024 '''
1025 return self._z
1027 @z.setter # PYCHOK setter!
1028 def z(self, z):
1029 '''Set the Z component, if different (C{float}).
1030 '''
1031 z = Float(z=z)
1032 if self._z != z:
1033 _update_all(self, needed=3)
1034 self._z = z
1037def _xyz3(where, x_xyz, *y_z): # in .cartesianBase._rtp3
1038 '''(INTERNAL) Get , Y and Z as 3-tuple C{(x, y, z)}.
1039 '''
1040 try:
1041 xyz3 = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for Vector*Tuple
1042 map2(_float0, x_xyz[:3]) if islistuple(x_xyz, minum=3) else
1043 x_xyz.xyz) # .xyz3
1044 except (AttributeError, TypeError, ValueError) as x:
1045 raise _xError(x, unstr(where, x_xyz, *y_z))
1046 return xyz3
1049__all__ += _ALL_DOCS(Vector3dBase)
1051# **) MIT License
1052#
1053# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1054#
1055# Permission is hereby granted, free of charge, to any person obtaining a
1056# copy of this software and associated documentation files (the "Software"),
1057# to deal in the Software without restriction, including without limitation
1058# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1059# and/or sell copies of the Software, and to permit persons to whom the
1060# Software is furnished to do so, subject to the following conditions:
1061#
1062# The above copyright notice and this permission notice shall be included
1063# in all copies or substantial portions of the Software.
1064#
1065# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1066# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1067# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1068# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1069# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1070# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1071# OTHER DEALINGS IN THE SOFTWARE.