Coverage for pygeodesy/vector3dBase.py: 96%

274 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-05-20 11:54 -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, _1_0 

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

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

16from pygeodesy.fsums import fsum1f_, _pos_self 

17from pygeodesy.interns import NN, _coincident_, _colinear_, _COMMASPACE_, _y_, _z_ 

18from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _sys, _sys_version_info2 

19from pygeodesy.named import _NamedBase, _NotImplemented, _xother3 

20from pygeodesy.namedTuples import Vector3Tuple 

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

22 property_doc_, _update_all 

23from pygeodesy.streprs import Fmt, strs 

24from pygeodesy.units import Float, Scalar 

25from pygeodesy.utily import atan2, fabs, sincos2 

26 

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

28 

29__all__ = _ALL_LAZY.vector3dBase 

30__version__ = '23.05.15' 

31 

32 

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

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

35 

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

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

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

39 - great circle normal to vector 

40 - motion vector on earth's surface 

41 - etc. 

42 ''' 

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

44 

45 _ll = None # original latlon, '_fromll' 

46 _x = INT0 # X component 

47 _y = INT0 # Y component 

48 _z = INT0 # Z component 

49 

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

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

52 

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

54 distance from earth centre or height relative to the surface 

55 of the earth' sphere or ellipsoid. 

56 

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

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

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

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

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

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

63 @kwarg z: Z 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 ll: Optional latlon reference (C{LatLon}). 

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

67 

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

69 ''' 

70 if isscalar(x_xyz): 

71 self._xyz(x_xyz, y, z) 

72 else: 

73 self.xyz = x_xyz 

74 if ll: 

75 self._ll = ll 

76 if name: 

77 self.name = name 

78 

79 def __abs__(self): 

80 '''Return the norm of this vector. 

81 

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

83 ''' 

84 return self.length 

85 

86 def __add__(self, other): 

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

88 

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

90 

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

92 ''' 

93 return self.plus(other) 

94 

95 def __bool__(self): # PYCHOK PyChecker 

96 '''Is this vector non-zero? 

97 ''' 

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

99 

100 def __ceil__(self): # PYCHOK no cover 

101 '''Not implemented.''' 

102 return _NotImplemented(self) 

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 n = self.others(other).length 

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

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

114 

115 cmp = __cmp__ 

116 

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

118 '''Not implemented.''' 

119 return _NotImplemented(self, other) 

120 

121 def __eq__(self, other): 

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

123 

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

125 

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

127 

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

129 ''' 

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

131 

132 def __float__(self): # PYCHOK no cover 

133 '''Not implemented.''' 

134 return _NotImplemented(self) 

135 

136 def __floor__(self): # PYCHOK no cover 

137 '''Not implemented.''' 

138 return _NotImplemented(self) 

139 

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

141 '''Not implemented.''' 

142 return _NotImplemented(self, other) 

143 

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

145 '''Not implemented.''' 

146 return _NotImplemented(self, *other) 

147 

148 def __ge__(self, other): 

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

150 

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

152 

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

154 

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

156 ''' 

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

158 

159# def __getitem__(self, key): 

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

161# ''' 

162# return self.xyz[key] 

163 

164 def __gt__(self, other): 

165 '''Is this vector longer than 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.others(other).length 

174 

175 def __hash__(self): # PYCHOK no cover 

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

177 ''' 

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

179 

180 def __iadd__(self, other): 

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

182 

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

184 

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

186 ''' 

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

188 

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

190 '''Not implemented.''' 

191 return _NotImplemented(self, other) 

192 

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

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

195 

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

197 

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

199 

200 @see: Luciano Ramalho, "Fluent Python", page 397-398, O'Reilly 2016. 

201 ''' 

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

203 

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

205 '''Not implemented.''' 

206 return _NotImplemented(self, other) 

207 

208 def __imul__(self, scalar): 

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

210 

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

212 

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

