Coverage for pygeodesy/vector3dBase.py: 96%

269 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-07-12 13:40 -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-2015} 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 copysign0, islistuple, isscalar, map1, _zip 

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

13 isnear0, isnear1, _pos_self, _1_0 

14from pygeodesy.errors import CrossError, _InvalidError, _IsnotError, \ 

15 VectorError 

16from pygeodesy.fmath import euclid_, fdot, hypot_, hypot2_ 

17from pygeodesy.interns import NN, _coincident_, _colinear_, \ 

18 _COMMASPACE_, _y_, _z_ 

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

20 _sys_version_info2 

21from pygeodesy.named import _NamedBase, _NotImplemented, _xother3 

22# from pygeodesy.namedTuples import Vector3Tuple # _MODS 

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

24 property_doc_, _update_all 

25from pygeodesy.streprs import Fmt, strs 

26from pygeodesy.units import Float, Scalar 

27from pygeodesy.utily import sincos2, atan2, fabs 

28 

29# from math import atan2, fabs # from .utily 

30 

31__all__ = _ALL_LAZY.vector3dBase 

32__version__ = '23.05.27' 

33 

34 

35class Vector3dBase(_NamedBase): # sync __methods__ with .fsums.Fsum 

36 '''(INTERNAL) Generic 3-D vector base class. 

37 

38 In a geodesy context, these may be used to represent: 

39 - n-vector representing a normal to point on earth's surface 

40 - earth-centered, earth-fixed cartesian (= spherical n-vector) 

41 - great circle normal to vector 

42 - motion vector on earth's surface 

43 - etc. 

44 ''' 

45 _crosserrors = True # un/set by .errors.crosserrors 

46 

47 _ll = None # original latlon, '_fromll' 

48 _x = INT0 # X component 

49 _y = INT0 # Y component 

50 _z = INT0 # Z component 

51 

52 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, name=NN): 

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

54 

55 The vector may be normalised or use x, y, z for position and 

56 distance from earth centre or height relative to the surface 

57 of the earth' sphere or ellipsoid. 

58 

59 @arg x_xyz: X component of vector (C{scalar}) or a (3-D) vector 

60 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector}, L{Vector3d}, 

61 L{Vector3Tuple}, L{Vector4Tuple} or a C{tuple} or 

62 C{list} of 3+ C{scalar} values). 

63 @kwarg y: Y component of vector (C{scalar}), ignored if B{C{x_xyz}} 

64 is not C{scalar}, otherwise same units as B{C{x_xyz}}. 

65 @kwarg z: Z component of vector (C{scalar}), ignored if B{C{x_xyz}} 

66 is not C{scalar}, otherwise same units as B{C{x_xyz}}. 

67 @kwarg ll: Optional latlon reference (C{LatLon}). 

68 @kwarg name: Optional name (C{str}). 

69 

70 @raise VectorError: Invalid B{C{x_xyz}}. 

71 ''' 

72 if isscalar(x_xyz): 

73 self._xyz(x_xyz, y, z) 

74 else: 

75 self.xyz = x_xyz 

76 if ll: 

77 self._ll = ll 

78 if name: 

79 self.name = name 

80 

81 def __abs__(self): 

82 '''Return the norm of this vector. 

83 

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

85 ''' 

86 return self.length 

87 

88 def __add__(self, other): 

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

90 

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

92 

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

94 ''' 

95 return self.plus(other) 

96 

97 def __bool__(self): # PYCHOK PyChecker 

98 '''Is this vector non-zero? 

99 ''' 

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

101 

102 def __ceil__(self): # PYCHOK no cover 

103 '''Not implemented.''' 

104 return _NotImplemented(self) 

105 

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

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

108 

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

110 

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

112 ''' 

113 n = self.others(other).length 

114 return -1 if self.length < n else ( 

115 +1 if self.length > n else 0) 

116 

117 cmp = __cmp__ 

118 

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

120 '''Not implemented.''' 

121 return _NotImplemented(self, other) 

122 

123 def __eq__(self, other): 

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

125 

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

127 

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

129 

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

131 ''' 

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

133 

134 def __float__(self): # PYCHOK no cover 

135 '''Not implemented.''' 

