Source code for Musica.Geometry.Vector

####################################################################################################
#
# Musica - A Music Theory Package for Python
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################

####################################################################################################

import math

import numpy as np

from IntervalArithmetic import Interval2D, IntervalInt2D

####################################################################################################

from .MathFunctions import sign, trignometric_clamp #, is_in_trignometric_range
from .Primitive import Primitive2D

####################################################################################################

[docs]class VectorAbc(Primitive2D): __data_type__ = None ##############################################
[docs] def __init__(self, array_size, v_size): # Fixme: v_size versus homogeneous self._array = np.zeros(array_size, dtype=self.__data_type__) self._v = self._array[:v_size] # view
##############################################
[docs] def clone(self): """ Return a copy of self """ return self.__class__(self)
############################################## @property def array(self): return self._array @property def v(self): return self._v # @v.setter # def v(self, value): # self._v = value @property def x(self): return self.__data_type__(self._array[0]) @property def y(self): return self.__data_type__(self._array[1]) @x.setter def x(self, x): self._array[0] = x @y.setter def y(self, y): self._array[1] = y ##############################################
[docs] def __repr__(self): return self.__class__.__name__ + str(self._array)
##############################################
[docs] def __len__(self): return NotImplementedError
##############################################
[docs] def __iter__(self): return iter(self._array)
##############################################
[docs] def __getitem__(self, a_slice): return self._array[a_slice]
##############################################
[docs] def __setitem__(self, index, value): self._array[index] = value
##############################################
[docs] def to_numpy(self): return np.array(self._array, dtype=self.__data_type__)
##############################################
[docs] def to_int_list(self): return [int(x) for x in self]
##############################################
[docs] def __nonzero__(self): return bool(self._array.any())
##############################################
[docs] def __eq__(v1, v2): """ self == other """ return np.array_equal(v1._array, v2.array)
##############################################
[docs] def transform(self, transformation): array = np.matmul(transformation.array, np.transpose(self._array)) return self.__class__(array)
####################################################################################################
[docs]class VectorArithmeticMixin: ##############################################
[docs] def __add__(self, other): """ Return a new vector equal to the addition of self and other """ return self.__class__(self._v + other.v)
##############################################
[docs] def __iadd__(self, other): """ Add other to self """ self._v += other.v return self
##############################################
[docs] def __sub__(self, other): """ Return a new vector """ return self.__class__(self._v - other.v)
##############################################
[docs] def __isub__(self, other): """ Return a new vector equal to the subtraction of self and other """ self._v -= other.v return self
##############################################
[docs] def __pos__(self): """ Return a new vector equal to self """ return self.__class__(self._v)
##############################################
[docs] def __neg__(self): """ Return a new vector equal to the negation of self """ return self.__class__(-self._v)
##############################################
[docs] def __abs__(self): """ Return a new vector equal to abs of self """ return self.__class__(np.abs(self._v))
##############################################
[docs] def __mul__(self, scale): """ Return a new vector equal to the self scaled by scale """ return self.__class__(scale * self._v)
##############################################
[docs] def __imul__(self, scale): """ Scale self by scale """ self._v *= scale return self
##############################################
[docs] def magnitude_square(self): """ Return the square of the magnitude of the vector """ return np.dot(self._v, self._v)
##############################################
[docs] def normal(self): """ Return a new vector equal to self rotated of 90 degree in the counter clockwise direction """ xp = -self._v[1] yp = self._v[0] return self.__class__((xp, yp))
##############################################
[docs] def anti_normal(self): """ Return a new vector equal to self rotated of 90 degree in the clockwise direction """ xp = self._v[1] yp = -self._v[0] return self.__class__((xp, yp))
##############################################
[docs] def parity(self): """ Return a new vector equal to self rotated of 180 degree """ # parity xp = -self._v[0] yp = -self._v[1] return self.__class__((xp, yp))
##############################################
[docs] def dot(self, other): """ Return the dot product of self with other """ return self.__data_type__(np.dot(self._v, other.v))
##############################################
[docs] def cross(self, other): """ Return the cross product of self with other """ return self.__data_type__(np.cross(self._v, other.v))
####################################################################################################
[docs]class VectorIntMixin: __data_type__ = np.int ##############################################
[docs] def bounding_box(self): x, y = self.x, self.y return IntervalInt2D((x, x) , (y, y))
####################################################################################################
[docs]class VectorFloatMixin: __data_type__ = np.float ##############################################
[docs] def bounding_box(self): x, y = self.x, self.y return Interval2D((x, x) , (y, y))
##############################################
[docs] def almost_equal(v1, v2, rtol=1e-05, atol=1e-08, equal_nan=False): """ self ~= other """ return np.allclose(tuple(v1), tuple(v2), rtol, atol, equal_nan)
##############################################
[docs] def __truediv__(self, scale): """ Return a new vector equal to the self dvivided by scale """ return self.__class__(self._v / scale)
##############################################
[docs] def __itruediv__(self, scale): """ Scale self by 1/scale """ self._v /= scale return self
##############################################
[docs] def normalise(self): """ Normalise the vector """ self._v /= self.magnitude()
##############################################
[docs] def magnitude(self): """ Return the magnitude of the vector """ return math.sqrt(self.magnitude_square())
##############################################
[docs] def orientation(self): """ Return the orientation in degree """ # # 2 | 1 # - + - # 4 | 3 # # | 1 | 2 | 3 | 4 | # x | + | - | + | - | # y | + | + | - | - | # tan | + | - | - | + | # atan | + | - | - | + | # theta | atan | atan + pi | atan | atan - pi | # if not bool(self): raise NameError("Null Vector") if self.x == 0: return math.copysign(90, self.y) elif self.y == 0: return 0 if self.x >= 0 else 180 else: orientation = math.degrees(math.atan(self.tan())) if self.x < 0: if self.y > 0: orientation += 180 else: orientation -= 180 return orientation
##############################################
[docs] def rotate(self, angle, counter_clockwise=True): """ Return a new vector equal to self rotated of angle degree in the counter clockwise direction """ radians = math.radians(angle) if not counter_clockwise: radians = -radians c = math.cos(radians) s = math.sin(radians) # Fixme: np matrice xp = c * self._v[0] -s * self._v[1] yp = s * self._v[0] +c * self._v[1] return self.__class__((xp, yp))
##############################################
[docs] def tan(self): """ Return the tangent """ # RuntimeWarning: divide by zero encountered in double_scalars return self.y / self.x
##############################################
[docs] def inverse_tan(self): """ Return the inverse tangent """ return self.x / self.y
##############################################
[docs] def is_parallel(self, other): """ Self is parallel with other """ return round(self.cross(other), 7) == 0
##############################################
[docs] def is_orthogonal(self, other): """ Self is orthogonal with other """ return round(self.dot(other), 7) == 0
##############################################
[docs] def cos_with(self, direction): """ Return the cosinus of self with direction """ cos = direction.dot(self) / (direction.magnitude() * self.magnitude()) return trignometric_clamp(cos)
##############################################
[docs] def projection_on(self, direction): """ Return the projection of self on direction """ return direction.dot(self) / direction.magnitude()
##############################################
[docs] def sin_with(self, direction): """ Return the sinus of self with other """ # turn from direction to self sin = direction.cross(self) / (direction.magnitude() * self.magnitude()) return trignometric_clamp(sin)
##############################################
[docs] def deviation_with(self, direction): """ Return the deviation of self with other """ return direction.cross(self) / direction.magnitude()
##############################################
[docs] def orientation_with(self, direction): # Fixme: check all cases # -> angle_with """ Return the angle of self on direction """ angle = math.acos(self.cos_with(direction)) angle_sign = sign(self.sin_with(direction)) return angle_sign * math.degrees(angle)
####################################################################################################
[docs]class Vector2DBase(VectorAbc): ##############################################
[docs] def __init__(self, *args): """ Example of usage:: Vector(1, 3) Vector((1, 3)) Vector([1, 3]) Vector(iterable) Vector(vector) """ array = self._check_arguments(args) super().__init__(2, 2) self._v[...] = array[:2]
##############################################
[docs] def _check_arguments(self, args): size = len(args) if size == 1: array = args[0] # iterable, vector elif size == 2: array = args # (x, y) else: raise ValueError("More than 2 arguments where given") # if not (np.iterable(array) and len(array) == 2): # raise ValueError("Argument must be iterable and of length 2") return array
##############################################
[docs] def __len__(self): return 2
##############################################
[docs] def transform(self, transformation): if transformation.size == 2: return super().transform(transformation) elif transformation.size == 3: return HomogeneousVector2D(self).transform(transformation) else: raise ValueError("Incompatible size")
####################################################################################################
[docs]class Vector2DInt(VectorIntMixin, VectorArithmeticMixin, Vector2DBase): pass
####################################################################################################
[docs]class Vector2D(VectorFloatMixin, VectorArithmeticMixin, Vector2DBase): """ 2D Vector """ ##############################################
[docs] @classmethod def from_angle(cls, angle): """ Create the unitary vector (cos(angle), sin(angle)). The *angle* is in degree. """ rad = math.radians(angle) return cls((math.cos(rad), math.sin(rad)))
##############################################
[docs] @classmethod def middle(cls, p0, p1): """ Return the middle point. """ return cls(p0 + p1) * .5
##############################################
[docs] def rint(self): return Vector2DInt(np.rint(self._v))
##############################################
[docs] def to_normalised(self): """ Return a normalised vector """ return NormalisedVector2D(self._v / self.magnitude())
####################################################################################################
[docs]class NormalisedVector2D(VectorAbc): """ 2D Normalised Vector """ ##############################################
[docs] def __init__(self, array): VectorAbc.__init__(self, 2, 2) self._v[...] = array
#! if self.magnitude() != 1.: #! raise ValueError("Magnitude != 1") # if not (is_in_trignometric_range(self.x) and # is_in_trignometric_range(self.y)): # raise ValueError("Values must be in trignometric range") ##############################################
[docs] def __mul__(self, scale): """ Return a new vector equal to the self scaled by scale """ return Vector2D(scale * self._v)
####################################################################################################
[docs]class HomogeneousVector2D(VectorAbc): """ 2D Homogeneous Coordinate Vector """ __data_type__ = float ##############################################
[docs] def __init__(self, *args): array = self._check_arguments(args) VectorAbc.__init__(self, 3, 2) if len(array) == 2: self._v[...] = array self.w = 1 else: self._array[...] = array[:3]
##############################################
[docs] def _check_arguments(self, args): size = len(args) if size == 1: array = args[0] # iterable, vector elif 2 <= size <= 3: array = args else: raise ValueError("More than 3 arguments where given") return array
##############################################
[docs] def to_vector(self): return Vector2D(self._v)
############################################## @property def w(self): return self._array[2] @w.setter def w(self, value): self._array[2] = value ##############################################
[docs] def __len__(self): return 3