trafo 0.1.0
Library for handling 3D transformations
Requires Python: >=3.0

# trafo 

Dependency-free Python library for robustly handling 3D rigid object transformations

(in development)

## Features

### Core

* a Vector class for representing 3D positions and translations and for performing basic vector algebra and with many useful helpers
* a Rotation class for representing 3D orientations and rotations with conversions from and to many popular rotation representations
* a chainable Trafo class representing 3D poses and transformations comprising a Vector and a Rotation, allowing conversions from and to 4x4 matrices and from Denavit Hartenberg parameters
* a Node class for constructing Trafo hierarchies (scene graphs) and querying transformations between the nodes
* an abstract DebugDrawer class that helps visualize all of the above, only the `line()` method has to be overloaded

### Tools

* a powerful dependency-free HTML DebugDrawer that lets you explore debug drawings in the browser; hit space to shift objects randomly so you can distinguish coinsiding lines

## Design

### Prioritises...

* Usability:
	* can be included as a single pure Python file
	* depends only on the standard library (not even on numpy)
* Precision:
	* internally uses quaternions and renormalizes them everywhere
	* does not use numerically awkward functions with infinite slope like asin or acos, sqrt is scaled so that it is not called close to 0
* Clarity:
	* Things like the component order in quaternions are always explicit and cannot be confused.
* OCD:
	* Everything is typed, tested and docstrung balls to the wall.

### ... at the cost of

* performance

## Usage

### Installation

```
pip install trafo
```
or just copy `src/trafo/trafo.py` into your project

### Complete API

(click to unfold)

<!-- GENERATED API START -->
<details><summary><b><code>class Vector</code></b> — An immutable 3D vector representing a point or a direction</summary><br>

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<code>x: float = 0.0</code>


&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<code>y: float = 0.0</code>


&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<code>z: float = 0.0</code>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>zero()</code> — Create a zero vector while being extra explicit about it.</summary>

```py
@staticmethod
def zero() -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Create a zero vector while being extra explicit about it."""
    return Vector(0.0, 0.0, 0.0)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>ex()</code> — Create a unit vector in x-direction.</summary>

```py
@staticmethod
def ex() -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Create a unit vector in x-direction."""
    return Vector(1.0, 0.0, 0.0)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>ey()</code> — Create a unit vector in y-direction.</summary>

```py
@staticmethod
def ey() -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Create a unit vector in y-direction."""
    return Vector(0.0, 1.0, 0.0)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>ez()</code> — Create a unit vector in z-direction.</summary>

```py
@staticmethod
def ez() -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Create a unit vector in z-direction."""
    return Vector(0.0, 0.0, 1.0)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>rand_box(...)</code> — Create a random vector with uniform distribution within a cuboid.</summary>

```py
@staticmethod
def rand_box(
    min: Sequence[float] = (0.0, 0.0, 0.0),
    max: Sequence[float] = (1.0, 1.0, 1.0),
    generator: random.Random = cast(random.Random, random),
) -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Create a random vector with uniform distribution within a cuboid."""
    return Vector(
        generator.uniform(min[0], max[0]),
        generator.uniform(min[1], max[1]),
        generator.uniform(min[2], max[2]),
    )
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>rand_sphere(...)</code> — Create a random vector with uniform distribution on or in a sphere.</summary>

```py
@staticmethod
def rand_sphere(
    radius: float = 1.0,
    center: Optional["Vector"] = None,
    fill: bool = False,
    generator: random.Random = cast(random.Random, random),
) -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Create a random vector with uniform distribution on or in a sphere."""
    v = Vector(generator.gauss(), generator.gauss(), generator.gauss()).normalized()
    if center is None:
        center = Vector.zero()
    if fill:
        v = v * generator.random() ** (1 / 3)
    return Vector(
        center[0] + radius * v[0],
        center[1] + radius * v[1],
        center[2] + radius * v[2],
    )
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__eq__(...)</code> — Check equality with another vector; true if all elements are equal.</summary>

```py
def __eq__(self, other: object) -> bool:
```
<details><summary><i>source code</i></summary>

```py
    """Check equality with another vector; true if all elements are equal."""
    if isinstance(other, Vector):
        return self[:] == other[:]
    else:
        return NotImplemented
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__ne__(...)</code> — Check inequality with another vector; true if any element is unequal.</summary>

```py
def __ne__(self, other: object) -> bool:
```
<details><summary><i>source code</i></summary>

```py
    """Check inequality with another vector; true if any element is unequal."""
    return not self == other
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__add__(...)</code> — Add another vector element-wise (returns a new Vector).</summary>

