# `Monty.Geometry.Rotation`
[🔗](https://github.com/d-led/ex.tbp.monty/blob/v0.1.0/packages/ex_tbp_monty_core/lib/monty/geometry/rotation.ex#L1)

A rotation, as the quaternion `wxyz` Monty passes around.

Mirrors `tbp/monty/geometry.py::Rotation`, which wraps `scipy`'s
`Rotation` — so what is mirrored here is the behaviour Monty relies on, not
scipy's surface: rotation vectors, rotation matrices, Euler angles and
quaternions each convert to the others, a rotation can be inverted, and it can
be applied to a vector or a matrix of them.

Two conventions are kept exactly as scipy has them, because getting either
wrong rotates a reference frame without any number looking wrong:

  * a quaternion is `{w, x, y, z}` here (Monty's `QuaternionWXYZ`), while the
    `[x, y, z, w]` order is what scipy returns from `as_quat()` — the
    conversion functions say which is which;
  * `from_euler("xyz", angles, degrees: true)` is scipy's *extrinsic* order:
    rotate about x, then about the (fixed) y, then about the (fixed) z.

# `matrix`

```elixir
@type matrix() :: [[float()]]
```

# `t`

```elixir
@type t() :: %Monty.Geometry.Rotation{wxyz: {float(), float(), float(), float()}}
```

# `vector`

```elixir
@type vector() :: [float()]
```

# `align_vectors`

```elixir
@spec align_vectors([[number()]], [[number()]], [number()] | nil) :: {t(), float()}
```

The rotation that best aligns two sets of vectors: scipy's
`Rotation.align_vectors`.

Answers `{rotation, rssd}`, where `rotation` turns each `b` towards the `a` it
was paired with, and `rssd` is what is left over — the square root of the
weighted sum of squared distances, `sqrt(Σ wᵢ |aᵢ − R bᵢ|²)`, which is zero
when the two sets are the same up to a rotation.

scipy minimises that sum by singular value decomposition (the Kabsch
algorithm). The same minimiser is the leading eigenvector of a 4×4 symmetric
matrix built from the two sets' correlation — Horn's closed form — and that is
what this uses, because `mean/2` already finds leading eigenvectors the same
way. Both answer a proper rotation, never a reflection.

Two sets that lie on one line do not determine a rotation about that line.
scipy warns and answers with one of the rotations that fit; so does this, and
any of them is as close as the others.

# `apply`

```elixir
@spec apply(t(), vector() | [vector()]) :: vector() | [vector()]
```

Applies this rotation to a 3-vector, or to each row of a list of them.

# `from_axis_angle`

```elixir
@spec from_axis_angle(vector(), float()) :: {:ok, t()} | {:error, :zero_axis}
```

The rotation of `angle` radians about `axis`.

A zero axis has no rotation to describe, and `{:error, :zero_axis}` is the
honest answer — scipy normalises the axis and produces NaN, which is worse.

# `from_euler_degrees`

```elixir
@spec from_euler_degrees(vector()) :: {:ok, t()}
```

The rotation of three Euler angles in degrees, applied about the fixed x, then
y, then z axes (scipy's `from_euler("xyz", angles, degrees=True)`).

Extrinsic order means the composed rotation is `Rz · Ry · Rx`: `x` turns
first, in the frame the axes were given in.

# `from_matrix`

```elixir
@spec from_matrix(matrix()) :: {:ok, t()} | {:error, :not_a_rotation}
```

The rotation a 3×3 rotation matrix describes.

# `from_quaternion`

```elixir
@spec from_quaternion({number(), number(), number(), number()}) ::
  {:ok, t()} | {:error, term()}
```

The rotation a quaternion `{w, x, y, z}` describes, normalised.

# `identity`

```elixir
@spec identity() :: t()
```

The rotation of no angle, `w = 1`.

# `invert`

```elixir
@spec invert(t()) :: t()
```

The rotation that undoes this one.

# `magnitude`

```elixir
@spec magnitude(t()) :: float()
```

The angle this rotation turns through, in radians, in `[0, π]`.

This is what Monty compares against `pose_similarity_threshold`: two rotations
are the same pose when rotating from one to the other turns by less than the
threshold.

# `mean`

```elixir
@spec mean([t()], [number()] | nil) :: t()
```

The weighted mean of several rotations: the rotation closest to all of them.

Averaging rotation *matrices* and calling the result a rotation gives
something that is not orthonormal, so the average is the rotation nearest to
the weighted sum — scipy's `Rotation.mean`, which is the chordal L2 mean.
Quaternions make that a dominant-eigenvector problem: the sum of weighted
outer products of the quaternions has the answer as its leading eigenvector.

The dominant eigenvector is found by power iteration. Where the two leading
eigenvalues tie, more than one rotation is equally close to the inputs and any
of them is a correct answer.

# `multiply`

```elixir
@spec multiply(t(), t()) :: t()
```

This rotation after `other`: applying the result is applying `other` then this.

# `to_euler_degrees`

```elixir
@spec to_euler_degrees(t(), non_neg_integer() | nil) :: vector()
```

The three Euler angles in degrees about the fixed x, y and z axes.

The angles are rounded to `decimal_places`, because Monty compares them as
labels (`np.round(pose.as_euler(...), 3) % 360`) and an unrounded 359.9999
would read as a different pose from 0.0. `nil` leaves them as computed, which
is what scipy answered and what a log keeps.

# `to_euler_radians`

```elixir
@spec to_euler_radians(t()) :: vector()
```

The three Euler angles about the fixed x, y and z axes, in radians.

scipy's `as_euler("xyz")`, unrounded — which is what Monty compares when it
asks whether a pose has changed: any difference, however small, is a
difference. Use `to_euler_degrees/2` to show angles to a reader instead.

# `to_matrix`

```elixir
@spec to_matrix(t()) :: matrix()
```

The 3×3 rotation matrix this rotation applies.

# `to_quaternion`

```elixir
@spec to_quaternion(t()) :: {float(), float(), float(), float()}
```

The quaternion `{w, x, y, z}`.

# `to_xyzw`

```elixir
@spec to_xyzw(t()) :: [float()]
```

The quaternion `[x, y, z, w]`, which is the order scipy returns.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
