Repository: ManimCommunity/manim

Issue:
Adding Docstrings and Typehints for `space_ops`
In manim, there are lots of space_ops: 
https://docs.manim.community/en/latest/reference/manim.utils.space_ops.html
which neither have docstrings, nor type hints.
This might be a nice take for people who contribute to the repo the first time :)
It is basically e.g. changing
```python
def cross(v1, v2):
    return np.array(
        [
            v1[1] * v2[2] - v1[2] * v2[1],
            v1[2] * v2[0] - v1[0] * v2[2],
            v1[0] * v2[1] - v1[1] * v2[0],
        ]
    )
```
to
```python
def cross(v1: Union[np.ndarray, List], v2: Union[np.ndarray, List])  -> np.ndarray:
"""Calculate the cross product."""
    return np.array(
        [
            v1[1] * v2[2] - v1[2] * v2[1],
            v1[2] * v2[0] - v1[0] * v2[2],
            v1[0] * v2[1] - v1[1] * v2[0],
        ]
    )
```
etc.


Discussion:
I’ll do this after the PR #1354 is done

Resolving patch:
diff --git a/manim/utils/space_ops.py b/manim/utils/space_ops.py
--- a/manim/utils/space_ops.py
+++ b/manim/utils/space_ops.py
@@ -36,7 +36,7 @@
 import itertools as it
 import math
 from functools import reduce
-from typing import Optional, Tuple
+from typing import List, Optional, Sequence, Tuple, Union
 
 import numpy as np
 from mapbox_earcut import triangulate_float32 as earcut
@@ -52,7 +52,7 @@ def get_norm(vect):
     return np.linalg.norm(vect)
 
 
-def norm_squared(v):
+def norm_squared(v: float) -> float:
     return np.dot(v, v)
 
 
@@ -60,7 +60,18 @@ def norm_squared(v):
 # TODO, implement quaternion type
 
 
-def quaternion_mult(*quats):
+def quaternion_mult(
+    *quats: Sequence[float],
+) -> Union[np.ndarray, List[Union[float, np.ndarray]]]:
+    """Gets the Hamilton product of the quaternions provided.
+    For more information, check `this Wikipedia page
+    <https://en.wikipedia.org/wiki/Quaternion>`_.
+
+    Returns
+    -------
+    Union[np.ndarray, List[Union[float, np.ndarray]]]
+        Returns a list of product of two quaternions.
+    """
     if config.renderer == "opengl":
         if len(quats) == 0:
             return [1, 0, 0, 0]
@@ -91,7 +102,27 @@ def quaternion_mult(*quats):
         )
 
 
-def quaternion_from_angle_axis(angle, axis, axis_normalized=False):
+def quaternion_from_angle_axis(
+    angle: float, axis: np.ndarray, axis_normalized: bool = False
+) -> List[float]:
+    """Gets a quaternion from an angle and an axis.
+    For more information, check `this Wikipedia page
+    <https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles>`_.
+
+    Parameters
+    ----------
+    angle
+        The angle for the quaternion.
+    axis
+        The axis for the quaternion
+    axis_normalized : bool, optional
+        Checks whether the axis is normalized, by default False
+
+    Returns
+    -------
+    List[float]
+        Gives back a quaternion from the angle and axis
+    """
     if config.renderer == "opengl":
         if not axis_normalized:
             axis = normalize(axis)
@@ -100,7 +131,19 @@ def quaternion_from_angle_axis(angle, axis, axis_normalized=False):
         return np.append(np.cos(angle / 2), np.sin(angle / 2) * normalize(axis))
 
 
-def angle_axis_from_quaternion(quaternion):
+def angle_axis_from_quaternion(quaternion: Sequence[float]) -> Sequence[float]:
+    """Gets angle and axis from a quaternion.
+
+    Parameters
+    ----------
+    quaternion
+        The quaternion from which we get the angle and axis.
+
+    Returns
+    -------
+    Sequence[float]
+        Gives the angle and axis
+    """
     axis = normalize(quaternion[1:], fall_back=np.array([1, 0, 0]))
     angle = 2 * np.arccos(quaternion[0])
     if angle > TAU / 2:
@@ -108,13 +151,47 @@ def angle_axis_from_quaternion(quaternion):
     return angle, axis
 
 
