Monty.Geometry.Rotation (ExTbpMonty.Core v0.1.0)

Copy Markdown View Source

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.

Summary

Functions

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

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

The rotation of angle radians about axis.

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

The rotation a 3×3 rotation matrix describes.

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

The rotation of no angle, w = 1.

The rotation that undoes this one.

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

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

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

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

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

The 3×3 rotation matrix this rotation applies.

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

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

Types

matrix()

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

t()

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

vector()

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

Functions

align_vectors(a_vectors, b_vectors, weights \\ nil)

@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(rotation, vectors)

@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(axis, angle)

@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(list)

@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(list)

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

The rotation a 3×3 rotation matrix describes.

from_quaternion(quaternion)

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

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

identity()

@spec identity() :: t()

The rotation of no angle, w = 1.

invert(rotation)

@spec invert(t()) :: t()

The rotation that undoes this one.

magnitude(rotation)

@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(rotations, weights \\ nil)

@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(rotation1, rotation2)

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

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

to_euler_degrees(rotation, decimal_places \\ 3)

@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(rotation)

@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(rotation)

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

The 3×3 rotation matrix this rotation applies.

to_quaternion(rotation)

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

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

to_xyzw(rotation)

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

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