Coverage for pygeodesy/vector3dBase.py: 91%

281 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-09-30 14:00 -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, map2, \ 

12 _signOf, _zip 

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

14 _float0, isnear0, isnear1, isneg0, \ 

15 _pos_self, _1_0 

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

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

18from pygeodesy.interns import _coincident_, _colinear_, _COMMASPACE_, _xyz_ 

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

20 _sys_version_info2 

21from pygeodesy.named import _NamedBase, _NotImplemented, _xother3 

22# from pygeodesy.namedTuples import Vector3Tuple # _MODS 

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

24 property_doc_, property_RO, _update_all 

25from pygeodesy.streprs import Fmt, strs, unstr 

26from pygeodesy.units import Float, Scalar 

27from pygeodesy.utily import sincos2, atan2, fabs 

28 

29from math import ceil as _ceil, floor as _floor, trunc as _trunc 

30 

31__all__ = _ALL_LAZY.vector3dBase 

32__version__ = '24.08.18' 

33 

34 

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

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

37 ''' 

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

39 

40 _ll = None # original latlon, '_fromll' 

41# _x = INT0 # X component 

42# _y = INT0 # Y component 

43# _z = INT0 # Z component 

44 

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

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

47 

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

49 distance from earth centre or height relative to the surface 

50 of the earth' sphere or ellipsoid. 

51 

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

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

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

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

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

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

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

61 @kwarg name: Optional C{B{name}=NN} (C{str}). 

62 

63 @raise VectorError: Invalid B{C{x_xyz}}, B{C{y}} or B{C{z}}. 

64 ''' 

65 self._x, \ 

66 self._y, \ 

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

68 _xyz3(type(self), x_xyz) 

69 if ll: 

70 self._ll = ll 

71 if name: 

72 self.name = name 

73 

74 def __abs__(self): 

75 '''Return the norm of this vector. 

76 

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

78 ''' 

79 return self.length 

80 

81 def __add__(self, other): 

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

83 

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

85 

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

87 ''' 

88 return self.plus(other) 

89 

90 def __bool__(self): # PYCHOK PyChecker 

91 '''Is this vector non-zero? 

92 

93 @see: Method C{bools}. 

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 return _signOf(self.length, self._other_cmp(other)) 

112 

113 cmp = __cmp__ 

114 

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

116 '''Not implemented.''' 

117 return _NotImplemented(self, other) 

118 

119 def __eq__(self, other): 

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

121 

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

123 

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

125 

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

127 ''' 

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

129 

130 def __float__(self): # PYCHOK no cover 

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

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

133 

134 def __floor__(self): # PYCHOK no cover 

135 '''Return a vector with the C{floor} of these components. 

136 

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

138 ''' 

139 return self._mapped(_floor) 

140 

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

142 '''Not implemented.''' 

143 return _NotImplemented(self, other) 

144 

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

146 '''Not implemented.''' 

147 return _NotImplemented(self, *other) 

148 

149 def __ge__(self, other): 

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

151 

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

153 

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

155 

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

157 ''' 

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

159 

160# def __getitem__(self, key): 

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

162# ''' 

163# return self.xyz[key] 

164 

165 def __gt__(self, other): 

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

167 

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

169 

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

171 

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

173 ''' 

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

175 

176 def __hash__(self): # PYCHOK no cover 

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

178 ''' 

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

180 

181 def __iadd__(self, other): 

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

183 

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

185 

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

187 ''' 

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

189 

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

191 '''Not implemented.''' 

192 return _NotImplemented(self, other) 

193 

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

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

196 

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

198 

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

200 

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

202 ''' 

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

204 

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

206 '''Not implemented.''' 

207 return _NotImplemented(self, other) 

208 

209 def __imul__(self, scalar): 

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

211 

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

213 

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

215 ''' 

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

217 

218 def __int__(self): # PYCHOK no cover 

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

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

221 

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

223 '''Not implemented.''' 

224 return _NotImplemented(self, other, *mod) 

225 

226 def __isub__(self, other): 

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

228 

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

230 

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

232 ''' 

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

234 

235# def __iter__(self): 

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

237# ''' 

238# return iter(self.xyz3) 

239 

240 def __itruediv__(self, scalar): 

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

242 

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

244 

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

246 ''' 

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

248 

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

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

251 

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

253 

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

255 

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

257 ''' 

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

