Coverage for /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/hypervehicle/geometry/vector.py: 85%
119 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 02:51 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 02:51 +0000
1from __future__ import annotations
2import numpy as np
3from typing import Union, Optional
6class Vector3:
7 """A 3-dimensional vector in Cartesian coordinates."""
9 PRECISION = 3 # for display purposes only
11 def __init__(
12 self,
13 x: Optional[float] = None,
14 y: Optional[float] = None,
15 z: Optional[float] = None,
16 ):
17 """Define a new vector.
19 Parameters
20 ----------
21 x : float, optional
22 The x-component of the vector.
24 y : float, optional
25 The y-component of the vector.
27 z : float, optional
28 The z-component of the vector.
29 """
30 # Check input types
31 if isinstance(x, Vector3):
32 # Vector passed in, inherit coordinates
33 self._x = x.x
34 self._y = x.y
35 self._z = x.z
36 elif isinstance(x, (float, int, np.floating)):
37 self._x = x
38 self._y = y if y is not None else 0.0
39 self._z = z if z is not None else 0.0
40 else:
41 raise ValueError("Invalid argument type specified.")
43 def __str__(self) -> str:
44 round_non_none = [
45 str(round(i, self.PRECISION))
46 for i in [self._x, self._y, self._z]
47 if i is not None
48 ]
49 dimensions = len(round_non_none)
50 s = f"{dimensions}-dimensional vector: ({', '.join(round_non_none)})"
51 return s
53 def __repr__(self) -> str:
54 round_non_none = [
55 str(round(i, self.PRECISION))
56 for i in [self._x, self._y, self._z]
57 if i is not None
58 ]
59 return f"Vector({', '.join(round_non_none)})"
61 def __neg__(self):
62 """Returns the vector pointing in the opposite direction."""
63 return Vector3(x=-1 * self.x, y=-1 * self.y, z=-1 * self.z)
65 def __add__(self, other):
66 """Element-wise vector addition.
68 Parameters
69 ----------
70 other : Vector
71 Another Vector object to be added. This Vector must be of the same
72 dimension as the one it is being added to.
73 """
74 if not isinstance(other, Vector3):
75 raise Exception(f"Cannot add a {type(other)} to a vector.")
76 return Vector3(x=self.x + other.x, y=self.y + other.y, z=self.z + other.z)
78 def __sub__(self, other):
79 """Element-wise vector subtraction.
81 Parameters
82 ----------
83 other : Vector
84 Another Vector object to be added. This Vector must be of the same
85 dimension as the one it is being added to.
86 """
87 if not isinstance(other, Vector3):
88 raise Exception(f"Cannot add a {type(other)} to a vector.")
89 return Vector3(x=self.x - other.x, y=self.y - other.y, z=self.z - other.z)
91 def __truediv__(self, denominator: Union[float, int]):
92 """Element-wise vector division.
94 Parameters
95 ----------
96 denominator : float | int
97 The denominator to use in the division.
98 """
99 return Vector3(
100 x=self.x / denominator, y=self.y / denominator, z=self.z / denominator
101 )
103 def __mul__(self, multiple: Union[float, int]):
104 """Element-wise vector multiplication.
106 Parameters
107 ----------
108 multiple : float | int
109 The multiple to use in the multiplication.
110 """
111 return Vector3(x=self.x * multiple, y=self.y * multiple, z=self.z * multiple)
113 def __rmul__(self, other):
114 """Multiplication operand for vectors on the right."""
115 return self * other
117 def __abs__(self):
118 """Vector magnitude."""
119 return np.sqrt(self.x**2 + self.y**2 + self.z**2)
121 def __eq__(self, other: object) -> bool:
122 if not isinstance(other, Vector3):
123 raise Exception(f"Cannot compare a {type(other)} to a vector.")
124 return (self.x == other.x) & (self.y == other.y) & (self.z == other.z)
126 def __imul__(self, other):
127 """Returns self *= other number."""
128 if isinstance(other, (float, int)) or isinstance(other, np.ndarray):
129 self._x *= other
130 self._y *= other
131 self._z *= other
132 return self
133 else:
134 return NotImplemented
136 def __itruediv__(self, other):
137 """Returns self /= other number."""
138 if isinstance(other, (float, int)) or isinstance(other, np.ndarray):
139 self._x /= other
140 self._y /= other
141 self._z /= other
142 return self
143 else:
144 return NotImplementedError
146 def __abs__(self):
147 """Returns magnitude."""
148 return np.sqrt(self.x**2 + self.y**2 + self.z**2)
150 @property
151 def x(self) -> float:
152 return self._x
154 @property
155 def y(self) -> float:
156 return self._y
158 @property
159 def z(self) -> float:
160 return self._z
162 @property
163 def vec(self) -> np.array:
164 """The vector represented as a Numpy array."""
165 non_none = [str(i) for i in [self._x, self._y, self._z] if i is not None]
167 return np.array([float(i) for i in non_none])
169 @property
170 def unit(self) -> Vector3:
171 """The unit vector associated with the Vector."""
172 return self / self.norm
174 @property
175 def norm(self) -> Vector3:
176 """The norm associated with the Vector."""
177 return np.linalg.norm(self.vec)
179 def normalize(self):
180 mag = abs(self)
181 self /= mag
183 @classmethod
184 def from_coordinates(cls, coordinates: np.array) -> Vector3:
185 """Constructs a Vector object from an array of coordinates.
187 Parameters
188 ----------
189 coordinates : np.array
190 The coordinates of the vector.
192 Returns
193 -------
194 Vector
196 Examples
197 --------
198 >>> Vector.from_coordinates([1,2,3])
199 Vector(1, 2, 3)
200 """
201 return cls(*coordinates)
203 def transform_to_global_frame(
204 self, n: Vector3, t1: Vector3, t2: Vector3, c: Vector3 = None
205 ):
206 """Change the coordinates from the local right-handed (RH) system at point c."""
207 new_x = self.x * n.x + self.y * t1.x + self.z * t2.x
208 new_y = self.x * n.y + self.y * t1.y + self.y * t2.y
209 new_z = self.x * n.z + self.y * t1.z + self.z * t2.z
210 if c is not None:
211 new_x += c.x
212 new_y += c.y
213 new_z += c.z
214 self._x = new_x
215 self._y = new_y
216 self._z = new_z
217 return self
219 def transform_to_local_frame(
220 self, n: Vector3, t1: Vector3, t2: Vector3, c: Vector3 = None
221 ):
222 """Change coordinates into the local right-handed (RH) system at point c."""
223 if c is not None:
224 self -= c
226 x = self.dot(n)
227 y = self.dot(t1)
228 z = self.dot(t2)
230 self._x = x
231 self._y = y
232 self._z = z
233 return self
235 def dot(self, other: Vector3):
236 """Returns dot product of self with another Vector3 object."""
237 if isinstance(other, Vector3):
238 return self.x * other.x + self.y * other.y + self.z * other.z
239 else:
240 raise Exception(f"dot() not implemented for {type(other)}")
243def approximately_equal_vectors(
244 v1: Vector3,
245 v2: Vector3,
246 rel_tol: Optional[float] = 1.0e-2,
247 abs_tol: Optional[float] = 1.0e-5,
248):
249 """Returns a boolean indicating whether v1 and v2 are approximately equal."""
250 return np.all(
251 [
252 np.isclose(v1.x, v2.x, rtol=rel_tol, atol=abs_tol),
253 np.isclose(v1.y, v2.y, rtol=rel_tol, atol=abs_tol),
254 np.isclose(v1.z, v2.z, rtol=rel_tol, atol=abs_tol),
255 ]
256 )
259def cross_product(v1: Vector3, v2: Vector3):
260 """Returns the cross product between two vectors."""
261 x = v1.y * v2.z - v2.y * v1.z
262 y = v2.x * v1.z - v1.x * v2.z
263 z = v1.x * v2.y - v2.x * v1.y
264 return Vector3(x, y, z)