136 return _NotImplemented(self) 

137 

138 def __floor__(self): # PYCHOK no cover 

139 '''Not implemented.''' 

140 return _NotImplemented(self) 

141 

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

143 '''Not implemented.''' 

144 return _NotImplemented(self, other) 

145 

146 def __format__(self, *other): # PYCHOK no cover 

147 '''Not implemented.''' 

148 return _NotImplemented(self, *other) 

149 

150 def __ge__(self, other): 

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

152 

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

154 

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

156 

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

158 ''' 

159 return self.length >= self.others(other).length 

160 

161# def __getitem__(self, key): 

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

163# ''' 

164# return self.xyz[key] 

165 

166 def __gt__(self, other): 

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

168 

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

170 

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

172 

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

174 ''' 

175 return self.length > self.others(other).length 

176 

177 def __hash__(self): # PYCHOK no cover 

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

179 ''' 

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

181 

182 def __iadd__(self, other): 

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

184 

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

186 

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

188 ''' 

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

190 

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

192 '''Not implemented.''' 

193 return _NotImplemented(self, other) 

194 

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

196 '''Cross multiply 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 @see: Luciano Ramalho, "Fluent Python", page 397-398, O'Reilly 2016. 

203 ''' 

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

205 

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

207 '''Not implemented.''' 

208 return _NotImplemented(self, other) 

209 

210 def __imul__(self, scalar): 

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

212 

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

214 

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

216 ''' 

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

218 

219 def __int__(self): # PYCHOK no cover 

220 '''Not implemented.''' 

221 return _NotImplemented(self) 

222 

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

224 '''Not implemented.''' 

225 return _NotImplemented(self, other, *mod) 

226 

227 def __isub__(self, other): 

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

229 

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

231 

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

233 ''' 

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

235 

236# def __iter__(self): 

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

238# ''' 

239# return iter(self.xyz) 

240 

241 def __itruediv__(self, scalar): 

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

243 

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

245 

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

247 ''' 

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

249 

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

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

252 

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

254 

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

256 

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

258 ''' 

259 return self.length <= self.others(other).length 

260 

261# def __len__(self): 

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

263# ''' 

264# return len(self.xyz) 

265 

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

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

268 

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

270 

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

272 

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

274 ''' 

275 return self.length < self.others(other).length 

276 

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

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

279 

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

281 

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

283 

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

285 ''' 

286 return self.cross(other) 

287 

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

289 '''Not implemented.''' 

290 return _NotImplemented(self, other) 

291 

292 def __mul__(self, scalar): 

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

294 

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

296 

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

298 ''' 

299 return self.times(scalar) 

300 

301 def __ne__(self, other): 

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

303 

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

305 

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

307 

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

309 ''' 

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

311 

312 def __neg__(self): 

313 '''Return the opposite of this vector. 

314 

315 @return: This instance negated (L{Vector3d}) 

316 ''' 

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

318 

319 def __pos__(self): # PYCHOK no cover 

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

321 

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

323 ''' 

324 return self if _pos_self else self.copy() 

325 

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

327 '''Not implemented.''' 

328 return _NotImplemented(self, other, *mod) 

329 

330 __radd__ = __add__ # PYCHOK no cover 

331 

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

333 '''Not implemented.''' 

334 return _NotImplemented(self, other) 

335 

336# def __repr__(self): 

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

338# ''' 

339# return self.toRepr() 

340 

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

342 '''Not implemented.''' 

343 return _NotImplemented(self, other) 

344 

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

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

347 

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

349 

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

351 

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

353 ''' 

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

355 

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

357 '''Not implemented.''' 

358 return _NotImplemented(self, other) 

359 

360 __rmul__ = __mul__ 

361 

362 def __round__(self, ndigits=None): # PYCHOK no cover 

363 '''Not implemented.''' 

364 return _NotImplemented(self, ndigits=ndigits) 

365 

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

367 '''Not implemented.''' 

368 return _NotImplemented(self, other, *mod) 

369 

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

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

372 

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

374 

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

376 

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

378 ''' 

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

380 

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

382 '''Not implemented.''' 

383 return _NotImplemented(self, scalar) 

384 

