Coverage for pygeodesy/vector3dBase.py: 93%

274 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-15 16:36 -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 _copysign, islistuple, isscalar, map1, \ 

12 map2, _zip 

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

14 _float0, isnear0, isnear1, isneg0, \ 

15 _pos_self, _0_0, _1_0 

16from pygeodesy.errors import CrossError, VectorError, _xcallable, _xError 

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

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

19 _COMMASPACE_, _xyz_ 

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

27from pygeodesy.units import Float, Scalar 

28# from pygeodesy.utily import sincos2 # _MODS 

29 

30# from builtints import hash, int, isinstance, map, max, round, type, zip 

31from math import atan2, ceil, fabs, floor, trunc 

32 

33__all__ = _ALL_LAZY.vector3dBase 

34__version__ = '24.05.10' 

35 

36 

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

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

39 ''' 

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

41 

42 _ll = None # original latlon, '_fromll' 

43# _x = INT0 # X component 

44# _y = INT0 # Y component 

45# _z = INT0 # Z component 

46 

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

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

49 

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

51 distance from earth centre or height relative to the surface 

52 of the earth' sphere or ellipsoid. 

53 

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

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

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

57 C{list} of 3+ C{scalar} items). 

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

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

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

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

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

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

64 

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

66 ''' 

67 self._x, \ 

68 self._y, \ 

69 self._z = _xyz3(type(self), x_xyz, y, z) if isscalar(x_xyz) else \ 

70 _xyz3(type(self), x_xyz) 

71 if ll: 

72 self._ll = ll 

73 if name: 

74 self.name = name 

75 

76 def __abs__(self): 

77 '''Return the norm of this vector. 

78 

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

80 ''' 

81 return self.length 

82 

83 def __add__(self, other): 

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

85 

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

87 

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

89 ''' 

90 return self.plus(other) 

91 

92 def __bool__(self): # PYCHOK PyChecker 

93 '''Is this vector non-zero? 

94 ''' 

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

96 

97 def __ceil__(self): # PYCHOK no cover 

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

99 

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

101 ''' 

102 return self._mapped(ceil) 

103 

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

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

106 

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

108 

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

110 ''' 

111 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 '''Return a vector with the C{floor} of these components. 

138 

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

140 ''' 

141 return self._mapped(floor) 

142 

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

144 '''Not implemented.''' 

145 return _NotImplemented(self, other) 

146 

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

148 '''Not implemented.''' 

149 return _NotImplemented(self, *other) 

150 

151 def __ge__(self, other): 

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

153 

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

155 

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

157 

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

159 ''' 

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

161 

162# def __getitem__(self, key): 

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

164# ''' 

165# return self.xyz[key] 

166 

167 def __gt__(self, other): 

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

169 

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

171 

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

173 

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

175 ''' 

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

177 

178 def __hash__(self): # PYCHOK no cover 

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

180 ''' 

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

182 

183 def __iadd__(self, other): 

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

185 

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

187 

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

189 ''' 

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

191 

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

193 '''Not implemented.''' 

194 return _NotImplemented(self, other) 

195 

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

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

198 

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

200 

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

202 

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

204 ''' 

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

206 

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

208 '''Not implemented.''' 

209 return _NotImplemented(self, other) 

210 

211 def __imul__(self, scalar): 

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

213 

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

215 

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

217 ''' 

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

219 

220 def __int__(self): # PYCHOK no cover 

221 '''Return a vector with the C{int} of these components. 

222 

223 @return: Int-ed (L{Vector3d}). 

224 ''' 

225 v = self.classof(_0_0) 

226 v._x, v._y, v._z = map2(int, self.xyz) 

227 return v 

228 

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

230 '''Not implemented.''' 

231 return _NotImplemented(self, other, *mod) 

232 

233 def __isub__(self, other): 

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

235 

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

237 

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

239 ''' 

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

241 

242# def __iter__(self): 

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

244# ''' 

245# return iter(self.xyz) 

246 

247 def __itruediv__(self, scalar): 

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

249 

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

251 

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

253 ''' 

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

255 

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

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

258 

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

260 

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

262 

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

264 ''' 

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

266 

267# def __len__(self): 

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

269# ''' 

270# return len(self.xyz) 

271 

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

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

274 

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