Note that this violates LSP for tuples which are expected to concatenate instead.

```py
def __add__(self, other: "Vector") -> "Vector":  # type: ignore[override]
```
<details><summary><i>source code</i></summary>

```py
    """Add another vector element-wise (returns a new Vector).
    Note that this violates LSP for tuples which are expected to concatenate instead.
    """
    if isinstance(other, Vector):
        return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
    else:
        return NotImplemented
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__neg__()</code> — Return the negated vector (as a new Vector).</summary>

```py
def __neg__(self) -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Return the negated vector (as a new Vector)."""
    return Vector(-self.x, -self.y, -self.z)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__sub__(...)</code> — Subtract another vector (returns a new Vector).</summary>

```py
def __sub__(self, other: "Vector") -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Subtract another vector (returns a new Vector)."""
    if isinstance(other, Vector):
        return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
    else:
        return NotImplemented
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__mul__(...)</code> — Multiply by a scalar element-wise (returns a new Vector).</summary>

Note that this violates LSP for tuples which are expected to repeat instead.

```py
def __mul__(self, scalar: float) -> "Vector":  # type: ignore[override]
```
<details><summary><i>source code</i></summary>

```py
    """Multiply by a scalar element-wise (returns a new Vector).
    Note that this violates LSP for tuples which are expected to repeat instead."""
    if isinstance(scalar, (int, float)):
        return Vector(self.x * scalar, self.y * scalar, self.z * scalar)
    else:
        return NotImplemented
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__rmul__(...)</code> — Multiply by a scalar element-wise (returns a new Vector).</summary>

Note that this violates LSP for tuples which are expected to repeat instead.

```py
def __rmul__(self, scalar: float) -> "Vector":  # type: ignore[override]
```
<details><summary><i>source code</i></summary>

```py
    """Multiply by a scalar element-wise (returns a new Vector).
    Note that this violates LSP for tuples which are expected to repeat instead."""
    return self * scalar
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__truediv__(...)</code> — Divide by a scalar element-wise (returns a new Vector).</summary>

```py
def __truediv__(self, scalar: float) -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Divide by a scalar element-wise (returns a new Vector)."""
    if isinstance(scalar, (int, float)):
        return Vector(self.x / scalar, self.y / scalar, self.z / scalar)
    else:
        return NotImplemented
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>dot(...)</code> — Calculate the dot product with another vector.</summary>

```py
def dot(self, other: "Vector") -> float:
```
<details><summary><i>source code</i></summary>

```py
    """Calculate the dot product with another vector."""
    return self.x * other.x + self.y * other.y + self.z * other.z
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>cross(...)</code> — Calculate the cross product with another vector (returns a new Vector).</summary>

```py
def cross(self, other: "Vector") -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Calculate the cross product with another vector (returns a new Vector)."""
    return Vector(
        self.y * other.z - self.z * other.y,
        self.z * other.x - self.x * other.z,
        self.x * other.y - self.y * other.x,
    )
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>norm()</code> — Calculate the Euclidean norm (length) of the vector.</summary>

```py
def norm(self) -> float:
```
<details><summary><i>source code</i></summary>

```py
    """Calculate the Euclidean norm (length) of the vector."""
    return norm(*self)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>length()</code> — Calculate the length (Euclidean norm) of the vector.</summary>

```py
def length(self) -> float:
```
<details><summary><i>source code</i></summary>

```py
    """Calculate the length (Euclidean norm) of the vector."""
    return norm(*self)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>normalized()</code> — Return a (new) normalized vector with the same direction.</summary>

Raises when called on a zero vector.

```py
def normalized(self) -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Return a (new) normalized vector with the same direction.
    Raises when called on a zero vector."""
    x, y, z = self.x, self.y, self.z
    m = max(abs(x), abs(y), abs(z))
    if m == 0:
        raise ValueError("cannot normalize a zero vector")
    x, y, z = x / m, y / m, z / m
    norm = sqrt(x * x + y * y + z * z)
    return Vector(x / norm, y / norm, z / norm)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>perp(...)</code> — Calculate a vector perpendicular to this vector.</summary>

If other is given, the result is perpendicular to both.<br>Raises when called on a zero vector or when the vectors are parallel.

```py
def perp(self, other: Optional["Vector"] = None) -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Calculate a vector perpendicular to this vector.
    If other is given, the result is perpendicular to both.
    Raises when called on a zero vector or when the vectors are parallel."""
    if other is None:
        if abs(self.x) < abs(self.y) and abs(self.x) <= abs(self.z):
            other = Vector(1.0, 0.0, 0.0)
        elif abs(self.y) < abs(self.z):
            other = Vector(0.0, 1.0, 0.0)
        else:
            other = Vector(0.0, 0.0, 1.0)
    return self.cross(other).normalized()
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>make_basis(...)</code> — Create an orthonormal basis from two vectors.</summary>

