Coverage for pygeodesy/vector3dBase.py: 91%

288 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-09-09 13:03 -0400

1 

2# -*- coding: utf-8 -*- 

3 

4u'''(INTERNAL) Private, 3-D vector base class C{Vector3dBase}. 

5 

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''' 

10 

11from pygeodesy.basics import _copysign, islistuple, isscalar, \ 

12 map1, map2, _signOf, _zip 

13from pygeodesy.constants import EPS, EPS0, INT0, PI, PI2, \ 

14 _1_0, isnear0, isnear1, isneg0, \ 

15 _copysignINF, _float0, _pos_self 

16from pygeodesy.errors import CrossError, VectorError, _xcallable, _xError 

17from pygeodesy.fmath import euclid_, fdot, fdot_, hypot_, hypot2_ # _MODS.fmath.fma 

18from pygeodesy.interns import _coincident_, _colinear_, _COMMASPACE_, _xyz_ 

19from pygeodesy.lazily import _ALL_LAZY, _ALL_DOCS, _ALL_MODS as _MODS 

20from pygeodesy.named import _NamedBase, _NotImplemented, _xother3 

21# from pygeodesy.namedTuples import Vector3Tuple # _MODS 

22# from pygeodesy.nvectorBase import _N_Vector # _MODS 

23from pygeodesy.props import deprecated_method, Property, Property_RO, \ 

24 property_doc_, property_RO, _update_all 

25from pygeodesy.streprs import Fmt, strs, unstr 

26from pygeodesy.units import Float, Scalar 

27from pygeodesy.utily import atan2, sincos2, fabs 

28 

29from math import ceil as _ceil, floor as _floor, trunc as _trunc 

30 

31__all__ = _ALL_LAZY.vector3dBase 

32__version__ = '25.08.31' 

33 

34 

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 

39 

40 _ll = None # original latlon, '_fromll' 

41# _x = INT0 # X component 

42# _y = INT0 # Y component 

43# _z = INT0 # Z component 

44 

45 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, **name): 

46 '''New L{Vector3d} or C{Vector3dBase} instance. 

47 

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. 

51 

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}). 

62 

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 

73 

74 def __abs__(self): 

75 '''Return the norm of this vector. 

76 

77 @return: Norm, unit length (C{float}); 

78 ''' 

79 return self.length 

80 

81 def __add__(self, other): 

82 '''Add this to an other vector (L{Vector3d}). 

83 

84 @return: Vectorial sum (L{Vector3d}). 

85 

86 @raise TypeError: Incompatible B{C{other}} C{type}. 

87 ''' 

88 return self.plus(other) 

89 

90 def __bool__(self): # PYCHOK PyChecker 

91 '''Is this vector non-zero? 

92 

93 @see: Method C{bools}. 

94 ''' 

95 return bool(self.x or self.y or self.z) 

96 

97 def __ceil__(self): # PYCHOK no cover 

98 '''Return a vector with the C{ceil} of these components. 

99 

100 @return: Ceil-ed (L{Vector3d}). 

101 ''' 

102 return self._mapped(_ceil) 

103 

104 def __cmp__(self, other): # Python 2- 

105 '''Compare this and an other vector (L{Vector3d}). 

106 

107 @return: -1, 0 or +1 (C{int}). 

108 

109 @raise TypeError: Incompatible B{C{other}} C{type}. 

110 ''' 

111 return _signOf(self.length, self._other_cmp(other)) 

112 

113 cmp = __cmp__ 

114 

115 def __divmod__(self, other): # PYCHOK no cover 

116 '''Not implemented.''' 

117 return _NotImplemented(self, other) 

118 

119 def __eq__(self, other): 

120 '''Is this vector equal to an other vector? 

121 

122 @arg other: The other vector (L{Vector3d}). 

123 

124 @return: C{True} if equal, C{False} otherwise. 

125 

126 @raise TypeError: Incompatible B{C{other}} C{type}. 

127 ''' 

128 return self.isequalTo(other, eps=EPS0) 

129 

130 def __float__(self): # PYCHOK no cover 

131 '''Not implemented, see method C{float}.''' 

132 return _NotImplemented(self) # must return C{float} 

133 

134 def __floor__(self): # PYCHOK no cover 

135 '''Return a vector with the C{floor} of these components. 

136 

137 @return: Floor-ed (L{Vector3d}). 

138 ''' 

139 return self._mapped(_floor) 

140 

141 def __floordiv__(self, other): # PYCHOK no cover 

142 '''Not implemented.''' 

143 return _NotImplemented(self, other) 

144 

145 def __ge__(self, other): 

146 '''Is this vector longer than or equal to an other vector? 

147 

148 @arg other: The other vector (L{Vector3d}). 

149 

150 @return: C{True} if so, C{False} otherwise. 

