lieplusplus_py
Swarm Systems Lab Python lieplusplus library
⚠️ Warning: This project is a work in progress.
Installation
pip install lieplusplus_py
Python Lie++ API Documentation
SO(3) - 3D Rotations
import numpy as np
from lieplusplus import SO3
# --- Constructors ---
R = SO3() # Identity rotation
R = SO3([1, 0, 0, 0]) # Quaternion [w, x, y, z]
R = SO3(np.eye(3)) # Rotation matrix (3×3)
R = SO3([0.1, 0.2, 0.3]) # Rotation vector (exp map)
R = SO3([1, 0, 0], [0, 1, 0]) # Rotation sending u -> v
R = SO3.random() # Random rotation
# --- Group operations ---
R3 = R1 * R2 # Compose rotations
v_rot = R * v # Rotate a 3-vector
M_rot = R * M # Rotate a 3×3 matrix
# --- Lie-theoretic operations (static) ---
u = SO3.log(R) # R^3 log-coordinate
R2 = SO3.exp(u) # Exponential map
U = SO3.wedge(u) # 3×3 skew-symmetric matrix
u2 = SO3.vee(U) # Vector from skew matrix
u0 = SO3.tangent_zero() # Zero element of so(3)
# --- Jacobians ---
J = SO3.leftJacobian(u)
Jinv = SO3.invLeftJacobian(u)
# --- Accessors ---
R.asMatrix() # 3×3 rotation matrix
R.Adjoint() # 3×3 Adjoint matrix
R.inv() # Inverse rotation
R.q() # Quaternion [w, x, y, z]
SE(3) - Poses
import numpy as np
from lieplusplus import SE3, SO3
# --- Constructors ---
T = SE3() # Identity pose
T = SE3(R, t) # SO(3) + translation vector
T = SE3(np.eye(4)) # Full 4×4 pose matrix
T = SE3([0.1, 0.2, 0.3], [1, 2, 3]) # Rotation vector + translation
T = SE3.random() # Random pose
# --- Group operations ---
T3 = T1 * T2 # Compose poses
p_world = T * p # Transform a 3D point
# --- Lie-theoretic operations (static) ---
u = SE3.log(T) # R^6 tangent vector
T2 = SE3.exp(u)
U = SE3.wedge(u)
u2 = SE3.vee(U)
u0 = SE3.tangent_zero()
# --- Accessors ---
T.asMatrix() # 4×4 transformation matrix
T.Adjoint() # 6×6 Adjoint matrix
T.inv() # Inverse pose
T.R() # Rotation (SO3)
T.q() # Quaternion
T.translation() # 3D translation vector
SE_2(3) - Extended Poses
import numpy as np
from lieplusplus import SE3_2, SO3
# --- Constructors ---
X = SE3_2() # Identity extended pose
X = SE3_2(R, [v, p]) # Rotation + [velocity, position]
X = SE3_2(R, [np.zeros(3), np.ones(3)]) # Example
X = SE3_2.random() # Random extended pose
# --- Group operations ---
X3 = X1 * X2
# --- Lie-theoretic operations (static) ---
u = SE3_2.log(X) # R^9 tangent vector
X2 = SE3_2.exp(u)
U = SE3_2.wedge(u)
u2 = SE3_2.vee(U)
u0 = SE3_2.tangent_zero()
# --- Accessors ---
X.asMatrix() # 5×5 extended matrix
X.Adjoint() # 9×9 Adjoint matrix
X.inv() # Inverse extended pose
X.R() # Rotation (SO3)
X.v() # Velocity vector
X.p() # Position vector
Array operators
The group classes above are object-oriented and single-element by design: the algebra of one
robot rarely needs vectorization. Calling them from Python in a loop is dominated not by the
arithmetic (tens of nanoseconds) but by the pybind boundary crossing (~1-1.5 us per call), which a
loop over N entities pays N times.
So the same maths is also exposed as plain functions on numpy arrays, crossing into C++ once per call:
import numpy as np
import lieplusplus as lpp
xi = np.random.normal(size=(1000, 3)) * 0.01
R = lpp.so3_exp(xi) # (1000, 3, 3)
R = lpp.so3_retract(R, xi) # R * exp(xi), the integration primitive
Measured on SO(3) retraction: ~3 us/entity for a Python loop vs ~0.04 us/entity (~68x at N=1000), and faster than an equivalent vectorized NumPy implementation, since the C++ loop materializes no temporaries.
One function, any input
There is no separate batch API to choose. Each operator is shape-polymorphic, and takes whatever array you happen to have:
lpp.so3_exp(v) # (3,) -> (3, 3) one element
lpp.so3_exp(v_stack) # (N, 3) -> (N, 3, 3) or a stack
lpp.so3_retract(one_R, tangents) # (3,3) x (N,3) -> (N,3,3) leading axis broadcasts N vs 1
lpp.so3_retract(R, tangents, out=R) # writes in place, allocating nothing
lpp.so3_exp(fortran_or_float32_or_list) # any layout/dtype converts - no ascontiguousarray needed
lpp.so3_log(SO3.random()) # group objects work too (via __array__)
out= accepts any writable array of the right shape. A C-contiguous float64 buffer is written
directly (zero copy - the fast path an integrator wants); anything else (an F-ordered array, a
slice, a float32 buffer) goes through a temporary and is copied back, so the result always lands
in your array. It is never silently discarded.
Shape errors name the operator and the argument:
so3_exp: argument 'x' must have shape (3) or (N, 3), got (4, 5)
so3_retract: leading dimensions do not broadcast, got 4 and 3
Available operators
| SO(3) | shapes (one element, or N of them) |
|---|---|
so3_exp / so3_log |
(3,) -> (3,3) / (3,3) -> (3,) |
so3_inv |
(3,3) -> (3,3) |
so3_compose |
(3,3) x (3,3) -> (3,3) |
so3_retract |
(3,3) x (3,) -> (3,3) |
so3_rotate |
(3,3) x (3,) -> (3,) |
so3_right_jacobian / so3_inv_right_jacobian |
(3,) -> (3,3) |
| SE(3) | shapes (one element, or N of them) |
|---|---|
se3_exp / se3_log |
(6,) -> (4,4) / (4,4) -> (6,) |
se3_inv |
(4,4) -> (4,4) |
se3_compose |
(4,4) x (4,4) -> (4,4) |
se3_retract |
(4,4) x (6,) -> (4,4) |
se3_transform |
(4,4) x (3,) -> (3,) |
Adding an operator
The machinery lives in src/pybind_batch.hpp and is library-agnostic - it only assumes
fixed-size Eigen types in and out, so it can be dropped into any pybind11 project. Adding an
operator is one line; shape checking, broadcasting, out=, layout conversion and error messages
come for free:
namespace pb = pybind_batch;
pb::def_unary<Vec3d, Matrix3d>(m, "so3_exp",
[](const Vec3d& u) { return SO3d::exp(u).asMatrix(); }, "Exponential map.");
pb::def_binary<Matrix3d, Vec3d, Matrix3d>(m, "so3_retract",
[](const Matrix3d& R, const Vec3d& u) { return (SO3d(R) * SO3d::exp(u)).asMatrix(); },
"Right retraction R*exp(u).");
The element types determine the array shapes: Vec3d means (3,) or (N, 3), Matrix4d means
(4, 4) or (N, 4, 4).
Note. The array operators are free functions (
lpp.so3_exp) rather than methods on the classes (SO3.exp) because the two have different contracts.SO3.expaccepts a(3,)vector and a(3,1)column - convenient for one element, but(3,1)would be ambiguous with a stack of 3 under batching. The free functions are strict about rank instead, which is what lets a single name serve one element and a million.
License
MIT
Metadata
Release files for lieplusplus-py 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lieplusplus_py-0.7.0.tar.gz | 88.6 kB | Details |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lieplusplus_py-0.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | CPython 3.14 | CPython 3.14 | Linux glibc 2.27+ x86-64, Linux glibc 2.28+ x86-64 | Details |
| lieplusplus_py-0.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | CPython 3.13 | CPython 3.13 | Linux glibc 2.27+ x86-64, Linux glibc 2.28+ x86-64 | Details |
| lieplusplus_py-0.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | CPython 3.12 | CPython 3.12 | Linux glibc 2.27+ x86-64, Linux glibc 2.28+ x86-64 | Details |
Total release size: 6.3 MB
Release files / lieplusplus_py-0.7.0.tar.gz
| Download URL | lieplusplus_py-0.7.0.tar.gz |
|---|---|
| Size | 88.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7821c7d40cc81dcff979bdc83d16fbf934ddea207844d11a10a169e6e3d835aa
|
|
BLAKE2b-256 checksum How to use checksums |
adc963141b0e045dec31acef0fc7943cba75591c5d059ba75b7d37fb33d9aad0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Release files / lieplusplus_py-0.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
| Download URL | lieplusplus_py-0.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.1 MB |
| Tags | CPython 3.14 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
ea7241a72fb292f3deabcaca10f8fc194e1ada25ebed73ad0701726dd1db87ac
|
|
BLAKE2b-256 checksum How to use checksums |
39f70940d4f0384e421b114e534fd7102186d395b58a3cbc4f8a0bcdf371e15b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Release files / lieplusplus_py-0.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
| Download URL | lieplusplus_py-0.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.1 MB |
| Tags | CPython 3.13 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
ad589eb94474fe85d897f384c87b98fc22d354a68a5652470ca8d8ebbfaea2a9
|
|
BLAKE2b-256 checksum How to use checksums |
13977c0e484d268b58d2837070fe0d1eaac1ca676d0936727f09429186f74710
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Release files / lieplusplus_py-0.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
| Download URL | lieplusplus_py-0.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.1 MB |
| Tags | CPython 3.12 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
32197e1a8b3b4989ec0d426c4c88bbd7e099210b3bc3fa656fdbf1f7aadc47a7
|
|
BLAKE2b-256 checksum How to use checksums |
12e0742b9ab544889fc080813ced2ec18a442e7ef9f9720dddbb13b013fae18c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|