214 ''' 

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

216 

217 def __int__(self): # PYCHOK no cover 

218 '''Not implemented.''' 

219 return _NotImplemented(self) 

220 

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

222 '''Not implemented.''' 

223 return _NotImplemented(self, other, *mod) 

224 

225 def __isub__(self, other): 

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

227 

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

229 

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

231 ''' 

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

233 

234# def __iter__(self): 

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

236# ''' 

237# return iter(self.xyz) 

238 

239 def __itruediv__(self, scalar): 

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

241 

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

243 

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

245 ''' 

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

247 

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

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

250 

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

252 

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

254 

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

256 ''' 

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

258 

259# def __len__(self): 

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

261# ''' 

262# return len(self.xyz) 

263 

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

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

266 

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

268 

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

270 

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

272 ''' 

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

274 

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

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

277 

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

279 

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

281 

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

283 ''' 

284 return self.cross(other) 

285 

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

287 '''Not implemented.''' 

288 return _NotImplemented(self, other) 

289 

290 def __mul__(self, scalar): 

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

292 

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

294 

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

296 ''' 

297 return self.times(scalar) 

298 

299 def __ne__(self, other): 

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

301 

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

303 

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

305 

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

307 ''' 

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

309 

310 def __neg__(self): 

311 '''Return the opposite of this vector. 

312 

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

314 ''' 

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

316 

317 def __pos__(self): # PYCHOK no cover 

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

319 

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

321 ''' 

322 return self if _pos_self else self.copy() 

323 

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

325 '''Not implemented.''' 

326 return _NotImplemented(self, other, *mod) 

327 

328 __radd__ = __add__ # PYCHOK no cover 

329 

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

331 '''Not implemented.''' 

332 return _NotImplemented(self, other) 

333 

334# def __repr__(self): 

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

336# ''' 

337# return self.toRepr() 

338 

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

340 '''Not implemented.''' 

341 return _NotImplemented(self, other) 

342 

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

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

345 

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

347 

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

349 

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

351 ''' 

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

353 

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

355 '''Not implemented.''' 

356 return _NotImplemented(self, other) 

357 

358 __rmul__ = __mul__ 

359 

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

361 '''Not implemented.''' 

362 return _NotImplemented(self, ndigits=ndigits) 

363 

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

365 '''Not implemented.''' 

366 return _NotImplemented(self, other, *mod) 

367 

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

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

370 

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

372 

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

374 

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

376 ''' 

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

378 

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

380 '''Not implemented.''' 

381 return _NotImplemented(self, scalar) 

382 

383 def __sizeof__(self): # PYCHOK not special in Python 2- 

384 '''Return the current size of this vector in C{bytes}. 

385 ''' 

386 # self._x, self._y, self._z, self._ll, ... 

387 v = self.__dict__.values # avoid recursion 

388 return sum(map1(_sys.getsizeof, (o for o in v() if o is not self))) 

389 

390# def __str__(self): 

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

392# ''' 

393# return self.toStr() 

394 

395 def __sub__(self, other): 

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

397 

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

399 

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

401 

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

403 ''' 

404 return self.minus(other) 

405 

406 def __truediv__(self, scalar): 

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

408 

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

410 

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

412 

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

414 ''' 

415 return self.dividedBy(scalar) 

416 

417 __trunc__ = __int__ 

418 

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

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

421 __div__ = __truediv__ 

422 __idiv__ = __itruediv__ 

423 __long__ = __int__ 

424 __nonzero__ = __bool__ 

425 __rdiv__ = __rtruediv__ 

426 

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

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

429 

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

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

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

433 positive if this->other is clockwise looking 

434 along vSign or negative in opposite direction, 

435 otherwise angle is unsigned. 

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

437 

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

439 

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

441 ''' 

442 x = self.cross(other) 

443 s = x.length 

444 # use vSign as reference to set sign of s 

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

446 s = -s 

447 

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

449 if wrap and fabs(a) > PI: 

450 a -= copysign0(PI2, a) 

451 return a 

452 

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

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

455 of this and an other vector. 

456 

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

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

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

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

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

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

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

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

465 

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

467 

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

469 ''' 

470 if not callable(fun2): 

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

472 

473 if fun2_kwds: 