The direction of the first vector is preserved,<br>the second is made perpendicular to the first,<br>the third is perpendicular to the first two.<br>Raises when called on a zero vector or when the vectors are parallel.

```py
@staticmethod
def make_basis(v1: "Vector", v2: "Vector") -> Tuple["Vector", "Vector", "Vector"]:
```
<details><summary><i>source code</i></summary>

```py
    """Create an orthonormal basis from two vectors.
    The direction of the first vector is preserved,
    the second is made perpendicular to the first,
    the third is perpendicular to the first two.
    Raises when called on a zero vector or when the vectors are parallel."""
    v1 = v1.normalized()
    v3 = v1.perp(v2)
    v2 = v3.perp(v1)
    return v1, v2, v3
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>distance_to(...)</code> — Calculate the Euclidean distance to another vector.</summary>

```py
def distance_to(self, other: "Vector") -> float:
```
<details><summary><i>source code</i></summary>

```py
    """Calculate the Euclidean distance to another vector."""
    return (self - other).norm()
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>angle_to(...)</code> — Calculate the angle to another vector (in rad).</summary>

```py
def angle_to(self, other: "Vector") -> float:
```
<details><summary><i>source code</i></summary>

```py
    """Calculate the angle to another vector (in rad)."""
    v1 = self.normalized()
    v2 = other.normalized()
    cos_angle = v1.dot(v2)
    sin_angle = v1.cross(v2).norm()
    return atan2(sin_angle, cos_angle)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>lerp(...)</code> — Linearly interpolate between two vectors (returns a new Vector).</summary>

```py
def lerp(self, other: "Vector", f: float) -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Linearly interpolate between two vectors (returns a new Vector)."""
    return Vector(
        self.x + f * (other.x - self.x),
        self.y + f * (other.y - self.y),
        self.z + f * (other.z - self.z),
    )
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>mean(...)</code> — Calculate the weighted mean of a sequence of vectors.</summary>

Raises when called with an empty sequence or when the sum of weights is zero.

```py
@staticmethod
def mean(
    vectors: Iterable["Vector"], weights: Optional[Iterable[float]] = None
) -> "Vector":
```
<details><summary><i>source code</i></summary>

```py
    """Calculate the weighted mean of a sequence of vectors.
    Raises when called with an empty sequence or when the sum of weights is zero."""

    weights = weights or repeat(1.0)

    if (
        hasattr(vectors, "__len__")
        and hasattr(weights, "__len__")
        and vectors.__len__() != weights.__len__()
    ):
        raise ValueError("vectors and weights must have the same length")

    x, y, z = 0.0, 0.0, 0.0
    w_sum = 0.0

    for v, weight in zip(vectors, weights):
        x += v.x * weight
        y += v.y * weight
        z += v.z * weight
        w_sum += weight

    if w_sum == 0.0:
        raise ValueError(
            "cannot compute mean if sequence is empty or sum of weights is 0"
        )

    return Vector(x / w_sum, y / w_sum, z / w_sum)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__str__()</code> — Return a string representation of the vector.</summary>

```py
def __str__(self) -> str:
```
<details><summary><i>source code</i></summary>

```py
    """Return a string representation of the vector."""
    return f"(x={self.x}, y={self.y}, z={self.z})"
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__format__(...)</code> — Return a formatted string representation of the vector.</summary>

The format_spec is applied to each element.

```py
def __format__(self, format_spec: str) -> str:
```
<details><summary><i>source code</i></summary>

```py
    """Return a formatted string representation of the vector.
    The format_spec is applied to each element."""
    fx = self.x.__format__(format_spec)
    fy = self.y.__format__(format_spec)
    fz = self.z.__format__(format_spec)
    return f"(x={fx}, y={fy}, z={fz})"
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__repr__()</code> — Return an eval-able string representation of the vector.</summary>

```py
def __repr__(self) -> str:
```
<details><summary><i>source code</i></summary>

```py
    """Return an eval-able string representation of the vector."""
    return f"Vector({self.x}, {self.y}, {self.z})"
```
</details>
</details>

<br></details>

<details><summary><b><code>class Rotation</code></b> — An immutable 3D orientation or rotation</summary><br>
<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__setattr__(...)</code> — Deleted, always raises.</summary>

```py
def __setattr__(self, name: str, value: float) -> None:
```
<details><summary><i>source code</i></summary>

```py
    """Deleted, always raises."""
    raise AttributeError("Rotation is immutable")
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__delattr__(...)</code> — Deleted, always raises.</summary>

```py
def __delattr__(self, name: str) -> None:
```
<details><summary><i>source code</i></summary>

```py
    """Deleted, always raises."""
    raise AttributeError("Rotation is immutable")
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__new__(...)</code> — Construct a rotation from quaternion components without normalization.</summary>

Intended only for use in classmethods. Use from_quat instead.

```py
def __new__(
    cls: Type[_T], *, x: float = 0.0, y: float = 0.0, z: float = 0.0, w: float = 1.0
) -> _T:
```
<details><summary><i>source code</i></summary>

```py
    """Construct a rotation from quaternion components without normalization.
    Intended only for use in classmethods. Use from_quat instead."""
    instance = super().__new__(cls)
    object.__setattr__(instance, "_x", x)
    object.__setattr__(instance, "_y", y)
    object.__setattr__(instance, "_z", z)
    object.__setattr__(instance, "_w", w)
    return instance
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__init__()</code> — Create the identity rotation.</summary>

```py
def __init__(self) -> None:
```
<details><summary><i>source code</i></summary>

```py
    """Create the identity rotation."""
    # we only allow creation of the identity rotation via standard constructor
    # x, y, z, w are implementation details and should only be set via from_quat from the outside
    pass
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>identity()</code> — Create the identity rotation while being extra explicit about it.</summary>

```py
@staticmethod
def identity() -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create the identity rotation while being extra explicit about it."""
    return Rotation()
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>x(...)</code> — Create a rotation about the x-axis.</summary>

