Coverage for pygeodesy/vector3dBase.py: 93%

282 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-11-12 13:23 -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-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 _copysign, islistuple, isscalar, map1, _zip 

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

13 _float0, isnear0, isnear1, isneg0, \ 

14 _pos_self, _1_0 

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

16 VectorError 

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

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

19 _COMMASPACE_, _y_, _z_ 

20from pygeodesy.lazily import _ALL_LAZY, _ALL_DOCS, _ALL_MODS as _MODS, \ 

21 _sys_version_info2 

22from pygeodesy.named import _NamedBase, _NotImplemented, _xother3 

23# from pygeodesy.namedTuples import Vector3Tuple # _MODS 

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

25 property_doc_, property_RO, _update_all 

26from pygeodesy.streprs import Fmt, strs 

27from pygeodesy.units import Float, Scalar 

28from pygeodesy.utily import sincos2, atan2, fabs 

29 

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

31 

32__all__ = _ALL_LAZY.vector3dBase 

33__version__ = '23.10.15' 

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=NN): 

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

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 name (C{str}). 

63 

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

65 ''' 

66 if isscalar(x_xyz): 

67 self._xyz(x_xyz, y, z) 

68 else: 

69 self.xyz = 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 return bool(self.x or self.y or self.z) 

95 

96 def __ceil__(self): # PYCHOK no cover 

97 '''Not implemented.''' 

98 return _NotImplemented(self) 

99 

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

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

102 

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

104 

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

106 ''' 

107 n = self.others(other).length 

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

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

110 

111 cmp = __cmp__ 

112 

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

114 '''Not implemented.''' 

115 return _NotImplemented(self, other) 

116 

117 def __eq__(self, other): 

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

119 

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

121 

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

123 

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

125 ''' 

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

127 

128 def __float__(self): # PYCHOK no cover 

129 '''Not implemented.''' 

130 return _NotImplemented(self) 

131 

132 def __floor__(self): # PYCHOK no cover 

133 '''Not implemented.''' 

134 return _NotImplemented(self) 

135 

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

137 '''Not implemented.''' 

138 return _NotImplemented(self, other) 

139 

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

141 '''Not implemented.''' 

142 return _NotImplemented(self, *other) 

143 

144 def __ge__(self, other): 

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

146 

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

148 

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

150 

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

152 ''' 

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

154 

155# def __getitem__(self, key): 

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

157# ''' 

158# return self.xyz[key] 

159 

160 def __gt__(self, other): 

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

162 

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

164 

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

166 

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

168 ''' 

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

170 

171 def __hash__(self): # PYCHOK no cover 

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

173 ''' 

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

175 

176 def __iadd__(self, other): 

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

178 

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

180 

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

182 ''' 

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

184 

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

186 '''Not implemented.''' 

187 return _NotImplemented(self, other) 

188 

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

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

191 

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

193 

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

195 

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

197 ''' 

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

199 

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

201 '''Not implemented.''' 

202 return _NotImplemented(self, other) 

203 

204 def __imul__(self, scalar): 

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

206 

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

208 

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

210 ''' 

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

212 

213 def __int__(self): # PYCHOK no cover 

214 '''Not implemented.''' 

215 return _NotImplemented(self) 

216 

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

218 '''Not implemented.''' 

219 return _NotImplemented(self, other, *mod) 

220 

221 def __isub__(self, other): 

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

223 

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

225 

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

227 ''' 

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

229 

230# def __iter__(self): 

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

232# ''' 

233# return iter(self.xyz) 

234 

235 def __itruediv__(self, scalar): 

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

237 

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

239 

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

241 ''' 

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

243 

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

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

246 

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

248 

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

250 

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

252 ''' 

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

254 

255# def __len__(self): 

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

257# ''' 

258# return len(self.xyz) 

259 

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

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

262 

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

264 

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

266 

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

268 ''' 

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

270 

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

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

273 

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

275 

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

277 

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

279 ''' 

280 return self.cross(other) 

281 

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

283 '''Not implemented.''' 

284 return _NotImplemented(self, other) 

285 

286 def __mul__(self, scalar): 

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

288 

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

290 

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

292 ''' 

293 return self.times(scalar) 

294 

295 def __ne__(self, other): 

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

297 

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

299 

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

301 

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

303 ''' 

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

305 

306 def __neg__(self): 

307 '''Return the opposite of this vector. 

308 

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

310 ''' 

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

312 

313 def __pos__(self): # PYCHOK no cover 

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

315 

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

317 ''' 

318 return self if _pos_self else self.copy() 

319 

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

321 '''Not implemented.''' 

322 return _NotImplemented(self, other, *mod) 

323 

324 __radd__ = __add__ # PYCHOK no cover 

325 

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

327 '''Not implemented.''' 

328 return _NotImplemented(self, other) 

329 

330# def __repr__(self): 

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

332# ''' 

333# return self.toRepr() 

334 

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

336 '''Not implemented.''' 

337 return _NotImplemented(self, other) 

338 

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

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

341 

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

343 

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

345 

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