474 def _f2(a, b): 

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

476 else: 

477 _f2 = fun2 

478 

479 xyz = _other_x_y_z3(other_x, y_z) 

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

481 return self.classof(*xyz) 

482 

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

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

485 

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

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

488 non-L{NN}). 

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

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

491 

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

493 

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

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

496 

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

498 ''' 

499 other = self.others(other) 

500 

501 x = fsum1f_(self.y * other.z, -self.z * other.y) 

502 y = fsum1f_(self.z * other.x, -self.x * other.z) 

503 z = fsum1f_(self.x * other.y, -self.y * other.x) 

504 

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

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

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

508 s = self._fromll or self 

509 r = other._fromll or other 

510 t = _coincident_ if t else _colinear_ 

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

512 

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

514 

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

516 def crosserrors(self): 

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

518 ''' 

519 return self._crosserrors 

520 

521 @crosserrors.setter # PYCHOK setter! 

522 def crosserrors(self, raiser): 

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

524 ''' 

525 self._crosserrors = bool(raiser) 

526 

527 def dividedBy(self, divisor): 

528 '''Divide this vector by a scalar. 

529 

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

531 

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

533 

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

535 

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

537 ''' 

538 d = Scalar(divisor=divisor) 

539 try: 

540 return self._times(_1_0 / d) 

541 except (ValueError, ZeroDivisionError) as x: 

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

543 

544 def dot(self, other): 

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

546 

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

548 

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

550 

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

552 ''' 

553 return self.length2 if other is self else \ 

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

555 

556 @deprecated_method 

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

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

559 ''' 

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

561 

562 @Property_RO 

563 def euclid(self): 

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

565 

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

567 L{pygeodesy.euclid_}. 

568 ''' 

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

570 

571 def equirectangular(self, other): 

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

573 

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

575 

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

577 

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

579 

580 @see: Property C{length2}. 

581 ''' 

582 d = self.minus(other) 

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

584 

585 @Property 

586 def _fromll(self): 

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

588 ''' 

589 return self._ll 

590 

591 @_fromll.setter # PYCHOK setter! 

592 def _fromll(self, ll): 

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

594 ''' 

595 self._ll = ll or None 

596 

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

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

599 and an other vector. 

600 

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

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

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

604 

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

606 

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

608 ''' 

609 f = Scalar(fraction=fraction) 

610 if isnear0(f): # PYCHOK no cover 

611 r = self 

612 else: 

613 r = self.others(other) 

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

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

616 return r 

617 

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

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

620 

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

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

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

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

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

626 

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

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

629 

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

631 

632 @see: Method C{isequalTo}. 

633 ''' 

634 self.others(other) 

635 n = 0 

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

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

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

639 n += 1 # conjugate 

640 elif fabs(a - b) > eps: 

641 return False # unequal 

642 return bool(n >= minum) 

643 

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

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

646 

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

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

649 version of both vectors. 

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

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

652 

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

654 

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

656 

657 @see: Method C{isconjugateTo}. 

658 ''' 

659 if units: 

660 self.others(other) 

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

662 else: 

663 d = self.minus(other) 

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

665 

666 @Property_RO 

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

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

669 

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

671 ''' 

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

673 

674 @Property_RO 

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

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

677 

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

679 ''' 

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

681 

682 def minus(self, other): 

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

684 

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

686 

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

688 

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

690 ''' 

691 self.others(other) 

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

693 

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

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

696 ''' 

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

698 

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

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

701 

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

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

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

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

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

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

708 

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

710 

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

712 ''' 

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

714 

715 def negate(self): 

716 '''Return this vector in opposite direction. 

717 

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

719 ''' 

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

721 

722 __neg__ = negate # PYCHOK no cover 

723 

724 @Property_RO 

725 def _N_vector(self): 

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

727 ''' 

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

729 

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

731 '''Refined class comparison. 

732 

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

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

735 keyword arguments. 

736 

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

738 

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

740 ''' 

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

742 if not isinstance(other, Vector3dBase): 

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

744 return other 

745 

746 def plus(self, other): 

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

