def test_vector_cross(): v1 = Vector([2, 3]) v2 = Vector([1, 7]) assert v1.cross(v2) == Vector([11]) v3 = Vector([2, 7, 4]) v4 = Vector([3, 9, 8]) assert v3.cross(v4) == Vector([20, -4, -3]) assert v1.cross(v3) == Vector([12, -8, 8]) assert v4.cross(v2) == Vector([-56, 8, 12])
class Quaternion(CopyableMixin): def __init__(self, w: Real = 1, x: Real = 0, y: Real = 0, z: Real = 0): self.scalar = w self.vector = Vector([x, y, z]) @property def w(self) -> Real: return self.scalar @w.setter def w(self, value: Real) -> None: self.scalar = value @property def x(self) -> Real: return self.vector[0] @x.setter def x(self, value: Real) -> None: self.vector[0] = value @property def y(self) -> Real: return self.vector[1] @y.setter def y(self, value: Real) -> None: self.vector[1] = value @property def z(self) -> Real: return self.vector[2] @z.setter def z(self, value: Real) -> None: self.vector[2] = value def __iter__(self) -> Generator[Real, None, None]: yield self.scalar yield from self.vector def __str__(self) -> str: return f"{self.__class__.__name__}(w={self.w:.4f}, x={self.x:.4f}, y={self.y:.4f}, z={self.z:.4f})" def __repr__(self) -> str: return self.__str__() def __neg__(self) -> "Quaternion": return Quaternion(*(-i for i in self)) def __add__(self, other: "Quaternion") -> "Quaternion": if not isinstance(other, Quaternion): raise TypeError( f"Cannot add instances of type {type(other)} and {type(self)}") return Quaternion(self.scalar + other.scalar, *(self.vector + other.vector)) def __sub__(self, other: "Quaternion") -> "Quaternion": if not isinstance(other, Quaternion): raise TypeError( f"Cannot subtract instances of type {type(other)} and {type(self)}" ) return Quaternion(self.scalar - other.scalar, *(self.vector - other.vector)) def __div__(self, other: "Quaternion") -> "Quaternion": if not isinstance(other, Quaternion): other = Quaternion.from_scalar(other) if other.is_zero_quaternion(): raise ZeroDivisionError("other is a zero quaternion!") return self * other.inverse() def __idiv__(self, other: "Quaternion") -> "Quaternion": return self.__div__(other) def __rdiv__(self, other: "Quaternion") -> "Quaternion": if not isinstance(other, Quaternion): other = Quaternion.from_scalar(other) return other * self.inverse() def __truediv__(self, other: "Quaternion") -> "Quaternion": return self.__div__(other) def __itruediv__(self, other: "Quaternion") -> "Quaternion": return self.__idiv__(other) def __rtruediv__(self, other: "Quaternion") -> "Quaternion": return self.__rdiv__(other) def __mul__(self, other: "Quaternion") -> "Quaternion": if not isinstance(other, Quaternion): other = Quaternion.from_scalar(other) _scalar = self.scalar * other.scalar - self.vector.dot(other.vector) _vector = other.vector.scale(self.scalar) + self.vector.scale( other.scalar) + self.vector.cross(other.vector) return Quaternion(_scalar, *_vector) def __imul__(self, other: "Quaternion") -> "Quaternion": return self * other def __rmul__(self, other: "Quaternion") -> "Quaternion": if not isinstance(other, Quaternion): other = Quaternion.from_scalar(other) return other * self def __pow__(self, exponent: Real) -> "Quaternion": norm = self.norm() if norm > 0: vector_mag = self.vector.magnitude() if vector_mag <= 0: return Quaternion.from_scalar(self.scalar**exponent) unit_vector = self.vector.scale(1 / vector_mag) phi = acos(self.scalar / norm) _scalar = cos(exponent * phi) _vector = unit_vector.scale(sin(exponent * phi)) return (norm**exponent) * Quaternion(_scalar, *_vector) return self.copy() def __eq__(self, other: "Quaternion") -> "Quaternion": return all( isclose(i, j, rel_tol=1e-09, abs_tol=1e-09) for i, j in zip(self, other)) def __hash__(self) -> int: return hash(self.as_tuple()) def is_zero_quaternion(self) -> bool: return self == Quaternion(0, 0, 0, 0) def is_unit_quaternion(self) -> bool: return isclose(self._squared_sum(), 1.0, rel_tol=1e-09) def magnitude(self) -> Real: return self.norm() def _squared_sum(self) -> Real: return self.scalar**2 + self.vector.dot(self.vector) def norm(self) -> Real: return sqrt(self._squared_sum()) def __len__(self): return 4 def __getitem__(self, idx: int) -> Real: if idx > 3: raise IndexError( f"Index {idx} is out of range for a Quaternion of size 4") return self.scalar if idx == 0 else self.vector[idx - 1] def __setitem__(self, idx: int, value: Real) -> None: if idx > 3: raise IndexError( f"Index {idx} is out of range for a Quaternion of size 4") if idx == 0: self.scalar = value else: self.vector[idx - 1] = value def normalize(self) -> "Quaternion": if self.is_zero_quaternion(): raise ValueError("Cannot normalize a zero quaternion!") n = self.norm() return Quaternion(*(i / n for i in self)) def inverse(self) -> "Quaternion": if self.is_zero_quaternion(): raise ValueError("Cannot invert a zero quaternion!") square_sum = self._squared_sum() return Quaternion(self.scalar / square_sum, *-self.vector.scale(1 / square_sum)) def conjugate(self) -> "Quaternion": return Quaternion(self.scalar, -self.vector) def as_tuple(self) -> Tuple[Real]: return tuple(i for i in self) @classmethod def from_tuple(cls, tup: Tuple[Real]) -> "Quaternion": assert len(tup) == 4 return cls(*tup) @classmethod def identity(cls) -> "Quaternion": return cls(1, 0, 0, 0) @classmethod def from_scalar(cls, scalar: Real) -> "Quaternion": return cls(scalar, 0, 0, 0)
class VectorCase(unittest.TestCase): def setUp(self): self.a = Vector(1.0, 1.0, 1.0) self.b = Vector(1.0, 0.0, 0.0) self.c = Vector(0.0, 0.0, 1.0) def tearDown(self): del self.a del self.b del self.c def test_richmap(self): self.assertTrue(self.a==self.a) # Test equality self.assertFalse(self.a==self.b) # Test equality self.assertTrue(self.a!=self.b) # Test unequality self.assertRaises(TypeError, operator.gt, self.a, self.b) # Test not implemented operators. self.assertFalse(self.a==3.0) # Test other type self.assertFalse(3.0==self.a) # Test other type def test_arithmetics(self): """add and mul""" self.assertAlmostEqual( self.a + self.a, 2.0 * self.a) """sub""" self.assertAlmostEqual( self.a - self.a, Vector(0.0, 0.0, 0.0)) """div and truediv""" self.assertAlmostEqual( self.a / 2.0, Vector(0.5, 0.5, 0.5)) """pow""" """neg""" self.assertEqual( -self.a, Vector(-1.0, -1.0, -1.0)) def test_dot(self): self.assertEqual( self.a.dot(self.b) , 1.0) self.assertEqual( self.b.dot(self.a) , 1.0) def test_cross(self): self.assertEqual( self.a.cross(self.b), Vector(0.0, +1.0, -1.0) ) self.assertEqual( self.b.cross(self.a), Vector(0.0, -1.0, +1.0) ) self.assertEqual( self.b.cross(self.c), Vector(0.0, -1.0, 0.0) ) self.assertEqual( self.c.cross(self.b), Vector(0.0, +1.0, 0.0) ) self.assertEqual( self.a.cross(self.c), Vector(+1.0, -1.0, 0.0) ) self.assertEqual( self.c.cross(self.a), Vector(-1.0, +1.0, 0.0) ) def test_norm(self): # Comparing floating point numbers. self.assertAlmostEqual(self.a.norm(), 3.0**(0.5)) self.assertAlmostEqual(self.b.norm(), 1.0) self.assertAlmostEqual(self.c.norm(), 1.0) def test_normal(self): self.assertIs(type(self.a.normalize()), Vector) # Check that we get a vector back. self.assertEqual(self.b, self.b.normalize()) self.assertAlmostEqual(self.a.normalize(), self.a/self.a.norm()) def test_cosines_with(self): self.assertAlmostEqual(self.b.cosines_with(self.c), 0.0) # Vectors are orthogonal, so cos(90)=0 self.assertAlmostEqual(self.b.cosines_with(self.b), 1.0) # Vectors are parralel