151 

152 @raise TypeError: Incompatible B{C{other}} C{type}. 

153 ''' 

154 return self.length >= self._other_cmp(other) 

155 

156# def __getitem__(self, key): 

157# '''Return C{item} at index or slice C{[B{key}]}. 

158# ''' 

159# return self.xyz[key] 

160 

161 def __gt__(self, other): 

162 '''Is this vector longer than an other vector? 

163 

164 @arg other: The other vector (L{Vector3d}). 

165 

166 @return: C{True} if so, C{False} otherwise. 

167 

168 @raise TypeError: Incompatible B{C{other}} C{type}. 

169 ''' 

170 return self.length > self._other_cmp(other) 

171 

172 def __hash__(self): # PYCHOK no cover 

173 '''Return this instance' C{hash}. 

174 ''' 

175 return hash(self.xyz) # XXX id(self)? 

176 

177 def __iadd__(self, other): 

178 '''Add this and an other vector I{in-place}, C{this += B{other}}. 

179 

180 @arg other: The other vector (L{Vector3d}). 

181 

182 @raise TypeError: Incompatible B{C{other}} C{type}. 

183 ''' 

184 return self._xyz(self.plus(other)) 

185 

186 def __ifloordiv__(self, other): # PYCHOK no cover 

187 '''Not implemented.''' 

188 return _NotImplemented(self, other) 

189 

190 def __imatmul__(self, other): # PYCHOK Python 3.5+ 

191 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}. 

192 

193 @arg other: The other vector (L{Vector3d}). 

194 

195 @raise TypeError: Incompatible B{C{other}} C{type}. 

196 

197 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 397+, 2022 p. 578+. 

198 ''' 

199 return self._xyz(self.cross(other)) 

200 

201 def __imod__(self, other): # PYCHOK no cover 

202 '''Not implemented.''' 

203 return _NotImplemented(self, other) 

204 

205 def __imul__(self, scalar): 

206 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}. 

207 

208 @arg scalar: Factor (C{scalar}). 

209 

210 @raise TypeError: Non-scalar B{C{scalar}}. 

211 ''' 

212 return self._xyz(self.times(scalar)) 

213 

214 def __int__(self): # PYCHOK no cover 

215 '''Not implemented, see method C{ints}.''' 

216 return _NotImplemented(self) # must return C{int} 

217 

218 def __ipow__(self, other, *mod): # PYCHOK no cover 

219 '''Not implemented.''' 

220 return _NotImplemented(self, other, *mod) 

221 

222 def __isub__(self, other): 

223 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}. 

224 

225 @arg other: The other vector (L{Vector3d}). 

226 

227 @raise TypeError: Incompatible B{C{other}} C{type}. 

228 ''' 

229 return self._xyz(self.minus(other)) 

230 

231# def __iter__(self): 

232# '''Return an C{iter}ator over this vector's components. 

233# ''' 

234# return iter(self.xyz3) 

235 

236 def __itruediv__(self, scalar): 

237 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}. 

238 

239 @arg scalar: The divisor (C{scalar}). 

240 

241 @raise TypeError: Non-scalar B{C{scalar}}. 

242 ''' 

243 return self._xyz(self.dividedBy(scalar)) 

244 

245 def __le__(self, other): # Python 3+ 

246 '''Is this vector shorter than or equal to an other vector? 

247 

248 @arg other: The other vector (L{Vector3d}). 

249 

250 @return: C{True} if so, C{False} otherwise. 

251 

252 @raise TypeError: Incompatible B{C{other}} C{type}. 

253 ''' 

254 return self.length <= self._other_cmp(other) 

255 

256# def __len__(self): 

257# '''Return C{3}, always. 

258# ''' 

259# return len(self.xyz) 

260 

261 def __lt__(self, other): # Python 3+ 

262 '''Is this vector shorter than an other vector? 

263 

264 @arg other: The other vector (L{Vector3d}). 

265 

266 @return: C{True} if so, C{False} otherwise. 

267 

268 @raise TypeError: Incompatible B{C{other}} C{type}. 

269 ''' 

270 return self.length < self._other_cmp(other) 

271 

272 def __matmul__(self, other): # PYCHOK Python 3.5+ 

273 '''Compute the cross product of this and an other vector, C{this @ B{other}}. 

274 

275 @arg other: The other vector (L{Vector3d}). 

276 

277 @return: Cross product (L{Vector3d}). 

278 

279 @raise TypeError: Incompatible B{C{other}} C{type}. 

280 ''' 

281 return self.cross(other) 

282 

283 def __mod__(self, other): # PYCHOK no cover 

284 '''Not implemented.''' 

285 return _NotImplemented(self, other) 

286 

287 def __mul__(self, scalar): 

