Coverage for pygeodesy/vector3dBase.py: 91%

281 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-22 18:16 -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-2024} 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 

20from pygeodesy.named import _NamedBase, _NotImplemented, _xother3 

21# from pygeodesy.namedTuples import Vector3Tuple # _MODS 

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

23 property_doc_, property_RO, _update_all 

24from pygeodesy.streprs import Fmt, strs, unstr 

25from pygeodesy.units import Float, Scalar 

26from pygeodesy.utily import sincos2, atan2, fabs 

27 

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

29 

30__all__ = _ALL_LAZY.vector3dBase 

31__version__ = '24.10.12' 

32 

33 

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

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

36 ''' 

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

38 

39 _ll = None # original latlon, '_fromll' 

40# _x = INT0 # X component 

41# _y = INT0 # Y component 

42# _z = INT0 # Z component 

43 

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

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

46 

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

48 distance from earth centre or height relative to the surface 

49 of the earth' sphere or ellipsoid. 

50 

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

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

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

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

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

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

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

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

61 

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

63 ''' 

64 self._x, \ 

65 self._y, \ 

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

67 _xyz3(type(self), x_xyz) 

68 if ll: 

69 self._ll = ll 

70 if name: 

71 self.name = name 

72 

73 def __abs__(self): 

74 '''Return the norm of this vector. 

75 

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

77 ''' 

78 return self.length 

79 

80 def __add__(self, other): 

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

82 

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

84 

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

86 ''' 

87 return self.plus(other) 

88 

89 def __bool__(self): # PYCHOK PyChecker 

90 '''Is this vector non-zero? 

91 

92 @see: Method C{bools}. 

93 ''' 

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

95 

96 def __ceil__(self): # PYCHOK no cover 

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

98 

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

100 ''' 

101 return self._mapped(_ceil) 

102 

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

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

105 

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

107 

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

109 ''' 

110 return _signOf(self.length, self._other_cmp(other)) 

111 

112 cmp = __cmp__ 

113 

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

115 '''Not implemented.''' 

116 return _NotImplemented(self, other) 

117 

118 def __eq__(self, other): 

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

120 

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

122 

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

124 

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

126 ''' 

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

128 

129 def __float__(self): # PYCHOK no cover 

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

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

132 

133 def __floor__(self): # PYCHOK no cover 

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

135 

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

137 ''' 

138 return self._mapped(_floor) 

139 

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

141 '''Not implemented.''' 

142 return _NotImplemented(self, other) 

143 

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

145 '''Not implemented.''' 

146 return _NotImplemented(self, *other) 

147 

148 def __ge__(self, other): 

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

150 

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

152 

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

154 

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

156 ''' 

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

158 

159# def __getitem__(self, key): 

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

161# ''' 

162# return self.xyz[key] 

163 

164 def __gt__(self, other): 

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

166 

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

168 

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

170 

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

172 ''' 

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

174 

175 def __hash__(self): # PYCHOK no cover 

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

177 ''' 

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

179 

180 def __iadd__(self, other): 

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

182 

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

184 

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

186 ''' 

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

188 

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

190 '''Not implemented.''' 

191 return _NotImplemented(self, other) 

192 

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

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

195 

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

197 

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

199 

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

201 ''' 

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

203 

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

205 '''Not implemented.''' 

206 return _NotImplemented(self, other) 

207 

208 def __imul__(self, scalar): 

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

210 

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

212 

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

214 ''' 

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

216 

217 def __int__(self): # PYCHOK no cover 

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

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

220 

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

222 '''Not implemented.''' 

223 return _NotImplemented(self, other, *mod) 

224 

225 def __isub__(self, other): 

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

227 

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

229 

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

231 ''' 

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

233 

234# def __iter__(self): 

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

236# ''' 

237# return iter(self.xyz3) 

238 

239 def __itruediv__(self, scalar): 

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

241 

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

243 

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

245 ''' 

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

247 

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

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

250 

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

252 

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

254 

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

256 ''' 

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

258 

