Coverage for pygeodesy/vector3dBase.py: 88%

1

2# -*- coding: utf-8 -*-

3

4 u'''(INTERNAL) Private, 3-D vector base class C{Vector3dBase}.

5

6 A pure Python implementation of vector-based functions by I{(C) Chris Veness

7 2011-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

11 from pygeodesy.basics import _copysign, islistuple, isscalar, \

12 map1, map2, _signOf, _zip

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

14 _copysignINF, isnear0, isnear1, \

15 _flipsign, _float0, _pos_self

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

17 from pygeodesy.fmath import euclid_, fdot, fdot_, hypot_, hypot2_ # _MODS.fmath.fma

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

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

20 from pygeodesy.named import _NamedBase, _NotImplemented, _xother3

21# from pygeodesy.namedTuples import Vector3Tuple # _MODS

22# from pygeodesy.nvectorBase import _N_Vector # _MODS

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

24 property_doc_, property_RO, _update_all

25 from pygeodesy.streprs import Fmt, strs, unstr

26 from pygeodesy.units import Float, Scalar

27 from pygeodesy.utily import atan2, sincos2, fabs

28

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

30

31 __all__ = _ALL_LAZY.vector3dBase

32 __version__ = '25.10.25'

33

34

35 class 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, scalar): # PYCHOK no cover

116 '''Apply C{scalar} divmod to this vector's components.

117

118 @arg scalar: Divisor (C{scalar}).

119

120 @return: 2-Tuple C{(d, m)} each a L{Vector3d}.

121 '''

122 s = Scalar(divisor=scalar)

123 d, m = [], []

124 for x in self.xyz3:

125 q, r = divmod(x, s)

126 d.append(q)

127 m.append(r)

128 return self.classof(d), self.classof(m)

129

130 def __eq__(self, other):

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

132

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

134

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

136

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

138 '''

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

140

141 def __float__(self): # PYCHOK no cover

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

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

144

145 def __floor__(self): # PYCHOK no cover

146 '''Return a vector with the C{floor} of each components.

147

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

149 '''

150 return self._mapped(_floor)

151

152 def __floordiv__(self, scalar): # PYCHOK no cover

153 '''Floor-divide this vector by a scalar, C{this // B{scalar}}.

154

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

156

157 @return: Floored quotient (L{Vector3d}).

158

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

160 '''

161 return self.floorDividedBy(scalar)

162

163 def __ge__(self, other):

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

165

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

167

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

169

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

171 '''

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

173

174# def __getitem__(self, key):

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

176# '''

177# return self.xyz[key]

178

179 def __gt__(self, other):

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

181

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

183

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

185

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

187 '''

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

189

190 def __hash__(self): # PYCHOK no cover

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

192 '''

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

194

195 def __iadd__(self, other):

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

197

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

199

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

