Skip to content

Transform3D

Represents a rigid 3D transform (SE(3)): a rotation plus a translation.

Parameters

FieldTypeDescription
datanp.ndarray | list | tupleArray-like input of shape (4, 4), converted to a contiguous float32 array.

Raises

ExceptionCondition
TypeErrordata can't be converted to float32 (e.g. non-numeric elements)
ValueErrordata is not rank-2, or its shape isn't (4, 4)
ValueErrordata contains a non-finite value (NaN/Inf)
ValueErrorLast row isn't [0, 0, 0, 1] (within transform_atol)
ValueErrorThe 3x3 rotation block isn't orthonormal (within transform_atol), or its determinant isn't ~1.0 (a determinant of -1 is a reflection, not a rotation)

Attributes

AttributeTypeDescription
datanp.ndarrayDefensive copy of the underlying (4, 4) float32 matrix. Assigning a new value re-validates it the same way as construction.
shapetuple[int, ...]Always (4, 4).
ndimintAlways 2.
dtypenp.dtypeAlways float32.
sizeintAlways 16.
transform_atolfloatClass-level absolute tolerance (1e-4) used when validating the last row and the rotation block.
quat_orderstrClass-level, "wxyz" — the quaternion component order produced/consumed by to_pose/from_pose when rot_type=RotationType.QUATERNION, and by the inherited to_xyzw/from_xyzw helpers.

Methods

MethodDescription
to_pose(rot_type=RotationType.QUATERNION)Converts to a flat pose vector [x, y, z, ...]: shape (7,) with a scalar-first quaternion [qw, qx, qy, qz] for RotationType.QUATERNION (default), or shape (6,) with Euler angles (degrees/radians) or a rotation vector otherwise. rot_type accepts a RotationType member or its string value. Raises TypeError/ValueError for an unrecognized rot_type.
Transform3D.from_pose(pose, rot_type=RotationType.QUATERNION)Classmethod; the inverse of to_pose. Builds a Transform3D from a flat pose vector, shape (7,) for QUATERNION or (6,) for the other rotation types. Raises TypeError if pose isn't an np.ndarray/list or rot_type is invalid; ValueError on a shape/rot_type mismatch.
inverse()Returns the inverse homogeneous transform as a plain (4, 4) np.ndarray (not a Transform3D): [[Rᵗ, -Rᵗ·t], [0, 1]].
compute_transformation_error(other_transform)Computes a scalar SE(3) pose error (combined rotation + translation magnitude, via the matrix logarithm) between this transform and other_transform. Raises ValueError if other_transform is not a Transform3D.
Transform3D.to_xyzw(q)Inherited from QuaternionValidationMixin. Converts a quaternion array from quat_order ("wxyz") to scalar-last [x, y, z, w], e.g. before handing it to scipy.
Transform3D.from_xyzw(q)Inherited from QuaternionValidationMixin. Converts a quaternion array from scalar-last [x, y, z, w] to quat_order ("wxyz"), e.g. after reading scipy's Rotation.as_quat().
to_numpy(copy=True)Returns the matrix as np.ndarray. Pass copy=False for a reference to the internal array instead — faster, but mutating it mutates the Transform3D too.
copy()Returns a new Transform3D with an independent copy of the data.
Transform3D.coerce(value)Returns value unchanged if it's already a Transform3D; otherwise wraps an array-like into one (running full validation). Raises TypeError for any other input.

Operators

OperationBehavior
t == otherTrue only if other is also a Transform3D with element-equal data. False for anything else.
len(t)Always 4 (length of the first axis).
np.asarray(t)Works directly via __array__. Always returns a copy; use to_numpy(copy=False) for a zero-copy view.
hash(t)Not supported — mutable via the data setter.

Visualization

datatypes.visualize(transform, entity_path=...) logs the transform's posed frame (rotated basis axes from data[:3, :3], translated by data[:3, 3]) alongside a labeled world-origin frame, so the rotation/translation are visible relative to identity. Per-axis X/Y/Z labels are omitted on both frames; only the world-origin frame gets an "origin" text label, distinguishing it from the posed one. Passing label="..." attaches a floating text label at the posed frame's translation.