259# def __len__(self): 

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

261# ''' 

262# return len(self.xyz) 

263 

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

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

266 

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

268 

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

270 

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

272 ''' 

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

274 

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

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

277 

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

279 

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

281 

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

283 ''' 

284 return self.cross(other) 

285 

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

287 '''Not implemented.''' 

288 return _NotImplemented(self, other) 

289 

290 def __mul__(self, scalar): 

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

292 

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

294 

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

296 ''' 

297 return self.times(scalar) 

298 

299 def __ne__(self, other): 

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

301 

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

303 

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

305 

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

307 ''' 

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

309 

310 def __neg__(self): 

311 '''Return the opposite of this vector. 

312 

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

314 ''' 

315 return self.negate() 

316 

317 def __pos__(self): # PYCHOK no cover 

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

319 

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

321 ''' 

322 return self if _pos_self else self.copy() 

323 

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

325 '''Not implemented.''' 

326 return _NotImplemented(self, other, *mod) 

327 

328 __radd__ = __add__ # PYCHOK no cover 

329 

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

331 '''Not implemented.''' 

332 return _NotImplemented(self, other) 

333 

334# def __repr__(self): 

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

336# ''' 

337# return self.toRepr() 

338 

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

340 '''Not implemented.''' 

341 return _NotImplemented(self, other) 

342 

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

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

345 

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

347 

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

349 

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

351 ''' 

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

353 

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

355 '''Not implemented.''' 

356 return _NotImplemented(self, other) 

357 

358 __rmul__ = __mul__ 

359 

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

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

362 

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

364 

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

366 ''' 

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

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

369 

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

371 '''Not implemented.''' 

372 return _NotImplemented(self, other, *mod) 

373 

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

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

376 

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

378 

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

380 

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

382 ''' 

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

384 

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

386 '''Not implemented.''' 

387 return _NotImplemented(self, scalar) 

388 

389# def __str__(self): 

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

391# ''' 

392# return self.toStr() 

393 

394 def __sub__(self, other): 

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

396 

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

398 

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

400 

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

402 ''' 

403 return self.minus(other) 

404 

405 def __truediv__(self, scalar): 

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

407 

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

409 

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

411 

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

413 ''' 

414 return self.dividedBy(scalar) 

415 

416 def __trunc__(self): # PYCHOK no cover 

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

418 

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

420 ''' 

421 return self._mapped(_trunc) 

422 

423 if _MODS.sys_version_info2 < (3, 0): # PYCHOK no cover 

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

425 __div__ = __truediv__ 

426 __idiv__ = __itruediv__ 

427 __long__ = __int__ 

428 __nonzero__ = __bool__ 

429 __rdiv__ = __rtruediv__ 

430 

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

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

433 

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

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

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

437 positive if this->other is clockwise looking 

438 along vSign or negative in opposite direction, 

439 otherwise angle is unsigned. 

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

441 

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

443 

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

445 ''' 

446 x = self.cross(other) 

447 s = x.length 

448 # use vSign as reference to set sign of s 

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

450 s = -s 

451 

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

453 if wrap and fabs(a) > PI: 

454 a -= _copysign(PI2, a) 

455 return a 

456 

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

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

459 of this and an other vector. 

460 

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

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

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

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

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

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

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

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

469 

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

471 

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

473 ''' 

474 _xcallable(fun2=fun2) 

475 if fun2_kwds: 

476 def _f2(a, b): 

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

478 else: 

479 _f2 = fun2 

480 

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

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

483 return self.classof(*xyz) 

484 

485 def bools(self): 

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

487 ''' 

488 return self._mapped(bool) 

489 

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

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

492 

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

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

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

496 C{z}. 

497 

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

499 

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

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

502 

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

504 ''' 

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

506 x, y, z = self.xyz3 

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

508 (z * X - x * Z), 

509 (x * Y - y * X)) 

510 

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

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

513 r = other._fromll or other 

514 s = self._fromll or self 

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

516 t = _coincident_ if t else _colinear_ 

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

518 

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

520 

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

522 def crosserrors(self): 

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

524 ''' 