259 

260# def __len__(self): 

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

262# ''' 

263# return len(self.xyz) 

264 

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

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

267 

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

269 

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

271 

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

273 ''' 

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

275 

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

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

278 

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

280 

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

282 

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

284 ''' 

285 return self.cross(other) 

286 

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

288 '''Not implemented.''' 

289 return _NotImplemented(self, other) 

290 

291 def __mul__(self, scalar): 

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

293 

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

295 

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

297 ''' 

298 return self.times(scalar) 

299 

300 def __ne__(self, other): 

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

302 

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

304 

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

306 

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

308 ''' 

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

310 

311 def __neg__(self): 

312 '''Return the opposite of this vector. 

313 

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

315 ''' 

316 return self.negate() 

317 

318 def __pos__(self): # PYCHOK no cover 

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

320 

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

322 ''' 

323 return self if _pos_self else self.copy() 

324 

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

326 '''Not implemented.''' 

327 return _NotImplemented(self, other, *mod) 

328 

329 __radd__ = __add__ # PYCHOK no cover 

330 

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

332 '''Not implemented.''' 

333 return _NotImplemented(self, other) 

334 

335# def __repr__(self): 

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

337# ''' 

338# return self.toRepr() 

339 

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

341 '''Not implemented.''' 

342 return _NotImplemented(self, other) 

343 

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

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

346 

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

348 

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

350 

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

352 ''' 

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

354 

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

356 '''Not implemented.''' 

357 return _NotImplemented(self, other) 

358 

359 __rmul__ = __mul__ 

360 

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

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

363 

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

365 

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

367 ''' 

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

369 return self.classof(*(round(_, *ndigits) for _ in self.xyz3)) 

370 

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

372 '''Not implemented.''' 

373 return _NotImplemented(self, other, *mod) 

374 

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

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

377 

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

379 

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

381 

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

383 ''' 

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

385 

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

387 '''Not implemented.''' 

388 return _NotImplemented(self, scalar) 

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 def __trunc__(self): # PYCHOK no cover 

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

419 

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

421 ''' 

422 return self._mapped(_trunc) 

423 

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

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

426 __div__ = __truediv__ 

427 __idiv__ = __itruediv__ 

428 __long__ = __int__ 

429 __nonzero__ = __bool__ 

430 __rdiv__ = __rtruediv__ 

431 

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

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

434 

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

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

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

438 positive if this->other is clockwise looking 

439 along vSign or negative in opposite direction, 

440 otherwise angle is unsigned. 

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

442 

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

444 

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

446 ''' 

447 x = self.cross(other) 

448 s = x.length 

449 # use vSign as reference to set sign of s 

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

451 s = -s 

452 

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

454 if wrap and fabs(a) > PI: 

455 a -= _copysign(PI2, a) 

456 return a 

457 

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

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

460 of this and an other vector. 

461 

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

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

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

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

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

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

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

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

470 

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

472 

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

474 ''' 

475 _xcallable(fun2=fun2) 

476 if fun2_kwds: 

477 def _f2(a, b): 

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

479 else: 

480 _f2 = fun2 

481 

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

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

484 return self.classof(*xyz) 

485 

486 def bools(self): 

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

488 ''' 

489 return self._mapped(bool) 

490 

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

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

493 

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

495 @kwarg raiser: Optional, L{CrossError} label if raised (C{str}, non-L{NN}). 

496 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as C{x}, C{y} and 

497 C{z}. 

498 

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

500 

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

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

503 

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

505 ''' 

506 X, Y, Z = self.others(other).xyz3 

507 x, y, z = self.xyz3 

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

509 (z * X - x * Z), 

510 (x * Y - y * X)) 

511 

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

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

514 r = other._fromll or other 

515 s = self._fromll or self 

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

517 t = _coincident_ if t else _colinear_ 

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

519 

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

521 

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

523 def crosserrors(self): 

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

525 ''' 

526 return self._crosserrors 

527 

528 @crosserrors.setter # PYCHOK setter! 

529 def crosserrors(self, raiser): 

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

531 ''' 

532 self._crosserrors = bool(raiser) 

533 

534 def dividedBy(self, divisor): 

535 '''Divide this vector by a scalar. 

536 

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

538 

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

540 

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

542 

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

544 ''' 

