diff --git a/gtsam/geometry/doc/Rot2.ipynb b/gtsam/geometry/doc/Rot2.ipynb
index 704915b4c6..6107270905 100644
--- a/gtsam/geometry/doc/Rot2.ipynb
+++ b/gtsam/geometry/doc/Rot2.ipynb
@@ -67,7 +67,7 @@
{
"cell_type": "markdown",
"source": [
- "# Initialization and properties"
+ "## Initialization and properties"
],
"metadata": {
"id": "gZRXZTrJ7mqJ"
diff --git a/gtsam/geometry/doc/Rot3.ipynb b/gtsam/geometry/doc/Rot3.ipynb
new file mode 100644
index 0000000000..c0ba050673
--- /dev/null
+++ b/gtsam/geometry/doc/Rot3.ipynb
@@ -0,0 +1,1342 @@
+{
+ "nbformat": 4,
+ "nbformat_minor": 0,
+ "metadata": {
+ "colab": {
+ "provenance": []
+ },
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": [
+ "# Rot3"
+ ],
+ "metadata": {
+ "id": "Wy0JIcGioHI9"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "A `gtsam.Rot3` represents a rotation in 3D space. It can be manipulated and presented as a rotation matrix, a quaternion, roll-pitch-yaw angles (Euler angles), or as an axis-angle combination. It models a 3D rotation in the [Special Orthogonal Group $\\text{SO}(3)$](https://en.wikipedia.org/wiki/SO(3)), which is a [Lie group](https://en.wikipedia.org/wiki/Lie_group)."
+ ],
+ "metadata": {
+ "id": "YqaxPKyloJG_"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "
"
+ ],
+ "metadata": {
+ "id": "Hmwbhz75pcQT"
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "HyXPOMakoDkY",
+ "outputId": "665a19a8-4265-4d54-bc43-b5f4df9b62c8"
+ },
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "Collecting gtsam\n",
+ " Downloading gtsam-4.2-cp311-cp311-manylinux2014_x86_64.whl.metadata (7.6 kB)\n",
+ "Requirement already satisfied: numpy>=1.11.0 in /usr/local/lib/python3.11/dist-packages (from gtsam) (1.26.4)\n",
+ "Requirement already satisfied: pyparsing>=2.4.2 in /usr/local/lib/python3.11/dist-packages (from gtsam) (3.2.1)\n",
+ "Downloading gtsam-4.2-cp311-cp311-manylinux2014_x86_64.whl (22.4 MB)\n",
+ "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m22.4/22.4 MB\u001b[0m \u001b[31m10.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
+ "\u001b[?25hInstalling collected packages: gtsam\n",
+ "Successfully installed gtsam-4.2\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pip install gtsam"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "import gtsam\n",
+ "from gtsam import Rot3, Point3, Quaternion\n",
+ "import numpy as np"
+ ],
+ "metadata": {
+ "id": "_fWy46Mepoxh"
+ },
+ "execution_count": 2,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Initialization"
+ ],
+ "metadata": {
+ "id": "3DkkBKyAqGnY"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "A `Rot3` can be initialized in many different ways, which are detailed in this section. Note that printing a `Rot3` displays its 3x3 rotation matrix representation, which in general is a 3x3 matrix where the columns are unit vectors that define a coordinate frame."
+ ],
+ "metadata": {
+ "id": "RrJ5ZEdhqJPU"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Constructor\n",
+ "\n",
+ "The `Rot3` constructor provides for initialization with no arguments, yielding the identity rotation (equivalent to $I_3$), initialization with a precalculated rotation matrix (either as a 3x3 `np.ndarray`, as three 3-vectors, or as 9 floats), and initialization with a quaternion's $w, x, y, z$."
+ ],
+ "metadata": {
+ "id": "AqEy5JLe5X1t"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "# No-argument constructor\n",
+ "a = Rot3()\n",
+ "print(a)\n",
+ "\n",
+ "# Construct from a rotation matrix\n",
+ "theta = np.pi / 2\n",
+ "b = Rot3(np.array([ # Rotate around X axis by PI / 2\n",
+ " [1, 0, 0],\n",
+ " [0, np.cos(theta), -np.sin(theta)],\n",
+ " [0, np.sin(theta), np.cos(theta)]\n",
+ "]))\n",
+ "print(b)\n",
+ "\n",
+ "# Construct from three column vectors\n",
+ "c = Rot3([11, 21, 31], [12, 22, 32], [13, 23, 33])\n",
+ "print(c)\n",
+ "\n",
+ "# Construct from 9 floats\n",
+ "d = Rot3(1, 2, 3, 4, 5, 6, 7, 8, 9)\n",
+ "print(d)\n",
+ "\n",
+ "# Construct from quaternion values\n",
+ "e = Rot3(0, 0, 0, 1) # Rotate around Z axis by pi\n",
+ "print(e)"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "mj8X-wIdq6GR",
+ "outputId": "48a6921d-df39-4fd8-aaf8-76d4bcdb70b1"
+ },
+ "execution_count": 3,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t1, 0, 0;\n",
+ "\t0, 1, 0;\n",
+ "\t0, 0, 1\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t1, 0, 0;\n",
+ "\t0, 6.12323e-17, -1;\n",
+ "\t0, 1, 6.12323e-17\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t11, 12, 13;\n",
+ "\t21, 22, 23;\n",
+ "\t31, 32, 33\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t1, 2, 3;\n",
+ "\t4, 5, 6;\n",
+ "\t7, 8, 9\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t-1, 0, 0;\n",
+ "\t0, -1, 0;\n",
+ "\t0, 0, 1\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Named constructors\n",
+ "\n",
+ "In addition to its constructors, `Rot3` has several named constructors, or factory functions, that allow instantiation from a wide variety of methods."
+ ],
+ "metadata": {
+ "id": "EMaB3yVoJ_qv"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "`Rot3.Identity()` returns the 3x3 rotation identity matrix."
+ ],
+ "metadata": {
+ "id": "3s9Ym_6BaE_r"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "print(Rot3.Identity())"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "GcAB8GtVaLjK",
+ "outputId": "b9e701cd-6a3f-4171-a518-158a2f7b60fd"
+ },
+ "execution_count": 4,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t1, 0, 0;\n",
+ "\t0, 1, 0;\n",
+ "\t0, 0, 1\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "`Rx`, `Ry`, `Rz`, and `RzRyRx` create rotations around these axes."
+ ],
+ "metadata": {
+ "id": "F2MXz29VXqLR"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "# Rotation around X axis\n",
+ "x = Rot3.Rx(np.pi / 2)\n",
+ "print(x)\n",
+ "\n",
+ "# Rotation around Y axis\n",
+ "y = Rot3.Ry(np.pi / 4)\n",
+ "print(y)\n",
+ "\n",
+ "# Rotation around Z axis\n",
+ "z = Rot3.Rz(np.pi / 6)\n",
+ "print(z)\n",
+ "\n",
+ "# Rotate around Z, then Y, then X axes\n",
+ "# Note that the order of angles in the function signature (x, y, z) is the\n",
+ "# reverse of the order in which the rotations are applied (z, y, x).\n",
+ "zyx = Rot3.RzRyRx(np.pi / 2, np.pi / 4, np.pi / 6)\n",
+ "# Rot3.RzRyRx is overloaded: it also accepts an array of [x, y, z].\n",
+ "# e.g. the above is identical to zyx = Rot3.RzRyRx([np.pi / 2, np.pi / 4, np.pi / 6]).\n",
+ "print(zyx)\n",
+ "# Of course, zyx is the same as z * y * x, since we fed the same angles to each.\n",
+ "print(z * y * x)"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "w-qh5dX6VWAW",
+ "outputId": "8fe29ae6-eb47-4460-c27b-3acd8ee5e5bf"
+ },
+ "execution_count": 5,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t1, 0, 0;\n",
+ "\t0, 6.12323e-17, -1;\n",
+ "\t0, 1, 6.12323e-17\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t0.707107, 0, 0.707107;\n",
+ "\t0, 1, 0;\n",
+ "\t-0.707107, 0, 0.707107\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t0.866025, -0.5, 0;\n",
+ "\t0.5, 0.866025, 0;\n",
+ "\t0, 0, 1\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t0.612372, 0.612372, 0.5;\n",
+ "\t0.353553, 0.353553, -0.866025;\n",
+ "\t-0.707107, 0.707107, 4.32978e-17\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t0.612372, 0.612372, 0.5;\n",
+ "\t0.353553, 0.353553, -0.866025;\n",
+ "\t-0.707107, 0.707107, 4.32978e-17\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "Similarly, `Yaw`, `Pitch`, `Roll`, and `Ypr` are available."
+ ],
+ "metadata": {
+ "id": "c-_H7XmUYAd_"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "# Yaw around Z axis (positive yaw is to the right, as in aircraft heading)\n",
+ "y = Rot3.Yaw(np.pi / 6)\n",
+ "print(y)\n",
+ "\n",
+ "# Pitch around Y axis (positive pitch is up, as in increasing aircraft altitude)\n",
+ "p = Rot3.Pitch(np.pi / 4)\n",
+ "print(p)\n",
+ "\n",
+ "# Roll around X axis\n",
+ "r = Rot3.Roll(np.pi / 2)\n",
+ "print(r)\n",
+ "\n",
+ "# Yaw, pitch, then roll\n",
+ "# Unlike RzRyRx, rotations are applied in the same order as supplied.\n",
+ "# Ypr is not overloaded to support an array.\n",
+ "ypr = Rot3.Ypr(np.pi / 6, np.pi / 4, np.pi / 2)\n",
+ "print(ypr)"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "bGEMGXkpYT9t",
+ "outputId": "31655b9f-045f-4b51-dff2-42de4382427f"
+ },
+ "execution_count": 6,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t0.866025, -0.5, 0;\n",
+ "\t0.5, 0.866025, 0;\n",
+ "\t0, 0, 1\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t0.707107, 0, 0.707107;\n",
+ "\t0, 1, 0;\n",
+ "\t-0.707107, 0, 0.707107\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t1, 0, 0;\n",
+ "\t0, 6.12323e-17, -1;\n",
+ "\t0, 1, 6.12323e-17\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t0.612372, 0.612372, 0.5;\n",
+ "\t0.353553, 0.353553, -0.866025;\n",
+ "\t-0.707107, 0.707107, 4.32978e-17\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "`Rot3.Quaternion` is identical to the four-argument `Rot3` constructor."
+ ],
+ "metadata": {
+ "id": "_ks-ohhZZ5Ap"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "# Create from quaternion w, x, y, z\n",
+ "q = Rot3.Quaternion(0, 0, 0, 1)\n",
+ "print(q)\n",
+ "print(q.equals(Rot3(0, 0, 0, 1), 1e-8))"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "uO9hb2RBaG3g",
+ "outputId": "5409ef2e-3651-439b-af9f-6ed7888aa9d5"
+ },
+ "execution_count": 7,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t-1, 0, 0;\n",
+ "\t0, -1, 0;\n",
+ "\t0, 0, 1\n",
+ "]\n",
+ "\n",
+ "True\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "`Rot3.AxisAngle` creates a `Rot3` from an axis and an angle around that axis."
+ ],
+ "metadata": {
+ "id": "jy7dn6_vabsK"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "aa = Rot3.AxisAngle([0, 1, 0], np.pi / 2)\n",
+ "print(aa)"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "M_OOSKgAaqhF",
+ "outputId": "cbb875b7-9204-4a90-c225-dbea46718c6f"
+ },
+ "execution_count": 8,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t2.22045e-16, 0, 1;\n",
+ "\t0, 1, 0;\n",
+ "\t-1, 0, 2.22045e-16\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "`Rot3.Rodrigues` creates a `Rot3` from incremental roll, pitch, and yaw values. It is identical to the exponential map at identity."
+ ],
+ "metadata": {
+ "id": "ruyunRlUclFX"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "rod = Rot3.Rodrigues(np.pi / 6, np.pi / 4, np.pi / 2)\n",
+ "# Rodrigues is overloaded to support an array.\n",
+ "# e.g. Rot3.Rodrigues([np.pi / 6, np.pi / 4, np.pi / 2])\n",
+ "print(rod)"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "NUgeRQzIcqYp",
+ "outputId": "8c189748-267b-4c25-e0cf-4eed8721bc4a"
+ },
+ "execution_count": 9,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t-0.156058, -0.673795, 0.72225;\n",
+ "\t0.982078, -0.0276074, 0.186445;\n",
+ "\t-0.105686, 0.738402, 0.666028\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "`Rot3.ClosestTo` finds the closest valid `Rot3` to the input matrix which minimizes the Frobenius norm. The Frobenius norm is a measure of matrix difference:\n",
+ "\n",
+ "$$\n",
+ "||A - B||_F = \\sqrt{\\sum_{i,j} (A_{ij} - B_{ij})^2}\n",
+ "$$"
+ ],
+ "metadata": {
+ "id": "INihgtF6fI22"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "closest = Rot3.ClosestTo([\n",
+ " [1, 0, 0],\n",
+ " [0, 2, 0],\n",
+ " [0, 0, 3]\n",
+ "])\n",
+ "print(closest)"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "EFMbwiTKfLfJ",
+ "outputId": "72bf7784-6a8c-4052-c444-d85d6d9014e7"
+ },
+ "execution_count": 10,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t1, 0, 0;\n",
+ "\t0, 1, 0;\n",
+ "\t0, 0, 1\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Properties"
+ ],
+ "metadata": {
+ "id": "Sm3oUTObqxJl"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "The following properties are available from the standard interface:\n",
+ "- `matrix()`: Returns the 3x3 rotation matrix.\n",
+ "- `transpose()`: Returns the transpose of the 3x3 rotation matrix.\n",
+ "- `xyz()`: Returns the 3 Euler angles as $x, y, z$.\n",
+ "- `ypr()`: Returns the 3 Euler angles as yaw, pitch, and roll.\n",
+ "- `rpy()`: Returns the 3 Euler angles as roll, pitch, and yaw.\n",
+ "- `roll()`: Returns the roll angle.\n",
+ "- `pitch()`: Returns the pitch angle.\n",
+ "- `yaw()`: Returns the yaw angle.\n",
+ "- `axisAngle()`: Returns the axis-angle representation.\n",
+ "- `toQuaternion()`: Returns the quaternion representation. The quaternion's attributes can then be accessed either individually with `w()`, `x()`, `y()`, `z()` or together with `coeffs()`.\n",
+ "\n",
+ "Note that accessing `roll()`, `pitch()`, and `yaw()` separately is less efficient than calling `rpy()` or `ypr()`."
+ ],
+ "metadata": {
+ "id": "0Gax04WXqyeE"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "props = Rot3.RzRyRx(0, np.pi / 6, np.pi / 2)\n",
+ "\n",
+ "print(\"Matrix:\\n\", props.matrix())\n",
+ "print(\"Transpose:\\n\", props.transpose())\n",
+ "print()\n",
+ "print(\"x, y, z:\", props.xyz())\n",
+ "print(\"y, p, r:\", props.ypr())\n",
+ "print(\"r, p, y:\", props.rpy())\n",
+ "print()\n",
+ "print(\"Roll: \", props.roll())\n",
+ "print(\"Pitch: \", props.pitch())\n",
+ "print(\"Yaw: \", props.yaw())\n",
+ "print()\n",
+ "print(\"Axis-angle:\\n\", props.axisAngle())\n",
+ "print()\n",
+ "print(\"Quaternion:\", props.toQuaternion().coeffs())"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "zbNPBHiwDAE2",
+ "outputId": "716f9db1-f7c7-4418-f7c7-5c6bae0b6a2c"
+ },
+ "execution_count": 11,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "Matrix:\n",
+ " [[ 5.30287619e-17 -1.00000000e+00 3.06161700e-17]\n",
+ " [ 8.66025404e-01 6.12323400e-17 5.00000000e-01]\n",
+ " [-5.00000000e-01 0.00000000e+00 8.66025404e-01]]\n",
+ "Transpose:\n",
+ " [[ 5.30287619e-17 8.66025404e-01 -5.00000000e-01]\n",
+ " [-1.00000000e+00 6.12323400e-17 0.00000000e+00]\n",
+ " [ 3.06161700e-17 5.00000000e-01 8.66025404e-01]]\n",
+ "\n",
+ "x, y, z: [0. 0.52359878 1.57079633]\n",
+ "y, p, r: [1.57079633 0.52359878 0. ]\n",
+ "r, p, y: [0. 0.52359878 1.57079633]\n",
+ "\n",
+ "Roll: 0.0\n",
+ "Pitch: 0.5235987755982988\n",
+ "Yaw: 1.5707963267948966\n",
+ "\n",
+ "Axis-angle:\n",
+ " (:-0.250563\n",
+ " 0.250563\n",
+ " 0.935113\n",
+ ", 1.6378338249998232)\n",
+ "\n",
+ "Quaternion: [-0.1830127 0.1830127 0.6830127 0.6830127]\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Basic operations"
+ ],
+ "metadata": {
+ "id": "-XnTJ-psGeJf"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "`Rot3` can rotate and unrotate a 3D point or vector. Rotation is calculated by the simple matrix product $Rx$, and unrotation by $R^{-1}x$."
+ ],
+ "metadata": {
+ "id": "gj3OlBlGGfjj"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "z90 = Rot3.Rz(np.pi / 2)\n",
+ "point = [2, 0, 0]\n",
+ "\n",
+ "# Rotate by 90 degrees around the Z axis\n",
+ "rotated = z90.rotate(point)\n",
+ "print(rotated)\n",
+ "# Undo the rotation\n",
+ "print(z90.unrotate(rotated))\n",
+ "# Rotate backwards by 90 degrees around the Z axis\n",
+ "print(z90.unrotate(point))"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "rtbkHyp3GgWx",
+ "outputId": "43b1178c-39d3-4df2-face-9e8cef162cdd"
+ },
+ "execution_count": 12,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "[1.2246468e-16 2.0000000e+00 0.0000000e+00]\n",
+ "[2. 0. 0.]\n",
+ "[ 1.2246468e-16 -2.0000000e+00 0.0000000e+00]\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "Check whether two `Rot3` instances are equal within a certain tolerance using `equals()`. Be careful with the `==` operator; it does not compare rotational equivalence, it compares object reference. If you wish to use more fine-grained equality comparison, convert to `np.ndarray` with `matrix()`."
+ ],
+ "metadata": {
+ "id": "d0bQ-tmwHmZ5"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "xyz = Rot3.RzRyRx(np.pi / 2, np.pi / 4, np.pi / 6)\n",
+ "ypr = Rot3.Ypr(np.pi / 6, np.pi / 4, np.pi / 2)\n",
+ "\n",
+ "print(\"xyz.equals(ypr, 1e-8):\", xyz.equals(ypr, 1e-8))\n",
+ "print(\"xyz == ypr:\", xyz == ypr)\n",
+ "print(\"xyz == xyz:\", xyz == xyz)\n",
+ "print(\"xyz.matrix() == ypr.matrix():\", np.all(xyz.matrix() == ypr.matrix()))"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "MYiKxq4vItz3",
+ "outputId": "e8f8fdd0-c539-476f-f233-2f78f98ca671"
+ },
+ "execution_count": 13,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "xyz.equals(ypr, 1e-8): True\n",
+ "xyz == ypr: False\n",
+ "xyz == xyz: True\n",
+ "xyz.matrix() == ypr.matrix(): True\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "Use SLERP (spherical linear interpolation) to interpolate between two `Rot3` instances. In terms of the Lie algebra (see below), SLERP can be calculated by scaling the log mapped relative rotation by the interpolation term $t$, then converting back to $\\text{SO}(3)$ using the exponential map. The formula is thus:\n",
+ "\n",
+ "$$\n",
+ "R(t) = R_1 \\exp(t \\cdot \\log(R_1^{-1}R_2))\n",
+ "$$\n",
+ "\n",
+ "where $R_1$ and $R_2$ are the start `Rot3` and end `Rot3` of the interpolation and $t$ is the interpolation term, usually but not necessarily in the range $[0, 1]$."
+ ],
+ "metadata": {
+ "id": "bQzlRJ_2HWNz"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "a = Rot3.RzRyRx(0, np.pi / 4, 0)\n",
+ "b = Rot3.RzRyRx(np.pi / 6, 0, 0)\n",
+ "\n",
+ "print(a.slerp(0.5, b))"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "y45qZPivHkRR",
+ "outputId": "e2320424-004f-47bf-e844-bf04df63a916"
+ },
+ "execution_count": 14,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t0.922613, 0.0523387, 0.382159;\n",
+ "\t0.0523387, 0.964602, -0.258464;\n",
+ "\t-0.382159, 0.258464, 0.887215\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Lie group $\\text{SO}(3)$"
+ ],
+ "metadata": {
+ "id": "XlmNuuxSGgoj"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Group operations\n",
+ "\n",
+ "`Rot3` implements the group operations `inverse`, `compose`, `between` and `identity`. For more information on groups and their use here, see [GTSAM concepts](https://gtsam.org/notes/GTSAM-Concepts.html)."
+ ],
+ "metadata": {
+ "id": "XtUmE-QSGsh9"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "a = Rot3.Rz(np.pi / 4)\n",
+ "b = Rot3.Roll(np.pi / 2)\n",
+ "\n",
+ "print(\"a:\\n\", a.matrix(), \"\\nb:\\n\", b.matrix())"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "axvFPtxYdGru",
+ "outputId": "977f9582-5b23-43c7-a6f5-a7bfce6d5cea"
+ },
+ "execution_count": 15,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "a:\n",
+ " [[ 0.70710678 -0.70710678 0. ]\n",
+ " [ 0.70710678 0.70710678 0. ]\n",
+ " [ 0. 0. 1. ]] \n",
+ "b:\n",
+ " [[ 1.000000e+00 0.000000e+00 0.000000e+00]\n",
+ " [ 0.000000e+00 6.123234e-17 -1.000000e+00]\n",
+ " [ 0.000000e+00 1.000000e+00 6.123234e-17]]\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "The inverse of an $\\text{SO}(3)$ rotation matrix is the same as its transpose."
+ ],
+ "metadata": {
+ "id": "3eH9K5VH9jTb"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "print(a.inverse())\n",
+ "# The inverse is the same as the transpose.\n",
+ "print(a.inverse().equals(Rot3(a.transpose()), 1e-8))"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "ffVBzuOhGugd",
+ "outputId": "ff207bed-850c-422a-d9a6-a0b59e801989"
+ },
+ "execution_count": 16,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t0.707107, 0.707107, 0;\n",
+ "\t-0.707107, 0.707107, 0;\n",
+ "\t0, 0, 1\n",
+ "]\n",
+ "\n",
+ "True\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "The product of the composition operation $A * B$ is the rotation matrix which applies the rotation of $A$ and then the rotation of $B$. The composition of two rotation matrices is just the product of the two matrices."
+ ],
+ "metadata": {
+ "id": "P1bYYGkGdjxm"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "print(a.compose(b))\n",
+ "\n",
+ "# The * operator is syntactic sugar for the compose operation.\n",
+ "print(a.compose(b).equals(a * b, 1e-8))\n",
+ "\n",
+ "# The composition of two rotation matrices is just the product of the matrices.\n",
+ "print(np.all(a.compose(b).matrix() == a.matrix() @ b.matrix()))"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "4zXJJ77FdLBB",
+ "outputId": "c5875121-0d97-475c-8669-93b92ac37c1b"
+ },
+ "execution_count": 17,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t0.707107, -4.32978e-17, 0.707107;\n",
+ "\t0.707107, 4.32978e-17, -0.707107;\n",
+ "\t0, 1, 6.12323e-17\n",
+ "]\n",
+ "\n",
+ "True\n",
+ "True\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "The between operation calculates the rotation from one `Rot3` to another. It is defined as simply:\n",
+ "\n",
+ "$$\n",
+ "R_{relative} = R_1^{-1}R_2\n",
+ "$$"
+ ],
+ "metadata": {
+ "id": "TIuwUygjfECu"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "print(a.between(b))\n",
+ "print(a.between(b).equals(a.inverse() * b, 1e-8))"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "_qGyDV15dgmU",
+ "outputId": "be748925-3425-41c7-b06b-57ab87955699"
+ },
+ "execution_count": 18,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t0.707107, 4.32978e-17, -0.707107;\n",
+ "\t-0.707107, 4.32978e-17, -0.707107;\n",
+ "\t0, 1, 6.12323e-17\n",
+ "]\n",
+ "\n",
+ "True\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "The identity is $I_3$, as described above."
+ ],
+ "metadata": {
+ "id": "6_jR6zhMfa1l"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "print(Rot3.Identity())"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "SchtjDIPfXtb",
+ "outputId": "7d199a1e-b9a7-4775-81e7-9c1328012b89"
+ },
+ "execution_count": 19,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t1, 0, 0;\n",
+ "\t0, 1, 0;\n",
+ "\t0, 0, 1\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "#### Group operation invariants\n",
+ "\n",
+ "See that the following group invariants hold:"
+ ],
+ "metadata": {
+ "id": "fjfUIqKHflXY"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "print(\"Compose(a, Inverse(a)) == Identity: \", (a * a.inverse()).equals(Rot3.Identity(), 1e-8))\n",
+ "print(\"Compose(a, Between(a, b)) == b:\", (a * a.between(b)).equals(b, 1e-8))\n",
+ "print(\"Between(a, b) == Compose(Inverse(a), b):\", a.between(b).equals(a.inverse() * b, 1e-8))"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "ysQSPxuwfnen",
+ "outputId": "4a7d8404-fc2a-46ca-ba18-236fd417382b"
+ },
+ "execution_count": 20,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "Compose(a, Inverse(a)) == Identity: True\n",
+ "Compose(a, Between(a, b)) == b: True\n",
+ "Between(a, b) == Compose(Inverse(a), b): True\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Lie group operations\n",
+ "\n",
+ "`Rot3` implements the Lie group operations for exponential mapping and log mapping. For more information on Lie groups and their use here, see [GTSAM concepts](https://gtsam.org/notes/GTSAM-Concepts.html)."
+ ],
+ "metadata": {
+ "id": "nKxTJy8YGuxg"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "The exponential map for $\\text{SO}(3)$ converts a 3D rotation vector (Lie algebra element in $\\mathfrak{so}(3)$) into a rotation matrix (Lie group element in $\\text{SO}(3)$). This is used to map a rotation vector $\\boldsymbol{\\omega} \\in \\mathbb{R}^3$ to a rotation matrix $R \\in \\text{SO}(3)$.\n",
+ "\n",
+ "Given a rotation vector $\\boldsymbol{\\omega} = (\\omega_x, \\omega_y, \\omega_z)$, define:\n",
+ "\n",
+ "- The rotation axis as the unit vector:\n",
+ " \n",
+ " $$\n",
+ " \\hat{\\omega} = \\frac{\\boldsymbol{\\omega}}{\\theta}, \\quad \\text{where } \\theta = \\|\\boldsymbol{\\omega}\\|\n",
+ " $$\n",
+ "\n",
+ "- The skew-symmetric matrix $[\\boldsymbol{\\omega}]_\\times$:\n",
+ "\n",
+ " $$\n",
+ " [\\boldsymbol{\\omega}]_\\times =\n",
+ " \\begin{bmatrix}\n",
+ " 0 & -\\omega_z & \\omega_y \\\\\n",
+ " \\omega_z & 0 & -\\omega_x \\\\\n",
+ " -\\omega_y & \\omega_x & 0\n",
+ " \\end{bmatrix}\n",
+ " $$\n",
+ "\n",
+ "Using Rodrigues' formula, the exponential map is:\n",
+ "\n",
+ "$$\n",
+ "\\exp([\\boldsymbol{\\omega}]_\\times) = I + \\frac{\\sin\\theta}{\\theta} [\\boldsymbol{\\omega}]_\\times + \\frac{1 - \\cos\\theta}{\\theta^2} [\\boldsymbol{\\omega}]_\\times^2\n",
+ "$$\n",
+ "\n",
+ "which results in the rotation matrix:\n",
+ "\n",
+ "$$\n",
+ "R = I + \\frac{\\sin\\theta}{\\theta} [\\boldsymbol{\\omega}]_\\times + \\frac{1 - \\cos\\theta}{\\theta^2} [\\boldsymbol{\\omega}]_\\times^2\n",
+ "$$\n",
+ "\n",
+ "where:\n",
+ "- $ I $ is the $ 3 \\times 3 $ identity matrix.\n",
+ "- $ \\theta $ is the magnitude of the rotation vector (rotation angle).\n",
+ "- $ [\\boldsymbol{\\omega}]_\\times $ represents the skew-symmetric matrix, the infinitesimal rotation generator.\n",
+ "\n",
+ "For very small $ \\theta $, we use the Taylor series approximation:\n",
+ "\n",
+ "$$\n",
+ "R \\approx I + [\\boldsymbol{\\omega}]_\\times\n",
+ "$$\n",
+ "\n",
+ "since $ \\sin\\theta \\approx \\theta$ and $ 1 - \\cos\\theta \\approx \\frac{\\theta^2}{2} $."
+ ],
+ "metadata": {
+ "id": "MmBfK0ad1KZ6"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "r1 = Rot3.RzRyRx(np.pi / 6, np.pi / 2, 0)\n",
+ "r2 = Rot3.RzRyRx(0, 0, np.pi / 4)\n",
+ "p1 = [np.pi / 2, 0, 0]\n",
+ "\n",
+ "# The exponential map at identity creates a rotation using Rodrigues' formula.\n",
+ "print(Rot3.Expmap(p1))\n",
+ "# The retract function takes the exponential map of the supplied vector and\n",
+ "# composes it with the calling Rot3. In other words, it maps from the tangent\n",
+ "# space to the manifold.\n",
+ "print(r1)\n",
+ "print(r1.retract(p1))"
+ ],
+ "metadata": {
+ "id": "yA5wO-5jGw2u",
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "outputId": "e0c75e07-2b6d-4f84-a90d-f1cceb3ad9fa"
+ },
+ "execution_count": 25,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t1, 0, 0;\n",
+ "\t0, 2.22045e-16, -1;\n",
+ "\t0, 1, 2.22045e-16\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t6.12323e-17, 0.5, 0.866025;\n",
+ "\t0, 0.866025, -0.5;\n",
+ "\t-1, 3.06162e-17, 5.30288e-17\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t6.12323e-17, 0.866025, -0.5;\n",
+ "\t0, -0.5, -0.866025;\n",
+ "\t-1, 5.30288e-17, -3.06162e-17\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "The logarithm map for $ \\text{SO}(3) $ is the inverse of the exponential map It converts a rotation matrix $ R \\in SO(3) $ into a 3D rotation vector (a Lie algebra element in $ \\mathfrak{so}(3) $).\n",
+ "\n",
+ "Given a rotation matrix $ R $, the corresponding rotation vector $ \\boldsymbol{\\omega} \\in \\mathbb{R}^3 $ is computed as:\n",
+ "\n",
+ "$$\n",
+ "\\boldsymbol{\\omega} = \\theta \\hat{\\omega}\n",
+ "$$\n",
+ "\n",
+ "where:\n",
+ "- $ \\theta $ is the rotation angle:\n",
+ " \n",
+ " $$\n",
+ " \\theta = \\cos^{-1} \\left( \\frac{\\text{Tr}(R) - 1}{2} \\right)\n",
+ " $$\n",
+ "\n",
+ "- $ \\hat{\\omega} $ is the rotation axis, obtained from the skew-symmetric matrix $ [\\boldsymbol{\\omega}]_\\times $:\n",
+ "\n",
+ " $$\n",
+ " [\\boldsymbol{\\omega}]_\\times = \\frac{\\theta}{2 \\sin\\theta} (R - R^T)\n",
+ " $$\n",
+ "\n",
+ "- Extracting the components:\n",
+ "\n",
+ " $$\n",
+ " \\boldsymbol{\\omega} =\n",
+ " \\frac{\\theta}{2 \\sin\\theta} (R - R^T)^\\vee\n",
+ " $$\n",
+ "\n",
+ "For small $ \\theta $, we use the Taylor series approximation:\n",
+ "\n",
+ "$$\n",
+ "\\boldsymbol{\\omega} \\approx \\frac{1}{2} (R - R^T)^\\vee\n",
+ "$$\n",
+ "\n",
+ "In both cases, $ (R - R^T)^\\vee $ extracts the unique 3D vector from a skew-symmetric matrix:\n",
+ "\n",
+ "$$\n",
+ "(R - R^T)^\\vee=\n",
+ "\\begin{bmatrix}\n",
+ " R_{32} - R_{23} \\\\\n",
+ " R_{13} - R_{31} \\\\\n",
+ " R_{21} - R_{12}\n",
+ " \\end{bmatrix}\n",
+ "$$\n",
+ "\n",
+ "where $ R_{ij} $ are the elements of $ R $."
+ ],
+ "metadata": {
+ "id": "Yk2nazsK6ixV"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "# Calculate the log map of r at identity. Returns the coordinates of the rotation\n",
+ "# in the tangent space.\n",
+ "print(Rot3.Logmap(r1))\n",
+ "\n",
+ "# Transform r2 into its vector representation relative to r1.\n",
+ "print(r1.logmap(r2))\n",
+ "# logmap is the same as calculating the coordinate of the second Rot3 in the\n",
+ "# local frame of the first, which localCoordinates (inherited from LieGroup) does.\n",
+ "print(r1.localCoordinates(r2))"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "0V2oQQ0lxS2-",
+ "outputId": "62b40acb-799e-4a91-dacd-c9e0266665c3"
+ },
+ "execution_count": 26,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "[ 0.41038024 1.53155991 -0.41038024]\n",
+ "[-1.01420581 -1.32173874 1.01420581]\n",
+ "[-1.01420581 -1.32173874 1.01420581]\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "# Applying localCoordinates and then retract cancels out, returning r2 given any\n",
+ "# r1. This is because it transforms r2 from the manifold to the tangent space\n",
+ "# using the log map, then transforms that result back into the manifold using\n",
+ "# the exponential map.\n",
+ "print(r2)\n",
+ "print(r1.retract(r1.localCoordinates(r2)))"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "-kTgSGJS06EC",
+ "outputId": "97051c49-284e-4ee8-d806-53f0d842fc31"
+ },
+ "execution_count": 27,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "R: [\n",
+ "\t0.707107, -0.707107, 0;\n",
+ "\t0.707107, 0.707107, 0;\n",
+ "\t-0, 0, 1\n",
+ "]\n",
+ "\n",
+ "R: [\n",
+ "\t0.707107, -0.707107, 9.04269e-17;\n",
+ "\t0.707107, 0.707107, 4.0637e-17;\n",
+ "\t-6.77245e-17, 6.77245e-17, 1\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Serialization\n",
+ "\n",
+ "A `Rot3` can be serialized to a string for saving, then later used by deserializing the string."
+ ],
+ "metadata": {
+ "id": "s_qu2bGt1-vr"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "a = Rot3.Rx(np.pi / 2)\n",
+ "print(\"Before serialization:\", a)\n",
+ "\n",
+ "str_val = a.serialize()\n",
+ "print(str_val)\n",
+ "print(\"The serialized value is a string:\", type(str_val))\n",
+ "# Save to file, etc...\n",
+ "\n",
+ "b = Rot3()\n",
+ "b.deserialize(str_val)\n",
+ "print(\"After deserialization:\", b)"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "m6ku7L_768Ta",
+ "outputId": "8cb6fe04-6759-4cd9-8145-42f4fc2a72dd"
+ },
+ "execution_count": 28,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "Before serialization: R: [\n",
+ "\t1, 0, 0;\n",
+ "\t0, 6.12323e-17, -1;\n",
+ "\t0, 1, 6.12323e-17\n",
+ "]\n",
+ "\n",
+ "22 serialization::archive 15 1 0\n",
+ "0 1.00000000000000000e+00 0.00000000000000000e+00 0.00000000000000000e+00 0.00000000000000000e+00 6.12323399573676604e-17 -1.00000000000000000e+00 0.00000000000000000e+00 1.00000000000000000e+00 6.12323399573676604e-17\n",
+ "\n",
+ "The serialized value is a string: \n",
+ "After deserialization: R: [\n",
+ "\t1, 0, 0;\n",
+ "\t0, 6.12323e-17, -1;\n",
+ "\t0, 1, 6.12323e-17\n",
+ "]\n",
+ "\n"
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file