288 '''Multiply this vector by a scalar, C{this * B{scalar}}. 

289 

290 @arg scalar: Factor (C{scalar}). 

291 

292 @return: Product (L{Vector3d}). 

293 ''' 

294 return self.times(scalar) 

295 

296 def __ne__(self, other): 

297 '''Is this vector not equal to an other vector? 

298 

299 @arg other: The other vector (L{Vector3d}). 

300 

301 @return: C{True} if so, C{False} otherwise. 

302 

303 @raise TypeError: Incompatible B{C{other}} C{type}. 

304 ''' 

305 return not self.isequalTo(other, eps=EPS0) 

306 

307 def __neg__(self): 

308 '''Return the opposite of this vector. 

309 

310 @return: A negated copy (L{Vector3d}) 

311 ''' 

312 return self.negate() 

313 

314 def __pos__(self): # PYCHOK no cover 

315 '''Return this vector I{as-is} or a copy. 

316 

317 @return: This instance (L{Vector3d}) 

318 ''' 

319 return self if _pos_self else self.copy() 

320 

321 def __pow__(self, other, *mod): # PYCHOK no cover 

322 '''Not implemented.''' 

323 return _NotImplemented(self, other, *mod) 

324 

325 __radd__ = __add__ # PYCHOK no cover 

326 

327 def __rdivmod__ (self, other): # PYCHOK no cover 

328 '''Not implemented.''' 

329 return _NotImplemented(self, other) 

330 

331# def __repr__(self): 

332# '''Return the default C{repr(this)}. 

333# ''' 

334# return self.toRepr() 

335 

336 def __rfloordiv__(self, other): # PYCHOK no cover 

337 '''Not implemented.''' 

338 return _NotImplemented(self, other) 

339 

340 def __rmatmul__(self, other): # PYCHOK Python 3.5+ 

341 '''Compute the cross product of an other and this vector, C{B{other} @ this}. 

342 

343 @arg other: The other vector (L{Vector3d}). 

344 

345 @return: Cross product (L{Vector3d}). 

346 

347 @raise TypeError: Incompatible B{C{other}} C{type}. 

348 ''' 

349 return self.others(other).cross(self) 

350 

351 def __rmod__(self, other): # PYCHOK no cover 

352 '''Not implemented.''' 

353 return _NotImplemented(self, other) 

354 

355 __rmul__ = __mul__ 

356 

357 def __round__(self, *ndigits): # PYCHOK no cover 

358 '''Return a vector with these components C{rounded}. 

359 

360 @arg ndigits: Optional number of digits (C{int}). 

361 

362 @return: Rounded (L{Vector3d}). 

363 ''' 

364 # <https://docs.Python.org/3.12/reference/datamodel.html?#object.__round__> 

365 return self.classof(*(round(_, *ndigits) for _ in self.xyz3)) 

366 

367 def __rpow__(self, other, *mod): # PYCHOK no cover 

368 '''Not implemented.''' 

369 return _NotImplemented(self, other, *mod) 

370 

371 def __rsub__(self, other): # PYCHOK no cover 

372 '''Subtract this vector from an other vector, C{B{other} - this}. 

373 

374 @arg other: The other vector (L{Vector3d}). 

375 

376 @return: Difference (L{Vector3d}). 

377 

378 @raise TypeError: Incompatible B{C{other}} C{type}. 

379 ''' 

380 return self.others(other).minus(self) 

381 

382 def __rtruediv__(self, scalar): # PYCHOK no cover 

383 '''Not implemented.''' 

384 return _NotImplemented(self, scalar) 

385 

386# def __str__(self): 

387# '''Return the default C{str(self)}. 

388# ''' 

389# return self.toStr() 

390 

391 def __sub__(self, other): 

392 '''Subtract an other vector from this vector, C{this - B{other}}. 

393 

394 @arg other: The other vector (L{Vector3d}). 

395 

396 @return: Difference (L{Vector3d}). 

397 

398 @raise TypeError: Incompatible B{C{other}} C{type}. 

399 ''' 

400 return self.minus(other) 

401 

402 def __truediv__(self, scalar): 

403 '''Divide this vector by a scalar, C{this / B{scalar}}. 

404 

405 @arg scalar: The divisor (C{scalar}). 

406 

407 @return: Quotient (L{Vector3d}). 

408 

409 @raise TypeError: Non-scalar B{C{scalar}}. 

410 ''' 

411 return self.dividedBy(scalar) 

412 

413 def __trunc__(self): # PYCHOK no cover 

414 '''Return a vector with the C{trunc} of these components. 

415 

416 @return: Trunc-ed (L{Vector3d}). 

417 ''' 

418 return self._mapped(_trunc) 

419 

420 if _MODS.sys_version_info2 < (3, 0): # PYCHOK no cover 

421 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions> 

422 __div__ = __truediv__ 

