Coverage for pygeodesy/vector3dBase.py: 88%
322 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'''(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, _1_0, \
14 _copysignINF, isnear0, isnear1, \
15 _flipsign, _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.10.25'
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, scalar): # PYCHOK no cover
116 '''Apply C{scalar} divmod to this vector's components.
118 @arg scalar: Divisor (C{scalar}).
120 @return: 2-Tuple C{(d, m)} each a L{Vector3d}.
121 '''
122 s = Scalar(divisor=scalar)
123 d, m = [], []
124 for x in self.xyz3:
125 q, r = divmod(x, s)
126 d.append(q)
127 m.append(r)
128 return self.classof(d), self.classof(m)
130 def __eq__(self, other):
131 '''Is this vector equal to an other vector?
133 @arg other: The other vector (L{Vector3d}).
135 @return: C{True} if equal, C{False} otherwise.
137 @raise TypeError: Incompatible B{C{other}} C{type}.
138 '''
139 return self.isequalTo(other, eps=EPS0)
141 def __float__(self): # PYCHOK no cover
142 '''Not implemented, see method C{float}.'''
143 return _NotImplemented(self) # must return C{float}
145 def __floor__(self): # PYCHOK no cover
146 '''Return a vector with the C{floor} of each components.
148 @return: Floor-ed (L{Vector3d}).
149 '''
150 return self._mapped(_floor)
152 def __floordiv__(self, scalar): # PYCHOK no cover
153 '''Floor-divide this vector by a scalar, C{this // B{scalar}}.
155 @arg scalar: The divisor (C{scalar}).
157 @return: Floored quotient (L{Vector3d}).
159 @raise TypeError: Non-scalar B{C{scalar}}.
160 '''
161 return self.floorDividedBy(scalar)
163 def __ge__(self, other):
164 '''Is this vector longer than or equal to an other vector?
166 @arg other: The other vector (L{Vector3d}).
168 @return: C{True} if so, C{False} otherwise.
170 @raise TypeError: Incompatible B{C{other}} C{type}.
171 '''
172 return self.length >= self._other_cmp(other)
174# def __getitem__(self, key):
175# '''Return C{item} at index or slice C{[B{key}]}.
176# '''
177# return self.xyz[key]
179 def __gt__(self, other):
180 '''Is this vector longer than an other vector?
182 @arg other: The other vector (L{Vector3d}).
184 @return: C{True} if so, C{False} otherwise.
186 @raise TypeError: Incompatible B{C{other}} C{type}.
187 '''
188 return self.length > self._other_cmp(other)
190 def __hash__(self): # PYCHOK no cover
191 '''Return this instance' C{hash}.
192 '''
193 return hash(self.xyz) # XXX id(self)?
195 def __iadd__(self, other):
196 '''Add this and an other vector I{in-place}, C{this += B{other}}.
198 @arg other: The other vector (L{Vector3d}).
200 @raise TypeError: Incompatible B{C{other}} C{type}.
201 '''
202 return self._xyz(self.plus(other))
204 def __ifloordiv__(self, scalar): # PYCHOK no cover
205 '''Floor-divide this vector by a scalar I{in-place}, C{this //= B{scalar}}.
207 @arg scalar: The divisor (C{scalar}).
209 @raise TypeError: Non-scalar B{C{scalar}}.
210 '''
211 return self._xyz(self.floorDividedBy(scalar))
213 def __imatmul__(self, other): # PYCHOK Python 3.5+
214 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}.
216 @arg other: The other vector (L{Vector3d}).
218 @raise TypeError: Incompatible B{C{other}} C{type}.
220 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 397+, 2022 p. 578+.
221 '''
222 return self._xyz(self.cross(other))
224 def __imod__(self, other): # PYCHOK no cover
225 '''Not implemented.'''
226 return _NotImplemented(self, other)
228 def __imul__(self, scalar):
229 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}.
231 @arg scalar: Factor (C{scalar}).
233 @raise TypeError: Non-scalar B{C{scalar}}.
234 '''
235 return self._xyz(self.times(scalar))
237 def __int__(self): # PYCHOK no cover
238 '''Not implemented, see method C{ints}.'''
239 return _NotImplemented(self) # must return C{int}
241 def __ipow__(self, scalar, *mod): # PYCHOK no cover
242 '''Raise each component I{in-place} as C{pow(C, B{scalar})}.
244 @arg scalar: Exponent (C{scalar}).
246 @return: Power (L{Vector3d}).
248 @raise TypeError: Non-scalar B{C{scalar}}.
249 '''
250 return self._xyz(self.pow(scalar, *mod))
252 def __isub__(self, other):
253 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.
255 @arg other: The other vector (L{Vector3d}).
257 @raise TypeError: Incompatible B{C{other}} C{type}.
258 '''
259 return self._xyz(self.minus(other))
261# def __iter__(self):
262# '''Return an C{iter}ator over this vector's components.
263# '''
264# return iter(self.xyz3)
266 def __itruediv__(self, scalar):
267 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.
269 @arg scalar: The divisor (C{scalar}).
271 @raise TypeError: Non-scalar B{C{scalar}}.
272 '''
273 return self._xyz(self.dividedBy(scalar))
275 def __le__(self, other): # Python 3+
276 '''Is this vector shorter than or equal to an other vector?
278 @arg other: The other vector (L{Vector3d}).
280 @return: C{True} if so, C{False} otherwise.
282 @raise TypeError: Incompatible B{C{other}} C{type}.
283 '''
284 return self.length <= self._other_cmp(other)
286# def __len__(self):
287# '''Return C{3}, always.
288# '''
289# return len(self.xyz)
291 def __lt__(self, other): # Python 3+
292 '''Is this vector shorter than an other vector?
294 @arg other: The other vector (L{Vector3d}).
296 @return: C{True} if so, C{False} otherwise.
298 @raise TypeError: Incompatible B{C{other}} C{type}.
299 '''
300 return self.length < self._other_cmp(other)
302 def __matmul__(self, other): # PYCHOK Python 3.5+
303 '''Compute the cross product of this and an other vector, C{this @ B{other}}.
305 @arg other: The other vector (L{Vector3d}).
307 @return: Cross product (L{Vector3d}).
309 @raise TypeError: Incompatible B{C{other}} C{type}.
310 '''
311 return self.cross(other)
313 def __mod__(self, modulus): # PYCHOK no cover
314 '''Apply C{scalar} modulo to this vector's components.
316 @arg modulus: Modulus (C{scalar}).
318 @return: Modulo (L{Vector3d}).
319 '''
320 m = Scalar(modulus=modulus)
322 def _mod(x):
323 return x % m
325 return self._mapped(_mod)
327 def __mul__(self, scalar):
328 '''Multiply this vector by a scalar, C{this * B{scalar}}.
330 @arg scalar: Factor (C{scalar}).
332 @return: Product (L{Vector3d}).
333 '''
334 return self.times(scalar)
336 def __ne__(self, other):
337 '''Is this vector not equal to an other vector?
339 @arg other: The other vector (L{Vector3d}).
341 @return: C{True} if so, C{False} otherwise.
343 @raise TypeError: Incompatible B{C{other}} C{type}.
344 '''
345 return not self.isequalTo(other, eps=EPS0)
347 def __neg__(self):
348 '''Return the opposite of this vector.
350 @return: A negated copy (L{Vector3d})
351 '''
352 return self.negate()
354 def __pos__(self): # PYCHOK no cover
355 '''Return this vector I{as-is} or a copy.
357 @return: This instance (L{Vector3d})
358 '''
359 return self if _pos_self else self.copy()
361 def __pow__(self, scalar, *mod): # PYCHOK no cover
362 '''Return a vector with components as C{pow(C, B{scalar})}.
364 @arg scalar: Exponent (C{scalar}).
366 @return: Power (L{Vector3d}).
368 @raise TypeError: Non-scalar B{C{scalar}}.
369 '''
370 return self.pow(scalar, *mod)
372 def __radd__(self, other): # PYCHOK no cover
373 '''Add this vector to an other vector, C{B{other} + this}.
375 @arg other: The other vector (L{Vector3d}).
377 @return: Sum (L{Vector3d}).
379 @raise TypeError: Incompatible B{C{other}} C{type}.
380 '''
381 return self.others(other).plus(self)
383 def __rdivmod__(self, other):
384 '''Not implemented.'''
385 return _NotImplemented(self, other)
387# def __repr__(self):
388# '''Return the default C{repr(this)}.
389# '''
390# return self.toRepr()
392 def __rfloordiv__(self, other):
393 '''Not implemented.'''
394 return _NotImplemented(self, other)
396 def __rmatmul__(self, other): # PYCHOK Python 3.5+
397 '''Compute the cross product of an other and this vector, C{B{other} @ this}.
399 @arg other: The other vector (L{Vector3d}).
401 @return: Cross product (L{Vector3d}).
403 @raise TypeError: Incompatible B{C{other}} C{type}.
404 '''
405 return self.others(other).cross(self)
407 def __rmod__(self, other):
408 '''Not implemented.'''
409 return _NotImplemented(self, other)
411 __rmul__ = __mul__ # scalar other
413 def __round__(self, *ndigits): # PYCHOK no cover
414 '''Return a vector with these components C{rounded}.
416 @arg ndigits: Optional number of digits (C{int}).
418 @return: Rounded (L{Vector3d}).
419 '''
420 def _rnd(x):
421 return round(x, *ndigits)
423 # <https://docs.Python.org/3.12/reference/datamodel.html?#object.__round__>
424 return self._mapped(_rnd)
426 def __rpow__(self, other, *mod): # PYCHOK no cover
427 '''Not implemented.'''
428 return _NotImplemented(self, other, *mod)
430 def __rsub__(self, other): # PYCHOK no cover
431 '''Subtract this vector from an other vector, C{B{other} - this}.
433 @arg other: The other vector (L{Vector3d}).
435 @return: Difference (L{Vector3d}).
437 @raise TypeError: Incompatible B{C{other}} C{type}.
438 '''
439 return self.others(other).minus(self)
441 def __rtruediv__(self, other):
442 '''Not implemented.'''
443 return _NotImplemented(self, other)
445# def __str__(self):
446# '''Return the default C{str(self)}.
447# '''
448# return self.toStr()
450 def __sub__(self, other):
451 '''Subtract an other vector from this vector, C{this - B{other}}.
453 @arg other: The other vector (L{Vector3d}).
455 @return: Difference (L{Vector3d}).
457 @raise TypeError: Incompatible B{C{other}} C{type}.
458 '''
459 return self.minus(other)
461 def __truediv__(self, scalar):
462 '''Divide this vector by a scalar, C{this / B{scalar}}.
464 @arg scalar: The divisor (C{scalar}).
466 @return: Quotient (L{Vector3d}).
468 @raise TypeError: Non-scalar B{C{scalar}}.
469 '''
470 return self.dividedBy(scalar)
472 def __trunc__(self): # PYCHOK no cover
473 '''Return a vector with the C{trunc} of these components.
475 @return: Trunc-ed (L{Vector3d}).
476 '''
477 return self._mapped(_trunc)
479 if _MODS.sys_version_info2 < (3, 0): # PYCHOK no cover
480 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
481 __div__ = __truediv__
482 __idiv__ = __itruediv__
483 __long__ = __int__
484 __nonzero__ = __bool__
485 __rdiv__ = __rtruediv__
487 def angleTo(self, other, vSign=None, wrap=False):
488 '''Compute the angle between this and an other vector.
490 @arg other: The other vector (L{Vector3d}).
491 @kwarg vSign: Optional vector, if supplied (and out of the
492 plane of this and the other), angle is signed
493 positive if this->other is clockwise looking
494 along vSign or negative in opposite direction,
495 otherwise angle is unsigned.
496 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).
498 @return: Angle (C{radians}).
500 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.
501 '''
502 x = self.cross(other)
503 s = x.length
504 # use vSign as reference to set sign of s
505 if s and vSign and x.dot(vSign) < 0:
506 s = -s
508 a = atan2(s, self.dot(other))
509 if wrap and fabs(a) > PI:
510 a -= _copysign(PI2, a)
511 return a
513 def apply(self, fun2, other_x, *y_z, **fun2_kwds):
514 '''Apply a 2-argument function pairwise to the components
515 of this and an other vector.
517 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),
518 return a C{scalar} or L{INT0} result.
519 @arg other_x: Other X component (C{scalar}) or a vector
520 with X, Y and Z components (C{Cartesian},
521 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
522 L{Vector3Tuple} or L{Vector4Tuple}).
523 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).
524 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.
526 @return: New, applied vector (L{Vector3d}).
528 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
529 '''
530 _xcallable(fun2=fun2)
531 if fun2_kwds:
532 def _f2(a, b):
533 return fun2(a, b, **fun2_kwds)
534 else:
535 _f2 = fun2
537 xyz = _xyz3(self.apply, other_x, *y_z)
538 xyz = (_f2(a, b) for a, b in _zip(self.xyz3, xyz)) # strict=True
539 return self.classof(*xyz)
541 def bools(self):
542 '''Return the vector with C{bool} components.
543 '''
544 return self._mapped(bool)
546 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN
547 '''Compute the cross product of this and an other vector.
549 @arg other: The other vector (L{Vector3d}).
550 @kwarg raiser: Optional, L{CrossError} label if raised (C{str}, non-L{NN}).
551 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as C{x}, C{y} and
552 C{z}.
554 @return: Cross product (L{Vector3d}).
556 @raise CrossError: Zero or near-zero cross product and if B{C{raiser}} and
557 L{crosserrors<pygeodesy.crosserrors>} are both C{True}.
559 @raise TypeError: Incompatible B{C{other}} C{type}.
560 '''
561 X, Y, Z = self.others(other).xyz3
562 x, y, z = self.xyz3
563 xyz = (fdot_(y, Z, -z, Y),
564 fdot_(z, X, -x, Z),
565 fdot_(x, Y, -y, X))
567 if raiser and self.crosserrors and eps0 > 0 \
568 and max(map(fabs, xyz)) < eps0:
569 r = other._fromll or other
570 s = self._fromll or self
571 t = self.isequalTo(other, eps=eps0)
572 t = _coincident_ if t else _colinear_
573 raise CrossError(raiser, s, other=r, txt=t)
575 return self.classof(*xyz) # name__=self.cross
577 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')
578 def crosserrors(self):
579 '''Get L{CrossError} exceptions (C{bool}).
580 '''
581 return self._crosserrors
583 @crosserrors.setter # PYCHOK setter!
584 def crosserrors(self, raiser):
585 '''Raise or ignore L{CrossError} exceptions (C{bool}).
586 '''
587 self._crosserrors = bool(raiser)
589 def dividedBy(self, scalar):
590 '''Divide this vector by a scalar.
592 @arg scalar: The divisor (C{scalar}).
594 @return: New, scaled vector (L{Vector3d}).
596 @raise TypeError: Non-scalar B{C{scalar}}.
598 @raise VectorError: Invalid or zero B{C{scalar}}.
599 '''
600 d = Scalar(divisor=scalar)
601 try:
602 return self._times(_1_0 / d)
603 except (ValueError, ZeroDivisionError) as x:
604 raise VectorError(divisor=scalar, cause=x)
606 def dividedBy_(self, scalar_x, *y_z):
607 '''Divide this vector by separate X, Y and Z divisors.
609 @arg scalar_x: X divisor (C{scalar}) or a vector's
610 X, Y, and Z components as divisors
611 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
612 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
613 @arg y_z: Y and Z divisors (C{scalar}, C{scalar}), ignored
614 if B{C{scalar_x}} is not C{scalar}.
616 @return: New, scaled vector (L{Vector3d}).
618 @raise VectorError: Invalid B{C{scalar_x}} or B{C{y_z}}.
619 '''
620 x, y, z = _xyz3(self.dividedBy_, scalar_x, *y_z)
621 return self.classof(self.x / x, self.y / y, self.z / z)
623 def dot(self, other):
624 '''Compute the dot (scalar) product of this and an other vector.
626 @arg other: The other vector (L{Vector3d}).
628 @return: Dot product (C{float}).
630 @raise TypeError: Incompatible B{C{other}} C{type}.
631 '''
632 return self.length2 if other is self else fdot(
633 self.xyz3, *self.others(other).xyz3)
635 @deprecated_method
636 def equals(self, other, units=False): # PYCHOK no cover
637 '''DEPRECATED, use method C{isequalTo}.
638 '''
639 return self.isequalTo(other, units=units)
641 @Property_RO
642 def euclid(self):
643 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).
645 @see: Properties C{length} and C{length2} and function
646 L{pygeodesy.euclid_}.
647 '''
648 return Float(euclid=euclid_(*self.xyz3))
650 def equirectangular(self, other):
651 '''I{Approximate} the difference between this and an other vector.
653 @arg other: Vector to subtract (C{Vector3dBase}).
655 @return: The length I{squared} of the difference (C{Float}).
657 @raise TypeError: Incompatible B{C{other}} C{type}.
659 @see: Property C{length2}.
660 '''
661 d = self.minus(other)
662 return Float(equirectangular=hypot2_(*d.xyz3))
664 def fabs(self):
665 '''Return the vector with C{fabs} components.
666 '''
667 return self._mapped(fabs)
669 def floats(self):
670 '''Return the vector with C{float} components.
671 '''
672 return self._mapped(_float0)
674 def floorDividedBy(self, scalar):
675 '''Floor-divide this vector by a scalar.
677 @arg scalar: The divisor (C{scalar}).
679 @return: New, scaled vector (L{Vector3d}).
681 @raise TypeError: Non-scalar B{C{scalar}}.
683 @raise VectorError: Invalid or zero B{C{scalar}}.
684 '''
685 d = Scalar(divisor=scalar)
686 try:
687 def _floor_d(x):
688 return x // d
690 return self._mapped(_floor_d)
691 except (ValueError, ZeroDivisionError) as x:
692 raise VectorError(divisor=scalar, cause=x)
694 def floorDividedBy_(self, scalar_x, *y_z):
695 '''Floor-divide this vector by separate X, Y and Z divisors.
697 @arg scalar_x: X divisor (C{scalar}) or a vector's
698 X, Y, and Z components as divisors
699 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
700 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
701 @arg y_z: Y and Z divisors (C{scalar}, C{scalar}), ignored
702 if B{C{scalar_x}} is not C{scalar}.
704 @return: New, scaled vector (L{Vector3d}).
706 @raise VectorError: Invalid B{C{scalar_x}} or B{C{y_z}}.
707 '''
708 x, y, z = _xyz3(self.floorDividedBy_, scalar_x, *y_z)
709 return self.classof(self.x // x, self.y // y, self.z // z)
711 @Property
712 def _fromll(self):
713 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.
714 '''
715 return self._ll
717 @_fromll.setter # PYCHOK setter!
718 def _fromll(self, ll):
719 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.
720 '''
721 self._ll = ll or None
723 @property_RO
724 def homogeneous(self):
725 '''Get this vector's homogeneous representation (L{Vector3d}).
726 '''
727 x, y, z = self.xyz3
728 if z:
729 x = x / z # /= chokes PyChecker
730 y = y / z
731# z = _1_0
732 else:
733 x = _copysignINF(_flipsign(x, z))
734 y = _copysignINF(_flipsign(y, z))
735# z = NAN
736 return self.classof(x, y, _1_0)
738 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False
739 '''Locate the vector at a given fraction between (or along) this
740 and an other vector.
742 @arg other: The other vector (L{Vector3d}).
743 @arg fraction: Fraction between both vectors (C{scalar},
744 0.0 for this and 1.0 for the other vector).
746 @return: Intermediate vector (L{Vector3d}).
748 @raise TypeError: Incompatible B{C{other}} C{type}.
749 '''
750 f = Scalar(fraction=fraction)
751 if isnear0(f): # PYCHOK no cover
752 r = self
753 else:
754 r = self.others(other)
755 if not isnear1(f): # self * (1 - f) + r * f
756 r = self.plus(r.minus(self)._times(f))
757 return r
759 def ints(self):
760 '''Return the vector with C{int} components.
761 '''
762 return self._mapped(int)
764 def isconjugateTo(self, other, minum=1, eps=EPS):
765 '''Determine whether this and an other vector are conjugates.
767 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},
768 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).
769 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).
770 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),
771 same units as C{x}, C{y}, and C{z}.
773 @return: C{True} if both vector's components either match
774 or at least C{B{minum}} have opposite signs.
776 @raise TypeError: Incompatible B{C{other}} C{type}.
778 @see: Method C{isequalTo}.
779 '''
780 self.others(other)
781 n = 0
782 for a, b in zip(self.xyz3, other.xyz3):
783 if fabs(a + b) < eps and ((a < 0 and b > 0) or
784 (a > 0 and b < 0)):
785 n += 1 # conjugate
786 elif fabs(a - b) > eps:
787 return False # unequal
788 return bool(n >= minum)
790 def isequalTo(self, other, units=False, eps=EPS):
791 '''Check if this and an other vector are equal or equivalent.
793 @arg other: The other vector (L{Vector3d}).
794 @kwarg units: Optionally, compare the normalized, unit
795 version of both vectors.
796 @kwarg eps: Tolerance for equality (C{scalar}), same units as
797 C{x}, C{y}, and C{z}.
799 @return: C{True} if vectors are identical, C{False} otherwise.
801 @raise TypeError: Incompatible B{C{other}} C{type}.
803 @see: Method C{isconjugateTo}.
804 '''
805 if units:
806 self.others(other)
807 d = self.unit().minus(other.unit())
808 else:
809 d = self.minus(other)
810 return max(map(fabs, d.xyz3)) < eps
812 @Property_RO
813 def length(self): # __dict__ value overwritten by Property_RO C{_united}
814 '''Get the length (norm, magnitude) of this vector (C{Float}).
816 @see: Properties L{length2} and L{euclid}.
817 '''
818 return Float(length=hypot_(*self.xyz3))
820 @Property_RO
821 def length2(self): # __dict__ value overwritten by Property_RO C{_united}
822 '''Get the length I{squared} of this vector (C{Float}).
824 @see: Property L{length} and method C{equirectangular}.
825 '''
826 return Float(length2=hypot2_(*self.xyz3))
828 def _mapped(self, func):
829 '''(INTERNAL) Apply C{func} to all components.
830 '''
831 return self.classof(*map2(func, self.xyz3))
833 def minus(self, other):
834 '''Subtract an other vector from this vector.
836 @arg other: The other vector (L{Vector3d}).
838 @return: New vector difference (L{Vector3d}).
840 @raise TypeError: Incompatible B{C{other}} C{type}.
841 '''
842 return self._minus(*self.others(other).xyz3)
844 def _minus(self, x, y, z):
845 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.
846 '''
847 return self.classof(self.x - x, self.y - y, self.z - z)
849 def minus_(self, other_x, *y_z):
850 '''Subtract separate X, Y and Z components from this vector.
852 @arg other_x: X component (C{scalar}) or a vector's
853 X, Y, and Z components (C{Cartesian},
854 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
855 L{Vector3Tuple}, L{Vector4Tuple}).
856 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
857 ignored if B{C{other_x}} is not C{scalar}.
859 @return: New, vectiorial vector (L{Vector3d}).
861 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
862 '''
863 return self._minus(*_xyz3(self.minus_, other_x, *y_z))
865 def negate(self):
866 '''Return the opposite of this vector.
868 @return: A negated copy (L{Vector3d})
869 '''
870 return self.classof(-self.x, -self.y, -self.z)
872 @Property_RO
873 def _N_vector(self):
874 '''(INTERNAL) Get the (C{nvectorBase._N_Vector})
875 '''
876 _N = _MODS.nvectorBase._N_Vector
877 return _N(*self.xyz3, name=self.name)
879 def _other_cmp(self, other):
880 '''(INTERNAL) Return the value for comparison.
881 '''
882 return other if isscalar(other) else self.others(other).length
884 def others(self, *other, **name_other_up):
885 '''Refined class comparison.
887 @arg other: The other vector (L{Vector3d}).
888 @kwarg name_other_up: Overriding C{name=other} and C{up=1}
889 keyword arguments.
891 @return: The B{C{other}} if compatible.
893 @raise TypeError: Incompatible B{C{other}} C{type}.
894 '''
895 other, name, up = _xother3(self, other, **name_other_up)
896 if not isinstance(other, Vector3dBase):
897 _NamedBase.others(self, other, name=name, up=up + 1)
898 return other
900 def plus(self, other):
901 '''Add this vector and an other vector.
903 @arg other: The other vector (L{Vector3d}).
905 @return: Vectorial sum (L{Vector3d}).
907 @raise TypeError: Incompatible B{C{other}} C{type}.
908 '''
909 return self._plus(*self.others(other).xyz3)
911 sum = plus # alternate name
913 def _plus(self, x, y, z):
914 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.
915 '''
916 return self.classof(self.x + x, self.y + y, self.z + z)
918 def plus_(self, other_x, *y_z):
919 '''Sum of this vector and separate X, Y and Z components.
921 @arg other_x: X component (C{scalar}) or a vector's
922 X, Y, and Z components (C{Cartesian},
923 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
924 L{Vector3Tuple}, L{Vector4Tuple}).
925 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
926 ignored if B{C{other_x}} is not C{scalar}.
928 @return: New, vectorial vector (L{Vector3d}).
930 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
931 '''
932 return self._plus(*_xyz3(self.plus_, other_x, *y_z))
934 def pow(self, scalar, *mod):
935 '''Raise each X, Y and Z to C{scalar} power.
937 @arg scalar: Exponent (C{scalar}).
939 @return: Power (L{Vector3d}).
941 @raise TypeError: Non-scalar B{C{scalar}}.
942 '''
943 p = Scalar(power=scalar)
945 def _pow(x):
946 return pow(x, p, *mod)
948 return self._mapped(_pow)
950 def rotate(self, axis, theta, fma=False):
951 '''Rotate this vector around an axis by a specified angle.
953 @arg axis: The axis being rotated around (L{Vector3d}).
954 @arg theta: The angle of rotation (C{radians}).
955 @kwarg fma: If C{True}, use fused-multiply-add (C{bool}).
957 @return: New, rotated vector (L{Vector3d}).
959 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/
960 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and
961 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/
962 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.
963 '''
964 s, c = sincos2(theta) # rotation angle
965 d = _1_0 - c
966 if d or s:
967 p = self.unit().xyz # point being rotated
968 r = self.others(axis=axis).unit() # axis being rotated around
970 ax, ay, az = r.xyz3 # quaternion-derived rotation matrix
971 bx, by, bz = r.times(d).xyz3
972 sx, sy, sz = r.times(s).xyz3
974 if fma:
975 _fma = _MODS.fmath.fma
976 else:
977 def _fma(a, b, c):
978 return a * b + c
980 x = fdot(p, _fma(ax, bx, c), _fma(ax, by, -sz), _fma(ax, bz, sy))
981 y = fdot(p, _fma(ay, bx, sz), _fma(ay, by, c), _fma(ay, bz, -sx))
982 z = fdot(p, _fma(az, bx, -sy), _fma(az, by, sx), _fma(az, bz, c))
983 else: # unrotated
984 x, y, z = self.xyz3
985 return self.classof(x, y, z)
987 @deprecated_method
988 def rotateAround(self, axis, theta):
989 '''DEPRECATED, use method C{rotate}.'''
990 return self.rotate(axis, theta) # PYCHOK no cover
992 def _roty(self, *pos, **name):
993 '''(INTERNAL) Prolate rotation, for C{+1 if pos else -1}.
994 '''
995 v = self.copy(**name)
996 if pos:
997 x, _, z = v.xyz3
998 _update_all(v, needed=3)
999 v._x, v._z = (-z, x) if pos[0] else (z, -x)
1000 return v
1002 def times(self, factor):
1003 '''Multiply this vector by a scalar.
1005 @arg factor: Scale factor (C{scalar}).
1007 @return: New, scaled vector (L{Vector3d}).
1009 @raise TypeError: Non-scalar B{C{factor}}.
1010 '''
1011 return self._times(Scalar(factor=factor))
1013 def _times(self, s):
1014 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.
1015 '''
1016 return self.classof(self.x * s, self.y * s, self.z * s)
1018 def times_(self, other_x, *y_z):
1019 '''Multiply this vector's components by separate X, Y and Z factors.
1021 @arg other_x: X scale factor (C{scalar}) or a vector's
1022 X, Y, and Z components as scale factors
1023 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
1024 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
1025 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),
1026 ignored if B{C{other_x}} is not C{scalar}.
1028 @return: New, scaled vector (L{Vector3d}).
1030 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
1031 '''
1032 x, y, z = _xyz3(self.times_, other_x, *y_z)
1033 return self.classof(self.x * x, self.y * y, self.z * z)
1035# @deprecated_method
1036# def to2ab(self): # PYCHOK no cover
1037# '''DEPRECATED, use property C{Nvector.philam}.
1038#
1039# @return: A L{PhiLam2Tuple}C{(phi, lam)}.
1040# '''
1041# return _MODS.nvectorBase.n_xyz2philam(self.x, self.y, self.z)
1043# @deprecated_method
1044# def to2ll(self): # PYCHOK no cover
1045# '''DEPRECATED, use property C{Nvector.latlon}.
1046#
1047# @return: A L{LatLon2Tuple}C{(lat, lon)}.
1048# '''
1049# return _MODS.nvectorBase.n_xyz2latlon(self.x, self.y, self.z)
1051 @deprecated_method
1052 def to3xyz(self): # PYCHOK no cover
1053 '''DEPRECATED, use property L{xyz}.
1054 '''
1055 return self.xyz
1057 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected
1058 '''Return a string representation of this vector.
1060 @kwarg prec: Number of decimal places (C{int}).
1061 @kwarg fmt: Enclosing format to use (C{str}).
1062 @kwarg sep: Separator between components (C{str}).
1064 @return: Vector as "(x, y, z)" (C{str}).
1065 '''
1066 t = sep.join(strs(self.xyz3, prec=prec))
1067 return (fmt % (t,)) if fmt else t
1069 def unit(self, ll=None):
1070 '''Normalize this vector to unit length.
1072 @kwarg ll: Optional, original location (C{LatLon}).
1074 @return: Normalized vector (L{Vector3d}).
1075 '''
1076 u = self._united
1077 if ll:
1078 u._fromll = ll
1079 return u
1081 @Property_RO
1082 def _united(self): # __dict__ value overwritten below
1083 '''(INTERNAL) Get normalized vector (L{Vector3d}).
1084 '''
1085 n = self.length
1086 if n > EPS0 and fabs(n - _1_0) > EPS0:
1087 u = self._xnamed(self.dividedBy(n))
1088 u._update(False, length=_1_0, length2=_1_0, _united=u)
1089 else:
1090 u = self.copy()
1091 u._update(False, _united=u)
1092 if self._fromll:
1093 u._fromll = self._fromll
1094 return u
1096 @Property
1097 def x(self):
1098 '''Get the X component (C{float}).
1099 '''
1100 return self._x
1102 @x.setter # PYCHOK setter!
1103 def x(self, x):
1104 '''Set the X component, if different (C{float}).
1105 '''
1106 x = Float(x=x)
1107 if self._x != x:
1108 _update_all(self, needed=3)
1109 self._x = x
1111 @Property
1112 def xyz(self):
1113 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).
1114 '''
1115 return _MODS.namedTuples.Vector3Tuple(*self.xyz3, name=self.name)
1117 @xyz.setter # PYCHOK setter!
1118 def xyz(self, xyz):
1119 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},
1120 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}
1121 or a C{tuple} or C{list} of 3+ C{scalar} items).
1122 '''
1123 self._xyz(xyz)
1125 def _xyz(self, x_xyz, *y_z):
1126 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.
1127 '''
1128 xyz = _xyz3(_xyz_, x_xyz, *y_z)
1129 if self.xyz3 != xyz:
1130 _update_all(self, needed=3)
1131 self._x, self._y, self._z = xyz
1132 return self
1134 @property_RO
1135 def xyz3(self):
1136 '''Get the X, Y and Z components as C{3-tuple}.
1137 '''
1138 return self.x, self.y, self.z
1140 @Property_RO
1141 def x2y2z2(self):
1142 '''Get the X, Y and Z components, I{squared} (L{Vector3Tuple}).
1143 '''
1144 return _MODS.namedTuples.Vector3Tuple(*self.x2y2z23, name=self.name)
1146 @property_RO
1147 def x2y2z23(self):
1148 '''Get the X, Y and Z components, I{squared} (3-tuple C{(x**2, y**2, z**2)}).
1149 '''
1150 return self.x**2, self.y**2, self.z**2
1152 @Property
1153 def y(self):
1154 '''Get the Y component (C{float}).
1155 '''
1156 return self._y
1158 @y.setter # PYCHOK setter!
1159 def y(self, y):
1160 '''Set the Y component, if different (C{float}).
1161 '''
1162 y = Float(y=y)
1163 if self._y != y:
1164 _update_all(self, needed=3)
1165 self._y = y
1167 @Property
1168 def z(self):
1169 '''Get the Z component (C{float}).
1170 '''
1171 return self._z
1173 @z.setter # PYCHOK setter!
1174 def z(self, z):
1175 '''Set the Z component, if different (C{float}).
1176 '''
1177 z = Float(z=z)
1178 if self._z != z:
1179 _update_all(self, needed=3)
1180 self._z = z
1183def _xyz3(where, x_xyz, *y_z): # in .cartesianBase._rtp3
1184 '''(INTERNAL) Get X, Y and Z as 3-tuple C{(x, y, z)}.
1185 '''
1186 try:
1187 xyz3 = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for Vector*Tuple
1188 map2(_float0, x_xyz[:3]) if islistuple(x_xyz, minum=3) else
1189 x_xyz.xyz) # .xyz3
1190 except (AttributeError, TypeError, ValueError) as x:
1191 raise _xError(x, unstr(where, x_xyz, *y_z))
1192 return xyz3
1195__all__ += _ALL_DOCS(Vector3dBase)
1197# **) MIT License
1198#
1199# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.
1200#
1201# Permission is hereby granted, free of charge, to any person obtaining a
1202# copy of this software and associated documentation files (the "Software"),
1203# to deal in the Software without restriction, including without limitation
1204# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1205# and/or sell copies of the Software, and to permit persons to whom the
1206# Software is furnished to do so, subject to the following conditions:
1207#
1208# The above copyright notice and this permission notice shall be included
1209# in all copies or substantial portions of the Software.
1210#
1211# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1212# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1213# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1214# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1215# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1216# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1217# OTHER DEALINGS IN THE SOFTWARE.