-def quaternion_conjugate(quaternion):
+def quaternion_conjugate(quaternion: Sequence[float]) -> np.ndarray:
+    """Used for finding the conjugate of the quaternion
+
+    Parameters
+    ----------
+    quaternion
+        The quaternion for which you want to find the conjugate for.
+
+    Returns
+    -------
+    np.ndarray
+        The conjugate of the quaternion.
+    """
     result = np.array(quaternion)
     result[1:] *= -1
     return result
 
 
-def rotate_vector(vector, angle, axis=OUT):
+def rotate_vector(vector: np.ndarray, angle: int, axis: np.ndarray = OUT) -> np.ndarray:
+    """Function for rotating a vector.
+
+    Parameters
+    ----------
+    vector
+        The vector to be rotated.
+    angle
+        The angle to be rotated by.
+    axis
+        The axis to be rotated, by default OUT
+
+    Returns
+    -------
+    np.ndarray
+        The rotated vector with provided angle and axis.
+
+    Raises
+    ------
+    ValueError
+        If vector is not of dimension 2 or 3.
+    """
+
     if len(vector) == 2:
         # Use complex numbers...because why not
         z = complex(*vector) * np.exp(complex(0, angle))
@@ -129,13 +206,28 @@ def rotate_vector(vector, angle, axis=OUT):
         raise ValueError("vector must be of dimension 2 or 3")
 
 
-def thick_diagonal(dim, thickness=2):
+def thick_diagonal(dim: int, thickness=2) -> np.ndarray:
     row_indices = np.arange(dim).repeat(dim).reshape((dim, dim))
     col_indices = np.transpose(row_indices)
     return (np.abs(row_indices - col_indices) < thickness).astype("uint8")
 
 
-def rotation_matrix_transpose_from_quaternion(quat):
+def rotation_matrix_transpose_from_quaternion(quat: np.ndarray) -> List[np.ndarray]:
+    """Converts the quaternion, quat, to an equivalent rotation matrix representation.
+    For more information, check `this page
+    <https://in.mathworks.com/help/driving/ref/quaternion.rotmat.html>`_.
+
+    Parameters
+    ----------
+    quat
+        The quaternion which is to be converted.
+
+    Returns
+    -------
+    List[np.ndarray]
+        Gives back the Rotation matrix representation, returned as a 3-by-3
+        matrix or 3-by-3-by-N multidimensional array.
+    """
     quat_inv = quaternion_conjugate(quat)
     return [
         quaternion_mult(quat, [0, *basis], quat_inv)[1:]
@@ -147,11 +239,11 @@ def rotation_matrix_transpose_from_quaternion(quat):
     ]
 
 
-def rotation_matrix_from_quaternion(quat):
+def rotation_matrix_from_quaternion(quat: np.ndarray) -> np.ndarray:
     return np.transpose(rotation_matrix_transpose_from_quaternion(quat))
 
 
-def rotation_matrix_transpose(angle, axis):
+def rotation_matrix_transpose(angle: float, axis: np.ndarray) -> np.ndarray:
     if axis[0] == 0 and axis[1] == 0:
         # axis = [0, 0, z] case is common enough it's worth
         # having a shortcut
@@ -167,7 +259,9 @@ def rotation_matrix_transpose(angle, axis):
     return rotation_matrix_transpose_from_quaternion(quat)
 
 
-def rotation_matrix(angle, axis, homogeneous=False):
+def rotation_matrix(
+    angle: float, axis: np.ndarray, homogeneous: bool = False
+) -> np.ndarray:
     """
     Rotation in R^3 about a specified axis of rotation.
     """
@@ -183,7 +277,19 @@ def rotation_matrix(angle, axis, homogeneous=False):
         return rotation_matrix
 
 
-def rotation_about_z(angle):
+def rotation_about_z(angle: float) -> List[List[float]]:
+    """Returns a rotation matrix for a given angle.
+
+    Parameters
+    ----------
+    angle : float
+        Angle for the rotation matrix.
+
+    Returns
+    -------
+    List[float]
+        Gives back the rotated matrix.
+    """
     return [
         [np.cos(angle), -np.sin(angle), 0],
         [np.sin(angle), np.cos(angle), 0],
@@ -191,7 +297,7 @@ def rotation_about_z(angle):
     ]
 
 
