Coverage for pygeodesy/vector3dBase.py: 88%

322 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-10 16:16 -0500

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_, 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 

29 

30from math import ceil as _ceil, floor as _floor 

31 

32__all__ = _ALL_LAZY.vector3dBase 

33__version__ = '26.10.28' 

34 

35 

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 

40 

41 _ll = None # original latlon, '_fromll' 

42# _x = INT0 # X component 

43# _y = INT0 # Y component 

44# _z = INT0 # Z component 

45 

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

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

48 

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. 

52 

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

63 

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 

74 

75 def __abs__(self): 

76 '''Return the norm of this vector. 

77 

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

79 ''' 

80 return self.length 

81 

82 def __add__(self, other): 

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

84 

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

86 

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

88 ''' 

89 return self.plus(other) 

90 

91 def __bool__(self): # PYCHOK PyChecker 

92 '''Is this vector non-zero? 

93 

94 @see: Method C{bools}. 

95 ''' 

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

97 

98 def __ceil__(self): # PYCHOK no cover 

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

100 

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

102 ''' 

103 return self._mapped(_ceil) 

104 

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

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

107 

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

109 

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

111 ''' 

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

113 

114 cmp = __cmp__ 

115 

116 def __divmod__(self, scalar): # PYCHOK no cover 

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

118 

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

120 

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) 

130 

131 def __eq__(self, other): 

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

133 

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

135 

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

137 

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

139 ''' 

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

141 

142 def __float__(self): # PYCHOK no cover 

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

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

145 

146 def __floor__(self): # PYCHOK no cover 

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

148 

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

150 ''' 

151 return self._mapped(_floor) 

152 

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

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

155 

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

157 

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

159 

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

161 ''' 

162 return self.floorDividedBy(scalar) 

163 

164 def __ge__(self, other): 

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

166 

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

168 

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

170 

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

172 ''' 

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

174 

175# def __getitem__(self, key): 

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

177# ''' 

178# return self.xyz[key] 

179 

180 def __gt__(self, other): 

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

182 

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

184 

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

186 

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

188 ''' 

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

190 

191 def __hash__(self): # PYCHOK no cover 

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

193 ''' 

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

195 

196 def __iadd__(self, other): 

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

198 

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

200 

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

202 ''' 

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

204 

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

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

207 

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

209 

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

211 ''' 

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

213 

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

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

216 

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

218 

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

220 

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

222 ''' 

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

224 

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

226 '''Not implemented.''' 

227 return _NotImplemented(self, other) 

228 

229 def __imul__(self, scalar): 

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

231 

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

233 

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

235 ''' 

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

237 

238 def __int__(self): # PYCHOK no cover 

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

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

241 

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

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

244 

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

246 

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

248 

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

250 ''' 

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

252 

253 def __isub__(self, other): 

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

255 

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

257 

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

259 ''' 

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

261 

262# def __iter__(self): 

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

264# ''' 

265# return iter(self.xyz3) 

266 

267 def __itruediv__(self, scalar): 

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

269 

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

271 

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

273 ''' 

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

275 

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

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

278 

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

280 

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

282 

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

284 ''' 

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

286 

287# def __len__(self): 

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

289# ''' 

290# return len(self.xyz) 

291 

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

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

294 

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

296 

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

298 

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

300 ''' 

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

302 

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

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

305 

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

307 

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

309 

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

311 ''' 

312 return self.cross(other) 

313 

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

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

316 

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

318 

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

320 ''' 

321 m = Scalar(modulus=modulus) 

322 

323 def _mod(x): 

324 return x % m 

325 

326 return self._mapped(_mod) 

327 

328 def __mul__(self, scalar): 

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

330 

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

332 

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

334 ''' 

335 return self.times(scalar) 

336 

337 def __ne__(self, other): 

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

339 

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

341 

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

343 

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

345 ''' 

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

347 

348 def __neg__(self): 

349 '''Return the opposite of this vector. 

350 

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

352 ''' 

353 return self.negate() 

354 

355 def __pos__(self): # PYCHOK no cover 

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

357 

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

359 ''' 

360 return self if _pos_self else self.copy() 

361 

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

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

364 

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

366 

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

368 

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

370 ''' 

371 return self.pow(scalar, *mod) 

372 

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

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

375 

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

377 

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

379 

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

381 ''' 

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

383 

384 def __rdivmod__(self, other): 

385 '''Not implemented.''' 

386 return _NotImplemented(self, other) 

387 

388# def __repr__(self): 

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

390# ''' 

391# return self.toRepr() 

392 

393 def __rfloordiv__(self, other): 

394 '''Not implemented.''' 

395 return _NotImplemented(self, other) 

396 

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

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

399 

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

401 

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

403 

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

405 ''' 

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

407 

408 def __rmod__(self, other): 

409 '''Not implemented.''' 

410 return _NotImplemented(self, other) 

411 

412 __rmul__ = __mul__ # scalar other 

413 

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

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

416 

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

418 

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

420 ''' 

421 def _rnd(x): 

422 return round(x, *ndigits) 

423 

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

425 return self._mapped(_rnd) 

426 

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

428 '''Not implemented.''' 

429 return _NotImplemented(self, other, *mod) 

430 

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

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

433 

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

435 

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

437 

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

439 ''' 

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

441 

442 def __rtruediv__(self, other): 

443 '''Not implemented.''' 

444 return _NotImplemented(self, other) 

445 

446# def __str__(self): 

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

448# ''' 