```py
@staticmethod
def x(angle: float) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a rotation about the x-axis."""
    return Rotation.__new__(Rotation, x=sin(angle / 2.0), w=cos(angle / 2.0))
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>y(...)</code> — Create a rotation about the y-axis.</summary>

```py
@staticmethod
def y(angle: float) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a rotation about the y-axis."""
    return Rotation.__new__(Rotation, y=sin(angle / 2.0), w=cos(angle / 2.0))
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>z(...)</code> — Create a rotation about the z-axis.</summary>

```py
@staticmethod
def z(angle: float) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a rotation about the z-axis."""
    return Rotation.__new__(Rotation, z=sin(angle / 2.0), w=cos(angle / 2.0))
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>from_quat()</code> — Create a rotation from quaternion components.</summary>

Raises if the norm deviates from 1 beyond the specified tolerance<br>or if all components are 0.

```py
@staticmethod
def from_quat(
    *, x: float, y: float, z: float, w: float, tolerance: float = EPS
) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a rotation from quaternion components.
    Raises if the norm deviates from 1 beyond the specified tolerance
    or if all components are 0."""

    m = max(abs(x), abs(y), abs(z), abs(w))
    if m == 0:
        raise ValueError("cannot normalize a zero quaternion")
    x_, y_, z_, w_ = x / m, y / m, z / m, w / m
    norm_ = sqrt(x_ * x_ + y_ * y_ + z_ * z_ + w_ * w_)

    if abs(m * norm_ - 1.0) > tolerance:
        raise ValueError(
            f"quaternion is not normalized: (x={x}, y={y}, z={z}, w={w})"
        )

    return Rotation.__new__(
        Rotation, x=x_ / norm_, y=y_ / norm_, z=z_ / norm_, w=w_ / norm_
    )
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>as_quat(...)</code> — Return the quaternion components in the specified order.</summary>

```py
def as_quat(
    self, order: Literal["xyzw", "wxyz"]
) -> Tuple[float, float, float, float]:
```
<details><summary><i>source code</i></summary>

```py
    """Return the quaternion components in the specified order."""
    if order == "xyzw":
        return self._x, self._y, self._z, self._w
    elif order == "wxyz":
        return self._w, self._x, self._y, self._z
    else:
        raise ValueError("order must be either 'xyzw' or 'wxyz'")
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>from_axis_angle(...)</code> — Create a rotation from an axis and an angle (in rad).</summary>