385# def __str__(self): 

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

387# ''' 

388# return self.toStr() 

389 

390 def __sub__(self, other): 

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

392 

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

394 

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

396 

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

398 ''' 

399 return self.minus(other) 

400 

401 def __truediv__(self, scalar): 

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

403 

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

405 

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

407 

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

409 ''' 

410 return self.dividedBy(scalar) 

411 

412 __trunc__ = __int__ 

413 

414 if _sys_version_info2 < (3, 0): # PYCHOK no cover 

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

416 __div__ = __truediv__ 

417 __idiv__ = __itruediv__ 

418 __long__ = __int__ 

419 __nonzero__ = __bool__ 

420 __rdiv__ = __rtruediv__ 

421 

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

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

424 

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

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

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

428 positive if this->other is clockwise looking 

429 along vSign or negative in opposite direction, 

430 otherwise angle is unsigned. 

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

432 

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

434 

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

436 ''' 

437 x = self.cross(other) 

438 s = x.length 

439 # use vSign as reference to set sign of s 

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

441 s = -s 

442 

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

444 if wrap and fabs(a) > PI: 

445 a -= copysign0(PI2, a) 

446 return a 

447 

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

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

450 of this and an other vector. 

451 

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

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

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

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

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

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

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

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

460 

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

462 

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

464 ''' 

465 if not callable(fun2): 

466 raise _IsnotError(callable.__name__, fun2=fun2) 

467 

468 if fun2_kwds: 

469 def _f2(a, b): 

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

471 else: 

472 _f2 = fun2 

473 

474 xyz = _other_x_y_z3(other_x, y_z) 

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

476 return self.classof(*xyz) 

477 

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

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

480 

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

482 @kwarg raiser: Optional, L{CrossError} label if raised (C{str}, 

483 non-L{NN}). 

484 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as 

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

486 

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

488 

489 @raise CrossError: Zero or near-zero cross product and both 

490 B{C{raiser}} and L{pygeodesy.crosserrors} set. 

491 

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

493 ''' 

494 other = self.others(other) 

495 

496 x = self.y * other.z - self.z * other.y 

497 y = self.z * other.x - self.x * other.z 

498 z = self.x * other.y - self.y * other.x 

499 

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

501 and max(map1(fabs, x, y, z)) < eps0: 

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

503 s = self._fromll or self 

504 r = other._fromll or other 

505 t = _coincident_ if t else _colinear_ 

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

507 

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

509 

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

511 def crosserrors(self): 

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

513 ''' 

514 return self._crosserrors 

515 

516 @crosserrors.setter # PYCHOK setter! 

517 def crosserrors(self, raiser): 

518 '''Raise L{CrossError} exceptions (C{bool}). 

519 ''' 

520 self._crosserrors = bool(raiser) 

521 

522 def dividedBy(self, divisor): 

523 '''Divide this vector by a scalar. 

524 

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

526 

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

528 

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

530 

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

532 ''' 

533 d = Scalar(divisor=divisor) 

534 try: 

535 return self._times(_1_0 / d) 

536 except (ValueError, ZeroDivisionError) as x: 

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

538 

539 def dot(self, other): 

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

541 

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

543 

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

545 

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

547 ''' 

548 return self.length2 if other is self else \ 

549 fdot(self.xyz, *self.others(other).xyz) 

550 

551 @deprecated_method 

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

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

554 ''' 

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

556 

557 @Property_RO 

558 def euclid(self): 

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

560 

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

562 L{pygeodesy.euclid_}. 

563 ''' 

564 return Float(euclid=euclid_(self.x, self.y, self.z)) 

565 

566 def equirectangular(self, other): 

567 '''I{Approximate} the different between this and an other vector. 

568 

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

570 

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

572 

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

574 

575 @see: Property C{length2}. 

576 ''' 

577 d = self.minus(other) 

578 return Float(equirectangular=hypot2_(d.x, d.y, d.z)) 

579 

580 @Property 

581 def _fromll(self): 

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

583 ''' 

584 return self._ll 

585 

586 @_fromll.setter # PYCHOK setter! 

587 def _fromll(self, ll): 

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