201 '''

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

203

204 def __ifloordiv__(self, scalar): # PYCHOK no cover

205 '''Floor-divide this vector by a scalar I{in-place}, C{this //= B{scalar}}.

206

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

208

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

210 '''

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

212

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

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

215

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

217

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

219

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

221 '''

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

223

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

225 '''Not implemented.'''

226 return _NotImplemented(self, other)

227

228 def __imul__(self, scalar):

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

230

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

232

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

234 '''

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

236

237 def __int__(self): # PYCHOK no cover

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

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

240

241 def __ipow__(self, scalar, *mod): # PYCHOK no cover

242 '''Raise each component I{in-place} as C{pow(C, B{scalar})}.

243

244 @arg scalar: Exponent (C{scalar}).

245

246 @return: Power (L{Vector3d}).

247

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

249 '''

250 return self._xyz(self.pow(scalar, *mod))

251

252 def __isub__(self, other):

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

254

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

256

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

258 '''

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

260

261# def __iter__(self):

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

263# '''

264# return iter(self.xyz3)

265

266 def __itruediv__(self, scalar):

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

268

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

270

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

272 '''

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

274

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

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

277

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

279

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

281

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

283 '''

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

285

286# def __len__(self):

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

288# '''

289# return len(self.xyz)

290

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

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

293

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

295

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

297

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

299 '''

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

301

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

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

304

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

306

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

308

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

310 '''

311 return self.cross(other)

312

313 def __mod__(self, modulus): # PYCHOK no cover

314 '''Apply C{scalar} modulo to this vector's components.

315

316 @arg modulus: Modulus (C{scalar}).

317

318 @return: Modulo (L{Vector3d}).

319 '''

320 m = Scalar(modulus=modulus)

321

322 def _mod(x):

323 return x % m

324

325 return self._mapped(_mod)

326

327 def __mul__(self, scalar):

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

329

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

331

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

333 '''

334 return self.times(scalar)

335

336 def __ne__(self, other):

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

338

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

340

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

342

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

344 '''

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

346

347 def __neg__(self):

348 '''Return the opposite of this vector.

349

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

351 '''

352 return self.negate()

353

354 def __pos__(self): # PYCHOK no cover

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

356

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

358 '''

359 return self if _pos_self else self.copy()

360

361 def __pow__(self, scalar, *mod): # PYCHOK no cover

362 '''Return a vector with components as C{pow(C, B{scalar})}.

363

364 @arg scalar: Exponent (C{scalar}).

365

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

367

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

369 '''

370 return self.pow(scalar, *mod)

371

372 def __radd__(self, other): # PYCHOK no cover

373 '''Add this vector to an other vector, C{B{other} + this}.

374

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

376

377 @return: Sum (L{Vector3d}).

378

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

380 '''

381 return self.others(other).plus(self)

382

383 def __rdivmod__(self, other):

384 '''Not implemented.'''

385 return _NotImplemented(self, other)

386

387# def __repr__(self):

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

389# '''

390# return self.toRepr()

391

392 def __rfloordiv__(self, other):

393 '''Not implemented.'''

394 return _NotImplemented(self, other)

395

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

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

398

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

400

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

402

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

404 '''

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

406

407 def __rmod__(self, other):

408 '''Not implemented.'''

409 return _NotImplemented(self, other)

410

411 __rmul__ = __mul__ # scalar other

412

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

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

415

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

417

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

419 '''

420 def _rnd(x):

421 return round(x, *ndigits)

422

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

424 return self._mapped(_rnd)

425

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

427 '''Not implemented.'''

428 return _NotImplemented(self, other, *mod)

429

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

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

432

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

434

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

436

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

438 '''

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

440

441 def __rtruediv__(self, other):

442 '''Not implemented.'''

443 return _NotImplemented(self, other)

444

445# def __str__(self):

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

447# '''

448# return self.toStr()

449

450 def __sub__(self, other):

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

452

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

454

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

456

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

458 '''

459 return self.minus(other)

460

461 def __truediv__(self, scalar):

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

463

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

465

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

467

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

469 '''

470 return self.dividedBy(scalar)

471

472 def __trunc__(self): # PYCHOK no cover

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

474

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

476 '''

477 return self._mapped(_trunc)

478

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

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

481 __div__ = __truediv__

482 __idiv__ = __itruediv__

483 __long__ = __int__

484 __nonzero__ = __bool__

485 __rdiv__ = __rtruediv__

486

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

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

489

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

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

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

493 positive if this->other is clockwise looking

494 along vSign or negative in opposite direction,

495 otherwise angle is unsigned.

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

497

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

499

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

501 '''

502 x = self.cross(other)

503 s = x.length

504 # use vSign as reference to set sign of s

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

506 s = -s

507

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

509 if wrap and fabs(a) > PI:

510 a -= _copysign(PI2, a)

511 return a

512

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

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

515 of this and an other vector.

516

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

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

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

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

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

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

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

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

525

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

527

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

529 '''

530 _xcallable(fun2=fun2)

531 if fun2_kwds:

532 def _f2(a, b):

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

534 else:

535 _f2 = fun2

536

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

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

539 return self.classof(*xyz)

540

541 def bools(self):

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

543 '''

544 return self._mapped(bool)

545

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

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

548

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

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

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

552 C{z}.

553

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

555

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

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

558

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

560 '''

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

562 x, y, z = self.xyz3

563 xyz = (fdot_(y, Z, -z, Y),

564 fdot_(z, X, -x, Z),

565 fdot_(x, Y, -y, X))

566

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

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

569 r = other._fromll or other

570 s = self._fromll or self

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

572 t = _coincident_ if t else _colinear_

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

574

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

576

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

578 def crosserrors(self):

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

580 '''

581 return self._crosserrors

582

583 @crosserrors.setter # PYCHOK setter!

584 def crosserrors(self, raiser):

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

586 '''

587 self._crosserrors = bool(raiser)

588

589 def dividedBy(self, scalar):

590 '''Divide this vector by a scalar.

591

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

593

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

595

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

597

598 @raise VectorError: Invalid or zero B{C{scalar}}.

599 '''

600 d = Scalar(divisor=scalar)

601 try:

602 return self._times(_1_0 / d)

603 except (ValueError, ZeroDivisionError) as x:

604 raise VectorError(divisor=scalar, cause=x)

605

606 def dividedBy_(self, scalar_x, *y_z):

607 '''Divide this vector by separate X, Y and Z divisors.

608

609 @arg scalar_x: X divisor (C{scalar}) or a vector's

610 X, Y, and Z components as divisors

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

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

613 @arg y_z: Y and Z divisors (C{scalar}, C{scalar}), ignored

614 if B{C{scalar_x}} is not C{scalar}.

615

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

617

618 @raise VectorError: Invalid B{C{scalar_x}} or B{C{y_z}}.

619 '''

620 x, y, z = _xyz3(self.dividedBy_, scalar_x, *y_z)

621 return self.classof(self.x / x, self.y / y, self.z / z)

622

623 def dot(self, other):

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

625

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

627

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

629

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

631 '''

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

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

634

635 @deprecated_method

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

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

638 '''

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

640

641 @Property_RO

642 def euclid(self):

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

644

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

646 L{pygeodesy.euclid_}.

647 '''

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

649

650 def equirectangular(self, other):

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

652

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

654

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

656

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

658

659 @see: Property C{length2}.

660 '''

661 d = self.minus(other)

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

663

664 def fabs(self):

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

666 '''

667 return self._mapped(fabs)

668

669 def floats(self):

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

671 '''

672 return self._mapped(_float0)

673

674 def floorDividedBy(self, scalar):

675 '''Floor-divide this vector by a scalar.

676

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

678

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

680

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

682

683 @raise VectorError: Invalid or zero B{C{scalar}}.

684 '''

685 d = Scalar(divisor=scalar)

686 try:

687 def _floor_d(x):

688 return x // d

689

690 return self._mapped(_floor_d)

691 except (ValueError, ZeroDivisionError) as x:

692 raise VectorError(divisor=scalar, cause=x)

693

694 def floorDividedBy_(self, scalar_x, *y_z):

695 '''Floor-divide this vector by separate X, Y and Z divisors.

696

697 @arg scalar_x: X divisor (C{scalar}) or a vector's

698 X, Y, and Z components as divisors

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

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

701 @arg y_z: Y and Z divisors (C{scalar}, C{scalar}), ignored

702 if B{C{scalar_x}} is not C{scalar}.

703

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

705

706 @raise VectorError: Invalid B{C{scalar_x}} or B{C{y_z}}.

707 '''

708 x, y, z = _xyz3(self.floorDividedBy_, scalar_x, *y_z)

709 return self.classof(self.x // x, self.y // y, self.z // z)

710

711 @Property

712 def _fromll(self):

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

714 '''

715 return self._ll

716

717 @_fromll.setter # PYCHOK setter!

718 def _fromll(self, ll):

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

720 '''

721 self._ll = ll or None

722

723 @property_RO

724 def homogeneous(self):

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

726 '''

727 x, y, z = self.xyz3

728 if z:

729 x = x / z # /= chokes PyChecker

730 y = y / z

731# z = _1_0

732 else:

733 x = _copysignINF(_flipsign(x, z))

734 y = _copysignINF(_flipsign(y, z))

735# z = NAN

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

737

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

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

740 and an other vector.

741

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

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

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

745

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

747

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

749 '''

750 f = Scalar(fraction=fraction)

751 if isnear0(f): # PYCHOK no cover

752 r = self

753 else:

754 r = self.others(other)

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

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

757 return r

758

759 def ints(self):

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

761 '''

762 return self._mapped(int)

763

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

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

766

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

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

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

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

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

772

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

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

775

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

777

778 @see: Method C{isequalTo}.

779 '''

780 self.others(other)

781 n = 0

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

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

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

785 n += 1 # conjugate

786 elif fabs(a - b) > eps:

787 return False # unequal

788 return bool(n >= minum)

789

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

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

792

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

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

795 version of both vectors.

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

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

798

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

800

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

802

803 @see: Method C{isconjugateTo}.

804 '''

805 if units:

806 self.others(other)

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

808 else:

809 d = self.minus(other)

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

811

812 @Property_RO

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

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

815

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

817 '''

818 return Float(length=hypot_(*self.xyz3))

819

820 @Property_RO

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

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

823

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

825 '''

826 return Float(length2=hypot2_(*self.xyz3))

827

828 def _mapped(self, func):

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

830 '''

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

832

833 def minus(self, other):

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

835

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

837

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

839

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

841 '''

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

843

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

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

846 '''

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

848

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

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

851

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

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

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

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

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

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

858

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

860

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

862 '''

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

864

865 def negate(self):

866 '''Return the opposite of this vector.

867

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

869 '''

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

871

872 @Property_RO

873 def _N_vector(self):

874 '''(INTERNAL) Get the (C{nvectorBase._N_Vector})

875 '''

876 _N = _MODS.nvectorBase._N_Vector

877 return _N(*self.xyz3, name=self.name)

878

879 def _other_cmp(self, other):

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

881 '''

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

883

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

885 '''Refined class comparison.

886

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

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

889 keyword arguments.

890

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

892

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

894 '''

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

896 if not isinstance(other, Vector3dBase):

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

898 return other

899

900 def plus(self, other):

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

902

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

904

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

906

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

908 '''

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

910

911 sum = plus # alternate name

912

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

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

915 '''

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

917

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

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

920

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

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

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

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

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

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

927

928 @return: New, vectorial vector (L{Vector3d}).

929

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

931 '''

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

933

934 def pow(self, scalar, *mod):

935 '''Raise each X, Y and Z to C{scalar} power.

936

937 @arg scalar: Exponent (C{scalar}).

938

939 @return: Power (L{Vector3d}).

940

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

942 '''

943 p = Scalar(power=scalar)

944

945 def _pow(x):

946 return pow(x, p, *mod)

947

948 return self._mapped(_pow)

949

950 def rotate(self, axis, theta, fma=False):

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

952

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

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

955 @kwarg fma: If C{True}, use fused-multiply-add (C{bool}).

956

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

958

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

960 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and

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

962 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.

963 '''

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

965 d = _1_0 - c

966 if d or s:

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

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

969

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

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

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

973

974 if fma:

975 _fma = _MODS.fmath.fma

976 else:

977 def _fma(a, b, c):

978 return a * b + c

979

980 x = fdot(p, _fma(ax, bx, c), _fma(ax, by, -sz), _fma(ax, bz, sy))

981 y = fdot(p, _fma(ay, bx, sz), _fma(ay, by, c), _fma(ay, bz, -sx))

982 z = fdot(p, _fma(az, bx, -sy), _fma(az, by, sx), _fma(az, bz, c))

983 else: # unrotated

984 x, y, z = self.xyz3

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

986

987 @deprecated_method

988 def rotateAround(self, axis, theta):

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

990 return self.rotate(axis, theta) # PYCHOK no cover

991

992 def _roty(self, *pos, **name):

993 '''(INTERNAL) Prolate rotation, for C{+1 if pos else -1}.

994 '''

995 v = self.copy(**name)

996 if pos:

997 x, _, z = v.xyz3

998 _update_all(v, needed=3)

999 v._x, v._z = (-z, x) if pos[0] else (z, -x)

1000 return v

1001

1002 def times(self, factor):

1003 '''Multiply this vector by a scalar.

1004

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

1006

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

1008

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

1010 '''

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

1012

1013 def _times(self, s):

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

1015 '''

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

1017

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

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

1020

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

1022 X, Y, and Z components as scale factors

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

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

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

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

1027

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

1029

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

1031 '''

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

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

1034

1035# @deprecated_method

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

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

1038#

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

1040# '''

1041# return _MODS.nvectorBase.n_xyz2philam(self.x, self.y, self.z)

1042

1043# @deprecated_method

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

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

1046#

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

1048# '''

1049# return _MODS.nvectorBase.n_xyz2latlon(self.x, self.y, self.z)

1050

1051 @deprecated_method

1052 def to3xyz(self): # PYCHOK no cover

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

1054 '''

1055 return self.xyz

1056

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

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

1059

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

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

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

1063

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

1065 '''

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

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

1068

1069 def unit(self, ll=None):

1070 '''Normalize this vector to unit length.

1071

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

1073

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

1075 '''

1076 u = self._united

1077 if ll:

1078 u._fromll = ll

1079 return u

1080

1081 @Property_RO

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

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

1084 '''

1085 n = self.length

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

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

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

1089 else:

1090 u = self.copy()

1091 u._update(False, _united=u)

1092 if self._fromll:

1093 u._fromll = self._fromll

1094 return u

1095

1096 @Property

1097 def x(self):

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

1099 '''

1100 return self._x

1101

1102 @x.setter # PYCHOK setter!

1103 def x(self, x):

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

1105 '''

1106 x = Float(x=x)

1107 if self._x != x:

1108 _update_all(self, needed=3)

1109 self._x = x

1110

1111 @Property

1112 def xyz(self):

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

1114 '''

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

1116

1117 @xyz.setter # PYCHOK setter!

1118 def xyz(self, xyz):

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

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

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

1122 '''

1123 self._xyz(xyz)

1124

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

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

1127 '''

1128 xyz = _xyz3(_xyz_, x_xyz, *y_z)

1129 if self.xyz3 != xyz:

1130 _update_all(self, needed=3)

1131 self._x, self._y, self._z = xyz

1132 return self

1133

1134 @property_RO

1135 def xyz3(self):

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

1137 '''

1138 return self.x, self.y, self.z

1139

1140 @Property_RO

1141 def x2y2z2(self):

1142 '''Get the X, Y and Z components, I{squared} (L{Vector3Tuple}).

1143 '''

1144 return _MODS.namedTuples.Vector3Tuple(*self.x2y2z23, name=self.name)

1145

1146 @property_RO

1147 def x2y2z23(self):

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

1149 '''

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

1151

1152 @Property

1153 def y(self):

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

1155 '''

1156 return self._y

1157

1158 @y.setter # PYCHOK setter!

1159 def y(self, y):

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

1161 '''

1162 y = Float(y=y)

1163 if self._y != y:

1164 _update_all(self, needed=3)

1165 self._y = y

1166

1167 @Property

1168 def z(self):

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

1170 '''

1171 return self._z

1172

1173 @z.setter # PYCHOK setter!

1174 def z(self, z):

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

1176 '''

1177 z = Float(z=z)

1178 if self._z != z:

1179 _update_all(self, needed=3)

1180 self._z = z

1181

1182

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

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

1185 '''

1186 try:

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

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

1189 x_xyz.xyz) # .xyz3

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

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

1192 return xyz3

1193

1194

1195 __all__ += _ALL_DOCS(Vector3dBase)

1196

1197# **) MIT License

1198#

1199# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.

1200#

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

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

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

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

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

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

1207#

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

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

1210#

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

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

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

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

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

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

1217# OTHER DEALINGS IN THE SOFTWARE.