Coverage for pygeodesy/vector3dBase.py: 88%

322 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-03-25 15:01 -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, _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 

28 

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

30 

31__all__ = _ALL_LAZY.vector3dBase 

32__version__ = '25.10.25' 

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, scalar): # PYCHOK no cover 

116 '''Apply C{scalar} divmod to this vector's components. 

117 

118 @arg scalar: Divisor (C{scalar}). 

119 

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) 

129 

130 def __eq__(self, other): 

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

132 

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

134 

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

136 

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

138 ''' 

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

140 

141 def __float__(self): # PYCHOK no cover 

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

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

144 

145 def __floor__(self): # PYCHOK no cover 

146 '''Return a vector with the C{floor} of each components. 

147 

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

149 ''' 

150 return self._mapped(_floor) 

151 

152 def __floordiv__(self, scalar): # PYCHOK no cover 

153 '''Floor-divide this vector by a scalar, C{this // B{scalar}}. 

154 

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

156 

157 @return: Floored quotient (L{Vector3d}). 

158 

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

160 ''' 

161 return self.floorDividedBy(scalar) 

162 

163 def __ge__(self, other): 

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

165 

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

167 

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

169 

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

171 ''' 

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

173 

174# def __getitem__(self, key): 

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

176# ''' 

177# return self.xyz[key] 

178 

179 def __gt__(self, other): 

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

181 

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

183 

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

185 

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

187 ''' 

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

189 

190 def __hash__(self): # PYCHOK no cover 

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

192 ''' 

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

194 

195 def __iadd__(self, other): 

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

197 

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

199 

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