276 

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

278 

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

280 ''' 

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

282 

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

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

285 

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

287 

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

289 

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

291 ''' 

292 return self.cross(other) 

293 

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

295 '''Not implemented.''' 

296 return _NotImplemented(self, other) 

297 

298 def __mul__(self, scalar): 

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

300 

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

302 

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

304 ''' 

305 return self.times(scalar) 

306 

307 def __ne__(self, other): 

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

309 

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

311 

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

313 

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

315 ''' 

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

317 

318 def __neg__(self): 

319 '''Return the opposite of this vector. 

320 

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

322 ''' 

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

324 

325 def __pos__(self): # PYCHOK no cover 

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

327 

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

329 ''' 

330 return self if _pos_self else self.copy() 

331 

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

333 '''Not implemented.''' 

334 return _NotImplemented(self, other, *mod) 

335 

336 __radd__ = __add__ # PYCHOK no cover 

337 

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

339 '''Not implemented.''' 

340 return _NotImplemented(self, other) 

341 

342# def __repr__(self): 

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

344# ''' 

345# return self.toRepr() 

346 

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

348 '''Not implemented.''' 

349 return _NotImplemented(self, other) 

350 

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

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

353 

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

355 

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

357 

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

359 ''' 

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

361 

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

363 '''Not implemented.''' 

364 return _NotImplemented(self, other) 

365 

366 __rmul__ = __mul__ 

367 

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

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

370 

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

372 

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

374 ''' 

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

376 return self.classof(*(round(_, *ndigits) for _ in self.xyz)) 

377 

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

379 '''Not implemented.''' 

380 return _NotImplemented(self, other, *mod) 

381 

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

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

384 

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

386 

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

388 

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

390 ''' 

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

392 

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

394 '''Not implemented.''' 

395 return _NotImplemented(self, scalar) 

396 

397# def __str__(self): 

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

399# ''' 

400# return self.toStr() 

401 

402 def __sub__(self, other): 

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

404 

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

406 

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

408 

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

410 ''' 

411 return self.minus(other) 

412 

413 def __truediv__(self, scalar): 

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

415 

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

417 

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

419 

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

421 ''' 

422 return self.dividedBy(scalar) 

423 

424 def __trunc__(self): # PYCHOK no cover 

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

426 

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

428 ''' 

429 return self._mapped(trunc) 

430 

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

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

433 __div__ = __truediv__ 

434 __idiv__ = __itruediv__ 

435 __long__ = __int__ 

436 __nonzero__ = __bool__ 

437 __rdiv__ = __rtruediv__ 

438 

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

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

441 

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

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

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

445 positive if this->other is clockwise looking 

446 along vSign or negative in opposite direction, 

447 otherwise angle is unsigned. 

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

449 

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

451 

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

453 ''' 

454 x = self.cross(other) 

455 s = x.length 

456 # use vSign as reference to set sign of s 

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

458 s = -s 

459 

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

461 if wrap and fabs(a) > PI: 

462 a -= _copysign(PI2, a) 

463 return a 

464 

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

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

467 of this and an other vector. 

468 

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

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

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

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

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

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

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

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

477 

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

479 

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

481 ''' 

482 _xcallable(fun2=fun2) 

483 if fun2_kwds: 

484 def _f2(a, b): 

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

486 else: 

487 _f2 = fun2 

488 

489 xyz = _xyz3(self.apply, other_x, *y_z) 

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

491 return self.classof(*xyz) 

492 

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

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

495 

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

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

498 non-L{NN}). 

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

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

501 

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

503 

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

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

506 

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

508 ''' 

509 X, Y, Z = self.others(other).xyz 

510 x, y, z = self.xyz 

511 xyz = ((y * Z - Y * z), 

512 (z * X - Z * x), 

513 (x * Y - X * y)) 

514 

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

516 and max(map(fabs, xyz)) < eps0: 

517 r = other._fromll or other 

518 s = self._fromll or self 

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

520 t = _coincident_ if t else _colinear_ 

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

522 

523 return self.classof(*xyz) 

524 

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

526 def crosserrors(self): 

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

528 ''' 

529 return self._crosserrors 

530 

531 @crosserrors.setter # PYCHOK setter! 

532 def crosserrors(self, raiser): 

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

534 ''' 