545 d = Scalar(divisor=divisor) 

546 try: 

547 return self._times(_1_0 / d) 

548 except (ValueError, ZeroDivisionError) as x: 

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

550 

551 def dot(self, other): 

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

553 

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

555 

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

557 

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

559 ''' 

560 return self.length2 if other is self else fdot( 

561 self.xyz3, *self.others(other).xyz3) 

562 

563 @deprecated_method 

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

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

566 ''' 

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

568 

569 @Property_RO 

570 def euclid(self): 

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

572 

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

574 L{pygeodesy.euclid_}. 

575 ''' 

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

577 

578 def equirectangular(self, other): 

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

580 

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

582 

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

584 

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

586 

587 @see: Property C{length2}. 

588 ''' 

589 d = self.minus(other) 

590 return Float(equirectangular=hypot2_(*d.xyz3)) 

591 

592 def fabs(self): 

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

594 ''' 

595 return self._mapped(fabs) 

596 

597 def floats(self): 

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

599 ''' 

600 return self._mapped(_float0) 

601 

602 @Property 

603 def _fromll(self): 

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

605 ''' 

606 return self._ll 

607 

608 @_fromll.setter # PYCHOK setter! 

609 def _fromll(self, ll): 

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

611 ''' 

612 self._ll = ll or None 

613 

614 @property_RO 

615 def homogeneous(self): 

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

617 ''' 

618 x, y, z = self.xyz3 

619 if z: 

620 x = x / z # /= chokes PyChecker 

621 y = y / z 

622# z = _1_0 

623 else: 

624 if isneg0(z): 

625 x = -x 

626 y = -y 

627 x = _copysignINF(x) 

628 y = _copysignINF(y) 

629# z = NAN 

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

631 

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

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

634 and an other vector. 

635 

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

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

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

639 

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

641 

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

643 ''' 

644 f = Scalar(fraction=fraction) 

645 if isnear0(f): # PYCHOK no cover 

646 r = self 

647 else: 

648 r = self.others(other) 

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

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

651 return r 

652 

653 def ints(self): 

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

655 ''' 

656 return self._mapped(int) 

657 

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

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

660 

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

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

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

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

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

666 

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

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

669 

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

671 

672 @see: Method C{isequalTo}. 

673 ''' 

674 self.others(other) 

675 n = 0 

676 for a, b in zip(self.xyz3, other.xyz3): 

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

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

679 n += 1 # conjugate 

680 elif fabs(a - b) > eps: 

681 return False # unequal 

682 return bool(n >= minum) 

683 

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

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

686 

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

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

689 version of both vectors. 

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

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

692 

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

694 

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

696 

697 @see: Method C{isconjugateTo}. 

698 ''' 

699 if units: 

700 self.others(other) 

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

702 else: 

703 d = self.minus(other) 

704 return max(map(fabs, d.xyz3)) < eps 

705 

706 @Property_RO 

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

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

709 

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

711 ''' 

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

713 

714 @Property_RO 

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

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

717 

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

719 ''' 

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

721 

722 def _mapped(self, func): 

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

724 ''' 

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

726 

727 def minus(self, other): 

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

729 

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

731 

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

733 

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

735 ''' 

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

737 

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

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

740 ''' 

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

742 

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

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

745 

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

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

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

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

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

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

752 

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

754 

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

756 ''' 

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

758 

759 def negate(self): 

760 '''Return the opposite of this vector. 

761 

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

763 ''' 

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

765 

766 @Property_RO 

767 def _N_vector(self): 

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

769 ''' 

770 return _MODS.nvectorBase._N_vector_(*self.xyz3, name=self.name) 

771 

772 def _other_cmp(self, other): 

773 '''(INTERNAL) Return the value for comparison. 

774 ''' 

775 return other if isscalar(other) else self.others(other).length 

776 

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

778 '''Refined class comparison. 

779 

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

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

782 keyword arguments. 

783 

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

785 

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

787 ''' 

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

789 if not isinstance(other, Vector3dBase): 

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

791 return other 

792 

793 def plus(self, other): 

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

795 

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

797 

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

799 

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

801 ''' 

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

803 

804 sum = plus # alternate name 

805 

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

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

808 ''' 

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

810 

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

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

813 

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

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

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

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

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

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

820 

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

822 

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

