Coverage for pygeodesy/vector3dBase.py: 93%

275 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-10 14:08 -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, _signOf, _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 _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 

27# from pygeodesy.utily import sincos2 # _MODS 

28 

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

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

31 

32__all__ = _ALL_LAZY.vector3dBase 

33__version__ = '24.06.07' 

34 

35 

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

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

38 ''' 

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

40 

41 _ll = None # original latlon, '_fromll' 

42# _x = INT0 # X component 

43# _y = INT0 # Y component 

44# _z = INT0 # Z component 

45 

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

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

48 

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

50 distance from earth centre or height relative to the surface 

51 of the earth' sphere or ellipsoid. 

52 

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

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

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

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

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

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

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

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

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

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

63 

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

65 ''' 

66 self._x, \ 

67 self._y, \ 

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

69 _xyz3(type(self), x_xyz) 

70 if ll: 

71 self._ll = ll 

72 if name: 

73 self.name = name 

74 

75 def __abs__(self): 

76 '''Return the norm of this vector. 

77 

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

79 ''' 

80 return self.length 

81 

82 def __add__(self, other): 

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

84 

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

86 

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

88 ''' 

89 return self.plus(other) 

90 

91 def __bool__(self): # PYCHOK PyChecker 

92 '''Is this vector non-zero? 

93 ''' 

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

95 

96 def __ceil__(self): # PYCHOK no cover 

97 '''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.''' 

131 return _NotImplemented(self) 

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

219 

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

221 ''' 

222 v = self.classof(_0_0) 

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

224 return v 

225 

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

227 '''Not implemented.''' 

228 return _NotImplemented(self, other, *mod) 

229 

230 def __isub__(self, other): 

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

232 

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

234 

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

236 ''' 

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

238 

239# def __iter__(self): 

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

241# ''' 

242# return iter(self.xyz) 

243 

244 def __itruediv__(self, scalar): 

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

246 

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

248 

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

250 ''' 

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

252 

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

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

255 

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

257 

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

259 

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

261 ''' 

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

263 

264# def __len__(self): 

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

266# ''' 

267# return len(self.xyz) 

268 

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

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

271 

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

273 

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

275 

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

277 ''' 

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

279 

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

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

282 

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

284 

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

286 

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

288 ''' 

289 return self.cross(other) 

290 

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

292 '''Not implemented.''' 

293 return _NotImplemented(self, other) 

294 

295 def __mul__(self, scalar): 

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

297 

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

299 

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

301 ''' 

302 return self.times(scalar) 

303 

304 def __ne__(self, other): 

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

306 

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

308 

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

310 

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

312 ''' 

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

314 

315 def __neg__(self): 

316 '''Return the opposite of this vector. 

317 

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

319 ''' 

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

321 

322 def __pos__(self): # PYCHOK no cover 

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

324 

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

326 ''' 

327 return self if _pos_self else self.copy() 

328 

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

330 '''Not implemented.''' 

331 return _NotImplemented(self, other, *mod) 

332 

333 __radd__ = __add__ # PYCHOK no cover 

334 

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

336 '''Not implemented.''' 

337 return _NotImplemented(self, other) 

338 

339# def __repr__(self): 

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

341# ''' 

342# return self.toRepr() 

343 

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

345 '''Not implemented.''' 

346 return _NotImplemented(self, other) 

347 

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

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

350 

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

352 

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

354 

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

356 ''' 

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

358 

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

360 '''Not implemented.''' 

361 return _NotImplemented(self, other) 

362 

363 __rmul__ = __mul__ 

364 

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

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

367 

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

369 

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

371 ''' 

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

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

374 

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

376 '''Not implemented.''' 

377 return _NotImplemented(self, other, *mod) 

378 

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

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

381 

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

383 

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

385 

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

387 ''' 

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

389 

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

391 '''Not implemented.''' 

392 return _NotImplemented(self, scalar) 

393 

394# def __str__(self): 

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

396# ''' 

397# return self.toStr() 

398 

399 def __sub__(self, other): 

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

401 

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

403 

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

405 

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

407 ''' 

408 return self.minus(other) 

409 

410 def __truediv__(self, scalar): 

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

412 

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

414 

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

416 

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

418 ''' 