-def z_to_vector(vector):
+def z_to_vector(vector: np.ndarray) -> np.ndarray:
     """
     Returns some matrix in SO(3) which takes the z-axis to the
     (normalized) vector provided as an argument
@@ -220,9 +326,18 @@ def angle_between(v1, v2):
     return np.arccos(np.dot(v1 / np.linalg.norm(v1), v2 / np.linalg.norm(v2)))
 
 
-def angle_of_vector(vector):
-    """
-    Returns polar coordinate theta when vector is project on xy plane
+def angle_of_vector(vector: Sequence[float]) -> float:
+    """Returns polar coordinate theta when vector is projected on xy plane.
+
+    Parameters
+    ----------
+    vector
+        The vector to find the angle for.
+
+    Returns
+    -------
+    float
+        The angle of the vector projected.
     """
     if config.renderer == "opengl":
         return np.angle(complex(*vector[:2]))
@@ -233,10 +348,21 @@ def angle_of_vector(vector):
         return np.angle(complex(*vector[:2]))
 
 
-def angle_between_vectors(v1, v2):
-    """
-    Returns the angle between two 3D vectors.
-    This angle will always be btw 0 and pi
+def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> np.ndarray:
+    """Returns the angle between two vectors.
+    This angle will always be between 0 and pi
+
+    Parameters
+    ----------
+    v1
+        The first vector.
+    v2
+        The second vector.
+
+    Returns
+    -------
+    np.ndarray
+        The angle between the vectors.
     """
     if config["renderer"] == "opengl":
         diff = (angle_of_vector(v2) - angle_of_vector(v1)) % TAU
@@ -248,12 +374,26 @@ def angle_between_vectors(v1, v2):
         )
 
 
-def project_along_vector(point, vector):
+def project_along_vector(point: float, vector: np.ndarray) -> np.ndarray:
+    """Projects a vector along a point.
+
+    Parameters
+    ----------
+    point
+        The point to be project from.
+    vector
+        The vector which has to projected.
+
+    Returns
+    -------
+    np.ndarray
+        A dot product of the point and vector.
+    """
     matrix = np.identity(3) - np.outer(vector, vector)
     return np.dot(point, matrix.T)
 
 
-def normalize(vect, fall_back=None):
+def normalize(vect: Union[np.ndarray, Tuple[float]], fall_back=None) -> np.ndarray:
     norm = np.linalg.norm(vect)
     if norm > 0:
         return np.array(vect) / norm
@@ -264,7 +404,21 @@ def normalize(vect, fall_back=None):
             return np.zeros(len(vect))
 
 
-def normalize_along_axis(array, axis, fall_back=None):
+def normalize_along_axis(array: np.ndarray, axis: np.ndarray) -> np.ndarray:
+    """Normalizes an array with the provided axis.
+
+    Parameters
+    ----------
+    array
+        The array which has to be normalized.
+    axis
+        The axis to be normalized to.
+
+    Returns
+    -------
+    np.ndarray
+        Array which has been normalized according to the axis.
+    """
     norms = np.sqrt((array * array).sum(axis))
     norms[norms == 0] = 1
     buffed_norms = np.repeat(norms, array.shape[axis]).reshape(array.shape)
@@ -277,7 +431,23 @@ def cross(v1, v2):
     return np.cross(v1, v2)
 
 
-def get_unit_normal(v1, v2, tol=1e-6):
+def get_unit_normal(v1: np.ndarray, v2: np.ndarray, tol: float = 1e-6) -> np.ndarray:
+    """Gets the unit normal of the vectors.
+
+    Parameters
+    ----------
+    v1
+        The first vector.
+    v2
+        The second vector
+    tol
+        [description], by default 1e-6
+
+    Returns
+    -------
+    np.ndarray
+        The normal of the two vectors.
+    """
     if config.renderer == "opengl":
         v1 = normalize(v1)
         v2 = normalize(v2)
@@ -298,7 +468,21 @@ def get_unit_normal(v1, v2, tol=1e-6):
 ###
 
 
-def compass_directions(n=4, start_vect=RIGHT):
+def compass_directions(n: int = 4, start_vect: np.ndarray = RIGHT) -> np.ndarray:
+    """Finds the cardinal directions using tau.
+
+    Parameters
+    ----------
+    n
+        The amount to be rotated, by default 4
+    start_vect
+        The direction for the angle to start with, by default RIGHT
+
+    Returns
+    -------
+    np.ndarray
+        The angle which has been rotated.
+    """
     angle = TAU / n
     return np.array([rotate_vector(start_vect, k * angle) for k in range(n)])
 
@@ -340,11 +524,11 @@ def regular_vertices(
     return vertices, start_angle
 
 
-def complex_to_R3(complex_num):
+def complex_to_R3(complex_num: complex) -> np.ndarray:
     return np.array((complex_num.real, complex_num.imag, 0))
 
 
-def R3_to_complex(point):
+def R3_to_complex(point: Sequence[float]) -> np.ndarray:
     return complex(*point[:2])
 
 
@@ -352,20 +536,63 @@ def complex_func_to_R3_func(complex_func):
     return lambda p: complex_to_R3(complex_func(R3_to_complex(p)))
 
 
-def center_of_mass(points):
+def center_of_mass(points: Sequence[float]) -> np.ndarray:
+    """Gets the center of mass of the points in space.
+
+    Parameters
+    ----------
+    points
+        The points to find the center of mass from.
+
+    Returns
+    -------
+    np.ndarray
+        The center of mass of the points.
+    """
     points = [np.array(point).astype("float") for point in points]
     return sum(points) / len(points)
 
 
-def midpoint(point1, point2):
-    return center_of_mass([point1, point2])
+def midpoint(
+    point1: Sequence[float], point2: Sequence[float]
+) -> Union[float, np.ndarray]:
+    """Gets the midpoint of two points.
 