589 ''' 

590 self._ll = ll or None 

591 

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

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

594 and an other vector. 

595 

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

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

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

599 

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

601 

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

603 ''' 

604 f = Scalar(fraction=fraction) 

605 if isnear0(f): # PYCHOK no cover 

606 r = self 

607 else: 

608 r = self.others(other) 

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

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

611 return r 

612 

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

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

615 

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

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

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

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

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

621 

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

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

624 

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

626 

627 @see: Method C{isequalTo}. 

628 ''' 

629 self.others(other) 

630 n = 0 

631 for a, b in zip(self.xyz, other.xyz): 

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

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

634 n += 1 # conjugate 

635 elif fabs(a - b) > eps: 

636 return False # unequal 

637 return bool(n >= minum) 

638 

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

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

641 

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

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

644 version of both vectors. 

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

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

647 

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

649 

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

651 

652 @see: Method C{isconjugateTo}. 

653 ''' 

654 if units: 

655 self.others(other) 

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

657 else: 

658 d = self.minus(other) 

659 return max(map(fabs, d.xyz)) < eps 

660 

661 @Property_RO 

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

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

664 

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

666 ''' 

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

668 

669 @Property_RO 

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

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

672 

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

674 ''' 

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

676 

677 def minus(self, other): 

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

679 

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

681 

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

683 

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

685 ''' 

686 self.others(other) 

687 return self._minus(other.x, other.y, other.z) 

688 

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

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

691 ''' 

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

693 

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

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

696 

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

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

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

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

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

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

703 

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

705 

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

707 ''' 

708 return self._minus(*_other_x_y_z3(other_x, y_z)) 

709 

710 def negate(self): 

711 '''Return this vector in opposite direction. 

712 

713 @return: New, opposite vector (L{Vector3d}). 

714 ''' 

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

716 

717 __neg__ = negate # PYCHOK no cover 

718 

719 @Property_RO 

720 def _N_vector(self): 

721 '''(INTERNAL) Get the (C{nvectorBase._N_vector_}) 

722 ''' 

723 return _MODS.nvectorBase._N_vector_(*self.xyz, name=self.name) 

724 

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

726 '''Refined class comparison. 

727 

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

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

730 keyword arguments. 

731 

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

733 

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

735 ''' 

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

737 if not isinstance(other, Vector3dBase): 

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

739 return other 

740 

741 def plus(self, other): 

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

743 

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

745 

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

747 

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

749 ''' 

750 self.others(other) 

751 return self._plus(other.x, other.y, other.z) 

752 

753 sum = plus # alternate name 

754 

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

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

757 ''' 

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

759 

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

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

762 

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

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

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

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

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

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

769 

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

771 

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

773 ''' 

774 return self._plus(*_other_x_y_z3(other_x, y_z)) 

775 

776 def rotate(self, axis, theta): 

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

778 

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

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

781 

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

783 

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

785 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and 

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

787 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}. 

788 ''' 

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

790 d = _1_0 - c 

791 if d or s: 

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

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

794 

795 ax, ay, az = r.xyz # quaternion-derived rotation matrix 

796 bx, by, bz = r.times(d).xyz 

797 sx, sy, sz = r.times(s).xyz 

798 

799 x = fdot(p, ax * bx + c, ax * by - sz, ax * bz + sy) 

800 y = fdot(p, ay * bx + sz, ay * by + c, ay * bz - sx) 

801 z = fdot(p, az * bx - sy, az * by + sx, az * bz + c) 

802 else: # unrotated 

803 x, y, z = self.xyz 

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

805 

806 @deprecated_method 

807 def rotateAround(self, axis, theta): # PYCHOK no cover 

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

809 return self.rotate(axis, theta) 

810 

811 def times(self, factor): 

812 '''Multiply this vector by a scalar. 

813 

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

815 

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

817 

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

819 ''' 

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

821 

822 def _times(self, s): 

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

824 ''' 

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

826 

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

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

829 

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

831 X, Y, and Z components as scale factors 

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

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

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

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

836 

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

838 

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

840 ''' 

841 x, y, z = _other_x_y_z3(other_x, y_z) 

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

843 

844# @deprecated_method 

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

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

847# 

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

849# ''' 