419 return self.dividedBy(scalar) 

420 

421 def __trunc__(self): # PYCHOK no cover 

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

423 

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

425 ''' 

426 return self._mapped(trunc) 

427 

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

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

430 __div__ = __truediv__ 

431 __idiv__ = __itruediv__ 

432 __long__ = __int__ 

433 __nonzero__ = __bool__ 

434 __rdiv__ = __rtruediv__ 

435 

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

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

438 

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

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

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

442 positive if this->other is clockwise looking 

443 along vSign or negative in opposite direction, 

444 otherwise angle is unsigned. 

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

446 

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

448 

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

450 ''' 

451 x = self.cross(other) 

452 s = x.length 

453 # use vSign as reference to set sign of s 

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

455 s = -s 

456 

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

458 if wrap and fabs(a) > PI: 

459 a -= _copysign(PI2, a) 

460 return a 

461 

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

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

464 of this and an other vector. 

465 

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

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

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

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

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

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

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

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

474 

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

476 

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

478 ''' 

479 _xcallable(fun2=fun2) 

480 if fun2_kwds: 

481 def _f2(a, b): 

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

483 else: 

484 _f2 = fun2 

485 

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

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

488 return self.classof(*xyz) 

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

495 non-L{NN}). 

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

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

498 

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

500 

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

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

503 

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

505 ''' 

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

507 x, y, z = self.xyz 

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

509 (z * X - Z * x), 

510 (x * Y - X * y)) 

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) 

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 \ 

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

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.x, self.y, self.z)) 

577 

578 def equirectangular(self, other): 

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

580 

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

582 

583 @return: The lenght 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.x, d.y, d.z)) 

591 

592 @Property 

593 def _fromll(self): 

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

595 ''' 

596 return self._ll 

597 

598 @_fromll.setter # PYCHOK setter! 

599 def _fromll(self, ll): 

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

601 ''' 

602 self._ll = ll or None 

603 

604 @property_RO 

605 def homogeneous(self): 

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

607 ''' 

608 x, y, z = self.xyz 

609 if z: 

610 x = x / z # /= chokes PyChecker 

611 y = y / z 

612# z = _1_0 

613 else: 

614 if isneg0(z): 

615 x = -x 

616 y = -y 

617 x = _copysignINF(x) 

618 y = _copysignINF(y) 

619# z = NAN 

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

621 

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

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

624 and an other vector. 

625 

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

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

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

629 

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

631 

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

633 ''' 

634 f = Scalar(fraction=fraction) 

635 if isnear0(f): # PYCHOK no cover 

636 r = self 

637 else: 

638 r = self.others(other) 

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

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

641 return r 

642 

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

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

645 

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

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

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

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

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

651 

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

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

654 

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

656 

657 @see: Method C{isequalTo}. 

658 ''' 

659 self.others(other) 

660 n = 0 

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

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

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

664 n += 1 # conjugate 

665 elif fabs(a - b) > eps: 

666 return False # unequal 

667 return bool(n >= minum) 

668 

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

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

671 

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

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

674 version of both vectors. 

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

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

677 

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

679 

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

681 

682 @see: Method C{isconjugateTo}. 

683 ''' 

684 if units: 

685 self.others(other) 

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

687 else: 

688 d = self.minus(other) 

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

690 

691 @Property_RO 

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

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

694 

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

696 ''' 

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

698 

699 @Property_RO 

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

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

702 

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

704 ''' 

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

706 

707 def _mapped(self, func): 

708 '''(INTERNAL) Map these components. 

709 ''' 

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

711 

712 def minus(self, other): 

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

714 

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

716 

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

718 

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

720 ''' 

721 xyz = self.others(other).xyz 

722 return self._minus(*xyz) 

723 

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

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

726 ''' 

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

728 

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

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

731 

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

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

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

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

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

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

738 

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

740 

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

742 ''' 

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

744 

745 def negate(self): 

746 '''Return this vector in opposite direction. 

747 

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

749 ''' 

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

751 

752 __neg__ = negate # PYCHOK no cover 

753 

754 @Property_RO 

755 def _N_vector(self): 

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

757 ''' 

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

759 