Example

python
"""Demonstrates the Telekinesis Transform3D datatype."""

import time

import numpy as np
from loguru import logger
import rerun as rr

from telekinesis import datatypes

def transform3d_example():
    """Demonstrate creation, access, update, inverse, pose conversion, and serialization."""

    # ======================= Create ============================================
    matrix = np.array(
        [
            [0.5000000, -0.5000000, 0.7071068, 1],
            [0.8535534, 0.1464466, -0.5000000, 2],
            [0.1464466, 0.8535534, 0.5000000, 3],
            [0, 0, 0, 1],
        ]
    )
    transform3d = datatypes.Transform3D(matrix)

    logger.info(f"Created Transform3D: {transform3d}")

    # ======================= Inspect ===========================================
    data = transform3d.data
    shape = transform3d.shape
    size = transform3d.size
    dtype = transform3d.dtype
    ndim = transform3d.ndim
    numpy_array = transform3d.to_numpy()
    transform3d_copy = transform3d.copy()

    logger.info(f"shape={shape}, size={size}, ndim={ndim}, dtype={dtype}")
    logger.info(f"Transform3D data:\n{data}")
    logger.info(f"NumPy array:\n{numpy_array}")
    logger.info(f"Copied Transform3D: {transform3d_copy}")

    # ======================= Visualize =========================================
    rr.init("transform3d_example", spawn=True)
    datatypes.visualize(transform3d, entity_path="/Transform3D/main", label="transform3d")

    # ======================= Update ============================================
    new_matrix = np.array(
        [
            [0.5000000, -0.5000000, 0.7071068, 1.5],
            [0.8535534, 0.1464466, -0.5000000, 2.5],
            [0.1464466, 0.8535534, 0.5000000, 3],
            [0, 0, 0, 1],
        ]
    )
    transform3d.data = new_matrix

    logger.info(f"Updated Transform3D: {transform3d}")
    datatypes.visualize(
        transform3d, entity_path="/Transform3D/updated", label="updated_transform3d"
    )

    # ======================= Inverse ===========================================
    inverse_matrix = transform3d.inverse()
    inverse = datatypes.Transform3D(inverse_matrix)

    logger.info(f"Inverse Transform3D: {inverse_matrix}")
    datatypes.visualize(inverse, entity_path="/Transform3D/inverse", label="inverse_transform3d")

    # ======================= Pose Conversion ===================================
    pose_deg = transform3d.to_pose(rot_type="deg")
    pose_rotvec = transform3d.to_pose(rot_type="rotvec")
    pose_rad = transform3d.to_pose(rot_type="rad")
    pose_quat = transform3d.to_pose(rot_type="quat")

    logger.info(f"Pose (deg): {pose_deg}")
    logger.info(f"Pose (rotvec): {pose_rotvec}")
    logger.info(f"Pose (rad): {pose_rad}")
    logger.info(f"Pose (quat): {pose_quat}")

    new_transform3d = datatypes.Transform3D.from_pose(pose_quat)
    error = transform3d.compute_transformation_error(new_transform3d)

    logger.info(f"New Transform3D from pose: {new_transform3d}")
    logger.info(f"Transformation error: {error}")

    # ======================= NumPy Interop =====================================
    sum_result = np.array([1, 1, 1, 0]) + numpy_array

    logger.info(f"Sum of Transform3D with numpy array: {sum_result}")

    # ======================= Serialize / Deserialize ===========================
    start = time.perf_counter()
    serialized = datatypes.serialize(transform3d)
    serialization_ms = (time.perf_counter() - start) * 1000

    start = time.perf_counter()
    deserialized = datatypes.deserialize(serialized)["param_0"]
    deserialization_ms = (time.perf_counter() - start) * 1000

    logger.info(f"Deserialized Transform3D: {deserialized}")
    logger.info(f"Round-trip successful: {deserialized == transform3d}")
    logger.info(f"Serialization time: {serialization_ms:.3f} ms")
    logger.info(f"Deserialization time: {deserialization_ms:.3f} ms")


if __name__ == "__main__":
    transform3d_example()