Coverage for /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/hypervehicle/geometry/vector.py: 85%

119 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-25 22:58 +0000

1from __future__ import annotations 

2import numpy as np 

3from typing import Union, Optional 

4 

5 

6class Vector3: 

7 """A 3-dimensional vector in Cartesian coordinates.""" 

8 

9 PRECISION = 3 # for display purposes only 

10 

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. 

18 

19 Parameters 

20 ---------- 

21 x : float, optional 

22 The x-component of the vector. 

23 

24 y : float, optional 

25 The y-component of the vector. 

26 

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)): 

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.") 

42 

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 

52 

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)})" 

60 

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) 

64 

65 def __add__(self, other): 

66 """Element-wise vector addition. 

67 

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) 

77 

78 def __sub__(self, other): 

79 """Element-wise vector subtraction. 

80 

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) 

90 

91 def __truediv__(self, denominator: Union[float, int]): 

92 """Element-wise vector division. 

93 

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 ) 

102 

103 def __mul__(self, multiple: Union[float, int]): 

104 """Element-wise vector multiplication. 

105 

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) 

112 

113 def __rmul__(self, other): 

114 """Multiplication operand for vectors on the right.""" 

115 return self * other 

116 

117 def __abs__(self): 

118 """Vector magnitude.""" 

119 return np.sqrt(self.x**2 + self.y**2 + self.z**2) 

120 

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) 

125 

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 

135 

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 

145 

146 def __abs__(self): 

147 """Returns magnitude.""" 

148 return np.sqrt(self.x**2 + self.y**2 + self.z**2) 

149 

150 @property 

151 def x(self) -> float: 

152 return self._x 

153 

154 @property 

155 def y(self) -> float: 

156 return self._y 

157 

158 @property 

159 def z(self) -> float: 

160 return self._z 

161 

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] 

166 

167 return np.array([float(i) for i in non_none]) 

168 

169 @property 

170 def unit(self) -> Vector3: 

171 """The unit vector associated with the Vector.""" 

172 return self / self.norm 

173 

174 @property 

175 def norm(self) -> Vector3: 

176 """The norm associated with the Vector.""" 

177 return np.linalg.norm(self.vec) 

178 

179 def normalize(self): 

180 mag = abs(self) 

181 self /= mag 

182 

183 @classmethod 

184 def from_coordinates(cls, coordinates: np.array) -> Vector3: 

185 """Constructs a Vector object from an array of coordinates. 

186 

187 Parameters 

188 ---------- 

189 coordinates : np.array 

190 The coordinates of the vector. 

191 

192 Returns 

193 ------- 

194 Vector 

195 

196 Examples 

197 -------- 

198 >>> Vector.from_coordinates([1,2,3]) 

199 Vector(1, 2, 3) 

200 """ 

201 return cls(*coordinates) 

202 

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 

218 

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 

225 

226 x = self.dot(n) 

227 y = self.dot(t1) 

228 z = self.dot(t2) 

229 

230 self._x = x 

231 self._y = y 

232 self._z = z 

233 return self 

234 

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)}") 

241 

242 

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 ) 

257 

258 

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)