347 ''' 

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

349 

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

351 '''Not implemented.''' 

352 return _NotImplemented(self, other) 

353 

354 __rmul__ = __mul__ 

355 

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

357 '''Not implemented.''' 

358 return _NotImplemented(self, ndigits=ndigits) 

359 

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

361 '''Not implemented.''' 

362 return _NotImplemented(self, other, *mod) 

363 

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

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

366 

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

368 

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

370 

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

372 ''' 

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

374 

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

376 '''Not implemented.''' 

377 return _NotImplemented(self, scalar) 

378 

379# def __str__(self): 

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

381# ''' 

382# return self.toStr() 

383 

384 def __sub__(self, other): 

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

386 

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

388 

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

390 

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

392 ''' 

393 return self.minus(other) 

394 

395 def __truediv__(self, scalar): 

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

397 

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

399 

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

401 

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

403 ''' 

404 return self.dividedBy(scalar) 

405 

406 __trunc__ = __int__ 

407 

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

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

410 __div__ = __truediv__ 

411 __idiv__ = __itruediv__ 

412 __long__ = __int__ 

413 __nonzero__ = __bool__ 

414 __rdiv__ = __rtruediv__ 

415 

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

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

418 

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

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

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

422 positive if this->other is clockwise looking 

423 along vSign or negative in opposite direction, 

424 otherwise angle is unsigned. 

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

426 

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

428 

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

430 ''' 

431 x = self.cross(other) 

432 s = x.length 

433 # use vSign as reference to set sign of s 

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

435 s = -s 

436 

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

438 if wrap and fabs(a) > PI: 

439 a -= _copysign(PI2, a) 

440 return a 

441 

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

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

444 of this and an other vector. 

445 

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

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

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

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

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

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

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

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

454 

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

456 

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

458 ''' 

459 if not callable(fun2): 

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

461 

462 if fun2_kwds: 

463 def _f2(a, b): 

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

465 else: 

466 _f2 = fun2 

467 

468 xyz = _other_x_y_z3(other_x, y_z) 

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

470 return self.classof(*xyz) 

471 

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

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

474 

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

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

477 non-L{NN}). 

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

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

480 

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

482 

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

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

485 

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

487 ''' 

488 other = self.others(other) 

489 

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

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

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

493 

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

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

496 r = other._fromll or other 

497 s = self._fromll or self 

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

499 t = _coincident_ if t else _colinear_ 

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

501 

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

503 

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

505 def crosserrors(self): 

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

507 ''' 

508 return self._crosserrors 

509 

510 @crosserrors.setter # PYCHOK setter! 

511 def crosserrors(self, raiser): 

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

513 ''' 

514 self._crosserrors = bool(raiser) 

515 

516 def dividedBy(self, divisor): 

517 '''Divide this vector by a scalar. 

518 

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

520 

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

522 

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

524 

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

526 ''' 

527 d = Scalar(divisor=divisor) 

528 try: 

529 return self._times(_1_0 / d) 

530 except (ValueError, ZeroDivisionError) as x: 

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

532 

533 def dot(self, other): 

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

535 

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

537 

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

539 

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

541 ''' 

542 return self.length2 if other is self else \ 

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

544 

545 @deprecated_method 

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

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

548 ''' 

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

550 

551 @Property_RO 

552 def euclid(self): 

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

554 

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

556 L{pygeodesy.euclid_}. 

557 ''' 

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

559 

560 def equirectangular(self, other): 

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

562 

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

564 

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

566 

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

568 

569 @see: Property C{length2}. 

570 ''' 

571 d = self.minus(other) 

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

573 

574 @Property 

575 def _fromll(self): 

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

577 ''' 

578 return self._ll 

579 

580 @_fromll.setter # PYCHOK setter! 

581 def _fromll(self, ll): 

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

583 ''' 

584 self._ll = ll or None 

585 

586 @property_RO 

587 def homogeneous(self): 

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

589 ''' 

590 z = self.z 

591 if z: 

592 x = self.x / z 

593 y = self.y / z 

594# z = _1_0 

595 elif isneg0(z): 

596 x = _copysignINF(-self.x) 

597 y = _copysignINF(-self.y) 

598# z = NAN 

599 else: 

600 x = _copysignINF(self.x) 

601 y = _copysignINF(self.y) 

602# z = NAN 

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

604 

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

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

607 and an other vector. 

608 

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

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

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

612 

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

614 

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

616 ''' 

617 f = Scalar(fraction=fraction) 

618 if isnear0(f): # PYCHOK no cover 

619 r = self 

620 else: 

621 r = self.others(other) 

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

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

624 return r 

625 

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

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

628 

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

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

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

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

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

634 

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

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

637 

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

639 

640 @see: Method C{isequalTo}. 

641 ''' 

642 self.others(other) 

643 n = 0 

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

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

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

647 n += 1 # conjugate 

648 elif fabs(a - b) > eps: 

649 return False # unequal 

650 return bool(n >= minum) 

651 

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

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

654 

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

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

657 version of both vectors. 

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

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

660 

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

662 

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

664 

665 @see: Method C{isconjugateTo}. 