423 __idiv__ = __itruediv__ 

424 __long__ = __int__ 

425 __nonzero__ = __bool__ 

426 __rdiv__ = __rtruediv__ 

427 

428 def angleTo(self, other, vSign=None, wrap=False): 

429 '''Compute the angle between this and an other vector. 

430 

431 @arg other: The other vector (L{Vector3d}). 

432 @kwarg vSign: Optional vector, if supplied (and out of the 

433 plane of this and the other), angle is signed 

434 positive if this->other is clockwise looking 

435 along vSign or negative in opposite direction, 

436 otherwise angle is unsigned. 

437 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}). 

438 

439 @return: Angle (C{radians}). 

440 

441 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}. 

442 ''' 

443 x = self.cross(other) 

444 s = x.length 

445 # use vSign as reference to set sign of s 

446 if s and vSign and x.dot(vSign) < 0: 

447 s = -s 

448 

449 a = atan2(s, self.dot(other)) 

450 if wrap and fabs(a) > PI: 

451 a -= _copysign(PI2, a) 

452 return a 

453 

454 def apply(self, fun2, other_x, *y_z, **fun2_kwds): 

455 '''Apply a 2-argument function pairwise to the components 

456 of this and an other vector. 

457 

458 @arg fun2: 2-Argument callable (C{any(scalar, scalar}), 

459 return a C{scalar} or L{INT0} result. 

460 @arg other_x: Other X component (C{scalar}) or a vector 

461 with X, Y and Z components (C{Cartesian}, 

462 L{Ecef9Tuple}, C{Nvector}, L{Vector3d}, 

463 L{Vector3Tuple} or L{Vector4Tuple}). 

464 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}). 

465 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}. 

466 

467 @return: New, applied vector (L{Vector3d}). 

468 

469 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}. 

470 ''' 

471 _xcallable(fun2=fun2) 

472 if fun2_kwds: 

473 def _f2(a, b): 

474 return fun2(a, b, **fun2_kwds) 

475 else: 

476 _f2 = fun2 

477 

478 xyz = _xyz3(self.apply, other_x, *y_z) 

479 xyz = (_f2(a, b) for a, b in _zip(self.xyz3, xyz)) # strict=True 

480 return self.classof(*xyz) 

481 

482 def bools(self): 

483 '''Return the vector with C{bool} components. 

484 ''' 

485 return self._mapped(bool) 

486 

487 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN 

488 '''Compute the cross product of this and an other vector. 

489 

490 @arg other: The other vector (L{Vector3d}). 

491 @kwarg raiser: Optional, L{CrossError} label if raised (C{str}, non-L{NN}). 

492 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as C{x}, C{y} and 

493 C{z}. 

494 

495 @return: Cross product (L{Vector3d}). 

496 

497 @raise CrossError: Zero or near-zero cross product and if B{C{raiser}} and 

498 L{crosserrors<pygeodesy.crosserrors>} are both C{True}. 

499 

500 @raise TypeError: Incompatible B{C{other}} C{type}. 

501 ''' 

502 X, Y, Z = self.others(other).xyz3 

503 x, y, z = self.xyz3 

504 xyz = (fdot_(y, Z, -z, Y), 

505 fdot_(z, X, -x, Z), 

506 fdot_(x, Y, -y, X)) 

507 

508 if raiser and self.crosserrors and eps0 > 0 \ 

509 and max(map(fabs, xyz)) < eps0: 

510 r = other._fromll or other 

511 s = self._fromll or self 

512 t = self.isequalTo(other, eps=eps0) 

513 t = _coincident_ if t else _colinear_ 

514 raise CrossError(raiser, s, other=r, txt=t) 

515 

516 return self.classof(*xyz) # name__=self.cross 

517 

518 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''') 

519 def crosserrors(self): 

520 '''Get L{CrossError} exceptions (C{bool}). 

521 ''' 

522 return self._crosserrors 

523 

524 @crosserrors.setter # PYCHOK setter! 

525 def crosserrors(self, raiser): 

526 '''Raise or ignore L{CrossError} exceptions (C{bool}). 

527 ''' 

528 self._crosserrors = bool(raiser) 

529 

530 def dividedBy(self, divisor): 

531 '''Divide this vector by a scalar. 

532 

533 @arg divisor: The divisor (C{scalar}). 

534 

535 @return: New, scaled vector (L{Vector3d}). 

536 

537 @raise TypeError: Non-scalar B{C{divisor}}. 

538 

539 @raise VectorError: Invalid or zero B{C{divisor}}. 

540 ''' 

541 d = Scalar(divisor=divisor) 

542 try: 

543 return self._times(_1_0 / d) 

544 except (ValueError, ZeroDivisionError) as x: 

545 raise VectorError(divisor=divisor, cause=x) 

546 

547 def dot(self, other): 

548 '''Compute the dot (scalar) product of this and an other vector. 

