diff --git a/opencsp/common/lib/geometry/Vxyz.py b/opencsp/common/lib/geometry/Vxyz.py index 16b90f25f..d7971879b 100644 --- a/opencsp/common/lib/geometry/Vxyz.py +++ b/opencsp/common/lib/geometry/Vxyz.py @@ -14,12 +14,24 @@ class Vxyz: + """ + 3D vector class to represent 3D points/vectors. Contains N 3D vectors where len == N. + + The values for the contained vectors can be retrieved with + :py:meth:`data`(), or individual vectors can be retrieved with the indexing + or x/y/z methods. For example, the following can both be used to get the first contained vector:: + + vec = v3.Vxyz([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) + v0 = Vxyz([vec.x()[0], vec.y()[0], vec.z()[0]]) + # v0 == Vxyz([0, 3, 6]) + v0 = vec[0] + # v0 == Vxyz([0, 3, 6]) + """ + def __init__( self, data: Union[np.ndarray, tuple[float, float, float], tuple[list, list, list], Vxy, "Vxyz"], dtype=float ): """ - 3D vector class to represent 3D points/vectors. - To represent a single vector:: x = 1 @@ -74,14 +86,14 @@ def __init__( self._data = np.array(data, dtype=dtype).reshape((3, -1)) @property - def data(self): + def data(self) -> np.ndarray: """ An array with shape (3, N), where N is the number of 3D vectors in this instance. """ return self._data @property - def dtype(self): + def dtype(self) -> np.dtype: return self._data.dtype @property @@ -96,11 +108,28 @@ def y(self) -> np.ndarray: def z(self) -> np.ndarray: return self._data[2, :] - def as_Vxyz(self): + def as_Vxyz(self) -> "Vxyz": + """ + Returns + ------- + Vxyz + This instance + """ return self @classmethod - def _from_data(cls, data, dtype=float): + def _from_data(cls, data, dtype=None) -> "Vxyz": + """ + Builds a new instance with the given data and data type. + + Parameters + ---------- + data : array-like | list-like + The data to build this class with. Acceptable input types are + enumerated in the constructor type hints. + dtype : Literal[float] | Literal[int], optional + The data type used for representing the input data, by default float + """ return cls(data, dtype) @classmethod @@ -118,27 +147,31 @@ def _check_is_Vxyz(self, v_in): Checks if input data is instance of Vxyz for methods that require this type. + Raises + ------ + TypeError: + If the input v_in is not a Vxyz type object. """ if not isinstance(v_in, Vxyz): raise TypeError(f'Input operand must be {Vxyz}, not {type(v_in)}') def __add__(self, v_in): """ - Element wise addition. Operand 1 type must be Vxyz + Element wise addition. Operand 1 type must be Vxyz. Returns a new Vxyz. """ self._check_is_Vxyz(v_in) return self._from_data(self._data + v_in.data) def __sub__(self, v_in): """ - Element wise subtraction. Operand 1 type must be Vxyz + Element wise subtraction. Operand 1 type must be Vxyz. Returns a new Vxyz. """ self._check_is_Vxyz(v_in) return self._from_data(self._data - v_in.data) def __mul__(self, data_in): """ - Element wise addition. Operand 1 type must be int, float, or Vxyz. + Element wise multiplication. Operand 1 type must be int, float, or Vxyz. Returns a new Vxyz. """ if type(data_in) in [int, float, np.float32, np.float64, np.int32, np.int64]: return self._from_data(self._data * data_in) @@ -167,13 +200,17 @@ def __neg__(self): def _magnitude_with_zero_check(self) -> np.ndarray: """ - Returns ndarray of normalized vector data. + Returns magnitude of each vector as a new array. Returns ------- ndarray - 1d vector of normalized data + 1d vector of normalized data. Shape is (n). + Raises + ------ + ValueError: + If the magnitude of any of the contained vectors is 0. """ mag = self.magnitude() @@ -182,14 +219,14 @@ def _magnitude_with_zero_check(self) -> np.ndarray: return mag - def normalize(self): + def normalize(self) -> "Vxyz": """ - Returns copy of normalized vector. + Creates a copy of this instance and normalizes it. Returns ------- Vxyz - Normalized vector. + Normalized vector copy with the same shape. """ V_out = self._from_data(self._data.copy()) @@ -205,20 +242,20 @@ def normalize_in_place(self) -> None: def magnitude(self) -> npt.NDArray[np.float_]: """ - Returns magnitude of each vector. + Returns magnitude of each vector as a new array. Returns ------- np.ndarray - Length n ndarray of vector magnitudes. + Vector magnitudes copy. Shape is (n). """ return np.sqrt(np.sum(self._data**2, 0)) - def rotate(self, R: Rotation): + def rotate(self, R: Rotation) -> "Vxyz": """ - Returns a copy of the rotated vector rotated about the coordinate - system origin. + Returns a copy of the rotated vector rotated about the coordinate system + origin. The rotation is applied to each of the contained 3d coordinates. Parameters ---------- @@ -228,33 +265,35 @@ def rotate(self, R: Rotation): Returns ------- Vxyz - Rotated vector. + Rotated vector copy. """ # Check inputs if not isinstance(R, Rotation): - raise TypeError(f'Rotaion must be type {Rotation}, not {type(R)}') + raise TypeError(f'Rotation must be type {Rotation}, not {type(R)}') V_out = self._from_data(self._data.copy()) V_out.rotate_in_place(R) return V_out - def rotate_about(self, R: Rotation, V_pivot): + def rotate_about(self, R: Rotation, V_pivot: "Vxyz") -> "Vxyz": """ Returns a copy of the rotated vector rotated about the given pivot - point. + point. The rotation is applied to each of the contained 3d coordinates. Parameters ---------- R : Rotation Rotation object to apply to vector. V_pivot : Vxyz - Pivot point to rotate about. + Pivot point to rotate about. Must broadcast with the size of this + instance (must have length 1 or N, where N is the length of this + instance). Returns ------- Vxyz - Rotated vector. + Rotated vector copy. """ # Check inputs @@ -269,7 +308,8 @@ def rotate_about(self, R: Rotation, V_pivot): def rotate_in_place(self, R: Rotation) -> None: """ Rotates vector about the coordinate system origin. Replaces data in Vxyz - object with rotated data. + object with rotated data. The rotation is applied to each of the + contained 3d coordinates. Parameters ---------- @@ -287,17 +327,20 @@ def rotate_in_place(self, R: Rotation) -> None: self._data = R.apply(self._data.T).T - def rotate_about_in_place(self, R: Rotation, V_pivot) -> None: + def rotate_about_in_place(self, R: Rotation, V_pivot: "Vxyz") -> None: """ Rotates about the given pivot point. Replaces data in Vxyz object with - rotated data. + rotated data. The rotation is applied to each of the contained 3d + coordinates. Parameters ---------- R : Rotation Rotation object to apply to vector. V_pivot : Vxyz - Pivot point to rotate about. + Pivot point to rotate about. Must broadcast with the size of this + instance (must have length 1 or N, where N is the length of this + instance). Returns ------- @@ -316,7 +359,7 @@ def rotate_about_in_place(self, R: Rotation, V_pivot) -> None: # Recenter pivot point self._data += V_pivot.data - def dot(self, V) -> np.ndarray: + def dot(self, V: "Vxyz") -> np.ndarray: """ Calculated dot product. Size of input data must broadcast with size of data. @@ -324,12 +367,14 @@ def dot(self, V) -> np.ndarray: Parameters ---------- V : Vxyz - Input vector. + Input vector to compute the dot product with. Must broadcast with + the size of this instance (must have length 1 or N, where N is the + length of this instance). Returns ------- np.ndarray - Length n array of dot product values. + Array of dot product values with shape (N). """ # Check inputs @@ -337,7 +382,7 @@ def dot(self, V) -> np.ndarray: return (self._data * V.data).sum(axis=0) - def cross(self, V): + def cross(self, V: "Vxyz") -> "Vxyz": """ Calculates cross product. Operands 0 and 1 must have data sizes that can broadcast together. @@ -345,12 +390,15 @@ def cross(self, V): Parameters ---------- V : Vxyz - Input vector. + Input vector to computer the cross product with. Must broadcast with + the size of this instance (must have length 1 or N, where N is the + length of this instance). Returns ------- Vxyz - Cross product. + Cross product copy with shape (P), where O is the length of the + input V and P is the greater of N and O. """ # Check inputs @@ -361,14 +409,26 @@ def cross(self, V): # Calculate return self._from_data(np.cross(self._data.T, V.data.T).T) - def align_to(self, V) -> Rotation: + def align_to(self, V: "Vxyz") -> Rotation: """ Calculate shortest rotation that aligns current vector to input vector. + Both vectors must have length 1. The returned rotation can be applied to + the current vector so that it then aligns with the input vector. For + example:: + + vec = Vxyz([1, 2, 3]) + R = vec.align_to(Vxyz([1, 0, 0])) + vec_r = vec.rotate(R) + vec_r_n = vec_r.normalize() + + # vec.magnitude() == 3.74165739 + # vec_r == [ 3.74165739, -4.44089210e-16, -2.22044605e-16 ] + # vec_r_n == [ 1.00000000, -1.18687834e-16, -5.93439169e-17 ] Parameters ---------- V : Vxyz - 3D vector to align current vector to. + 3D vector to align current vector to. Must have length 1. Returns ------- @@ -394,7 +454,9 @@ def align_to(self, V) -> Rotation: return Rotation.from_matrix(Rmat) def concatenate(self, V: 'Vxyz') -> 'Vxyz': - """Concatenates Vxyz to end of current vector. + """ + Concatenates Vxyz to the end of current vector. Returns a copy as the + new vector. Parameters ---------- @@ -404,7 +466,8 @@ def concatenate(self, V: 'Vxyz') -> 'Vxyz': Returns ------- Vxyz - Concatenated vector + Concatenated vector copy. Shape is (3,N+O), where O is the length of + the input vector V. """ x = np.concatenate((self.x, V.x)) y = np.concatenate((self.y, V.y)) @@ -416,27 +479,32 @@ def copy(self) -> 'Vxyz': return Vxyz(self.data.copy()) def projXY(self) -> Vxy: - """Returns the x and y components of self as a Vxy + """Returns the x and y components of self as a Vxy. + + The components are not a view via indexing but rather a copy. - The components are deep copied. + Returns + ------- + Vxy + Output XY points as a new vector. Shape is (2,N). """ return Vxy([self.x.copy(), self.y.copy()]) @classmethod def from_lifted_points(cls, v: Vxy, func: Callable) -> 'Vxyz': - """Returns Vxyz from a Vxy and a function of form: z = func(x, y) + """Returns Vxyz from a Vxy and a function of form: z = func(x, y). Parameters ---------- v : Vxy - X/Y points + X/Y points with shape (2,N). func : Callable Z coordinate function of form z = func(x, y) Returns ------- Vxyz - Output XYZ points + Output XYZ points as a new vector. Shape is (3,N). """ zs = [] for x, y in zip(v.x, v.y): @@ -455,12 +523,12 @@ def empty(cls): return Vxyz([[], [], []]) def hasnan(self): - """returns True if there is a nan in self\n - Note: this method exists because: - ```python - >>> isinstance(np.nan, numbers.Number) - True - ```""" + """Returns True if there is a single NaN in the current vector. + + Note: this method exists because of the unintuitive behavior of isinstance in Python:: + + isinstance(np.nan, numbers.Number) # True + """ return np.isnan(self.data).any() @classmethod @@ -486,18 +554,6 @@ def merge(cls, V_list: list['Vxyz']): def origin(cls): return cls([0, 0, 0]) - # @classmethod - # def lift(cls, v: Vxy.Vxy, func: Callable): - # """Takes in a Vxy and and Callable that takes in 2 arguments. - # Returns the Vxyz where the z values correspond to the outputs of the x and y values.""" - - # xs = copy.deepcopy(v.x) - # ys = copy.deepcopy(v.y) - # zs = [] - # for x, y in zip(xs, ys): - # zs.append(func(x, y)) - # return cls([xs, ys, zs]) - def draw_line( self, figure: rcfr.RenderControlFigureRecord | v3d.View3d, @@ -507,8 +563,22 @@ def draw_line( ) -> None: """ Calls figure.draw_xyz_list(self.data.T) to draw all xyz points in a - single series. Uses the default arguments for draw_xyz_list in place of - any None's. + single series. Uses the default arguments for + :py:meth:`View3d.draw_xyz_list` in place of any None arguments. + + Parameters + ---------- + figure : rcfr.RenderControlFigureRecord | v3d.View3d + The figure to draw to. + close : bool, optional + True to add the first point again at the end of the plot, thereby + drawing this set of points as a closed polygon. None or False to not + add another point at the end (draw_xyz_list's default) + style : rcps.RenderControlPointSeq, optional + The style to use for the points and lines, or None for + :py:method:`RenderControlPointSequence.default`(). + label : str, optional + A string used to label this plot in the legend, or None for no label. """ kwargs = dict() for key, val in [('close', close), ('style', style), ('label', label)]: @@ -523,10 +593,25 @@ def draw_points( figure: rcfr.RenderControlFigureRecord | v3d.View3d, style: rcps.RenderControlPointSeq = None, labels: list[str] = None, - ): + ) -> None: """ Calls figure.draw_xyz(p) to draw all xyz points in this instance - individually. Uses the default arguments in place for any None's. + individually. Uses the default arguments for :py:meth:`View3d.draw_xyz` + in place of any None arguments. + + Parameters + ---------- + figure : rcfr.RenderControlFigureRecord | v3d.View3d + The figure to draw to. + close : bool, optional + True to add the first point again at the end of the plot, thereby + drawing this set of points as a closed polygon. None or False to not + add another point at the end (draw_xyz_list's default). + style : rcps.RenderControlPointSeq, optional + The style to use for the points and lines, or None for + :py:method:`RenderControlPointSequence.default`(). + label : str, optional + A string used to label this plot in the legend, or None for no label. """ if labels is None: labels = [None] * len(self) diff --git a/opencsp/common/lib/render/View3d.py b/opencsp/common/lib/render/View3d.py index e1d70d060..674a6dfc7 100644 --- a/opencsp/common/lib/render/View3d.py +++ b/opencsp/common/lib/render/View3d.py @@ -705,8 +705,8 @@ def draw_xyz( """ Plots one or more points. - This is similar to draw_xyz_list, except that it accepts the point - locations in a different format. Example usage:: + This is similar to :py:meth:`draw_xyz_list`, except that it accepts the + point locations in a different format. Example usage:: # viewspec xy or pq draw_xyz((0, 1)) @@ -770,8 +770,8 @@ def draw_xyz_list( the end of the input list as a new last value. Ignored if input_xyz_list < 3 points. Default is False. style: RenderControlPointSeq | None, optional - The style with which to render the input, or None for - rcps.default(). Default is None. + The style with which to render the input, or None for rcps.default() + (blue, marker '.', line style '-'). Default is None. label: str | None, optional The label used for this graph in the legend, or None to be excluded from the legend. Default is None.