525 return self._crosserrors 

526 

527 @crosserrors.setter # PYCHOK setter! 

528 def crosserrors(self, raiser): 

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

530 ''' 

531 self._crosserrors = bool(raiser) 

532 

533 def dividedBy(self, divisor): 

534 '''Divide this vector by a scalar. 

535 

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

537 

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

539 

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

541 

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

543 ''' 

544 d = Scalar(divisor=divisor) 

545 try: 

546 return self._times(_1_0 / d) 

547 except (ValueError, ZeroDivisionError) as x: 

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

549 

550 def dot(self, other): 

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

552 

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

554 

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

556 

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

558 ''' 

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

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

561 

562 @deprecated_method 

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

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

565 ''' 

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

567 

568 @Property_RO 

569 def euclid(self): 

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

571 

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

573 L{pygeodesy.euclid_}. 

574 ''' 

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

576 

577 def equirectangular(self, other): 

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

579 

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

581 

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

583 

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

585 

586 @see: Property C{length2}. 

587 ''' 

588 d = self.minus(other) 

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

590 

591 def fabs(self): 

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

593 ''' 

594 return self._mapped(fabs) 

595 

596 def floats(self): 

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

598 ''' 

599 return self._mapped(_float0) 

600 

601 @Property 

602 def _fromll(self): 

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

604 ''' 

605 return self._ll 

606 

607 @_fromll.setter # PYCHOK setter! 

608 def _fromll(self, ll): 

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

610 ''' 

611 self._ll = ll or None 

612 

613 @property_RO 

614 def homogeneous(self): 

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

616 ''' 

617 x, y, z = self.xyz3 

618 if z: 

619 x = x / z # /= chokes PyChecker 

620 y = y / z 

621# z = _1_0 

622 else: 

623 if isneg0(z): 

624 x = -x 

625 y = -y 

626 x = _copysignINF(x) 

627 y = _copysignINF(y) 

628# z = NAN 

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

630 

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

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

633 and an other vector. 

634 

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

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

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

638 

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

640 

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

642 ''' 

643 f = Scalar(fraction=fraction) 

644 if isnear0(f): # PYCHOK no cover 

645 r = self 

646 else: 

647 r = self.others(other) 

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

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

650 return r 

651 

652 def ints(self): 

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

654 ''' 

655 return self._mapped(int) 

656 

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

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

659 

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

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

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

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

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

665 

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

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

668 

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

670 

671 @see: Method C{isequalTo}. 

672 ''' 

673 self.others(other) 

674 n = 0 

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

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

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

678 n += 1 # conjugate 

679 elif fabs(a - b) > eps: 

680 return False # unequal 

681 return bool(n >= minum) 

682 

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

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

685 

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

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

688 version of both vectors. 

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

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

691 

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

693 

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

695 

696 @see: Method C{isconjugateTo}. 

697 ''' 

698 if units: 

699 self.others(other) 

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

701 else: 

702 d = self.minus(other) 

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

704 

705 @Property_RO 

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

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

708 

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

710 ''' 

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

712 

713 @Property_RO 

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

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

716 

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

718 ''' 

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

720 

721 def _mapped(self, func): 

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

723 ''' 

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

725 

726 def minus(self, other): 

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

728 

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

730 

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

732 

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

734 ''' 

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

736 

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

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

739 ''' 

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

741 

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

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

744 

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

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

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

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

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

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

751 

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

753 

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

755 ''' 

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

757 

758 def negate(self): 

759 '''Return the opposite of this vector. 

760 

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

762 ''' 

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

764 

765 @Property_RO 

766 def _N_vector(self): 

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

768 ''' 

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

770 

771 def _other_cmp(self, other): 

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

773 ''' 

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

775 

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

777 '''Refined class comparison. 

778 

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

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

781 keyword arguments. 

782 

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

784 

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

786 ''' 

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

788 if not isinstance(other, Vector3dBase): 

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

790 return other 

791 