201 ''' 

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

203 

204 def __ifloordiv__(self, scalar): # PYCHOK no cover 

205 '''Floor-divide this vector by a scalar I{in-place}, C{this //= B{scalar}}. 

206 

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

208 

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

210 ''' 

211 return self._xyz(self.floorDividedBy(scalar)) 

212 

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

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

215 

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

217 

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

219 

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

221 ''' 

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

223 

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

225 '''Not implemented.''' 

226 return _NotImplemented(self, other) 

227 

228 def __imul__(self, scalar): 

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

230 

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

232 

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

234 ''' 

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

236 

237 def __int__(self): # PYCHOK no cover 

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

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

240 

241 def __ipow__(self, scalar, *mod): # PYCHOK no cover 

242 '''Raise each component I{in-place} as C{pow(C, B{scalar})}. 

243 

244 @arg scalar: Exponent (C{scalar}). 

245 

246 @return: Power (L{Vector3d}). 

247 

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

249 ''' 

250 return self._xyz(self.pow(scalar, *mod)) 

251 

252 def __isub__(self, other): 

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

254 

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

256 

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

258 ''' 

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

260 

261# def __iter__(self): 

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

263# ''' 

264# return iter(self.xyz3) 

265 

266 def __itruediv__(self, scalar): 

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

268 

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

270 

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

272 ''' 

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

274 

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

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

277 

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

279 

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

281 

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

283 ''' 

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

285 

286# def __len__(self): 

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

288# ''' 

289# return len(self.xyz) 

290 

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

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

293 

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

295 

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

297 

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

299 ''' 

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

301 

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

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

304 

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

306 

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

308 

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

310 ''' 

311 return self.cross(other) 

312 

313 def __mod__(self, modulus): # PYCHOK no cover 

314 '''Apply C{scalar} modulo to this vector's components. 

315 

316 @arg modulus: Modulus (C{scalar}). 

317 

318 @return: Modulo (L{Vector3d}). 

319 ''' 

320 m = Scalar(modulus=modulus) 

321 

322 def _mod(x): 

323 return x % m 

324 

325 return self._mapped(_mod) 

326 

327 def __mul__(self, scalar): 

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

329 

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

331 

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

333 ''' 

334 return self.times(scalar) 

335 

336 def __ne__(self, other): 

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

338 

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

340 

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

342 

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

344 ''' 

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

346 

347 def __neg__(self): 

348 '''Return the opposite of this vector. 

349 

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

351 ''' 

352 return self.negate() 

353 

354 def __pos__(self): # PYCHOK no cover 

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

356 

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

358 ''' 

359 return self if _pos_self else self.copy() 

360 

361 def __pow__(self, scalar, *mod): # PYCHOK no cover 

362 '''Return a vector with components as C{pow(C, B{scalar})}. 

363 

364 @arg scalar: Exponent (C{scalar}). 

365 

366 @return: Power (L{Vector3d}). 

367 

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

369 ''' 

370 return self.pow(scalar, *mod) 

371 

372 def __radd__(self, other): # PYCHOK no cover 

373 '''Add this vector to an other vector, C{B{other} + this}. 

374 

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

376 

377 @return: Sum (L{Vector3d}). 

378 

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

380 ''' 

381 return self.others(other).plus(self) 

382 

383 def __rdivmod__(self, other): 

384 '''Not implemented.''' 

385 return _NotImplemented(self, other) 

386 

387# def __repr__(self): 

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

389# ''' 

390# return self.toRepr() 

391 

392 def __rfloordiv__(self, other): 

393 '''Not implemented.''' 

394 return _NotImplemented(self, other) 

395 

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

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

398 

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

400 

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

402 

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

404 ''' 

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

406 

407 def __rmod__(self, other): 

408 '''Not implemented.''' 

409 return _NotImplemented(self, other) 

410 

411 __rmul__ = __mul__ # scalar other 

412 

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

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

415 

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

417 

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

419 ''' 

420 def _rnd(x): 

421 return round(x, *ndigits) 

422 

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

424 return self._mapped(_rnd) 

425 

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

427 '''Not implemented.''' 

428 return _NotImplemented(self, other, *mod) 

429 

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

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

432 

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

434 

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

436 

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

438 ''' 

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

440 

441 def __rtruediv__(self, other): 

442 '''Not implemented.''' 

443 return _NotImplemented(self, other) 

444 

445# def __str__(self): 

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

447# ''' 

448# return self.toStr() 

449 

450 def __sub__(self, other): 

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

452 

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

454 

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

456 

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

458 ''' 

459 return self.minus(other) 

460 

461 def __truediv__(self, scalar): 

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

463 

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

465 

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

467 

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

469 ''' 

470 return self.dividedBy(scalar) 

471 

472 def __trunc__(self): # PYCHOK no cover 

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

474 

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

476 ''' 

477 return self._mapped(_trunc) 

478 

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__ 

486 

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

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

489 

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

497 

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

499 

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 

507 

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

509 if wrap and fabs(a) > PI: 

510 a -= _copysign(PI2, a) 

511 return a 

512 

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. 

516 

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

525 

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

527 

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 

536 

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) 

540 

541 def bools(self): 

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

543 ''' 

544 return self._mapped(bool) 

545 

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

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

548 

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

553 

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

555 

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

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

558 

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

566 

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) 

574 

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

576 

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 

582 

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) 

588 

589 def dividedBy(self, scalar): 

590 '''Divide this vector by a scalar. 

591 

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

593 

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

595 

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

597 

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) 

605 

606 def dividedBy_(self, scalar_x, *y_z): 

607 '''Divide this vector by separate X, Y and Z divisors. 

608 

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

615 

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

617 

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) 

622 

623 def dot(self, other): 

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

625 

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

627 

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

629 

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) 

634 

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) 

640 

641 @Property_RO 

642 def euclid(self): 

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

644 

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

646 L{pygeodesy.euclid_}. 

647 ''' 

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

649 

650 def equirectangular(self, other): 

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

652 

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

654 

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

656 

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

658 

659 @see: Property C{length2}. 

660 ''' 

661 d = self.minus(other) 

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

663 

664 def fabs(self): 

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

666 ''' 

667 return self._mapped(fabs) 

668 

669 def floats(self): 

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

671 ''' 

672 return self._mapped(_float0) 

673 

674 def floorDividedBy(self, scalar): 