824 ''' 

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

826 

827 def rotate(self, axis, theta): 

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

829 

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

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

832 

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

834 

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

836 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and 

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

838 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}. 

839 ''' 

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

841 d = _1_0 - c 

842 if d or s: 

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

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

845 

846 ax, ay, az = r.xyz3 # quaternion-derived rotation matrix 

847 bx, by, bz = r.times(d).xyz3 

848 sx, sy, sz = r.times(s).xyz3 

849 

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

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

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

853 else: # unrotated 

854 x, y, z = self.xyz3 

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

856 

857 @deprecated_method 

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

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

860 return self.rotate(axis, theta) 

861 

862 def times(self, factor): 

863 '''Multiply this vector by a scalar. 

864 

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

866 

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

868 

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

870 ''' 

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

872 

873 def _times(self, s): 

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

875 ''' 

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

877 

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

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

880 

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

882 X, Y, and Z components as scale factors 

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

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

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

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

887 

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

889 

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

891 ''' 

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

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

894 

895# @deprecated_method 

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

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

898# 

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

900# ''' 

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

902 

903# @deprecated_method 

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

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

906# 

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

908# ''' 

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

910 

911 @deprecated_method 

912 def to3xyz(self): # PYCHOK no cover 

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

914 ''' 

915 return self.xyz 

916 

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

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

919 

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

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

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

923 

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

925 ''' 

926 t = sep.join(strs(self.xyz3, prec=prec)) 

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

928 

929 def unit(self, ll=None): 

930 '''Normalize this vector to unit length. 

931 

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

933 

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

935 ''' 

936 u = self._united 

937 if ll: 

938 u._fromll = ll 

939 return u 

940 

941 @Property_RO 

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

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

944 ''' 

945 n = self.length 

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

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

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

949 else: 

950 u = self.copy() 

951 u._update(False, _united=u) 

952 if self._fromll: 

953 u._fromll = self._fromll 

954 return u 

955 

956 @Property 

957 def x(self): 

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

959 ''' 

960 return self._x 

961 

962 @x.setter # PYCHOK setter! 

963 def x(self, x): 

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

965 ''' 

966 x = Float(x=x) 

967 if self._x != x: 

968 _update_all(self, needed=3) 

969 self._x = x 

970 

971 @Property 

972 def xyz(self): 

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

974 ''' 

975 return _MODS.namedTuples.Vector3Tuple(*self.xyz3, name=self.name) 

976 

977 @xyz.setter # PYCHOK setter! 

978 def xyz(self, xyz): 

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

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

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

982 ''' 

983 self._xyz(xyz) 

984 

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

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

987 ''' 

988 _update_all(self, needed=3) 

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

990 return self 

991 

992 @property_RO 

993 def xyz3(self): 

994 '''Get the X, Y and Z components as C{3-tuple}. 

995 ''' 

996 return self.x, self.y, self.z 

997 

998 @property_RO 

999 def x2y2z2(self): 

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

1001 ''' 

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

1003 

1004 @Property 

1005 def y(self): 

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

1007 ''' 

1008 return self._y 

1009 

1010 @y.setter # PYCHOK setter! 

1011 def y(self, y): 

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

1013 ''' 

1014 y = Float(y=y) 

1015 if self._y != y: 

1016 _update_all(self, needed=3) 

1017 self._y = y 

1018 

1019 @Property 

1020 def z(self): 

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

1022 ''' 

1023 return self._z 

1024 

1025 @z.setter # PYCHOK setter! 

1026 def z(self, z): 

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

1028 ''' 

1029 z = Float(z=z) 

1030 if self._z != z: 

1031 _update_all(self, needed=3) 

1032 self._z = z 

1033 

1034 

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

1036 '''(INTERNAL) Get , Y and Z as 3-tuple C{(x, y, z)}. 

1037 ''' 

1038 try: 

1039 xyz3 = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for Vector*Tuple 

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

1041 x_xyz.xyz) # .xyz3 

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

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

1044 return xyz3 

1045 

1046 

1047__all__ += _ALL_DOCS(Vector3dBase) 

1048 

1049# **) MIT License 

1050# 

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

1052# 

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

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

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

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

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

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

1059# 

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

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

1062# 

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

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

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

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

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

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

1069# OTHER DEALINGS IN THE SOFTWARE.