792 def plus(self, other): 

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

794 

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

796 

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

798 

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

800 ''' 

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

802 

803 sum = plus # alternate name 

804 

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

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

807 ''' 

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

809 

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

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

812 

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

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

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

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

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

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

819 

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

821 

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

823 ''' 

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

825 

826 def rotate(self, axis, theta): 

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

828 

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

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

831 

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

833 

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

835 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and 

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

837 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}. 

838 ''' 

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

840 d = _1_0 - c 

841 if d or s: 

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

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

844 

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

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

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

848 

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

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

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

852 else: # unrotated 

853 x, y, z = self.xyz3 

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

855 

856 @deprecated_method 

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

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

859 return self.rotate(axis, theta) 

860 

861 def times(self, factor): 

862 '''Multiply this vector by a scalar. 

863 

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

865 

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

867 

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

869 ''' 

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

871 

872 def _times(self, s): 

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

874 ''' 

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

876 

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

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

879 

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

881 X, Y, and Z components as scale factors 

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

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

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

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

886 

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

888 

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

890 ''' 

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

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

893 

894# @deprecated_method 

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

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

897# 

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

899# ''' 

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

901 

902# @deprecated_method 

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

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

905# 

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

907# ''' 

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

909 

910 @deprecated_method 

911 def to3xyz(self): # PYCHOK no cover 

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

913 ''' 

914 return self.xyz 

915 

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

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

918 

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

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

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

922 

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

924 ''' 

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

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

927 

928 def unit(self, ll=None): 

929 '''Normalize this vector to unit length. 

930 

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

932 

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

934 ''' 

935 u = self._united 

936 if ll: 

937 u._fromll = ll 

938 return u 

939 

940 @Property_RO 

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

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

943 ''' 

944 n = self.length 

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

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

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

948 else: 

949 u = self.copy() 

950 u._update(False, _united=u) 

951 if self._fromll: 

952 u._fromll = self._fromll 

953 return u 

954 

955 @Property 

956 def x(self): 

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

958 ''' 

959 return self._x 

960 

961 @x.setter # PYCHOK setter! 

962 def x(self, x): 

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

964 ''' 

965 x = Float(x=x) 

966 if self._x != x: 

967 _update_all(self, needed=3) 

968 self._x = x 

969 

970 @Property 

971 def xyz(self): 

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

973 ''' 

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

975 

976 @xyz.setter # PYCHOK setter! 

977 def xyz(self, xyz): 

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

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

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

981 ''' 

982 self._xyz(xyz) 

983 

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

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

986 ''' 

987 _update_all(self, needed=3) 

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

989 return self 

990 

991 @property_RO 

992 def xyz3(self): 

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

994 ''' 

995 return self.x, self.y, self.z 

996 

997 @property_RO 

998 def x2y2z2(self): 

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

1000 ''' 

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

1002 

1003 @Property 

1004 def y(self): 

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

1006 ''' 

1007 return self._y 

1008 

1009 @y.setter # PYCHOK setter! 

1010 def y(self, y): 

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

1012 ''' 

1013 y = Float(y=y) 

1014 if self._y != y: 

1015 _update_all(self, needed=3) 

1016 self._y = y 

1017 

1018 @Property 

1019 def z(self): 

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

1021 ''' 

1022 return self._z 

1023 

1024 @z.setter # PYCHOK setter! 

1025 def z(self, z): 

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

1027 ''' 

1028 z = Float(z=z) 

1029 if self._z != z: 

1030 _update_all(self, needed=3) 

1031 self._z = z 

1032 

1033 

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

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

1036 ''' 

1037 try: 

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

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

1040 x_xyz.xyz) # .xyz3 

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

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

1043 return xyz3 

1044 

1045 

1046__all__ += _ALL_DOCS(Vector3dBase) 

1047 

1048# **) MIT License 

1049# 

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

1051# 

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

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

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

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

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

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

1058# 

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

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

1061# 

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

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

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

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

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

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

1068# OTHER DEALINGS IN THE SOFTWARE.