760 def _other_cmp(self, other): 

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

762 ''' 

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

764 

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

766 '''Refined class comparison. 

767 

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

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

770 keyword arguments. 

771 

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

773 

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

775 ''' 

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

777 if not isinstance(other, Vector3dBase): 

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

779 return other 

780 

781 def plus(self, other): 

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

783 

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

785 

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

787 

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

789 ''' 

790 xyz = self.others(other).xyz 

791 return self._plus(*xyz) 

792 

793 sum = plus # alternate name 

794 

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

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

797 ''' 

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

799 

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

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

802 

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

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

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

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

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

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

809 

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

811 

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

813 ''' 

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

815 

816 def rotate(self, axis, theta): 

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

818 

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

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

821 

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

823 

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

825 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and 

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

827 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}. 

828 ''' 

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

830 d = _1_0 - c 

831 if d or s: 

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

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

834 

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

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

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

838 

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

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

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

842 else: # unrotated 

843 x, y, z = self.xyz 

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

845 

846 @deprecated_method 

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

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

849 return self.rotate(axis, theta) 

850 

851 def times(self, factor): 

852 '''Multiply this vector by a scalar. 

853 

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

855 

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

857 

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

859 ''' 

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

861 

862 def _times(self, s): 

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

864 ''' 

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

866 

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

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

869 

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

871 X, Y, and Z components as scale factors 

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

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

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

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

876 

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

878 

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

880 ''' 

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

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

883 

884# @deprecated_method 

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

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

887# 

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

889# ''' 

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

891 

892# @deprecated_method 

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

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

895# 

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

897# ''' 

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

899 

900 @deprecated_method 

901 def to3xyz(self): # PYCHOK no cover 

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

903 ''' 

904 return self.xyz 

905 

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

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

908 

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

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

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

912 

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

914 ''' 

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

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

917 

918 def unit(self, ll=None): 

919 '''Normalize this vector to unit length. 

920 

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

922 

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

924 ''' 

925 u = self._united 

926 if ll: 

927 u._fromll = ll 

928 return u 

929 

930 @Property_RO 

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

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

933 ''' 

934 n = self.length 

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

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

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

938 else: 

939 u = self.copy() 

940 u._update(False, _united=u) 

941 if self._fromll: 

942 u._fromll = self._fromll 

943 return u 

944 

945 @Property 

946 def x(self): 

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

948 ''' 

949 return self._x 

950 

951 @x.setter # PYCHOK setter! 

952 def x(self, x): 

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

954 ''' 

955 x = Float(x=x) 

956 if self._x != x: 

957 _update_all(self, needed=3) 

958 self._x = x 

959 

960 @Property 

961 def xyz(self): 

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

963 ''' 

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

965 

966 @xyz.setter # PYCHOK setter! 

967 def xyz(self, xyz): 

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

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

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

971 ''' 

972 self._xyz(xyz) 

973 

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

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

976 ''' 

977 _update_all(self, needed=3) 

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

979 return self 

980 

981 @property_RO 

982 def x2y2z2(self): 

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

984 ''' 

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

986 

987 @Property 

988 def y(self): 

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

990 ''' 

991 return self._y 

992 

993 @y.setter # PYCHOK setter! 

994 def y(self, y): 

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

996 ''' 

997 y = Float(y=y) 

998 if self._y != y: 

999 _update_all(self, needed=3) 

1000 self._y = y 

1001 

1002 @Property 

1003 def z(self): 

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

1005 ''' 

1006 return self._z 

1007 

1008 @z.setter # PYCHOK setter! 

1009 def z(self, z): 

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

1011 ''' 

1012 z = Float(z=z) 

1013 if self._z != z: 

1014 _update_all(self, needed=3) 

1015 self._z = z 

1016 

1017 

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

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

1020 ''' 

1021 try: 

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

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

1024 x_xyz.xyz) 

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

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

1027 return x_y_z 

1028 

1029 

1030__all__ += _ALL_DOCS(Vector3dBase) 

1031 

1032# **) MIT License 

1033# 

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

1035# 

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

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

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

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

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

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

1042# 

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

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

1045# 

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

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

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

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

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

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

1052# OTHER DEALINGS IN THE SOFTWARE.