535 self._crosserrors = bool(raiser) 

536 

537 def dividedBy(self, divisor): 

538 '''Divide this vector by a scalar. 

539 

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

541 

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

543 

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

545 

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

547 ''' 

548 d = Scalar(divisor=divisor) 

549 try: 

550 return self._times(_1_0 / d) 

551 except (ValueError, ZeroDivisionError) as x: 

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

553 

554 def dot(self, other): 

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

556 

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

558 

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

560 

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

562 ''' 

563 return self.length2 if other is self else \ 

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

565 

566 @deprecated_method 

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

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

569 ''' 

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

571 

572 @Property_RO 

573 def euclid(self): 

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

575 

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

577 L{pygeodesy.euclid_}. 

578 ''' 

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

580 

581 def equirectangular(self, other): 

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

583 

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

585 

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

587 

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

589 

590 @see: Property C{length2}. 

591 ''' 

592 d = self.minus(other) 

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

594 

595 @Property 

596 def _fromll(self): 

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

598 ''' 

599 return self._ll 

600 

601 @_fromll.setter # PYCHOK setter! 

602 def _fromll(self, ll): 

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

604 ''' 

605 self._ll = ll or None 

606 

607 @property_RO 

608 def homogeneous(self): 

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

610 ''' 

611 x, y, z = self.xyz 

612 if z: 

613 x = x / z # /= chokes PyChecker 

614 y = y / z 

615# z = _1_0 

616 else: 

617 if isneg0(z): 

618 x = -x 

619 y = -y 

620 x = _copysignINF(x) 

621 y = _copysignINF(y) 

622# z = NAN 

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

624 

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

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

627 and an other vector. 

628 

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

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

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

632 

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

634 

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

636 ''' 

637 f = Scalar(fraction=fraction) 

638 if isnear0(f): # PYCHOK no cover 

639 r = self 

640 else: 

641 r = self.others(other) 

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

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

644 return r 

645 

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

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

648 

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

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

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

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

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

654 

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

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

657 

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

659 

660 @see: Method C{isequalTo}. 

661 ''' 

662 self.others(other) 

663 n = 0 

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

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

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

667 n += 1 # conjugate 

668 elif fabs(a - b) > eps: 

669 return False # unequal 

670 return bool(n >= minum) 

671 

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

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

674 

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

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

677 version of both vectors. 

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

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

680 

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

682 

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

684 

685 @see: Method C{isconjugateTo}. 

686 ''' 

687 if units: 

688 self.others(other) 

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

690 else: 

691 d = self.minus(other) 

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

693 

694 @Property_RO 

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

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

697 

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

699 ''' 

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

701 

702 @Property_RO 

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

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

705 

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

707 ''' 

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

709 

710 def _mapped(self, func): 

711 '''(INTERNAL) Map these components. 

712 ''' 

713 return self.classof(*map2(func, self.xyz)) 

714 

715 def minus(self, other): 

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

717 

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

719 

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

721 

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

723 ''' 

724 xyz = self.others(other).xyz 

725 return self._minus(*xyz) 

726 

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

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

729 ''' 

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

731 

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

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

734 

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

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

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

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

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

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

741 

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

743 

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

745 ''' 

746 return self._minus(*_xyz3(self.minus_, other_x, *y_z)) 

747 

748 def negate(self): 

749 '''Return this vector in opposite direction. 

750 

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

752 ''' 

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

754 

755 __neg__ = negate # PYCHOK no cover 

756 

757 @Property_RO 

758 def _N_vector(self): 

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

760 ''' 

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

762 

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

764 '''Refined class comparison. 

765 

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

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

768 keyword arguments. 

769 

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

771 

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

773 ''' 

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

775 if not isinstance(other, Vector3dBase): 

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

777 return other 

778 

779 def plus(self, other): 

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

781 

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

783 

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

785 

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

787 ''' 

788 xyz = self.others(other).xyz 

789 return self._plus(*xyz) 

790 

791 sum = plus # alternate name 

792 

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

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

795 ''' 

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

797 

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

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

800 

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

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

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

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

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

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

807 

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

809 

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

811 ''' 

812 return self._plus(*_xyz3(self.plus_, other_x, *y_z)) 

813 

814 def rotate(self, axis, theta): 

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

816 

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

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

819 

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

821 

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

823 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and 

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

825 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}. 

826 ''' 