748 

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

750 

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

752 

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

754 ''' 

755 self.others(other) 

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

757 

758 sum = plus # alternate name 

759 

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

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

762 ''' 

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

764 

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

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

767 

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

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

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

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

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

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

774 

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

776 

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

778 ''' 

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

780 

781 def rotate(self, axis, theta): 

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

783 

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

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

786 

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

788 

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

790 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and 

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

792 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}. 

793 ''' 

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

795 d = _1_0 - c 

796 if d or s: 

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

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

799 

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

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

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

803 

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

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

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

807 else: # unrotated 

808 x, y, z = self.xyz 

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

810 

811 @deprecated_method 

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

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

814 return self.rotate(axis, theta) 

815 

816 def times(self, factor): 

817 '''Multiply this vector by a scalar. 

818 

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

820 

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

822 

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

824 ''' 

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

826 

827 def _times(self, s): 

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

829 ''' 

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

831 

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

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

834 

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

836 X, Y, and Z components as scale factors 

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

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

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

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

841 

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

843 

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

845 ''' 

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

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

848 

849# @deprecated_method 

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

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

852# 

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

854# ''' 

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

856 

857# @deprecated_method 

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

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

860# 

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

862# ''' 

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

864 

865 @deprecated_method 

866 def to3xyz(self): # PYCHOK no cover 

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

868 ''' 

869 return self.xyz 

870 

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

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

873 

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

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

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

877 

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

879 ''' 

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

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

882 

883 def unit(self, ll=None): 

884 '''Normalize this vector to unit length. 

885 

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

887 

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

889 ''' 

890 u = self._united 

891 if ll: 

892 u._fromll = ll 

893 return u 

894 

895 @Property_RO 

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

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

898 ''' 

899 n = self.length 

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

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

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

903 else: 

904 u = self.copy() 

905 u._update(False, _united=u) 

906 if self._fromll: 

907 u._fromll = self._fromll 

908 return u 

909 

910 @Property 

911 def x(self): 

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

913 ''' 

914 return self._x 

915 

916 @x.setter # PYCHOK setter! 

917 def x(self, x): 

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

919 ''' 

920 x = Float(x=x) 

921 if self._x != x: 

922 _update_all(self) 

923 self._x = x 

924 

925 @Property 

926 def xyz(self): 

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

928 ''' 

929 return Vector3Tuple(self.x, self.y, self.z, name=self.name) 

930 

931 @xyz.setter # PYCHOK setter! 

932 def xyz(self, xyz): 

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

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

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

936 ''' 

937 if islistuple(xyz, 3): 

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

939 else: 

940 self._xyz(xyz) 

941 

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

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

944 ''' 

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

946 _update_all(self) 

947 try: 

948 self._x, \ 

949 self._y, \ 

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

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

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

953 return self 

954 

955 @Property_RO 

956 def x2y2z2(self): 

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

958 ''' 

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

960 

961 @Property 

962 def y(self): 

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

964 ''' 

965 return self._y 

966 

967 @y.setter # PYCHOK setter! 

968 def y(self, y): 

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

970 ''' 

971 y = Float(y=y) 

972 if self._y != y: 

973 _update_all(self) 

974 self._y = y 

975 

976 @Property 

977 def z(self): 

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

979 ''' 

980 return self._z 

981 

982 @z.setter # PYCHOK setter! 

983 def z(self, z): 

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

985 ''' 

986 z = Float(z=z) 

987 if self._z != z: 

988 _update_all(self) 

989 self._z = z 

990 

991 

992def _other_x_y_z3(other_x, y_z): 

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

994 ''' 

995 try: 

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

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

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

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

1000 

1001 

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

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

1004 ''' 

1005 if y_z: 

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

1007 for x in xyz.values(): 

1008 d.update(x=x) 

1009 return d 

1010 return xyz 

1011 

1012# **) MIT License 

1013# 

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

1015# 

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

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

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

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

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

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

1022# 

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

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

1025# 

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

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

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

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

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

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

1032# OTHER DEALINGS IN THE SOFTWARE.