850# return _MODS.formy.n_xyz2philam(self.x, self.y, self.z) 

851 

852# @deprecated_method 

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

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

855# 

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

857# ''' 

858# return _MODS.formy.n_xyz2latlon(self.x, self.y, self.z) 

859 

860 @deprecated_method 

861 def to3xyz(self): # PYCHOK no cover 

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

863 ''' 

864 return self.xyz 

865 

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

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

868 

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

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

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

872 

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

874 ''' 

875 t = sep.join(strs(self.xyz, prec=prec)) 

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

877 

878 def unit(self, ll=None): 

879 '''Normalize this vector to unit length. 

880 

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

882 

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

884 ''' 

885 u = self._united 

886 if ll: 

887 u._fromll = ll 

888 return u 

889 

890 @Property_RO 

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

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

893 ''' 

894 n = self.length 

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

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

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

898 else: 

899 u = self.copy() 

900 u._update(False, _united=u) 

901 if self._fromll: 

902 u._fromll = self._fromll 

903 return u 

904 

905 @Property 

906 def x(self): 

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

908 ''' 

909 return self._x 

910 

911 @x.setter # PYCHOK setter! 

912 def x(self, x): 

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

914 ''' 

915 x = Float(x=x) 

916 if self._x != x: 

917 _update_all(self) 

918 self._x = x 

919 

920 @Property 

921 def xyz(self): 

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

923 ''' 

924 return _MODS.namedTuples.Vector3Tuple(self.x, self.y, self.z, name=self.name) 

925 

926 @xyz.setter # PYCHOK setter! 

927 def xyz(self, xyz): 

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

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

930 or a C{tuple} or C{list} of 3+ C{scalar} values). 

931 ''' 

932 if islistuple(xyz, 3): 

933 self._xyz(*xyz[:3]) 

934 else: 

935 self._xyz(xyz) 

936 

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

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

939 ''' 

940 if len(self.__dict__) > 3: # any other than initial ._x, ._y and ._z attrs 

941 _update_all(self) 

942 try: 

943 self._x, \ 

944 self._y, \ 

945 self._z = map1(_float0, x_xyz, *y_z) if y_z else x_xyz.xyz 

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

947 raise VectorError(cause=x, **_xyzkwds(y_z, x_xyz=x_xyz)) 

948 return self 

949 

950 @Property_RO 

951 def x2y2z2(self): 

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

953 ''' 

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

955 

956 @Property 

957 def y(self): 

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

959 ''' 

960 return self._y 

961 

962 @y.setter # PYCHOK setter! 

963 def y(self, y): 

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

965 ''' 

966 y = Float(y=y) 

967 if self._y != y: 

968 _update_all(self) 

969 self._y = y 

970 

971 @Property 

972 def z(self): 

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

974 ''' 

975 return self._z 

976 

977 @z.setter # PYCHOK setter! 

978 def z(self, z): 

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

980 ''' 

981 z = Float(z=z) 

982 if self._z != z: 

983 _update_all(self) 

984 self._z = z 

985 

986 

987def _other_x_y_z3(other_x, y_z): 

988 '''(INTERNAL) Helper for C{Vector3dBase.apply} and C{Vector3dBase.times_}. 

989 ''' 

990 try: 

991 return map1(_float0, other_x, *y_z) if y_z else \ 

992 (other_x.x, other_x.y, other_x.z) # not .xyz! 

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

994 raise _InvalidError(cause=x, **_xyzkwds(y_z, other_x=other_x)) 

995 

996 

997def _xyzkwds(y_z, **xyz): # PYCHOK no cover 

998 '''(INTERANL) Helper for C{_other_x_y_z3} and C{Vector3dBase._xyz}. 

999 ''' 

1000 if y_z: 

1001 d = dict(_zip((_y_, _z_), y_z)) # if y_z else {}, strict=True 

1002 for x in xyz.values(): 

1003 d.update(x=x) 

1004 return d 

1005 return xyz 

1006 

1007# **) MIT License 

1008# 

1009# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved. 

1010# 

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

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

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

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

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

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

1017# 

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

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

1020# 

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

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

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

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

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

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

1027# OTHER DEALINGS IN THE SOFTWARE.