Coverage for pygeodesy/vector3dBase.py: 88%
322 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-10 16:16 -0500
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-10 16:16 -0500
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_, ftruncate, hypot_, \
18 hypot2_ # _MODS.fmath.fma
19from pygeodesy.interns import _coincident_, _colinear_, _COMMASPACE_, _xyz_
20from pygeodesy.lazily import _ALL_LAZY, _ALL_DOCS, _ALL_MODS as _MODS
21from pygeodesy.named import _NamedBase, _NotImplemented, _xother3
22# from pygeodesy.namedTuples import Vector3Tuple # _MODS
23# from pygeodesy.nvectorBase import _N_Vector # _MODS
24from pygeodesy.props import deprecated_method, Property, Property_RO, \
25 property_doc_, property_RO, _update_all
26from pygeodesy.streprs import Fmt, strs, unstr
27from pygeodesy.units import Float, Scalar
28from pygeodesy.utily import atan2, sincos2, fabs
30from math import ceil as _ceil, floor as _floor
32__all__ = _ALL_LAZY.vector3dBase
33__version__ = '26.10.28'
36class Vector3dBase(_NamedBase): # sync __methods__ with .fsums.Fsum
37 '''(INTERNAL) Generic 3-D vector base class.
38 '''
39 _crosserrors = True # un/set by .errors.crosserrors
41 _ll = None # original latlon, '_fromll'
42# _x = INT0 # X component
43# _y = INT0 # Y component
44# _z = INT0 # Z component
46 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, **name):
47 '''New L{Vector3d} or C{Vector3dBase} instance.
49 The vector may be normalised or use x, y, z for position and
50 distance from earth centre or height relative to the surface
51 of the earth' sphere or ellipsoid.
53 @arg x_xyz: X component of vector (C{scalar}) or a (3-D) vector
54 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
55 L{Vector3Tuple}, L{Vector4Tuple} or a C{tuple} or
56 C{list} of 3+ C{scalar} items).
57 @kwarg y: Y 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 z: Z component of vector (C{scalar}), ignored if B{C{x_xyz}}
60 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
61 @kwarg ll: Optional latlon reference (C{LatLon}).
62 @kwarg name: Optional C{B{name}=NN} (C{str}).
64 @raise VectorError: Invalid B{C{x_xyz}}, B{C{y}} or B{C{z}}.
65 '''
66 self._x, \
67 self._y, \
68 self._z = _xyz3(type(self), x_xyz, y, z) if isscalar(x_xyz) else \
69 _xyz3(type(self), x_xyz)
70 if ll:
71 self._ll = ll
72 if name:
73 self.name = name
75 def __abs__(self):
76 '''Return the norm of this vector.
78 @return: Norm, unit length (C{float});
79 '''
80 return self.length
82 def __add__(self, other):
83 '''Add this to an other vector (L{Vector3d}).
85 @return: Vectorial sum (L{Vector3d}).
87 @raise TypeError: Incompatible B{C{other}} C{type}.
88 '''
89 return self.plus(other)
91 def __bool__(self): # PYCHOK PyChecker
92 '''Is this vector non-zero?
94 @see: Method C{bools}.
95 '''
96 return bool(self.x or self.y or self.z)
98 def __ceil__(self): # PYCHOK no cover
99 '''Return a vector with the C{ceil} of these components.
101 @return: Ceil-ed (L{Vector3d}).
102 '''
103 return self._mapped(_ceil)
105 def __cmp__(self, other): # Python 2-
106 '''Compare this and an other vector (L{Vector3d}).
108 @return: -1, 0 or +1 (C{int}).
110 @raise TypeError: Incompatible B{C{other}} C{type}.
111 '''
112 return _signOf(self.length, self._other_cmp(other))
114 cmp = __cmp__
116 def __divmod__(self, scalar): # PYCHOK no cover
117 '''Apply C{scalar} divmod to this vector's components.
119 @arg scalar: Divisor (C{scalar}).
121 @return: 2-Tuple C{(d, m)} each a L{Vector3d}.
122 '''
123 s = Scalar(divisor=scalar)
124 d, m = [], []
125 for x in self.xyz3:
126 q, r = divmod(x, s)
127 d.append(q)
128 m.append(r)
129 return self.classof(d), self.classof(m)
131 def __eq__(self, other):
132 '''Is this vector equal to an other vector?
134 @arg other: The other vector (L{Vector3d}).
136 @return: C{True} if equal, C{False} otherwise.
138 @raise TypeError: Incompatible B{C{other}} C{type}.
139 '''
140 return self.isequalTo(other, eps=EPS0)
142 def __float__(self): # PYCHOK no cover
143 '''Not implemented, see method C{float}.'''
144 return _NotImplemented(self) # must return C{float}
146 def __floor__(self): # PYCHOK no cover
147 '''Return a vector with the C{floor} of each components.
149 @return: Floor-ed (L{Vector3d}).
150 '''
151 return self._mapped(_floor)
153 def __floordiv__(self, scalar): # PYCHOK no cover
154 '''Floor-divide this vector by a scalar, C{this // B{scalar}}.
156 @arg scalar: The divisor (C{scalar}).
158 @return: Floored quotient (L{Vector3d}).
160 @raise TypeError: Non-scalar B{C{scalar}}.
161 '''
162 return self.floorDividedBy(scalar)
164 def __ge__(self, other):
165 '''Is this vector longer than or equal to an other vector?
167 @arg other: The other vector (L{Vector3d}).
169 @return: C{True} if so, C{False} otherwise.
171 @raise TypeError: Incompatible B{C{other}} C{type}.
172 '''
173 return self.length >= self._other_cmp(other)
175# def __getitem__(self, key):
176# '''Return C{item} at index or slice C{[B{key}]}.
177# '''
178# return self.xyz[key]
180 def __gt__(self, other):
181 '''Is this vector longer than an other vector?
183 @arg other: The other vector (L{Vector3d}).
185 @return: C{True} if so, C{False} otherwise.
187 @raise TypeError: Incompatible B{C{other}} C{type}.
188 '''
189 return self.length > self._other_cmp(other)
191 def __hash__(self): # PYCHOK no cover
192 '''Return this instance' C{hash}.
193 '''
194 return hash(self.xyz) # XXX id(self)?
196 def __iadd__(self, other):
197 '''Add this and an other vector I{in-place}, C{this += B{other}}.
199 @arg other: The other vector (L{Vector3d}).
201 @raise TypeError: Incompatible B{C{other}} C{type}.
202 '''
203 return self._xyz(self.plus(other))
205 def __ifloordiv__(self, scalar): # PYCHOK no cover
206 '''Floor-divide this vector by a scalar I{in-place}, C{this //= B{scalar}}.
208 @arg scalar: The divisor (C{scalar}).
210 @raise TypeError: Non-scalar B{C{scalar}}.
211 '''
212 return self._xyz(self.floorDividedBy(scalar))
214 def __imatmul__(self, other): # PYCHOK Python 3.5+
215 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}.
217 @arg other: The other vector (L{Vector3d}).
219 @raise TypeError: Incompatible B{C{other}} C{type}.
221 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 397+, 2022 p. 578+.
222 '''
223 return self._xyz(self.cross(other))
225 def __imod__(self, other): # PYCHOK no cover
226 '''Not implemented.'''
227 return _NotImplemented(self, other)
229 def __imul__(self, scalar):
230 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}.
232 @arg scalar: Factor (C{scalar}).
234 @raise TypeError: Non-scalar B{C{scalar}}.
235 '''
236 return self._xyz(self.times(scalar))
238 def __int__(self): # PYCHOK no cover
239 '''Not implemented, see method C{ints}.'''
240 return _NotImplemented(self) # must return C{int}
242 def __ipow__(self, scalar, *mod): # PYCHOK no cover
243 '''Raise each component I{in-place} as C{pow(C, B{scalar})}.
245 @arg scalar: Exponent (C{scalar}).
247 @return: Power (L{Vector3d}).
249 @raise TypeError: Non-scalar B{C{scalar}}.
250 '''
251 return self._xyz(self.pow(scalar, *mod))
253 def __isub__(self, other):
254 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.
256 @arg other: The other vector (L{Vector3d}).
258 @raise TypeError: Incompatible B{C{other}} C{type}.
259 '''
260 return self._xyz(self.minus(other))
262# def __iter__(self):
263# '''Return an C{iter}ator over this vector's components.
264# '''
265# return iter(self.xyz3)
267 def __itruediv__(self, scalar):
268 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.
270 @arg scalar: The divisor (C{scalar}).
272 @raise TypeError: Non-scalar B{C{scalar}}.
273 '''
274 return self._xyz(self.dividedBy(scalar))
276 def __le__(self, other): # Python 3+
277 '''Is this vector shorter than or equal to an other vector?
279 @arg other: The other vector (L{Vector3d}).
281 @return: C{True} if so, C{False} otherwise.
283 @raise TypeError: Incompatible B{C{other}} C{type}.
284 '''
285 return self.length <= self._other_cmp(other)
287# def __len__(self):
288# '''Return C{3}, always.
289# '''
290# return len(self.xyz)
292 def __lt__(self, other): # Python 3+
293 '''Is this vector shorter than an other vector?
295 @arg other: The other vector (L{Vector3d}).
297 @return: C{True} if so, C{False} otherwise.
299 @raise TypeError: Incompatible B{C{other}} C{type}.
300 '''
301 return self.length < self._other_cmp(other)
303 def __matmul__(self, other): # PYCHOK Python 3.5+
304 '''Compute the cross product of this and an other vector, C{this @ B{other}}.
306 @arg other: The other vector (L{Vector3d}).
308 @return: Cross product (L{Vector3d}).
310 @raise TypeError: Incompatible B{C{other}} C{type}.
311 '''
312 return self.cross(other)
314 def __mod__(self, modulus): # PYCHOK no cover
315 '''Apply C{scalar} modulo to this vector's components.
317 @arg modulus: Modulus (C{scalar}).
319 @return: Modulo (L{Vector3d}).
320 '''
321 m = Scalar(modulus=modulus)
323 def _mod(x):
324 return x % m
326 return self._mapped(_mod)
328 def __mul__(self, scalar):
329 '''Multiply this vector by a scalar, C{this * B{scalar}}.
331 @arg scalar: Factor (C{scalar}).
333 @return: Product (L{Vector3d}).
334 '''
335 return self.times(scalar)
337 def __ne__(self, other):
338 '''Is this vector not equal to an other vector?
340 @arg other: The other vector (L{Vector3d}).
342 @return: C{True} if so, C{False} otherwise.
344 @raise TypeError: Incompatible B{C{other}} C{type}.
345 '''
346 return not self.isequalTo(other, eps=EPS0)
348 def __neg__(self):
349 '''Return the opposite of this vector.
351 @return: A negated copy (L{Vector3d})
352 '''
353 return self.negate()
355 def __pos__(self): # PYCHOK no cover
356 '''Return this vector I{as-is} or a copy.
358 @return: This instance (L{Vector3d})
359 '''
360 return self if _pos_self else self.copy()
362 def __pow__(self, scalar, *mod): # PYCHOK no cover
363 '''Return a vector with components as C{pow(C, B{scalar})}.
365 @arg scalar: Exponent (C{scalar}).
367 @return: Power (L{Vector3d}).
369 @raise TypeError: Non-scalar B{C{scalar}}.
370 '''
371 return self.pow(scalar, *mod)
373 def __radd__(self, other): # PYCHOK no cover
374 '''Add this vector to an other vector, C{B{other} + this}.
376 @arg other: The other vector (L{Vector3d}).
378 @return: Sum (L{Vector3d}).
380 @raise TypeError: Incompatible B{C{other}} C{type}.
381 '''
382 return self.others(other).plus(self)
384 def __rdivmod__(self, other):
385 '''Not implemented.'''
386 return _NotImplemented(self, other)
388# def __repr__(self):
389# '''Return the default C{repr(this)}.
390# '''
391# return self.toRepr()
393 def __rfloordiv__(self, other):
394 '''Not implemented.'''
395 return _NotImplemented(self, other)
397 def __rmatmul__(self, other): # PYCHOK Python 3.5+
398 '''Compute the cross product of an other and this vector, C{B{other} @ this}.
400 @arg other: The other vector (L{Vector3d}).
402 @return: Cross product (L{Vector3d}).
404 @raise TypeError: Incompatible B{C{other}} C{type}.
405 '''
406 return self.others(other).cross(self)
408 def __rmod__(self, other):
409 '''Not implemented.'''
410 return _NotImplemented(self, other)
412 __rmul__ = __mul__ # scalar other
414 def __round__(self, *ndigits): # PYCHOK no cover
415 '''Return a vector with these components C{rounded}.
417 @arg ndigits: Optional number of digits (C{int}).
419 @return: Rounded (L{Vector3d}).
420 '''
421 def _rnd(x):
422 return round(x, *ndigits)
424 # <https://docs.Python.org/3.12/reference/datamodel.html?#object.__round__>
425 return self._mapped(_rnd)
427 def __rpow__(self, other, *mod): # PYCHOK no cover
428 '''Not implemented.'''
429 return _NotImplemented(self, other, *mod)
431 def __rsub__(self, other): # PYCHOK no cover
432 '''Subtract this vector from an other vector, C{B{other} - this}.
434 @arg other: The other vector (L{Vector3d}).
436 @return: Difference (L{Vector3d}).
438 @raise TypeError: Incompatible B{C{other}} C{type}.
439 '''
440 return self.others(other).minus(self)
442 def __rtruediv__(self, other):
443 '''Not implemented.'''
444 return _NotImplemented(self, other)
446# def __str__(self):
447# '''Return the default C{str(self)}.
448# '''
449# return self.toStr()
451 def __sub__(self, other):
452 '''Subtract an other vector from this vector, C{this - B{other}}.
454 @arg other: The other vector (L{Vector3d}).
456 @return: Difference (L{Vector3d}).
458 @raise TypeError: Incompatible B{C{other}} C{type}.
459 '''
460 return self.minus(other)
462 def __truediv__(self, scalar):
463 '''Divide this vector by a scalar, C{this / B{scalar}}.
465 @arg scalar: The divisor (C{scalar}).
467 @return: Quotient (L{Vector3d}).
469 @raise TypeError: Non-scalar B{C{scalar}}.
470 '''
471 return self.dividedBy(scalar)
473 def __trunc__(self): # PYCHOK no cover
474 '''Return a vector with these components C{trunc}ated.
476 @return: Trunc-ed (L{Vector3d}).
477 '''
478 return self._mapped(ftruncate)
480 if _MODS.sys_version_info2 < (3, 0): # PYCHOK no cover
481 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
482 __div__ = __truediv__
483 __idiv__ = __itruediv__
484 __long__ = __int__
485 __nonzero__ = __bool__
486 __rdiv__ = __rtruediv__
488 def angleTo(self, other, vSign=None, wrap=False):
489 '''Compute the angle between this and an other vector.
491 @arg other: The other vector (L{Vector3d}).
492 @kwarg vSign: Optional vector, if supplied (and out of the
493 plane of this and the other), angle is signed
494 positive if this->other is clockwise looking
495 along vSign or negative in opposite direction,
496 otherwise angle is unsigned.
497 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).
499 @return: Angle (C{radians}).
501 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.
502 '''
503 x = self.cross(other)
504 s = x.length # PYLINT E1101
505 # use vSign as reference to set sign of s
506 if s and vSign and x.dot(vSign) < 0: # PYLINT E1101
507 s = -s
509 a = atan2(s, self.dot(other))
510 if wrap and fabs(a) > PI:
511 a -= _copysign(PI2, a)
512 return a
514 def apply(self, fun2, other_x, *y_z, **fun2_kwds):
515 '''Apply a 2-argument function pairwise to the components
516 of this and an other vector.
518 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),
519 return a C{scalar} or L{INT0} result.
520 @arg other_x: Other X component (C{scalar}) or a vector
521 with X, Y and Z components (C{Cartesian},
522 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
523 L{Vector3Tuple} or L{Vector4Tuple}).
524 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).
525 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.
527 @return: New, applied vector (L{Vector3d}).
529 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
530 '''
531 _xcallable(fun2=fun2)
532 if fun2_kwds:
533 def _f2(a, b):
534 return fun2(a, b, **fun2_kwds)
535 else:
536 _f2 = fun2
538 xyz = _xyz3(self.apply, other_x, *y_z)
539 xyz = (_f2(a, b) for a, b in _zip(self.xyz3, xyz)) # strict=True
540 return self.classof(*xyz)
542 def bools(self):
543 '''Return the vector with C{bool} components.
544 '''
545 return self._mapped(bool)
547 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN
548 '''Compute the cross product of this and an other vector.
550 @arg other: The other vector (L{Vector3d}).
551 @kwarg raiser: Optional, L{CrossError} label if raised (C{str}, non-L{NN}).
552 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as C{x}, C{y} and
553 C{z}.
555 @return: Cross product (L{Vector3d}).
557 @raise CrossError: Zero or near-zero cross product and if B{C{raiser}} and
558 L{crosserrors<pygeodesy.crosserrors>} are both C{True}.
560 @raise TypeError: Incompatible B{C{other}} C{type}.
561 '''
562 X, Y, Z = self.others(other).xyz3
563 x, y, z = self.xyz3
564 xyz = (fdot_(y, Z, -z, Y),
565 fdot_(z, X, -x, Z),
566 fdot_(x, Y, -y, X))
568 if raiser and self.crosserrors and eps0 > 0 \
569 and max(map(fabs, xyz)) < eps0:
570 r = other._fromll or other
571 s = self._fromll or self
572 t = self.isequalTo(other, eps=eps0)
573 t = _coincident_ if t else _colinear_
574 raise CrossError(raiser, s, other=r, txt=t)
576 return self.classof(*xyz) # name__=self.cross
578 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')
579 def crosserrors(self):
580 '''Get L{CrossError} exceptions (C{bool}).
581 '''
582 return self._crosserrors
584 @crosserrors.setter # PYCHOK setter!
585 def crosserrors(self, raiser):
586 '''Raise or ignore L{CrossError} exceptions (C{bool}).
587 '''
588 self._crosserrors = bool(raiser)
590 def dividedBy(self, scalar):
591 '''Divide this vector by a scalar.
593 @arg scalar: The divisor (C{scalar}).
595 @return: New, scaled vector (L{Vector3d}).
597 @raise TypeError: Non-scalar B{C{scalar}}.
599 @raise VectorError: Invalid or zero B{C{scalar}}.
600 '''
601 d = Scalar(divisor=scalar)
602 try:
603 return self._times(_1_0 / d)
604 except (ValueError, ZeroDivisionError) as x:
605 raise VectorError(divisor=scalar, cause=x)
607 def dividedBy_(self, scalar_x, *y_z):
608 '''Divide this vector by separate X, Y and Z divisors.
610 @arg scalar_x: X divisor (C{scalar}) or a vector's
611 X, Y, and Z components as divisors
612 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
613 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
614 @arg y_z: Y and Z divisors (C{scalar}, C{scalar}), ignored
615 if B{C{scalar_x}} is not C{scalar}.
617 @return: New, scaled vector (L{Vector3d}).
619 @raise VectorError: Invalid B{C{scalar_x}} or B{C{y_z}}.
620 '''
621 x, y, z = _xyz3(self.dividedBy_, scalar_x, *y_z)
622 return self.classof(self.x / x, self.y / y, self.z / z)
624 def dot(self, other):
625 '''Compute the dot (scalar) product of this and an other vector.
627 @arg other: The other vector (L{Vector3d}).
629 @return: Dot product (C{float}).
631 @raise TypeError: Incompatible B{C{other}} C{type}.
632 '''
633 return self.length2 if other is self else fdot(
634 self.xyz3, *self.others(other).xyz3)
636 @deprecated_method
637 def equals(self, other, units=False): # PYCHOK no cover
638 '''DEPRECATED, use method C{isequalTo}.
639 '''
640 return self.isequalTo(other, units=units)
642 @Property_RO
643 def euclid(self):
644 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).
646 @see: Properties C{length} and C{length2} and function
647 L{pygeodesy.euclid_}.
648 '''
649 return Float(euclid=euclid_(*self.xyz3))
651 def equirectangular(self, other):
652 '''I{Approximate} the difference between this and an other vector.
654 @arg other: Vector to subtract (C{Vector3dBase}).
656 @return: The length I{squared} of the difference (C{Float}).
658 @raise TypeError: Incompatible B{C{other}} C{type}.
660 @see: Property C{length2}.
661 '''
662 d = self.minus(other)
663 return Float(equirectangular=hypot2_(*d.xyz3)) # PYLINT E1101
665 def fabs(self):
666 '''Return the vector with C{fabs} components.
667 '''
668 return self._mapped(fabs)
670 def floats(self):
671 '''Return the vector with C{float} components.
672 '''
673 return self._mapped(_float0)
675 def floorDividedBy(self, scalar):
676 '''Floor-divide this vector by a scalar.
678 @arg scalar: The divisor (C{scalar}).
680 @return: New, scaled vector (L{Vector3d}).
682 @raise TypeError: Non-scalar B{C{scalar}}.
684 @raise VectorError: Invalid or zero B{C{scalar}}.
685 '''
686 d = Scalar(divisor=scalar)
687 try:
688 def _floor_d(x):
689 return x // d
691 return self._mapped(_floor_d)
692 except (ValueError, ZeroDivisionError) as x:
693 raise VectorError(divisor=scalar, cause=x)
695 def floorDividedBy_(self, scalar_x, *y_z):
696 '''Floor-divide this vector by separate X, Y and Z divisors.
698 @arg scalar_x: X divisor (C{scalar}) or a vector's
699 X, Y, and Z components as divisors
700 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
701 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
702 @arg y_z: Y and Z divisors (C{scalar}, C{scalar}), ignored
703 if B{C{scalar_x}} is not C{scalar}.
705 @return: New, scaled vector (L{Vector3d}).
707 @raise VectorError: Invalid B{C{scalar_x}} or B{C{y_z}}.
708 '''
709 x, y, z = _xyz3(self.floorDividedBy_, scalar_x, *y_z)
710 return self.classof(self.x // x, self.y // y, self.z // z)
712 @Property
713 def _fromll(self):
714 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.
715 '''
716 return self._ll
718 @_fromll.setter # PYCHOK setter!
719 def _fromll(self, ll):
720 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.
721 '''
722 self._ll = ll or None
724 @property_RO
725 def homogeneous(self):
726 '''Get this vector's homogeneous representation (L{Vector3d}).
727 '''
728 x, y, z = self.xyz3
729 if z:
730 x = x / z # /= chokes PyChecker
731 y = y / z
732# z = _1_0
733 else:
734 x = _copysignINF(_flipsign(x, z))
735 y = _copysignINF(_flipsign(y, z))
736# z = NAN
737 return self.classof(x, y, _1_0)
739 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False
740 '''Locate the vector at a given fraction between (or along) this
741 and an other vector.
743 @arg other: The other vector (L{Vector3d}).
744 @arg fraction: Fraction between both vectors (C{scalar},
745 0.0 for this and 1.0 for the other vector).
747 @return: Intermediate vector (L{Vector3d}).
749 @raise TypeError: Incompatible B{C{other}} C{type}.
750 '''
751 f = Scalar(fraction=fraction)
752 if isnear0(f): # PYCHOK no cover
753 r = self
754 else:
755 r = self.others(other)
756 if not isnear1(f): # self * (1 - f) + r * f
757 r = self.plus(r.minus(self)._times(f))
758 return r
760 def ints(self):
761 '''Return the vector with C{int} components.
762 '''
763 return self._mapped(int)
765 def isconjugateTo(self, other, minum=1, eps=EPS):
766 '''Determine whether this and an other vector are conjugates.
768 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},
769 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).
770 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).
771 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),
772 same units as C{x}, C{y}, and C{z}.
774 @return: C{True} if both vector's components either match
775 or at least C{B{minum}} have opposite signs.
777 @raise TypeError: Incompatible B{C{other}} C{type}.
779 @see: Method C{isequalTo}.
780 '''
781 self.others(other)
782 n = 0
783 for a, b in zip(self.xyz3, other.xyz3):
784 if fabs(a + b) < eps and ((a < 0 and b > 0) or
785 (a > 0 and b < 0)):
786 n += 1 # conjugate
787 elif fabs(a - b) > eps:
788 return False # unequal
789 return bool(n >= minum)
791 def isequalTo(self, other, units=False, eps=EPS):
792 '''Check if this and an other vector are equal or equivalent.
794 @arg other: The other vector (L{Vector3d}).
795 @kwarg units: Optionally, compare the normalized, unit
796 version of both vectors.
797 @kwarg eps: Tolerance for equality (C{scalar}), same units as
798 C{x}, C{y}, and C{z}.
800 @return: C{True} if vectors are identical, C{False} otherwise.
802 @raise TypeError: Incompatible B{C{other}} C{type}.
804 @see: Method C{isconjugateTo}.
805 '''
806 if units:
807 self.others(other)
808 d = self.unit().minus(other.unit())
809 else:
810 d = self.minus(other)
811 return max(map(fabs, d.xyz3)) < eps
813 @Property_RO
814 def length(self): # __dict__ value overwritten by Property_RO C{_united}
815 '''Get the length (norm, magnitude) of this vector (C{Float}).
817 @see: Properties L{length2} and L{euclid}.
818 '''
819 return Float(length=hypot_(*self.xyz3))
821 @Property_RO
822 def length2(self): # __dict__ value overwritten by Property_RO C{_united}
823 '''Get the length I{squared} of this vector (C{Float}).
825 @see: Property L{length} and method C{equirectangular}.
826 '''
827 return Float(length2=hypot2_(*self.xyz3))
829 def _mapped(self, func):
830 '''(INTERNAL) Apply C{func} to all components.
831 '''
832 return self.classof(*map2(func, self.xyz3))
834 def minus(self, other):
835 '''Subtract an other vector from this vector.
837 @arg other: The other vector (L{Vector3d}).
839 @return: New vector difference (L{Vector3d}).
841 @raise TypeError: Incompatible B{C{other}} C{type}.
842 '''
843 return self._minus(*self.others(other).xyz3)
845 def _minus(self, x, y, z):
846 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.
847 '''
848 return self.classof(self.x - x, self.y - y, self.z - z)
850 def minus_(self, other_x, *y_z):
851 '''Subtract separate X, Y and Z components from this vector.
853 @arg other_x: X component (C{scalar}) or a vector's
854 X, Y, and Z components (C{Cartesian},
855 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
856 L{Vector3Tuple}, L{Vector4Tuple}).
857 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
858 ignored if B{C{other_x}} is not C{scalar}.
860 @return: New, vectiorial vector (L{Vector3d}).
862 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
863 '''
864 return self._minus(*_xyz3(self.minus_, other_x, *y_z))
866 def negate(self):
867 '''Return the opposite of this vector.
869 @return: A negated copy (L{Vector3d})
870 '''
871 return self.classof(-self.x, -self.y, -self.z)
873 @Property_RO
874 def _N_vector(self):
875 '''(INTERNAL) Get the (C{nvectorBase._N_Vector})
876 '''
877 _N = _MODS.nvectorBase._N_Vector
878 return _N(*self.xyz3, name=self.name)
880 def _other_cmp(self, other):
881 '''(INTERNAL) Return the value for comparison.
882 '''
883 return other if isscalar(other) else self.others(other).length
885 def others(self, *other, **name_other_up):
886 '''Refined class comparison.
888 @arg other: The other vector (L{Vector3d}).
889 @kwarg name_other_up: Overriding C{name=other} and C{up=1}
890 keyword arguments.
892 @return: The B{C{other}} if compatible.
894 @raise TypeError: Incompatible B{C{other}} C{type}.
895 '''
896 other, name, up = _xother3(self, other, **name_other_up)
897 if not isinstance(other, Vector3dBase):
898 _NamedBase.others(self, other, name=name, up=up + 1)
899 return other
901 def plus(self, other):
902 '''Add this vector and an other vector.
904 @arg other: The other vector (L{Vector3d}).
906 @return: Vectorial sum (L{Vector3d}).
908 @raise TypeError: Incompatible B{C{other}} C{type}.
909 '''
910 return self._plus(*self.others(other).xyz3)
912 sum = plus # alternate name
914 def _plus(self, x, y, z):
915 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.
916 '''
917 return self.classof(self.x + x, self.y + y, self.z + z)
919 def plus_(self, other_x, *y_z):
920 '''Sum of this vector and separate X, Y and Z components.
922 @arg other_x: X component (C{scalar}) or a vector's
923 X, Y, and Z components (C{Cartesian},
924 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
925 L{Vector3Tuple}, L{Vector4Tuple}).
926 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
927 ignored if B{C{other_x}} is not C{scalar}.
929 @return: New, vectorial vector (L{Vector3d}).
931 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
932 '''
933 return self._plus(*_xyz3(self.plus_, other_x, *y_z))
935 def pow(self, scalar, *mod):
936 '''Raise each X, Y and Z to C{scalar} power.
938 @arg scalar: Exponent (C{scalar}).
940 @return: Power (L{Vector3d}).
942 @raise TypeError: Non-scalar B{C{scalar}}.
943 '''
944 p = Scalar(power=scalar)
946 def _pow(x):
947 return pow(x, p, *mod)
949 return self._mapped(_pow)
951 def rotate(self, axis, theta, fma=False):
952 '''Rotate this vector around an axis by a specified angle.
954 @arg axis: The axis being rotated around (L{Vector3d}).
955 @arg theta: The angle of rotation (C{radians}).
956 @kwarg fma: If C{True}, use fused-multiply-add (C{bool}).
958 @return: New, rotated vector (L{Vector3d}).
960 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/
961 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and
962 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/
963 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.
964 '''
965 s, c = sincos2(theta) # rotation angle
966 d = _1_0 - c
967 if d or s:
968 p = self.unit().xyz # point being rotated
969 r = self.others(axis=axis).unit() # axis being rotated around
971 ax, ay, az = r.xyz3 # quaternion-derived rotation matrix
972 bx, by, bz = r.times(d).xyz3
973 sx, sy, sz = r.times(s).xyz3
975 if fma:
976 _fma = _MODS.fmath.fma
977 else:
978 def _fma(a, b, c):
979 return a * b + c
981 x = fdot(p, _fma(ax, bx, c), _fma(ax, by, -sz), _fma(ax, bz, sy))
982 y = fdot(p, _fma(ay, bx, sz), _fma(ay, by, c), _fma(ay, bz, -sx))
983 z = fdot(p, _fma(az, bx, -sy), _fma(az, by, sx), _fma(az, bz, c))
984 else: # unrotated
985 x, y, z = self.xyz3
986 return self.classof(x, y, z)
988 @deprecated_method
989 def rotateAround(self, axis, theta):
990 '''DEPRECATED, use method C{rotate}.'''
991 return self.rotate(axis, theta) # PYCHOK no cover
993 def _roty(self, *pos, **name):
994 '''(INTERNAL) Prolate rotation, for C{+1 if pos else -1}.
995 '''
996 v = self.copy(**name)
997 if pos:
998 x, _, z = v.xyz3
999 _update_all(v, needed=3)
1000 v._x, v._z = (-z, x) if pos[0] else (z, -x)
1001 return v
1003 def times(self, factor):
1004 '''Multiply this vector by a scalar.
1006 @arg factor: Scale factor (C{scalar}).
1008 @return: New, scaled vector (L{Vector3d}).
1010 @raise TypeError: Non-scalar B{C{factor}}.
1011 '''
1012 return self._times(Scalar(factor=factor))
1014 def _times(self, s):
1015 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.
1016 '''
1017 return self.classof(self.x * s, self.y * s, self.z * s)
1019 def times_(self, other_x, *y_z):
1020 '''Multiply this vector's components by separate X, Y and Z factors.
1022 @arg other_x: X scale factor (C{scalar}) or a vector's
1023 X, Y, and Z components as scale factors
1024 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
1025 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
1026 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),
1027 ignored if B{C{other_x}} is not C{scalar}.
1029 @return: New, scaled vector (L{Vector3d}).
1031 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
1032 '''
1033 x, y, z = _xyz3(self.times_, other_x, *y_z)
1034 return self.classof(self.x * x, self.y * y, self.z * z)
1036# @deprecated_method
1037# def to2ab(self): # PYCHOK no cover
1038# '''DEPRECATED, use property C{Nvector.philam}.
1039#
1040# @return: A L{PhiLam2Tuple}C{(phi, lam)}.
1041# '''
1042# return _MODS.nvectorBase.n_xyz2philam(self.x, self.y, self.z)
1044# @deprecated_method
1045# def to2ll(self): # PYCHOK no cover
1046# '''DEPRECATED, use property C{Nvector.latlon}.
1047#
1048# @return: A L{LatLon2Tuple}C{(lat, lon)}.
1049# '''
1050# return _MODS.nvectorBase.n_xyz2latlon(self.x, self.y, self.z)
1052 @deprecated_method
1053 def to3xyz(self): # PYCHOK no cover
1054 '''DEPRECATED, use property L{xyz}.
1055 '''
1056 return self.xyz
1058 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected
1059 '''Return a string representation of this vector.
1061 @kwarg prec: Number of decimal places (C{int}).
1062 @kwarg fmt: Enclosing format to use (C{str}).
1063 @kwarg sep: Separator between components (C{str}).
1065 @return: Vector as "(x, y, z)" (C{str}).
1066 '''
1067 t = sep.join(strs(self.xyz3, prec=prec))
1068 return (fmt % (t,)) if fmt else t
1070 def unit(self, ll=None):
1071 '''Normalize this vector to unit length.
1073 @kwarg ll: Optional, original location (C{LatLon}).
1075 @return: Normalized vector (L{Vector3d}).
1076 '''
1077 u = self._united
1078 if ll:
1079 u._fromll = ll
1080 return u
1082 @Property_RO
1083 def _united(self): # __dict__ value overwritten below
1084 '''(INTERNAL) Get normalized vector (L{Vector3d}).
1085 '''
1086 n = self.length
1087 if n > EPS0 and fabs(n - _1_0) > EPS0:
1088 u = self._xnamed(self.dividedBy(n))
1089 u._update(False, length=_1_0, length2=_1_0, _united=u)
1090 else:
1091 u = self.copy()
1092 u._update(False, _united=u)
1093 if self._fromll:
1094 u._fromll = self._fromll
1095 return u
1097 @Property
1098 def x(self):
1099 '''Get the X component (C{float}).
1100 '''
1101 return self._x
1103 @x.setter # PYCHOK setter!
1104 def x(self, x):
1105 '''Set the X component, if different (C{float}).
1106 '''
1107 x = Float(x=x)
1108 if self._x != x:
1109 _update_all(self, needed=3)
1110 self._x = x
1112 @Property
1113 def xyz(self):
1114 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).
1115 '''
1116 return _MODS.namedTuples.Vector3Tuple(*self.xyz3, name=self.name)
1118 @xyz.setter # PYCHOK setter!
1119 def xyz(self, xyz):
1120 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},
1121 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}
1122 or a C{tuple} or C{list} of 3+ C{scalar} items).
1123 '''
1124 self._xyz(xyz)
1126 def _xyz(self, x_xyz, *y_z):
1127 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.
1128 '''
1129 xyz = _xyz3(_xyz_, x_xyz, *y_z)
1130 if self.xyz3 != xyz:
1131 _update_all(self, needed=3)
1132 self._x, self._y, self._z = xyz
1133 return self
1135 @property_RO
1136 def xyz3(self):
1137 '''Get the X, Y and Z components as C{3-tuple}.
1138 '''
1139 return self.x, self.y, self.z
1141 @Property_RO
1142 def x2y2z2(self):
1143 '''Get the X, Y and Z components, I{squared} (L{Vector3Tuple}).
1144 '''
1145 return _MODS.namedTuples.Vector3Tuple(*self.x2y2z23, name=self.name)
1147 @property_RO
1148 def x2y2z23(self):
1149 '''Get the X, Y and Z components, I{squared} (3-tuple C{(x**2, y**2, z**2)}).
1150 '''
1151 return self.x**2, self.y**2, self.z**2
1153 @Property
1154 def y(self):
1155 '''Get the Y component (C{float}).
1156 '''
1157 return self._y
1159 @y.setter # PYCHOK setter!
1160 def y(self, y):
1161 '''Set the Y component, if different (C{float}).
1162 '''
1163 y = Float(y=y)
1164 if self._y != y:
1165 _update_all(self, needed=3)
1166 self._y = y
1168 @Property
1169 def z(self):
1170 '''Get the Z component (C{float}).
1171 '''
1172 return self._z
1174 @z.setter # PYCHOK setter!
1175 def z(self, z):
1176 '''Set the Z component, if different (C{float}).
1177 '''
1178 z = Float(z=z)
1179 if self._z != z:
1180 _update_all(self, needed=3)
1181 self._z = z
1184def _xyz3(where, x_xyz, *y_z): # in .cartesianBase._rtp3
1185 '''(INTERNAL) Get X, Y and Z as 3-tuple C{(x, y, z)}.
1186 '''
1187 try:
1188 xyz3 = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for Vector*Tuple
1189 map2(_float0, x_xyz[:3]) if islistuple(x_xyz, minum=3) else
1190 x_xyz.xyz) # .xyz3
1191 except (AttributeError, TypeError, ValueError) as x:
1192 raise _xError(x, unstr(where, x_xyz, *y_z))
1193 return xyz3
1196__all__ += _ALL_DOCS(Vector3dBase)
1198# **) MIT License
1199#
1200# Copyright (C) 2016-2027 -- mrJean1 at Gmail -- All Rights Reserved.
1201#
1202# Permission is hereby granted, free of charge, to any person obtaining a
1203# copy of this software and associated documentation files (the "Software"),
1204# to deal in the Software without restriction, including without limitation
1205# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1206# and/or sell copies of the Software, and to permit persons to whom the
1207# Software is furnished to do so, subject to the following conditions:
1208#
1209# The above copyright notice and this permission notice shall be included
1210# in all copies or substantial portions of the Software.
1211#
1212# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1213# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1214# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1215# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1216# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1217# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1218# OTHER DEALINGS IN THE SOFTWARE.