827 s, c = _MODS.utily.sincos2(theta) # rotation angle 

828 d = _1_0 - c 

829 if d or s: 

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

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

832 

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

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

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

836 

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

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

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

840 else: # unrotated 

841 x, y, z = self.xyz 

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

843 

844 @deprecated_method 

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

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

847 return self.rotate(axis, theta) 

848 

849 def times(self, factor): 

850 '''Multiply this vector by a scalar. 

851 

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

853 

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

855 

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

857 ''' 

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

859 

860 def _times(self, s): 

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

862 ''' 

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

864 

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

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

867 

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

869 X, Y, and Z components as scale factors 

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

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

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

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

874 

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

876 

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

878 ''' 

879 x, y, z = _xyz3(self.times_, other_x, *y_z) 

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

881 

882# @deprecated_method 

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

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

885# 

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

887# ''' 

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

889 

890# @deprecated_method 

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

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

893# 

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

895# ''' 

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

897 

898 @deprecated_method 

899 def to3xyz(self): # PYCHOK no cover 

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

901 ''' 

902 return self.xyz 

903 

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

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

906 

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

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

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

910 

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

912 ''' 

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

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

915 

916 def unit(self, ll=None): 

917 '''Normalize this vector to unit length. 

918 

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

920 

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

922 ''' 

923 u = self._united 

924 if ll: 

925 u._fromll = ll 

926 return u 

927 

928 @Property_RO 

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

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

931 ''' 

932 n = self.length 

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

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

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

936 else: 

937 u = self.copy() 

938 u._update(False, _united=u) 

939 if self._fromll: 

940 u._fromll = self._fromll 

941 return u 

942 

943 @Property 

944 def x(self): 

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

946 ''' 

947 return self._x 

948 

949 @x.setter # PYCHOK setter! 

950 def x(self, x): 

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

952 ''' 

953 x = Float(x=x) 

954 if self._x != x: 

955 _update_all(self, needed=3) 

956 self._x = x 

957 

958 @Property 

959 def xyz(self): 

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

961 ''' 

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

963 

964 @xyz.setter # PYCHOK setter! 

965 def xyz(self, xyz): 

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

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

968 or a C{tuple} or C{list} of 3+ C{scalar} items). 

969 ''' 

970 self._xyz(xyz) 

971 

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

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

974 ''' 

975 _update_all(self, needed=3) 

976 self._x, self._y, self._z = _xyz3(_xyz_, x_xyz, *y_z) 

977 return self 

978 

979 @property_RO 

980 def x2y2z2(self): 

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

982 ''' 

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

984 

985 @Property 

986 def y(self): 

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

988 ''' 

989 return self._y 

990 

991 @y.setter # PYCHOK setter! 

992 def y(self, y): 

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

994 ''' 

995 y = Float(y=y) 

996 if self._y != y: 

997 _update_all(self, needed=3) 

998 self._y = y 

999 

1000 @Property 

1001 def z(self): 

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

1003 ''' 

1004 return self._z 

1005 

1006 @z.setter # PYCHOK setter! 

1007 def z(self, z): 

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

1009 ''' 

1010 z = Float(z=z) 

1011 if self._z != z: 

1012 _update_all(self, needed=3) 

1013 self._z = z 

1014 

1015 

1016def _xyz3(where, x_xyz, *y_z): # in .cartesianBase._rtp3 

1017 '''(INTERNAL) Helper for C{Vector3dBase.__init__}, C{-.apply}, C{-.times_} and C{-._xyz}. 

1018 ''' 

1019 try: 

1020 x_y_z = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for VectorXTuple 

1021 map2(_float0, x_xyz[:3]) if islistuple(x_xyz, minum=3) else 

1022 x_xyz.xyz) 

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

1024 raise _xError(x, unstr(where, x_xyz, *y_z)) 

1025 return x_y_z 

1026 

1027 

1028__all__ += _ALL_DOCS(Vector3dBase) 

1029 

1030# **) MIT License 

1031# 

1032# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

1033# 

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

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

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

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

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

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

1040# 

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

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

1043# 

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

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

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

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

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

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

1050# OTHER DEALINGS IN THE SOFTWARE.