+    Parameters
+    ----------
+    point1
+        The first point.
+    point2
+        The second point.
 
-def line_intersection(line1, line2):
+    Returns
+    -------
+    Union[float, np.ndarray]
+        The midpoint of the points
     """
-    Returns intersection point of two lines,
-    each defined with a pair of vectors determining
-    the end points
+    return center_of_mass([point1, point2])
+
+
+def line_intersection(line1: Sequence[float], line2: Sequence[float]) -> np.ndarray:
+    """Returns intersection point of two lines, each defined with
+    a pair of vectors determining the end points.
+
+    Parameters
+    ----------
+    line1
+        The first line.
+    line2
+        The second line.
+
+    Returns
+    -------
+    np.ndarray
+        The intersection points of the two lines which are intersecting.
+
+    Raises
+    ------
+    ValueError
+        Error is produced if the two lines don't intersect with each other
     """
     x_diff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0])
     y_diff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1])
@@ -382,7 +609,7 @@ def det(a, b):
     return np.array([x, y, 0])
 
 
-def find_intersection(p0, v0, p1, v1, threshold=1e-5):
+def find_intersection(p0, v0, p1, v1, threshold=1e-5) -> np.ndarray:
     """
     Return the intersection of a line passing through p0 in direction v0
     with one passing through p1 in direction v1.  (Or array of intersections
@@ -411,7 +638,7 @@ def find_intersection(p0, v0, p1, v1, threshold=1e-5):
     return p0 + ratio * v0
 
 
-def get_winding_number(points):
+def get_winding_number(points: Sequence[float]) -> float:
     total_angle = 0
     for p1, p2 in adjacent_pairs(points):
         d_angle = angle_of_vector(p2) - angle_of_vector(p1)
@@ -420,7 +647,7 @@ def get_winding_number(points):
     return total_angle / TAU
 
 
-def shoelace(x_y):
+def shoelace(x_y: np.ndarray) -> float:
     """2D implementation of the shoelace formula.
 
     Returns
@@ -434,7 +661,7 @@ def shoelace(x_y):
     return area
 
 
-def shoelace_direction(x_y):
+def shoelace_direction(x_y: np.ndarray) -> str:
     """
     Uses the area determined by the shoelace method to determine whether
     the input set of points is directed clockwise or counterclockwise.
@@ -455,17 +682,23 @@ def cross2d(a, b):
         return a[0] * b[1] - b[0] * a[1]
 
 
-def earclip_triangulation(verts, ring_ends):
-    """
-    Returns a list of indices giving a triangulation
-    of a polygon, potentially with holes
+def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list:
+    """Returns a list of indices giving a triangulation
+    of a polygon, potentially with holes.
 
-    - verts is a numpy array of points
+    Parameters
+    ----------
+    verts
+        verts is a numpy array of points.
+    ring_ends
+        ring_ends is a list of indices indicating where
+    the ends of new paths are.
 
-    - ring_ends is a list of indices indicating where
-    the ends of new paths are
+    Returns
+    -------
+    list
+        A list of indices giving a triangulation of a polygon.
     """
-
     # First, connect all the rings so that the polygon
     # with holes is instead treated as a (very convex)
     # polygon with one edge.  Do this by drawing connections


Test patch: