''' Three-dimension double-precision vector implementation
This is an implementation of Vector3d class. API is refered to Java3D_.
.. _Java3D: https://java3d.dev.java.net/
.. note::
The module is left for compatibility (some other modules depend
on this implementation).
For developing new module, there are no reasons of using this module.
Use ``numpy.array`` for normal purpose for vector manipulations
.. codeauthor:: Yoshifumi Futaana
'''
from math import *
import logging
_logger = logging.getLogger(__name__)
import warnings
warnings.warn('''The vector3d module is left for compatibility
(some other modules depend on this implementation).
For developing new module, there are no reasons of using this module.
Use ``numpy.array`` for normal purpose for vector manipulations''',
DeprecationWarning)
[docs]class Vector3d:
''' Double precision of 3-d vector, constituting of *x*, *y*, and *z*.
:param x: *x* component of the vector.
:type x: float
:param y: *y* component of the vector.
:type y: float
:param z: *z* component of the vector.
:type z: float
Simple to instance:
>>> v = Vector3d(5, 1, 2)
>>> print(v)
Vector3d( 5, 1, 2 )
Only float is acceptable as arguments.
The :meth:`clone` method deep-copies the vector.
Substite is reference copy.
>>> v1 = v
>>> id(v1) == id(v)
True
>>> v2 = v.clone()
>>> id(v2) == id(v)
False
>>> v2.equals(v)
True
Based on Java3D_, no accessor methods are available.
Thus, cccessing the element is using *.x*, *.y*, and *.z*
>>> print(v.x)
5
Apart from Java3D_, you can also get by index 0, 1, and 2.
This enable to use this class as array-like.
>>> print(v[2])
2
But not that this index-based access is slower than the direct
access by a factor of 2.
You can change the value by accessing .x, .y and .z
>>> v.y = -1
>>> print(v)
Vector3d( 5, -1, 2 )
This means that the Vector3d class is not immutable.
Thus, this instance cannot be used as a key of a dictionary.
Comparison is not defined.
:meth:`__eq__`, :meth:`__ne__`, :meth:`__cmp__`,
or any other comparator is not implemented.
:meth:`equals` and :meth:`epsilonEquals` support
checking equality by each component.
The API is based on Java3D_ API, thus methods are not
intuitive. For example, operaters like :math:`v_1+v_2`
are not supported. This isa Java3D_ policy:
Method should not instance, but user should instance
by himself.
'''
def __init__(self, x=0, y=0, z=0):
''' Constructor specifying all the argument. Deafult value is (0,0,0)
'''
self.x = x
self.y = y
self.z = z
def __str__(self):
return 'Vector3d( %g, %g, %g )' % (self.x, self.y, self.z)
def __repr__(self):
return 'Vector3d( %g, %g, %g) ' % (self.x, self.y, self.z)
def __getitem__(self, key):
''' Overload the [] access.
>>> v = Vector3d(1, 3.5, -3)
>>> v.x
1
>>> v[0]
1
>>> v[1]
3.5
>>> v[2]
-3
'''
if key == 0:
return self.x
elif key == 1:
return self.y
elif key == 2:
return self.z
else:
raise IndexError('Vector3d::Out of index.')
[docs] def length(self):
r''' Returns the length of this vector.
The length of the vector :math:`\mathbf{v}=(v_x, v_y, v_z)` is
:math:`\sqrt{v_x^2+v_y^2+v_z^2}`
'''
return sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2)
[docs] def lengthSquared(self):
''' Returns the squared length of this vector '''
return self.x ** 2 + self.y ** 2 + self.z ** 2
[docs] def angle(self, v1):
''' Returns the angle in radians between self vector and given vector
>>> v1 = Vector3d(1, 0, 0)
>>> v2 = Vector3d(0, 1, 0)
>>> print('%.1f' % (v1.angle(v2) * 180. / pi))
90.0
Note that there happens a rarely numerical effect that exceeds the
correct range of calculation.
For example, the following calculation will make "ValueError"
during cosine operation, unless specially treated.
>>> x = 0.939856076502615267465046144935
>>> y = 0.105396307433879829473788447558
>>> z = 0.324903329992804690284913249343
>>> v = Vector3d(x, y, z)
>>> v1 = Vector3d(x, y, z)
>>> print(v.angle(v1))
0.0
'''
inp = self.dot(v1)
ls = self.length()
l1 = v1.length()
inp = inp / ls / l1
try:
ang = acos(inp) # Theoretically, this should be [-1, 1]
return ang
except ValueError as e: # In rare case, numerically it violate the range
_logger.warning(
'Numerical range error happend for angle calculateion\n' +
('\t%.30f %.30f %.30f\n' % (self.x, self.y, self.z)) +
('\t%.30f %.30f %.30f\n' % (v1.x, v1.y, v1.z)) +
('Inner products = %.30f\nReturning 0 or 180 degs\n' % inp) +
'If the inner products are not very close to 1 or -1,' +
'I should think more carefully....')
if inp > 1:
return 0.
else:
return pi
[docs] def dot(self, v1):
''' Returns the inner product of this vector and the given vector '''
return self.x * v1.x + self.y * v1.y + self.z * v1.z
[docs] def cross(self, v1, v2):
''' Sets this vector to the outer product of the given vectors '''
self.x = v1.y * v2.z - v1.z * v2.y
self.y = v1.z * v2.x - v1.x * v2.z
self.z = v1.x * v2.y - v1.y * v2.x
[docs] def normalize(self, v1=None):
''' Normalize the vector.'''
if v1 is None:
v1 = self
l = v1.length()
try:
self.x = v1.x / l
self.y = v1.y / l
self.z = v1.z / l
except ZeroDivisionError as e:
raise ZeroDivisionError('Failed to normalize zero-length vector.')
[docs] def absolute(self, v1=None):
''' Set the each component of this vector to its aboslute values '''
if v1 is None:
v1 = self
self.x = abs(v1.x)
self.y = abs(v1.y)
self.z = abs(v1.z)
[docs] def add(self, v1, v2=None):
''' Sets the value of this vector to the sum of v1 and v2 (or self) '''
if v2 is None:
v2 = self
self.x = v1.x + v2.x
self.y = v1.y + v2.y
self.z = v1.z + v2.z
[docs] def sub(self, v1, v2=None):
''' Substitute vectors.
Set the self vector to be self=self-v1 if v2 is omitted.
Otherwise self=v1-v2 is set.
'''
if v2 is None:
self.x = self.x - v1.x
self.y = self.y - v1.y
self.z = self.z - v1.z
else:
self.x = v1.x - v2.x
self.y = v1.y - v2.y
self.z = v1.z - v2.z
[docs] def clamp(self, min, max):
''' Clamps the vector to the range [low,high]. '''
self.clampMax(max)
self.clampMin(min)
[docs] def clampMax(self, max, v1=None):
if v1 is None:
v1 = self
if v1.x > max:
v1.x = max
if v1.y > max:
v1.y = max
if v1.z > max:
v1.z = max
[docs] def clampMin(self, min, v1=None):
if v1 is None:
v1 = self
if v1.x < min:
v1.x = min
if v1.y < min:
v1.y = min
if v1.z < min:
v1.z = min
[docs] def clone(self):
return Vector3d(self.x, self.y, self.z)
[docs] def epsilonEquals(self, v1, epsilon):
''' Check whether the given vector is very close to self.
Returns True if L-infinite distance between this vector and v1
is less than epsilon.
>>> v1 = Vector3d(1.50, 2.80, 3.63)
>>> v2 = Vector3d(1.51, 2.82, 3.64)
>>> v1.epsilonEquals(v2, 0.1)
True
>>> v1.epsilonEquals(v2, 0.01)
False
'''
v3 = [abs(self.x - v1.x), abs(self.y - v1.y), abs(self.z - v1.z)]
l = max(v3)
_logger.debug('Diff=(%f,%f,%f) => Linf=%f' % (v3[0], v3[1], v3[2], l))
return l < epsilon
[docs] def equals(self, v1):
return (self.x == v1.x and self.y == v1.y and self.z == v1.z)
[docs] def getTuple(self):
''' Returns the three-element python tuple. '''
return (self.x, self.y, self.z)
[docs] def interpolate(self, alpha, v1, v2=None):
''' Returns the interplated vector.
Returns (1-alpha)*v1 + alpha*v2 or (1-alpha)*self+alpha*v1
Take care, this is different order of argument from Java3D API. '''
if v2 is None:
self.x = (1 - alpha) * self.x + alpha * v1.x
self.y = (1 - alpha) * self.y + alpha * v1.y
self.z = (1 - alpha) * self.z + alpha * v1.z
else:
self.x = (1 - alpha) * v1.x + alpha * v2.x
self.y = (1 - alpha) * v1.y + alpha * v2.y
self.z = (1 - alpha) * v1.z + alpha * v2.z
[docs] def negate(self, v1=None):
'''
>>> v = Vector3d(1, 2, 3)
>>> v.negate()
>>> print(v.x)
-1
'''
if v1 is None:
v1 = self
self.x = -v1.x
self.y = -v1.y
self.z = -v1.z
[docs] def scale(self, s, v1=None):
if v1 is None:
v1 = self
self.x = v1.x * s
self.y = v1.y * s
self.z = v1.z * s
[docs] def scaleAdd(self, s, v1, v2=None):
''' self = self*s+v1 (v2==None) or self = v1*s+v2 (v2!=None) '''
if v2 is None:
self.x = self.x * s + v1.x
self.y = self.y * s + v1.y
self.z = self.z * s + v1.z
else:
self.x = v1.x * s + v2.x
self.y = v1.y * s + v2.y
self.z = v1.z * s + v2.z
[docs] def setTuple(self, tuple):
''' Set a tuple to self.
>>> v = Vector3d()
>>> id0 = id(v)
>>> t = [1, 3, 5]
>>> v.setTuple(t)
>>> id0 == id(v)
True
>>> v.x
1
>>> v.y
3
>>> v.z
5
'''
self.x, self.y, self.z = tuple
import unittest
import doctest
[docs]def doctests():
return unittest.TestSuite((
doctest.DocTestSuite(),
))
if __name__ == '__main__':
unittest.main(defaultTest='doctests')