549 

550 @arg other: The other vector (L{Vector3d}). 

551 

552 @return: Dot product (C{float}). 

553 

554 @raise TypeError: Incompatible B{C{other}} C{type}. 

555 ''' 

556 return self.length2 if other is self else fdot( 

557 self.xyz3, *self.others(other).xyz3) 

558 

559 @deprecated_method 

560 def equals(self, other, units=False): # PYCHOK no cover 

561 '''DEPRECATED, use method C{isequalTo}. 

562 ''' 

563 return self.isequalTo(other, units=units) 

564 

565 @Property_RO 

566 def euclid(self): 

567 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}). 

568 

569 @see: Properties C{length} and C{length2} and function 

570 L{pygeodesy.euclid_}. 

571 ''' 

572 return Float(euclid=euclid_(*self.xyz3)) 

573 

574 def equirectangular(self, other): 

575 '''I{Approximate} the difference between this and an other vector. 

576 

577 @arg other: Vector to subtract (C{Vector3dBase}). 

578 

579 @return: The length I{squared} of the difference (C{Float}). 

580 

581 @raise TypeError: Incompatible B{C{other}} C{type}. 

582 

583 @see: Property C{length2}. 

584 ''' 

585 d = self.minus(other) 

586 return Float(equirectangular=hypot2_(*d.xyz3)) 

587 

588 def fabs(self): 

589 '''Return the vector with C{fabs} components. 

590 ''' 

591 return self._mapped(fabs) 

592 

593 def floats(self): 

594 '''Return the vector with C{float} components. 

595 ''' 

596 return self._mapped(_float0) 

597 

598 @Property 

599 def _fromll(self): 

600 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}. 

601 ''' 

602 return self._ll 

603 

604 @_fromll.setter # PYCHOK setter! 

605 def _fromll(self, ll): 

606 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}. 

607 ''' 

608 self._ll = ll or None 

609 

610 @property_RO 

611 def homogeneous(self): 

612 '''Get this vector's homogeneous representation (L{Vector3d}). 

613 ''' 

614 x, y, z = self.xyz3 

615 if z: 

616 x = x / z # /= chokes PyChecker 

617 y = y / z 

618# z = _1_0 

619 else: 

620 if isneg0(z): 

621 x = -x 

622 y = -y 

623 x = _copysignINF(x) 

624 y = _copysignINF(y) 

625# z = NAN 

626 return self.classof(x, y, _1_0) 

627 

628 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False 

629 '''Locate the vector at a given fraction between (or along) this 

630 and an other vector. 

631 

632 @arg other: The other vector (L{Vector3d}). 

633 @arg fraction: Fraction between both vectors (C{scalar}, 

634 0.0 for this and 1.0 for the other vector). 

635 

636 @return: Intermediate vector (L{Vector3d}). 

637 

638 @raise TypeError: Incompatible B{C{other}} C{type}. 

639 ''' 

640 f = Scalar(fraction=fraction) 

641 if isnear0(f): # PYCHOK no cover 

642 r = self 

643 else: 

644 r = self.others(other) 

645 if not isnear1(f): # self * (1 - f) + r * f 

646 r = self.plus(r.minus(self)._times(f)) 

647 return r 

648 

649 def ints(self): 

650 '''Return the vector with C{int} components. 

651 ''' 

652 return self._mapped(int) 

653 

654 def isconjugateTo(self, other, minum=1, eps=EPS): 

655 '''Determine whether this and an other vector are conjugates. 

656 

657 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple}, 

658 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}). 

659 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3). 

660 @kwarg eps: Tolerance for equality and conjugation (C{scalar}), 

661 same units as C{x}, C{y}, and C{z}. 

662 

663 @return: C{True} if both vector's components either match 

664 or at least C{B{minum}} have opposite signs. 

665 

666 @raise TypeError: Incompatible B{C{other}} C{type}. 

667 

668 @see: Method C{isequalTo}. 

669 ''' 

670 self.others(other) 

671 n = 0 

672 for a, b in zip(self.xyz3, other.xyz3): 

673 if fabs(a + b) < eps and ((a < 0 and b > 0) or 

674 (a > 0 and b < 0)): 

675 n += 1 # conjugate 

676 elif fabs(a - b) > eps: 

677 return False # unequal 

678 return bool(n >= minum) 

679 

680 def isequalTo(self, other, units=False, eps=EPS): 

681 '''Check if this and an other vector are equal or equivalent. 

682 

683 @arg other: The other vector (L{Vector3d}). 

684 @kwarg units: Optionally, compare the normalized, unit 

685 version of both vectors. 

686 @kwarg eps: Tolerance for equality (C{scalar}), same units as 

687 C{x}, C{y}, and C{z}. 

688 

689 @return: C{True} if vectors are identical, C{False} otherwise. 

690 

691 @raise TypeError: Incompatible B{C{other}} C{type}. 

692 

693 @see: Method C{isconjugateTo}. 

694 ''' 