449# return self.toStr() 

450 

451 def __sub__(self, other): 

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

453 

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

455 

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

457 

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

459 ''' 

460 return self.minus(other) 

461 

462 def __truediv__(self, scalar): 

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

464 

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

466 

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

468 

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

470 ''' 

471 return self.dividedBy(scalar) 

472 

473 def __trunc__(self): # PYCHOK no cover 

474 '''Return a vector with these components C{trunc}ated. 

475 

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

477 ''' 

478 return self._mapped(ftruncate) 

479 

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__ 

487 

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

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

490 

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

498 

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

500 

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 

508 

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

510 if wrap and fabs(a) > PI: 

511 a -= _copysign(PI2, a) 

512 return a 

513 

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. 

517 

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

526 

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

528 

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 

537 

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) 

541 

542 def bools(self): 

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

544 ''' 

545 return self._mapped(bool) 

546 

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

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

549 

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

554 

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

556 

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

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

559 

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

567 

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) 

575 

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

577 

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 

583 

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) 

589 

590 def dividedBy(self, scalar): 

591 '''Divide this vector by a scalar. 

592 

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

594 

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

596 

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

598 

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) 

606 

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

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

609 

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

616 

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

618 

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) 

623 

624 def dot(self, other): 

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

626 

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

628 

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

630 

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) 

635 

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) 

641 

642 @Property_RO 

643 def euclid(self): 

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

645 

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

647 L{pygeodesy.euclid_}. 

648 ''' 

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

650 

651 def equirectangular(self, other): 

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

653 

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

655 

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

657 

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

659 

660 @see: Property C{length2}. 

661 ''' 

662 d = self.minus(other) 

663 return Float(equirectangular=hypot2_(*d.xyz3)) # PYLINT E1101 

664 

665 def fabs(self): 

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

667 ''' 

668 return self._mapped(fabs) 

669 

670 def floats(self): 

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

672 ''' 

673 return self._mapped(_float0) 

674 

675 def floorDividedBy(self, scalar): 

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

677 

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

679 

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

681 

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

683 

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 

690 

691 return self._mapped(_floor_d) 

692 except (ValueError, ZeroDivisionError) as x: 

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

694 

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

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

697 

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

704 

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

706 

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) 

711 

712 @Property 

713 def _fromll(self): 

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

715 ''' 

716 return self._ll 

717 

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 

723 

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) 

738 

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. 

742 

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

746 

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

748 

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 

759 

760 def ints(self): 

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

762 ''' 

763 return self._mapped(int) 

764 

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

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

767 

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

773 

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

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

776 

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

778 

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) 

790 

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

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

793 

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

799 

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

801 

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

803 

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 

812 

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

816 

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

818 ''' 

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

820 

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

824 

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

826 ''' 

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

828 

829 def _mapped(self, func): 

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

831 ''' 

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

833 

834 def minus(self, other): 

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

836 

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

838 

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

840 

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

842 ''' 

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

844 

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) 

849 

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

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

852 

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

859 

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

861 

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

865 

866 def negate(self): 

867 '''Return the opposite of this vector. 

868 

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

870 ''' 

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

872 

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) 

879 

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 

884 

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

886 '''Refined class comparison. 

887 

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

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

890 keyword arguments. 

891 

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

893 

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 

900 

901 def plus(self, other): 

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

903 

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

905 

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

907 

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

909 ''' 

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

911 

912 sum = plus # alternate name 

913 

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) 

918 

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

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

921 

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

928 

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

930 

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

934 

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

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

937 

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

939 

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

941 

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

943 ''' 

944 p = Scalar(power=scalar) 

945 

946 def _pow(x): 

947 return pow(x, p, *mod) 

948 

949 return self._mapped(_pow) 

950 

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

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

953 

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

957 

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

959 

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 

970 

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 

974 

975 if fma: 

976 _fma = _MODS.fmath.fma 

977 else: 

978 def _fma(a, b, c): 

979 return a * b + c 

980 

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) 

987 

988 @deprecated_method 

989 def rotateAround(self, axis, theta): 

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

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

992 

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 

1002 

1003 def times(self, factor): 

1004 '''Multiply this vector by a scalar. 

1005 

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

1007 

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

1009 

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

1011 ''' 

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

1013 

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) 

1018 

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

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

1021 

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

1028 

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

1030 

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) 

1035 

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) 

1043 

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) 

1051 

1052 @deprecated_method 

1053 def to3xyz(self): # PYCHOK no cover 

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

1055 ''' 

1056 return self.xyz 

1057 

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

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

1060 

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

1064 

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 

1069 

1070 def unit(self, ll=None): 

1071 '''Normalize this vector to unit length. 

1072 

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

1074 

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

1076 ''' 

1077 u = self._united 

1078 if ll: 

1079 u._fromll = ll 

1080 return u 

1081 

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 

1096 

1097 @Property 

1098 def x(self): 

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

1100 ''' 

1101 return self._x 

1102 

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 

1111 

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) 

1117 

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) 

1125 

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 

1134 

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 

1140 

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) 

1146 

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 

1152 

1153 @Property 

1154 def y(self): 

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

1156 ''' 

1157 return self._y 

1158 

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 

1167 

1168 @Property 

1169 def z(self): 

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

1171 ''' 

1172 return self._z 

1173 

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 

1182 

1183 

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 

1194 

1195 

1196__all__ += _ALL_DOCS(Vector3dBase) 

1197 

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.