666 ''' 

667 if units: 

668 self.others(other) 

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

670 else: 

671 d = self.minus(other) 

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

673 

674 @Property_RO 

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

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

677 

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

679 ''' 

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

681 

682 @Property_RO 

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

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

685 

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

687 ''' 

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

689 

690 def minus(self, other): 

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

692 

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

694 

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

696 

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

698 ''' 

699 self.others(other) 

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

701 

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

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

704 ''' 

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

706 

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

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

709 

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

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

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

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

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

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

716 

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

718 

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

720 ''' 

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

722 

723 def negate(self): 

724 '''Return this vector in opposite direction. 

725 

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

727 ''' 

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

729 

730 __neg__ = negate # PYCHOK no cover 

731 

732 @Property_RO 

733 def _N_vector(self): 

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

735 ''' 

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

737 

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

739 '''Refined class comparison. 

740 

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

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

743 keyword arguments. 

744 

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

746 

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

748 ''' 

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

750 if not isinstance(other, Vector3dBase): 

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

752 return other 

753 

754 def plus(self, other): 

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

756 

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

758 

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

760 

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

762 ''' 

763 self.others(other) 

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

765 

766 sum = plus # alternate name 

767 

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

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

770 ''' 

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

772 

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

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

775 

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

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

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

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

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

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

782 

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

784 

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

786 ''' 

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

788 

789 def rotate(self, axis, theta): 

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

791 

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

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

794 

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

796 

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

798 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and 

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

800 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}. 

801 ''' 

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

803 d = _1_0 - c 

804 if d or s: 

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

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

807 

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

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

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

811 

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

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

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

815 else: # unrotated 

816 x, y, z = self.xyz 

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

818 

819 @deprecated_method 

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

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

822 return self.rotate(axis, theta) 

823 

824 def times(self, factor): 

825 '''Multiply this vector by a scalar. 

826 

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

828 

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

830 

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

832 ''' 

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

834 

835 def _times(self, s): 

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

837 ''' 

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

839 

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

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

842 

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

844 X, Y, and Z components as scale factors 

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

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

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

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

849 

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

851 

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

853 ''' 

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

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

856 

857# @deprecated_method 

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

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

860# 

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

862# ''' 

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

864 

865# @deprecated_method 

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

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

868# 

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

870# ''' 

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

872 

873 @deprecated_method 

874 def to3xyz(self): # PYCHOK no cover 

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

876 ''' 

877 return self.xyz 

878 

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

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

881 

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

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

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

885 

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

887 ''' 

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

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

890 

891 def unit(self, ll=None): 

892 '''Normalize this vector to unit length. 

893 

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

895 

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

897 ''' 

898 u = self._united 

899 if ll: 

900 u._fromll = ll 

901 return u 

902 

903 @Property_RO 

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

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

906 ''' 

907 n = self.length 

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

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

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

911 else: 

912 u = self.copy() 

913 u._update(False, _united=u) 

914 if self._fromll: 

915 u._fromll = self._fromll 

916 return u 

917 

918 @Property 

919 def x(self): 

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

921 ''' 

922 return self._x 

923 

924 @x.setter # PYCHOK setter! 

925 def x(self, x): 

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

927 ''' 

928 x = Float(x=x) 

929 if self._x != x: 

930 _update_all(self) 

931 self._x = x 

932 

933 @Property 

934 def xyz(self): 

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

936 ''' 

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

938 

939 @xyz.setter # PYCHOK setter! 

940 def xyz(self, xyz): 

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

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

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

944 ''' 

945 if islistuple(xyz, 3): 

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

947 else: 

948 self._xyz(xyz) 

949 

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

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

952 ''' 

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

954 _update_all(self) 

955 try: 

956 self._x, \ 

957 self._y, \ 

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

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

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

961 return self 

962 

963 @Property_RO 

964 def x2y2z2(self): 

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

966 ''' 

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

968 

969 @Property 

970 def y(self): 

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

972 ''' 

973 return self._y 

974 

975 @y.setter # PYCHOK setter! 

976 def y(self, y): 

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

978 ''' 

979 y = Float(y=y) 

980 if self._y != y: 

981 _update_all(self) 

982 self._y = y 

983 

984 @Property 

985 def z(self): 

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

987 ''' 

988 return self._z 

989 

990 @z.setter # PYCHOK setter! 

991 def z(self, z): 

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

993 ''' 

994 z = Float(z=z) 

995 if self._z != z: 

996 _update_all(self) 

997 self._z = z 

998 

999 

1000def _other_x_y_z3(other_x, y_z): 

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

1002 ''' 

1003 try: 

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

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

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

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

1008 

1009 

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

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

1012 ''' 

1013 if y_z: 

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

1015 for x in xyz.values(): 

1016 d.update(x=x) 

1017 return d 

1018 return xyz 

1019 

1020 

1021__all__ += _ALL_DOCS(Vector3dBase) 

1022 

1023# **) MIT License 

1024# 

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

1026# 

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

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

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

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

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

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

1033# 

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

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

1036# 

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

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

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

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

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

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

1043# OTHER DEALINGS IN THE SOFTWARE.