695 if units: 

696 self.others(other) 

697 d = self.unit().minus(other.unit()) 

698 else: 

699 d = self.minus(other) 

700 return max(map(fabs, d.xyz3)) < eps 

701 

702 @Property_RO 

703 def length(self): # __dict__ value overwritten by Property_RO C{_united} 

704 '''Get the length (norm, magnitude) of this vector (C{Float}). 

705 

706 @see: Properties L{length2} and L{euclid}. 

707 ''' 

708 return Float(length=hypot_(self.x, self.y, self.z)) 

709 

710 @Property_RO 

711 def length2(self): # __dict__ value overwritten by Property_RO C{_united} 

712 '''Get the length I{squared} of this vector (C{Float}). 

713 

714 @see: Property L{length} and method C{equirectangular}. 

715 ''' 

716 return Float(length2=hypot2_(self.x, self.y, self.z)) 

717 

718 def _mapped(self, func): 

719 '''(INTERNAL) Apply C{func} to all components. 

720 ''' 

721 return self.classof(*map2(func, self.xyz3)) 

722 

723 def minus(self, other): 

724 '''Subtract an other vector from this vector. 

725 

726 @arg other: The other vector (L{Vector3d}). 

727 

728 @return: New vector difference (L{Vector3d}). 

729 

730 @raise TypeError: Incompatible B{C{other}} C{type}. 

731 ''' 

732 return self._minus(*self.others(other).xyz3) 

733 

734 def _minus(self, x, y, z): 

735 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}. 

736 ''' 

737 return self.classof(self.x - x, self.y - y, self.z - z) 

738 

739 def minus_(self, other_x, *y_z): 

740 '''Subtract separate X, Y and Z components from this vector. 

741 

742 @arg other_x: X component (C{scalar}) or a vector's 

743 X, Y, and Z components (C{Cartesian}, 

744 L{Ecef9Tuple}, C{Nvector}, L{Vector3d}, 

745 L{Vector3Tuple}, L{Vector4Tuple}). 

746 @arg y_z: Y and Z components (C{scalar}, C{scalar}), 

747 ignored if B{C{other_x}} is not C{scalar}. 

748 

749 @return: New, vectiorial vector (L{Vector3d}). 

750 

751 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}. 

752 ''' 

753 return self._minus(*_xyz3(self.minus_, other_x, *y_z)) 

754 

755 def negate(self): 

756 '''Return the opposite of this vector. 

757 

758 @return: A negated copy (L{Vector3d}) 

759 ''' 

760 return self.classof(-self.x, -self.y, -self.z) 

761 

762 @Property_RO 

763 def _N_vector(self): 

764 '''(INTERNAL) Get the (C{nvectorBase._N_Vector}) 

765 ''' 

766 _N = _MODS.nvectorBase._N_Vector 

767 return _N(*self.xyz3, name=self.name) 

768 

769 def _other_cmp(self, other): 

770 '''(INTERNAL) Return the value for comparison. 

771 ''' 

772 return other if isscalar(other) else self.others(other).length 

773 

774 def others(self, *other, **name_other_up): 

775 '''Refined class comparison. 

776 

777 @arg other: The other vector (L{Vector3d}). 

778 @kwarg name_other_up: Overriding C{name=other} and C{up=1} 

779 keyword arguments. 

780 

781 @return: The B{C{other}} if compatible. 

782 

783 @raise TypeError: Incompatible B{C{other}} C{type}. 

784 ''' 

785 other, name, up = _xother3(self, other, **name_other_up) 

786 if not isinstance(other, Vector3dBase): 

787 _NamedBase.others(self, other, name=name, up=up + 1) 

788 return other 

789 

790 def plus(self, other): 

791 '''Add this vector and an other vector. 

792 

793 @arg other: The other vector (L{Vector3d}). 

794 

795 @return: Vectorial sum (L{Vector3d}). 

796 

797 @raise TypeError: Incompatible B{C{other}} C{type}. 

798 ''' 

799 return self._plus(*self.others(other).xyz3) 

800 

801 sum = plus # alternate name 

802 

803 def _plus(self, x, y, z): 

804 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}. 

805 ''' 

806 return self.classof(self.x + x, self.y + y, self.z + z) 

807 

808 def plus_(self, other_x, *y_z): 

809 '''Sum of this vector and separate X, Y and Z components. 

810 

811 @arg other_x: X component (C{scalar}) or a vector's 

812 X, Y, and Z components (C{Cartesian}, 

813 L{Ecef9Tuple}, C{Nvector}, L{Vector3d}, 

814 L{Vector3Tuple}, L{Vector4Tuple}). 

815 @arg y_z: Y and Z components (C{scalar}, C{scalar}), 

816 ignored if B{C{other_x}} is not C{scalar}. 

817 

818 @return: New, vectiorial vector (L{Vector3d}). 

819 

820 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}. 

821 ''' 

822 return self._plus(*_xyz3(self.plus_, other_x, *y_z)) 

823 

824 def rotate(self, axis, theta, fma=False): 

825 '''Rotate this vector around an axis by a specified angle. 

826 

827 @arg axis: The axis being rotated around (L{Vector3d}). 

828 @arg theta: The angle of rotation (C{radians}). 

829 @kwarg fma: If C{True}, use fused-multiply-add (C{bool}). 

830 

831 @return: New, rotated vector (L{Vector3d}). 

832 

833 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/ 

834 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and 

835 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/ 

836 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}. 

837 ''' 

838 s, c = sincos2(theta) # rotation angle 

839 d = _1_0 - c 

840 if d or s: 

841 p = self.unit().xyz # point being rotated 

842 r = self.others(axis=axis).unit() # axis being rotated around 

843 

844 ax, ay, az = r.xyz3 # quaternion-derived rotation matrix 

845 bx, by, bz = r.times(d).xyz3 

846 sx, sy, sz = r.times(s).xyz3 

847 

848 if fma: 

849 _fma = _MODS.fmath.fma 

850 else: 

851 def _fma(a, b, c): 

852 return a * b + c 

853 

854 x = fdot(p, _fma(ax, bx, c), _fma(ax, by, -sz), _fma(ax, bz, sy)) 

855 y = fdot(p, _fma(ay, bx, sz), _fma(ay, by, c), _fma(ay, bz, -sx)) 

856 z = fdot(p, _fma(az, bx, -sy), _fma(az, by, sx), _fma(az, bz, c)) 

857 else: # unrotated 

858 x, y, z = self.xyz3 

859 return self.classof(x, y, z) 

860 

861 @deprecated_method 

862 def rotateAround(self, axis, theta): 

863 '''DEPRECATED, use method C{rotate}.''' 

864 return self.rotate(axis, theta) # PYCHOK no cover 

865 

866 def times(self, factor): 

867 '''Multiply this vector by a scalar. 

868 

869 @arg factor: Scale factor (C{scalar}). 

870 

871 @return: New, scaled vector (L{Vector3d}). 

872 

873 @raise TypeError: Non-scalar B{C{factor}}. 

874 ''' 

875 return self._times(Scalar(factor=factor)) 

876 

877 def _times(self, s): 

878 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}. 

879 ''' 

880 return self.classof(self.x * s, self.y * s, self.z * s) 

881 

882 def times_(self, other_x, *y_z): 

883 '''Multiply this vector's components by separate X, Y and Z factors. 

884 

885 @arg other_x: X scale factor (C{scalar}) or a vector's 

886 X, Y, and Z components as scale factors 

887 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector}, 

888 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}). 

889 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}), 

890 ignored if B{C{other_x}} is not C{scalar}. 

891 

892 @return: New, scaled vector (L{Vector3d}). 

893 

894 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}. 

895 ''' 

896 x, y, z = _xyz3(self.times_, other_x, *y_z) 

897 return self.classof(self.x * x, self.y * y, self.z * z) 

898 

899# @deprecated_method 

900# def to2ab(self): # PYCHOK no cover 

901# '''DEPRECATED, use property C{Nvector.philam}. 

902# 

903# @return: A L{PhiLam2Tuple}C{(phi, lam)}. 

904# ''' 

905# return _MODS.nvectorBase.n_xyz2philam(self.x, self.y, self.z) 

906 

907# @deprecated_method 

908# def to2ll(self): # PYCHOK no cover 

909# '''DEPRECATED, use property C{Nvector.latlon}. 

910# 

911# @return: A L{LatLon2Tuple}C{(lat, lon)}. 

912# ''' 

913# return _MODS.nvectorBase.n_xyz2latlon(self.x, self.y, self.z) 

914 

915 @deprecated_method 

916 def to3xyz(self): # PYCHOK no cover 

917 '''DEPRECATED, use property L{xyz}. 

918 ''' 

919 return self.xyz 

920 

921 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected 

922 '''Return a string representation of this vector. 

923 

924 @kwarg prec: Number of decimal places (C{int}). 

925 @kwarg fmt: Enclosing format to use (C{str}). 

926 @kwarg sep: Separator between components (C{str}). 

927 

928 @return: Vector as "(x, y, z)" (C{str}). 

929 ''' 

930 t = sep.join(strs(self.xyz3, prec=prec)) 

931 return (fmt % (t,)) if fmt else t 

932 

933 def unit(self, ll=None): 

934 '''Normalize this vector to unit length. 

935 

936 @kwarg ll: Optional, original location (C{LatLon}). 

937 

938 @return: Normalized vector (L{Vector3d}). 

939 ''' 

940 u = self._united 

941 if ll: 

942 u._fromll = ll 

943 return u 

944 

945 @Property_RO 

946 def _united(self): # __dict__ value overwritten below 

947 '''(INTERNAL) Get normalized vector (L{Vector3d}). 

948 ''' 

949 n = self.length 

950 if n > EPS0 and fabs(n - _1_0) > EPS0: 

951 u = self._xnamed(self.dividedBy(n)) 

952 u._update(False, length=_1_0, length2=_1_0, _united=u) 

953 else: 

954 u = self.copy() 

955 u._update(False, _united=u) 

956 if self._fromll: 

957 u._fromll = self._fromll 

958 return u 

959 

960 @Property 

961 def x(self): 

962 '''Get the X component (C{float}). 

963 ''' 

964 return self._x 

965 

966 @x.setter # PYCHOK setter! 

967 def x(self, x): 

968 '''Set the X component, if different (C{float}). 

969 ''' 

970 x = Float(x=x) 

971 if self._x != x: 

972 _update_all(self, needed=3) 

973 self._x = x 

974 

975 @Property 

976 def xyz(self): 

977 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}). 

978 ''' 

979 return _MODS.namedTuples.Vector3Tuple(*self.xyz3, name=self.name) 

980 

981 @xyz.setter # PYCHOK setter! 

982 def xyz(self, xyz): 

983 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple}, 

984 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple} 

985 or a C{tuple} or C{list} of 3+ C{scalar} items). 

986 ''' 

987 self._xyz(xyz) 

988 

989 def _xyz(self, x_xyz, *y_z): 

990 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes. 

991 ''' 

992 _update_all(self, needed=3) 

993 self._x, self._y, self._z = _xyz3(_xyz_, x_xyz, *y_z) 

994 return self 

995 

996 @property_RO 

997 def xyz3(self): 

998 '''Get the X, Y and Z components as C{3-tuple}. 

999 ''' 

1000 return self.x, self.y, self.z 

1001 

1002 @property_RO 

1003 def x2y2z2(self): 

1004 '''Get the X, Y and Z components I{squared} (3-tuple C{(x**2, y**2, z**2)}). 

1005 ''' 

1006 return self.x**2, self.y**2, self.z**2 

1007 

1008 @Property 

1009 def y(self): 

1010 '''Get the Y component (C{float}). 

1011 ''' 

1012 return self._y 

1013 

1014 @y.setter # PYCHOK setter! 

1015 def y(self, y): 

1016 '''Set the Y component, if different (C{float}). 

1017 ''' 

1018 y = Float(y=y) 

1019 if self._y != y: 

1020 _update_all(self, needed=3) 

1021 self._y = y 

1022 

1023 @Property 

1024 def z(self): 

1025 '''Get the Z component (C{float}). 

1026 ''' 

1027 return self._z 

1028 

1029 @z.setter # PYCHOK setter! 

1030 def z(self, z): 

1031 '''Set the Z component, if different (C{float}). 

1032 ''' 

1033 z = Float(z=z) 

1034 if self._z != z: 

1035 _update_all(self, needed=3) 

1036 self._z = z 

1037 

1038 

1039def _xyz3(where, x_xyz, *y_z): # in .cartesianBase._rtp3 

1040 '''(INTERNAL) Get , Y and Z as 3-tuple C{(x, y, z)}. 

1041 ''' 

1042 try: 

1043 xyz3 = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for Vector*Tuple 

1044 map2(_float0, x_xyz[:3]) if islistuple(x_xyz, minum=3) else 

1045 x_xyz.xyz) # .xyz3 

1046 except (AttributeError, TypeError, ValueError) as x: 

1047 raise _xError(x, unstr(where, x_xyz, *y_z)) 

1048 return xyz3 

1049 

1050 

1051__all__ += _ALL_DOCS(Vector3dBase) 

1052 

1053# **) MIT License 

1054# 

1055# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

1056# 

1057# Permission is hereby granted, free of charge, to any person obtaining a 

1058# copy of this software and associated documentation files (the "Software"), 

1059# to deal in the Software without restriction, including without limitation 

1060# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

1061# and/or sell copies of the Software, and to permit persons to whom the 

1062# Software is furnished to do so, subject to the following conditions: 

1063# 

1064# The above copyright notice and this permission notice shall be included 

1065# in all copies or substantial portions of the Software. 

1066# 

1067# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

1068# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

1069# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

1070# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

1071# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

1072# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

1073# OTHER DEALINGS IN THE SOFTWARE.