```py
@staticmethod
def from_axis_angle(axis: Vector, angle: float) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a rotation from an axis and an angle (in rad)."""
    axis = axis.normalized()
    half_angle = angle / 2.0
    sin_half_angle = sin(half_angle)
    return Rotation.__new__(
        Rotation,
        x=sin_half_angle * axis.x,
        y=sin_half_angle * axis.y,
        z=sin_half_angle * axis.z,
        w=cos(half_angle),
    )
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>as_axis_angle()</code> — Return the axis and angle (in rad) of the rotation.</summary>

The angle is in the range [0, pi).<br>If the angle is 0, the axis is (1, 0, 0).

```py
def as_axis_angle(self) -> Tuple[Vector, float]:
```
<details><summary><i>source code</i></summary>

```py
    """Return the axis and angle (in rad) of the rotation.
    The angle is in the range [0, pi).
    If the angle is 0, the axis is (1, 0, 0)."""
    x, y, z, w = self._x, self._y, self._z, self._w
    cos_half_angle = w
    sin_half_angle = norm(x, y, z)
    if sin_half_angle == 0.0:
        return Vector(1.0, 0.0, 0.0), 0.0
    angle = 2.0 * atan2(sin_half_angle, cos_half_angle)
    angle = angle % (2.0 * pi)
    if angle <= pi:
        return Vector(x, y, z).normalized(), angle
    else:
        return Vector(-x, -y, -z).normalized(), 2.0 * pi - angle
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>from_rotvec(...)</code> — Create a rotation from a rotation vector.</summary>

A rotation vector is the axis of rotation<br>scaled by the angle of rotation.

```py
@staticmethod
def from_rotvec(rotvec: Vector) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a rotation from a rotation vector.
    A rotation vector is the axis of rotation
    scaled by the angle of rotation."""
    angle = rotvec.norm()
    if angle == 0.0:
        return Rotation()
    elif angle < EPS:
        return Rotation.from_quat(
            x=rotvec.x, y=rotvec.y, z=rotvec.z, w=1.0, tolerance=0.1
        )
    else:
        return Rotation.from_axis_angle(rotvec.normalized(), angle)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>as_rotvec()</code> — Return the rotation vector of the rotation.</summary>

A rotation vector is the axis of rotation<br>scaled by the angle of rotation.

```py
def as_rotvec(self) -> Vector:
```
<details><summary><i>source code</i></summary>

```py
    """Return the rotation vector of the rotation.
    A rotation vector is the axis of rotation
    scaled by the angle of rotation."""
    axis, angle = self.as_axis_angle()
    return axis * angle
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>from_matrix(...)</code> — Create a rotation from a 3x3 rotation matrix.</summary>

```py
@staticmethod
def from_matrix(
    matrix: Sequence[Sequence[float]],
    row_major: bool = True,
    check_matrix: bool = True,
) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a rotation from a 3x3 rotation matrix."""
    # https://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm

    if check_matrix:
        # row major or column major does not matter for these checks
        if (
            len(matrix) != 3
            or len(matrix[0]) != 3
            or len(matrix[1]) != 3
            or len(matrix[2]) != 3
        ):
            raise ValueError("matrix must be 3x3")
        vx = Vector(matrix[0][0], matrix[1][0], matrix[2][0])
        vy = Vector(matrix[0][1], matrix[1][1], matrix[2][1])
        vz = Vector(matrix[0][2], matrix[1][2], matrix[2][2])
        if (
            abs(vx.norm() - 1.0) > EPS
            or abs(vy.norm() - 1.0) > EPS
            or abs(vz.norm() - 1.0) > EPS
            or abs(vx.dot(vy)) > EPS
            or abs(vy.dot(vz)) > EPS
            or abs(vz.dot(vx)) > EPS
        ):
            raise ValueError("matrix must be orthonormal")
        if vx.cross(vy).dot(vz) < 0:
            raise ValueError("matrix must not flip handedness")

    if row_major:
        m00, m01, m02 = matrix[0]
        m10, m11, m12 = matrix[1]
        m20, m21, m22 = matrix[2]
    else:
        m00, m10, m20 = matrix[0]
        m01, m11, m21 = matrix[1]
        m02, m12, m22 = matrix[2]

    trace = m00 + m11 + m22

    if trace > 0.0:
        s = sqrt(trace + 1.0) * 2.0  # s = 4 * qw
        return Rotation.from_quat(
            x=(m21 - m12) / s, y=(m02 - m20) / s, z=(m10 - m01) / s, w=0.25 * s
        )
    elif (m00 > m11) and (m00 > m22):
        s = sqrt(1.0 + m00 - m11 - m22) * 2.0  # s = 4 * qx
        return Rotation.from_quat(
            x=0.25 * s, y=(m01 + m10) / s, z=(m02 + m20) / s, w=(m21 - m12) / s
        )
    elif m11 > m22:
        s = sqrt(1.0 + m11 - m00 - m22) * 2.0  # s = 4 * qy
        return Rotation.from_quat(
            x=(m01 + m10) / s, y=0.25 * s, z=(m12 + m21) / s, w=(m02 - m20) / s
        )
    else:
        s = sqrt(1.0 + m22 - m00 - m11) * 2.0  # s = 4 * qz
        return Rotation.from_quat(
            x=(m02 + m20) / s, y=(m12 + m21) / s, z=0.25 * s, w=(m10 - m01) / s
        )
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>as_matrix(...)</code> — Return the rotation as a 3x3 rotation matrix.</summary>

```py
def as_matrix(self, row_major: bool = True) -> List[List[float]]:
```
<details><summary><i>source code</i></summary>

```py
    """Return the rotation as a 3x3 rotation matrix."""
    # https://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToMatrix/index.htm
    x, y, z, w = self._x, self._y, self._z, self._w
    xx, xy, xz, xw = x * x, x * y, x * z, x * w
    yy, yz, yw = y * y, y * z, y * w
    zz, zw = z * z, z * w

    if row_major:
        return [
            [1.0 - 2.0 * (yy + zz), 2.0 * (xy - zw), 2.0 * (xz + yw)],
            [2.0 * (xy + zw), 1.0 - 2.0 * (xx + zz), 2.0 * (yz - xw)],
            [2.0 * (xz - yw), 2.0 * (yz + xw), 1.0 - 2.0 * (xx + yy)],
        ]
    else:
        return [
            [1.0 - 2.0 * (yy + zz), 2.0 * (xy + zw), 2.0 * (xz - yw)],
            [2.0 * (xy - zw), 1.0 - 2.0 * (xx + zz), 2.0 * (yz + xw)],
            [2.0 * (xz + yw), 2.0 * (yz - xw), 1.0 - 2.0 * (xx + yy)],
        ]
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>basis()</code> — Return the basis vectors of the rotation.</summary>