675 '''Floor-divide this vector by a scalar. 

676 

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

678 

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

680 

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

682 

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 

689 

690 return self._mapped(_floor_d) 

691 except (ValueError, ZeroDivisionError) as x: 

692 raise VectorError(divisor=scalar, cause=x) 

693 

694 def floorDividedBy_(self, scalar_x, *y_z): 

695 '''Floor-divide this vector by separate X, Y and Z divisors. 

696 

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

703 

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

705 

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) 

710 

711 @Property 

712 def _fromll(self): 

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

714 ''' 

715 return self._ll 

716 

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 

722 

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) 

737 

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. 

741 

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

745 

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

747 

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 

758 

759 def ints(self): 

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

761 ''' 

762 return self._mapped(int) 

763 

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

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

766 

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

772 

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

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

775 

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

777 

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) 

789 

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

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

792 

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

798 

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

800 

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

802 

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 

811 

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

815 

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

817 ''' 

818 return Float(length=hypot_(*self.xyz3)) 

819 

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

823 

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

825 ''' 

826 return Float(length2=hypot2_(*self.xyz3)) 

827 

828 def _mapped(self, func): 

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

830 ''' 

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

832 

833 def minus(self, other): 

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

835 

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

837 

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

839 

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

841 ''' 

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

843 

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) 

848 

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

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

851 

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

858 

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

860 

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

864 

865 def negate(self): 

866 '''Return the opposite of this vector. 

867 

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

869 ''' 

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

871 

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) 

878 

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 

883 

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

885 '''Refined class comparison. 

886 

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

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

889 keyword arguments. 

890 

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

892 

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 

899 

900 def plus(self, other): 

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

902 

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

904 

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

906 

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

908 ''' 

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

910 

911 sum = plus # alternate name 

912 

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) 

917 

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

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

920 

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

927 

928 @return: New, vectorial vector (L{Vector3d}). 

929 

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

933 

934 def pow(self, scalar, *mod): 

935 '''Raise each X, Y and Z to C{scalar} power. 

936 

937 @arg scalar: Exponent (C{scalar}). 

938 

939 @return: Power (L{Vector3d}). 

940 

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

942 ''' 

943 p = Scalar(power=scalar) 

944 

945 def _pow(x): 

946 return pow(x, p, *mod) 

947 

948 return self._mapped(_pow) 

949 

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

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

952 

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

956 

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

958 

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 

969 

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 

973 

974 if fma: 

975 _fma = _MODS.fmath.fma 

976 else: 

977 def _fma(a, b, c): 

978 return a * b + c 

979 

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) 

986 

987 @deprecated_method 

988 def rotateAround(self, axis, theta): 

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

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

991 

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 

1001 

1002 def times(self, factor): 

1003 '''Multiply this vector by a scalar. 

1004 

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

1006 

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

1008 

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

1010 ''' 

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

1012 

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) 

1017 

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

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

1020 

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

1027 

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

1029 

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) 

1034 

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) 

1042 

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) 

1050 

1051 @deprecated_method 

1052 def to3xyz(self): # PYCHOK no cover 

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

1054 ''' 

1055 return self.xyz 

1056 

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

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

1059 

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

1063 

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 

1068 

1069 def unit(self, ll=None): 

1070 '''Normalize this vector to unit length. 

1071 

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

1073 

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

1075 ''' 

1076 u = self._united 

1077 if ll: 

1078 u._fromll = ll 

1079 return u 

1080 

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 

1095 

1096 @Property 

1097 def x(self): 

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

1099 ''' 

1100 return self._x 

1101 

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 

1110 

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) 

1116 

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) 

1124 

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 

1133 

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 

1139 

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) 

1145 

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 

1151 

1152 @Property 

1153 def y(self): 

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

1155 ''' 

1156 return self._y 

1157 

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 

1166 

1167 @Property 

1168 def z(self): 

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

1170 ''' 

1171 return self._z 

1172 

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 

1181 

1182 

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 

1193 

1194 

1195__all__ += _ALL_DOCS(Vector3dBase) 

1196 

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.