```py
def basis(self) -> Tuple[Vector, Vector, Vector]:
```
<details><summary><i>source code</i></summary>

```py
    """Return the basis vectors of the rotation."""
    x, y, z = self.as_matrix(row_major=False)
    return Vector(*x), Vector(*y), Vector(*z)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>compose(...)</code> — Compose a rotation from a sequence of rotations about x, y and z.</summary>

The sequence is an arbitrarily long string of axis identifiers, e.g. 'XY' or 'zyxZ'.<br>Use Capital letters for intrinsic rotations (rotate about the new, rotated axes),<br>use lowercase letters for extrinsic rotations (rotate about the world axes).<br>Intrinsic and extrinsic rotations can be mixed.

```py
@staticmethod
def compose(sequence: str, angles: Sequence[float]) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Compose a rotation from a sequence of rotations about x, y and z.
    The sequence is an arbitrarily long string of axis identifiers, e.g. 'XY' or 'zyxZ'.
    Use Capital letters for intrinsic rotations (rotate about the new, rotated axes),
    use lowercase letters for extrinsic rotations (rotate about the world axes).
    Intrinsic and extrinsic rotations can be mixed."""
    if len(sequence) != len(angles):
        raise ValueError("sequence and angles must have the same length")
    x, y, z, w = 0.0, 0.0, 0.0, 1.0
    for axis, angle in zip(sequence, angles):
        temp = Rotation.__new__(Rotation, x=x, y=y, z=z, w=w)
        # intrinsic rotation
        if axis == "X":
            x, y, z, w = temp._rotate_quat(Rotation.x(angle))
        elif axis == "Y":
            x, y, z, w = temp._rotate_quat(Rotation.y(angle))
        elif axis == "Z":
            x, y, z, w = temp._rotate_quat(Rotation.z(angle))
        # extrinsic rotation
        elif axis == "x":
            x, y, z, w = Rotation.x(angle)._rotate_quat(temp)
        elif axis == "y":
            x, y, z, w = Rotation.y(angle)._rotate_quat(temp)
        elif axis == "z":
            x, y, z, w = Rotation.z(angle)._rotate_quat(temp)
        else:
            raise ValueError(
                f"unknown axis: {axis}; only X, Y, Z, x, y, z are allowed"
            )
    return Rotation.from_quat(x=x, y=y, z=z, w=w)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>from_euler(...)</code> — Create a rotation from Euler angles.</summary>

The following orders are allowed:<br>ZXZ, XYX, YZY, ZYZ, XZX, YXY (proper Euler, intrinsic)<br>XYZ, YZX, ZXY, XZY, ZYX, YXZ (Tait-Bryan, intrinsic)<br>zxz, xyx, yzy, zyz, xzx, yxy (proper Euler, extrinsic)<br>xyz, yzx, zxy, xzy, zyx, yxz (Tait-Bryan, extrinsic)<br>intrinsic: rotate about the new, rotated axes<br>extrinsic: rotate about the original axes

```py
@staticmethod
def from_euler(order: str, angles: Sequence[float]) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a rotation from Euler angles.
    The following orders are allowed:
    ZXZ, XYX, YZY, ZYZ, XZX, YXY (proper Euler, intrinsic)
    XYZ, YZX, ZXY, XZY, ZYX, YXZ (Tait-Bryan, intrinsic)
    zxz, xyx, yzy, zyz, xzx, yxy (proper Euler, extrinsic)
    xyz, yzx, zxy, xzy, zyx, yxz (Tait-Bryan, extrinsic)
    intrinsic: rotate about the new, rotated axes
    extrinsic: rotate about the original axes"""
    if order not in Rotation._euler_orders:
        raise ValueError(
            f"Euler order {order} unknown, must be one of {Rotation._euler_orders}"
        )
    return Rotation.compose(order, [angles[0], angles[1], angles[2]])
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>as_euler(...)</code> — Return the Euler angles of the rotation.</summary>

The order is one of:<br>ZXZ, XYX, YZY, ZYZ, XZX, YXY (proper Euler, intrinsic)<br>XYZ, YZX, ZXY, XZY, ZYX, YXZ (Tait-Bryan, intrinsic)<br>zxz, xyx, yzy, zyz, xzx, yxy (proper Euler, extrinsic)<br>xyz, yzx, zxy, xzy, zyx, yxz (Tait-Bryan, extrinsic)<br>intrinsic: rotate about the new, rotated axes<br>extrinsic: rotate about the original axes<br>In case of a singularity, the first angle is set to 0 for extrinsic rotations,<br>the third angle is set to 0 for intrinsic rotations.

```py
def as_euler(self, order: str) -> Tuple[float, float, float]:
```
<details><summary><i>source code</i></summary>

```py
    """Return the Euler angles of the rotation.
    The order is one of:
    ZXZ, XYX, YZY, ZYZ, XZX, YXY (proper Euler, intrinsic)
    XYZ, YZX, ZXY, XZY, ZYX, YXZ (Tait-Bryan, intrinsic)
    zxz, xyx, yzy, zyz, xzx, yxy (proper Euler, extrinsic)
    xyz, yzx, zxy, xzy, zyx, yxz (Tait-Bryan, extrinsic)
    intrinsic: rotate about the new, rotated axes
    extrinsic: rotate about the original axes
    In case of a singularity, the first angle is set to 0 for extrinsic rotations,
    the third angle is set to 0 for intrinsic rotations."""
    if order not in self._euler_orders:
        raise ValueError(
            f"Euler order {order} unknown, must be one of {self._euler_orders}"
        )
    is_extrinsic = order.islower()
    order = order.lower()
    is_tait_bryan = order[0] != order[2]

    m = self.as_matrix()
    ax_index = {"x": 0, "y": 1, "z": 2}
    a1, a2 = ax_index[order[0]], ax_index[order[1]]
    a3 = 3 - a1 - a2  # Tait-Bryan: 3rd axis, proper Euler: unused axis
    parity = 1.0 if a2 == (a1 + 1) % 3 else -1.0

    if is_tait_bryan:  # e.g. xyz
        if is_extrinsic:
            a1, a3 = a3, a1
            parity = -parity
        beta = atan2(parity * m[a1][a3], norm(m[a1][a2], m[a1][a1]))
        if abs(abs(beta) - pi / 2.0) > EPS:
            alpha = atan2(-parity * m[a2][a3], m[a3][a3])
            gamma = atan2(-parity * m[a1][a2], m[a1][a1])
        else:  # singularity
            alpha = atan2(parity * m[a3][a2], m[a2][a2])
            gamma = 0.0
    else:  # proper Euler, e.g. zxz
        beta = atan2(norm(m[a2][a1], m[a3][a1]), m[a1][a1])
        if abs(beta) > EPS and abs(beta) < pi - EPS:
            alpha = atan2(m[a2][a1], -parity * m[a3][a1])
            gamma = atan2(m[a1][a2], parity * m[a1][a3])
        else:  # singularity
            alpha = atan2(parity * m[a3][a2], m[a2][a2])
            gamma = 0.0

    if is_extrinsic:
        return gamma, beta, alpha
    return alpha, beta, gamma
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>from_ypr(...)</code> — Create a rotation from yaw, pitch and roll angles.</summary>

```py
@staticmethod
def from_ypr(yaw: float, pitch: float, roll: float) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a rotation from yaw, pitch and roll angles."""
    return Rotation.compose("ZYX", [yaw, pitch, roll])
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>as_ypr()</code> — Return the yaw, pitch and roll angles of the rotation.</summary>

In case of a singularity (gimbal lock), roll is set to 0.

```py
def as_ypr(self) -> Tuple[float, float, float]:
```
<details><summary><i>source code</i></summary>

```py
    """Return the yaw, pitch and roll angles of the rotation.
    In case of a singularity (gimbal lock), roll is set to 0."""
    return self.as_euler("ZYX")
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>from_rpy(...)</code> — Create a rotation from roll, pitch and yaw angles.</summary>

```py
@staticmethod
def from_rpy(roll: float, pitch: float, yaw: float) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a rotation from roll, pitch and yaw angles."""
    return Rotation.compose("xyz", [roll, pitch, yaw])
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>as_rpy()</code> — Return the roll, pitch and yaw angles of the rotation.</summary>

In case of a singularity (gimbal lock), roll is set to 0.

```py
def as_rpy(self) -> Tuple[float, float, float]:
```
<details><summary><i>source code</i></summary>

```py
    """Return the roll, pitch and yaw angles of the rotation.
    In case of a singularity (gimbal lock), roll is set to 0."""
    return self.as_euler("xyz")
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>rand(...)</code> — Create a random rotation with uniform distribution.</summary>

```py
@staticmethod
def rand(generator: random.Random = cast(random.Random, random)) -> "Rotation":
```
<details><summary><i>source code</i></summary>

```py
    """Create a random rotation with uniform distribution."""
    x = generator.gauss()
    y = generator.gauss()
    z = generator.gauss()
    w = generator.gauss()
    # should be impossible for Mersenne twister to generate a zero quaternion
    return Rotation.from_quat(x=x, y=y, z=z, w=w, tolerance=_inf)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>_rotate_vector(...)</code> — Rotate a vector by the rotation (returns a new Vector).</summary>

```py
def _rotate_vector(self, other: Vector) -> Vector:
```
<details><summary><i>source code</i></summary>

```py
    """Rotate a vector by the rotation (returns a new Vector)."""
    qx, qy, qz, qw = self._x, self._y, self._z, self._w
    px, py, pz = other  # pw = 0

    qpx = qw * px + qy * pz - qz * py
    qpy = qw * py - qx * pz + qz * px
    qpz = qw * pz + qx * py - qy * px
    qpw = -qx * px - qy * py - qz * pz

    qpq_x = -qpw * qx + qpx * qw - qpy * qz + qpz * qy
    qpq_y = -qpw * qy + qpx * qz + qpy * qw - qpz * qx
    qpq_z = -qpw * qz - qpx * qy + qpy * qx + qpz * qw

    return Vector(qpq_x, qpq_y, qpq_z)
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>_rotate_quat(...)</code> — Combine two rotations (quaternion multiplication).</summary>

```py
def _rotate_quat(self, other: "Rotation") -> Tuple[float, float, float, float]:
```
<details><summary><i>source code</i></summary>

```py
    """Combine two rotations (quaternion multiplication)."""
    x1, y1, z1, w1 = self._x, self._y, self._z, self._w
    x2, y2, z2, w2 = other._x, other._y, other._z, other._w
    x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
    y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2
    z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2
    w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
    # does not return Rotation to let the caller decide whether to normalize
    return x, y, z, w
```
</details>
</details>

<details><summary>&nbsp;&nbsp;&nbsp;&nbsp;<code>__matmul__(...)</code> — Combine two rotations or rotate a vector or a sequence of vectors.</summary>

```py
@overload
def __matmul__(self, other: "Rotation") -> "Rotation":
    """Combine two rotations."""
    ...

@overload
def __matmul__(self, other: Vector) -> Vector:
    """Rotate a vector."""
    ...

@overload
def __matmul__(self, other: Iterable[Vector]) -> Iterable[Vector]:
    """Rotate a sequence of vectors."""
    ...

def __matmul__(
    self, other: Union["Rotation", Vector, Iterable[Vector]]
) -> Union["Rotation", Vector, Iterable[Vector]]:
```
<details><summary><i>source code</i></summary>

```py
    """Combine tw