nanomanifold 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nanomanifold might be problematic. Click here for more details.
- nanomanifold-0.1.1/.github/workflows/release.yml +65 -0
- nanomanifold-0.1.1/.gitignore +10 -0
- nanomanifold-0.1.1/.python-version +1 -0
- nanomanifold-0.1.1/PKG-INFO +112 -0
- nanomanifold-0.1.1/README.md +101 -0
- nanomanifold-0.1.1/pyproject.toml +22 -0
- nanomanifold-0.1.1/src/nanomanifold/SE3/__init__.py +21 -0
- nanomanifold-0.1.1/src/nanomanifold/SE3/canonicalize.py +25 -0
- nanomanifold-0.1.1/src/nanomanifold/SE3/conversions/__init__.py +9 -0
- nanomanifold-0.1.1/src/nanomanifold/SE3/conversions/matrix.py +57 -0
- nanomanifold-0.1.1/src/nanomanifold/SE3/conversions/rt.py +30 -0
- nanomanifold-0.1.1/src/nanomanifold/SE3/exp.py +73 -0
- nanomanifold-0.1.1/src/nanomanifold/SE3/inverse.py +37 -0
- nanomanifold-0.1.1/src/nanomanifold/SE3/log.py +75 -0
- nanomanifold-0.1.1/src/nanomanifold/SE3/multiply.py +48 -0
- nanomanifold-0.1.1/src/nanomanifold/SE3/transform_points.py +34 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/__init__.py +35 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/canonicalize.py +17 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/conversions/__init__.py +14 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/conversions/axis_angle.py +63 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/conversions/euler.py +149 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/conversions/matrix.py +79 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/distance.py +52 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/exp.py +25 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/hat.py +36 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/inverse.py +16 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/log.py +25 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/multiply.py +40 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/rotate_points.py +50 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/slerp.py +69 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/vee.py +28 -0
- nanomanifold-0.1.1/src/nanomanifold/SO3/weighted_mean.py +89 -0
- nanomanifold-0.1.1/src/nanomanifold/__init__.py +3 -0
- nanomanifold-0.1.1/src/nanomanifold/common.py +25 -0
- nanomanifold-0.1.1/tests/conftest.py +137 -0
- nanomanifold-0.1.1/tests/test_se3_conversions_matrix.py +78 -0
- nanomanifold-0.1.1/tests/test_se3_conversions_rt.py +111 -0
- nanomanifold-0.1.1/tests/test_se3_inverse.py +330 -0
- nanomanifold-0.1.1/tests/test_se3_log_exp.py +329 -0
- nanomanifold-0.1.1/tests/test_se3_multiply.py +358 -0
- nanomanifold-0.1.1/tests/test_se3_singularities.py +376 -0
- nanomanifold-0.1.1/tests/test_se3_transform_points.py +318 -0
- nanomanifold-0.1.1/tests/test_so3_conversions_axis_angle.py +83 -0
- nanomanifold-0.1.1/tests/test_so3_conversions_euler.py +113 -0
- nanomanifold-0.1.1/tests/test_so3_conversions_matrix.py +84 -0
- nanomanifold-0.1.1/tests/test_so3_distance.py +242 -0
- nanomanifold-0.1.1/tests/test_so3_hat_vee.py +235 -0
- nanomanifold-0.1.1/tests/test_so3_inverse.py +82 -0
- nanomanifold-0.1.1/tests/test_so3_log_exp.py +344 -0
- nanomanifold-0.1.1/tests/test_so3_mean.py +393 -0
- nanomanifold-0.1.1/tests/test_so3_multiply.py +267 -0
- nanomanifold-0.1.1/tests/test_so3_rotate_points.py +196 -0
- nanomanifold-0.1.1/tests/test_so3_singularities.py +289 -0
- nanomanifold-0.1.1/tests/test_so3_slerp.py +342 -0
- nanomanifold-0.1.1/uv.lock +687 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
- uses: astral-sh/setup-uv@v4
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.11"
|
|
23
|
+
- name: Detect version change
|
|
24
|
+
id: version
|
|
25
|
+
run: |
|
|
26
|
+
new_version=$(uv version --short)
|
|
27
|
+
echo "version=$new_version" >> "$GITHUB_OUTPUT"
|
|
28
|
+
tmp_dir=$(mktemp -d)
|
|
29
|
+
git show HEAD^:pyproject.toml 2>/dev/null > "$tmp_dir/pyproject.toml" || true
|
|
30
|
+
if [ -s "$tmp_dir/pyproject.toml" ]; then
|
|
31
|
+
old_version=$(uv version --short --project "$tmp_dir")
|
|
32
|
+
else
|
|
33
|
+
old_version=""
|
|
34
|
+
fi
|
|
35
|
+
if [ "$new_version" != "$old_version" ]; then
|
|
36
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
37
|
+
else
|
|
38
|
+
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
39
|
+
fi
|
|
40
|
+
- name: Push tag
|
|
41
|
+
if: steps.version.outputs.changed == 'true'
|
|
42
|
+
run: |
|
|
43
|
+
git fetch --tags
|
|
44
|
+
if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
|
|
45
|
+
echo "Tag v${{ steps.version.outputs.version }} already exists"
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
git config user.name "${GITHUB_ACTOR}"
|
|
49
|
+
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
|
|
50
|
+
git tag "v${{ steps.version.outputs.version }}"
|
|
51
|
+
git push origin "v${{ steps.version.outputs.version }}"
|
|
52
|
+
- name: Build
|
|
53
|
+
if: steps.version.outputs.changed == 'true'
|
|
54
|
+
run: uv build
|
|
55
|
+
- name: Publish to PyPI
|
|
56
|
+
if: steps.version.outputs.changed == 'true'
|
|
57
|
+
run: uv publish --check-url https://pypi.org/simple
|
|
58
|
+
- name: Create GitHub release
|
|
59
|
+
if: steps.version.outputs.changed == 'true'
|
|
60
|
+
uses: softprops/action-gh-release@v1
|
|
61
|
+
with:
|
|
62
|
+
tag_name: v${{ steps.version.outputs.version }}
|
|
63
|
+
name: v${{ steps.version.outputs.version }}
|
|
64
|
+
generate_release_notes: true
|
|
65
|
+
files: dist/*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nanomanifold
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: SO3/SE3 operations on any backend
|
|
5
|
+
Author-email: Andrea Boscolo Camiletto <abcamiletto@gmail.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: array-api-compat>=1.12.0
|
|
8
|
+
Requires-Dist: jaxtyping>=0.3.2
|
|
9
|
+
Requires-Dist: numpy>=2.3.2
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# nanomanifold
|
|
13
|
+
|
|
14
|
+
Fast, batched and differentiable SO3/SE3 transforms for any backend (NumPy, PyTorch, JAX, ...)
|
|
15
|
+
Works directly on arrays, defined as:
|
|
16
|
+
|
|
17
|
+
- **SO3**: unit quaternions `[w, x, y, z]` for 3D rotations, shape `(..., 4)`
|
|
18
|
+
- **SE3**: concatenated `[quat, translation]`, shape `(..., 7)`
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import numpy as np
|
|
22
|
+
from nanomanifold import SO3, SE3
|
|
23
|
+
|
|
24
|
+
# Rotations stored as quaternion arrays [w,x,y,z]
|
|
25
|
+
q = SO3.from_axis_angle(np.array([0, 0, 1]), np.pi/4) # 45° around Z
|
|
26
|
+
points = np.array([[1, 0, 0], [0, 1, 0]])
|
|
27
|
+
rotated = SO3.rotate_points(q, points)
|
|
28
|
+
|
|
29
|
+
# Rigid transforms stored as 7D arrays [quat, translation]
|
|
30
|
+
T = SE3.from_rt(q, np.array([1, 0, 0])) # rotation + translation
|
|
31
|
+
transformed = SE3.transform_points(T, points)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
**Array-based API** — all functions operate directly on arrays from any backend
|
|
37
|
+
**Backend agnostic** — works with NumPy, PyTorch, JAX, CuPy, Dask, and more
|
|
38
|
+
**Batched operations** — process thousands of transforms at once
|
|
39
|
+
**Differentiable** — automatic gradients where supported (PyTorch/JAX)
|
|
40
|
+
**Memory efficient** — quaternions instead of matrices
|
|
41
|
+
**Numerically stable** — handles edge cases and singularities
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### Rotations (SO3)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from nanomanifold import SO3
|
|
49
|
+
|
|
50
|
+
# Create rotations
|
|
51
|
+
q1 = SO3.from_axis_angle([1, 0, 0], np.pi/2) # 90° around X
|
|
52
|
+
q2 = SO3.from_euler([0, 0, np.pi/4]) # 45° around Z
|
|
53
|
+
q3 = SO3.from_matrix(rotation_matrix)
|
|
54
|
+
|
|
55
|
+
# Compose and interpolate
|
|
56
|
+
q_combined = SO3.multiply(q1, q2)
|
|
57
|
+
q_halfway = SO3.slerp(q1, q2, t=0.5)
|
|
58
|
+
|
|
59
|
+
# Apply to points
|
|
60
|
+
points = np.array([[1, 0, 0], [0, 1, 0]])
|
|
61
|
+
rotated = SO3.rotate_points(q_combined, points)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Rigid Transforms (SE3)
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from nanomanifold import SE3
|
|
68
|
+
|
|
69
|
+
# Create transforms
|
|
70
|
+
T1 = SE3.from_rt(q1, [1, 2, 3]) # rotation + translation
|
|
71
|
+
T2 = SE3.from_matrix(transformation_matrix)
|
|
72
|
+
|
|
73
|
+
# Compose transforms
|
|
74
|
+
T_combined = SE3.multiply(T1, T2)
|
|
75
|
+
T_inverse = SE3.inverse(T_combined)
|
|
76
|
+
|
|
77
|
+
# Apply to points
|
|
78
|
+
transformed = SE3.transform_points(T_combined, points)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## API Reference
|
|
82
|
+
|
|
83
|
+
### SO3 (3D Rotations)
|
|
84
|
+
|
|
85
|
+
| Function | Input → Output | Description |
|
|
86
|
+
| ------------------------------ | ----------------------------------- | --------------------------- |
|
|
87
|
+
| `from_axis_angle(axis, angle)` | `(...,3), (...) → (...,4)` | Create from axis-angle |
|
|
88
|
+
| `from_euler(angles)` | `(...,3) → (...,4)` | Create from Euler angles |
|
|
89
|
+
| `from_matrix(R)` | `(...,3,3) → (...,4)` | Create from rotation matrix |
|
|
90
|
+
| `multiply(q1, q2)` | `(...,4), (...,4) → (...,4)` | Compose rotations |
|
|
91
|
+
| `slerp(q1, q2, t)` | `(...,4), (...,4), (...) → (...,4)` | Spherical interpolation |
|
|
92
|
+
| `rotate_points(q, points)` | `(...,4), (...,N,3) → (...,N,3)` | Rotate 3D points |
|
|
93
|
+
|
|
94
|
+
### SE3 (Rigid Transforms)
|
|
95
|
+
|
|
96
|
+
| Function | Input → Output | Description |
|
|
97
|
+
| ----------------------------- | -------------------------------- | ---------------------------------- |
|
|
98
|
+
| `from_rt(q, t)` | `(...,4), (...,3) → (...,7)` | Create from rotation + translation |
|
|
99
|
+
| `from_matrix(T)` | `(...,4,4) → (...,7)` | Create from 4×4 matrix |
|
|
100
|
+
| `multiply(T1, T2)` | `(...,7), (...,7) → (...,7)` | Compose transforms |
|
|
101
|
+
| `inverse(T)` | `(...,7) → (...,7)` | Invert transform |
|
|
102
|
+
| `transform_points(T, points)` | `(...,7), (...,N,3) → (...,N,3)` | Transform 3D points |
|
|
103
|
+
|
|
104
|
+
## Installation
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install nanomanifold
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# nanomanifold
|
|
2
|
+
|
|
3
|
+
Fast, batched and differentiable SO3/SE3 transforms for any backend (NumPy, PyTorch, JAX, ...)
|
|
4
|
+
Works directly on arrays, defined as:
|
|
5
|
+
|
|
6
|
+
- **SO3**: unit quaternions `[w, x, y, z]` for 3D rotations, shape `(..., 4)`
|
|
7
|
+
- **SE3**: concatenated `[quat, translation]`, shape `(..., 7)`
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import numpy as np
|
|
11
|
+
from nanomanifold import SO3, SE3
|
|
12
|
+
|
|
13
|
+
# Rotations stored as quaternion arrays [w,x,y,z]
|
|
14
|
+
q = SO3.from_axis_angle(np.array([0, 0, 1]), np.pi/4) # 45° around Z
|
|
15
|
+
points = np.array([[1, 0, 0], [0, 1, 0]])
|
|
16
|
+
rotated = SO3.rotate_points(q, points)
|
|
17
|
+
|
|
18
|
+
# Rigid transforms stored as 7D arrays [quat, translation]
|
|
19
|
+
T = SE3.from_rt(q, np.array([1, 0, 0])) # rotation + translation
|
|
20
|
+
transformed = SE3.transform_points(T, points)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
**Array-based API** — all functions operate directly on arrays from any backend
|
|
26
|
+
**Backend agnostic** — works with NumPy, PyTorch, JAX, CuPy, Dask, and more
|
|
27
|
+
**Batched operations** — process thousands of transforms at once
|
|
28
|
+
**Differentiable** — automatic gradients where supported (PyTorch/JAX)
|
|
29
|
+
**Memory efficient** — quaternions instead of matrices
|
|
30
|
+
**Numerically stable** — handles edge cases and singularities
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
### Rotations (SO3)
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from nanomanifold import SO3
|
|
38
|
+
|
|
39
|
+
# Create rotations
|
|
40
|
+
q1 = SO3.from_axis_angle([1, 0, 0], np.pi/2) # 90° around X
|
|
41
|
+
q2 = SO3.from_euler([0, 0, np.pi/4]) # 45° around Z
|
|
42
|
+
q3 = SO3.from_matrix(rotation_matrix)
|
|
43
|
+
|
|
44
|
+
# Compose and interpolate
|
|
45
|
+
q_combined = SO3.multiply(q1, q2)
|
|
46
|
+
q_halfway = SO3.slerp(q1, q2, t=0.5)
|
|
47
|
+
|
|
48
|
+
# Apply to points
|
|
49
|
+
points = np.array([[1, 0, 0], [0, 1, 0]])
|
|
50
|
+
rotated = SO3.rotate_points(q_combined, points)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Rigid Transforms (SE3)
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from nanomanifold import SE3
|
|
57
|
+
|
|
58
|
+
# Create transforms
|
|
59
|
+
T1 = SE3.from_rt(q1, [1, 2, 3]) # rotation + translation
|
|
60
|
+
T2 = SE3.from_matrix(transformation_matrix)
|
|
61
|
+
|
|
62
|
+
# Compose transforms
|
|
63
|
+
T_combined = SE3.multiply(T1, T2)
|
|
64
|
+
T_inverse = SE3.inverse(T_combined)
|
|
65
|
+
|
|
66
|
+
# Apply to points
|
|
67
|
+
transformed = SE3.transform_points(T_combined, points)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API Reference
|
|
71
|
+
|
|
72
|
+
### SO3 (3D Rotations)
|
|
73
|
+
|
|
74
|
+
| Function | Input → Output | Description |
|
|
75
|
+
| ------------------------------ | ----------------------------------- | --------------------------- |
|
|
76
|
+
| `from_axis_angle(axis, angle)` | `(...,3), (...) → (...,4)` | Create from axis-angle |
|
|
77
|
+
| `from_euler(angles)` | `(...,3) → (...,4)` | Create from Euler angles |
|
|
78
|
+
| `from_matrix(R)` | `(...,3,3) → (...,4)` | Create from rotation matrix |
|
|
79
|
+
| `multiply(q1, q2)` | `(...,4), (...,4) → (...,4)` | Compose rotations |
|
|
80
|
+
| `slerp(q1, q2, t)` | `(...,4), (...,4), (...) → (...,4)` | Spherical interpolation |
|
|
81
|
+
| `rotate_points(q, points)` | `(...,4), (...,N,3) → (...,N,3)` | Rotate 3D points |
|
|
82
|
+
|
|
83
|
+
### SE3 (Rigid Transforms)
|
|
84
|
+
|
|
85
|
+
| Function | Input → Output | Description |
|
|
86
|
+
| ----------------------------- | -------------------------------- | ---------------------------------- |
|
|
87
|
+
| `from_rt(q, t)` | `(...,4), (...,3) → (...,7)` | Create from rotation + translation |
|
|
88
|
+
| `from_matrix(T)` | `(...,4,4) → (...,7)` | Create from 4×4 matrix |
|
|
89
|
+
| `multiply(T1, T2)` | `(...,7), (...,7) → (...,7)` | Compose transforms |
|
|
90
|
+
| `inverse(T)` | `(...,7) → (...,7)` | Invert transform |
|
|
91
|
+
| `transform_points(T, points)` | `(...,7), (...,N,3) → (...,N,3)` | Transform 3D points |
|
|
92
|
+
|
|
93
|
+
## Installation
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install nanomanifold
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nanomanifold"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "SO3/SE3 operations on any backend"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Andrea Boscolo Camiletto", email = "abcamiletto@gmail.com" },
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = ["array-api-compat>=1.12.0", "jaxtyping>=0.3.2", "numpy>=2.3.2"]
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["hatchling"]
|
|
14
|
+
build-backend = "hatchling.build"
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
dev = ["jax>=0.7.1", "pytest>=8.4.1", "scipy>=1.16.1", "torch>=2.8.0"]
|
|
18
|
+
|
|
19
|
+
[tool.ruff]
|
|
20
|
+
line-length = 140
|
|
21
|
+
lint.ignore = ["E741", "E743", "F722"] # ignore ambiguous variable names
|
|
22
|
+
lint.extend-select = ["I"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .canonicalize import canonicalize
|
|
2
|
+
from .conversions.matrix import from_matrix, to_matrix
|
|
3
|
+
from .conversions.rt import from_rt, to_rt
|
|
4
|
+
from .exp import exp
|
|
5
|
+
from .inverse import inverse
|
|
6
|
+
from .log import log
|
|
7
|
+
from .multiply import multiply
|
|
8
|
+
from .transform_points import transform_points
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"from_matrix",
|
|
12
|
+
"to_matrix",
|
|
13
|
+
"from_rt",
|
|
14
|
+
"to_rt",
|
|
15
|
+
"canonicalize",
|
|
16
|
+
"multiply",
|
|
17
|
+
"inverse",
|
|
18
|
+
"transform_points",
|
|
19
|
+
"log",
|
|
20
|
+
"exp",
|
|
21
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from jaxtyping import Float
|
|
4
|
+
|
|
5
|
+
from nanomanifold.common import get_namespace
|
|
6
|
+
from nanomanifold.SO3.canonicalize import canonicalize as canonicalize_quat
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def canonicalize(se3: Float[Any, "... 7"]) -> Float[Any, "... 7"]:
|
|
10
|
+
"""Canonicalize SE(3) representation by canonicalizing the quaternion part.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
se3: SE(3) representation (..., 7) as [w, x, y, z, tx, ty, tz]
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Canonicalized SE(3) representation with quaternion w >= 0
|
|
17
|
+
"""
|
|
18
|
+
xp = get_namespace(se3)
|
|
19
|
+
|
|
20
|
+
quat = se3[..., :4]
|
|
21
|
+
translation = se3[..., 4:7]
|
|
22
|
+
|
|
23
|
+
quat_canonical = canonicalize_quat(quat)
|
|
24
|
+
|
|
25
|
+
return xp.concatenate([quat_canonical, translation], axis=-1)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Matrix conversions for SE(3) transformations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from jaxtyping import Float
|
|
6
|
+
|
|
7
|
+
from nanomanifold import SO3
|
|
8
|
+
from nanomanifold.common import get_namespace
|
|
9
|
+
|
|
10
|
+
from ..canonicalize import canonicalize
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def to_matrix(se3: Float[Any, "... 7"]) -> Float[Any, "... 4 4"]:
|
|
14
|
+
"""Convert SE(3) representation to 4x4 transformation matrix.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
se3: SE(3) representation (..., 7) as [w, x, y, z, tx, ty, tz]
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
4x4 transformation matrix (..., 4, 4)
|
|
21
|
+
"""
|
|
22
|
+
xp = get_namespace(se3)
|
|
23
|
+
|
|
24
|
+
quat = se3[..., :4]
|
|
25
|
+
translation = se3[..., 4:7]
|
|
26
|
+
|
|
27
|
+
R = SO3.to_matrix(quat)
|
|
28
|
+
|
|
29
|
+
translation_column = translation[..., None]
|
|
30
|
+
top_block = xp.concatenate([R, translation_column], axis=-1)
|
|
31
|
+
|
|
32
|
+
zeros = xp.zeros(top_block.shape[:-2] + (1, 3), dtype=se3.dtype)
|
|
33
|
+
ones = xp.ones(top_block.shape[:-2] + (1, 1), dtype=se3.dtype)
|
|
34
|
+
bottom_row = xp.concatenate([zeros, ones], axis=-1)
|
|
35
|
+
|
|
36
|
+
return xp.concatenate([top_block, bottom_row], axis=-2)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def from_matrix(matrix: Float[Any, "... 4 4"]) -> Float[Any, "... 7"]:
|
|
40
|
+
"""Convert 4x4 transformation matrix to SE(3) representation.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
matrix: 4x4 transformation matrix (..., 4, 4)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
SE(3) representation (..., 7) as [w, x, y, z, tx, ty, tz]
|
|
47
|
+
"""
|
|
48
|
+
xp = get_namespace(matrix)
|
|
49
|
+
|
|
50
|
+
R = matrix[..., :3, :3]
|
|
51
|
+
|
|
52
|
+
quat = SO3.from_matrix(R)
|
|
53
|
+
|
|
54
|
+
translation = matrix[..., :3, 3]
|
|
55
|
+
|
|
56
|
+
se3 = xp.concatenate([quat, translation], axis=-1)
|
|
57
|
+
return canonicalize(se3)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from nanomanifold.common import get_namespace
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def from_rt(quat, translation):
|
|
5
|
+
"""Create SE(3) representation from rotation quaternion and translation.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
quat: Rotation quaternion (..., 4) as [w, x, y, z]
|
|
9
|
+
translation: Translation vector (..., 3)
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
SE(3) representation (..., 7) as [w, x, y, z, tx, ty, tz]
|
|
13
|
+
"""
|
|
14
|
+
xp = get_namespace(quat)
|
|
15
|
+
return xp.concatenate([quat, translation], axis=-1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def to_rt(se3):
|
|
19
|
+
"""Extract rotation quaternion and translation from SE(3) representation.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
se3: SE(3) representation (..., 7) as [w, x, y, z, tx, ty, tz]
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
quat: Rotation quaternion (..., 4) as [w, x, y, z]
|
|
26
|
+
translation: Translation vector (..., 3)
|
|
27
|
+
"""
|
|
28
|
+
quat = se3[..., :4]
|
|
29
|
+
translation = se3[..., 4:7]
|
|
30
|
+
return quat, translation
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from jaxtyping import Float
|
|
4
|
+
|
|
5
|
+
from nanomanifold.common import get_namespace
|
|
6
|
+
from nanomanifold.SO3 import exp as so3_exp
|
|
7
|
+
from nanomanifold.SO3 import hat
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def exp(tangent_vector: Float[Any, "... 6"]) -> Float[Any, "... 7"]:
|
|
11
|
+
"""Compute the exponential map from se(3) tangent space to SE(3) manifold.
|
|
12
|
+
|
|
13
|
+
The exponential map takes a tangent vector in the Lie algebra se(3)
|
|
14
|
+
and returns the corresponding SE(3) transformation. This is the inverse
|
|
15
|
+
operation of log().
|
|
16
|
+
|
|
17
|
+
The se(3) exponential map takes a 6-vector [ω, ρ] where:
|
|
18
|
+
- ω ∈ ℝ³ is the angular velocity (rotation part)
|
|
19
|
+
- ρ ∈ ℝ³ is the translational velocity
|
|
20
|
+
|
|
21
|
+
The formula involves:
|
|
22
|
+
- R = exp_SO3(ω) for the rotation quaternion
|
|
23
|
+
- t = V * ρ where V is the left Jacobian matrix
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
tangent_vector: Tangent vector in se(3) as [ω, ρ] of shape (..., 6)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
SE(3) transformation in [w, x, y, z, tx, ty, tz] format of shape (..., 7)
|
|
30
|
+
"""
|
|
31
|
+
xp = get_namespace(tangent_vector)
|
|
32
|
+
|
|
33
|
+
omega = tangent_vector[..., :3]
|
|
34
|
+
rho = tangent_vector[..., 3:6]
|
|
35
|
+
|
|
36
|
+
q = so3_exp(omega)
|
|
37
|
+
omega_norm = xp.linalg.norm(omega, axis=-1, keepdims=True)
|
|
38
|
+
|
|
39
|
+
eps = xp.finfo(omega.dtype).eps
|
|
40
|
+
small_angle_threshold = xp.asarray(max(1e-6, float(eps)), dtype=omega.dtype)
|
|
41
|
+
small_angle_mask = omega_norm < small_angle_threshold
|
|
42
|
+
|
|
43
|
+
omega_cross = hat(omega)
|
|
44
|
+
omega_cross_sq = xp.matmul(omega_cross, omega_cross)
|
|
45
|
+
|
|
46
|
+
identity = xp.eye(3, dtype=omega.dtype)
|
|
47
|
+
identity = xp.broadcast_to(identity, omega.shape[:-1] + (3, 3))
|
|
48
|
+
|
|
49
|
+
V_small = identity + 0.5 * omega_cross + (1.0 / 12.0) * omega_cross_sq
|
|
50
|
+
|
|
51
|
+
cos_norm = xp.cos(omega_norm)
|
|
52
|
+
sin_norm = xp.sin(omega_norm)
|
|
53
|
+
|
|
54
|
+
safe_norm = xp.where(small_angle_mask, xp.ones_like(omega_norm), omega_norm)
|
|
55
|
+
safe_norm_sq = xp.where(small_angle_mask, xp.ones_like(omega_norm), omega_norm**2)
|
|
56
|
+
safe_norm_cub = safe_norm_sq * safe_norm
|
|
57
|
+
|
|
58
|
+
A = (1.0 - cos_norm) / safe_norm_sq
|
|
59
|
+
B = (safe_norm - sin_norm) / safe_norm_cub
|
|
60
|
+
A = xp.reshape(A, A.shape[:-1])[..., None, None]
|
|
61
|
+
B = xp.reshape(B, B.shape[:-1])[..., None, None]
|
|
62
|
+
|
|
63
|
+
V_large = identity + A * omega_cross + B * omega_cross_sq
|
|
64
|
+
|
|
65
|
+
mask = xp.reshape(small_angle_mask, small_angle_mask.shape[:-1])[..., None, None]
|
|
66
|
+
V = xp.where(mask, V_small, V_large)
|
|
67
|
+
V = xp.reshape(V, omega.shape[:-1] + (3, 3))
|
|
68
|
+
|
|
69
|
+
t = xp.matmul(V, rho[..., None])[..., 0]
|
|
70
|
+
|
|
71
|
+
se3 = xp.concatenate([q, t], axis=-1)
|
|
72
|
+
|
|
73
|
+
return se3
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from jaxtyping import Float
|
|
4
|
+
|
|
5
|
+
from nanomanifold.common import get_namespace
|
|
6
|
+
from nanomanifold.SO3 import inverse as so3_inverse
|
|
7
|
+
from nanomanifold.SO3 import rotate_points
|
|
8
|
+
|
|
9
|
+
from .canonicalize import canonicalize
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def inverse(se3: Float[Any, "... 7"]) -> Float[Any, "... 7"]:
|
|
13
|
+
"""Compute the inverse of SE(3) transformations.
|
|
14
|
+
|
|
15
|
+
For an SE(3) transformation T = [R, t] represented as [q, t],
|
|
16
|
+
the inverse is T^(-1) = [R^T, -R^T * t] represented as [q^(-1), -q^(-1) * t].
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
se3: SE(3) transformation in [w, x, y, z, tx, ty, tz] format
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Inverse SE(3) transformation
|
|
23
|
+
"""
|
|
24
|
+
xp = get_namespace(se3)
|
|
25
|
+
|
|
26
|
+
se3 = canonicalize(se3)
|
|
27
|
+
|
|
28
|
+
q = se3[..., :4]
|
|
29
|
+
t = se3[..., 4:7]
|
|
30
|
+
|
|
31
|
+
q_inv = so3_inverse(q)
|
|
32
|
+
|
|
33
|
+
t_inv = -rotate_points(q_inv, t[..., None, :]).squeeze(-2)
|
|
34
|
+
|
|
35
|
+
result = xp.concatenate([q_inv, t_inv], axis=-1)
|
|
36
|
+
|
|
37
|
+
return canonicalize(result)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from jaxtyping import Float
|
|
4
|
+
|
|
5
|
+
from nanomanifold.common import get_namespace
|
|
6
|
+
from nanomanifold.SO3 import hat
|
|
7
|
+
from nanomanifold.SO3 import log as so3_log
|
|
8
|
+
|
|
9
|
+
from .canonicalize import canonicalize
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def log(se3: Float[Any, "... 7"]) -> Float[Any, "... 6"]:
|
|
13
|
+
"""Compute the logarithmic map of SE(3) to its Lie algebra se(3).
|
|
14
|
+
|
|
15
|
+
The logarithmic map takes an SE(3) transformation and returns the corresponding
|
|
16
|
+
tangent vector in the Lie algebra se(3). This is the inverse operation of exp().
|
|
17
|
+
|
|
18
|
+
The SE(3) logarithmic map computes a 6-vector [ω, ρ] where:
|
|
19
|
+
- ω ∈ ℝ³ is the angular velocity (rotation part, same as SO(3) log)
|
|
20
|
+
- ρ ∈ ℝ³ is the transformed translation part
|
|
21
|
+
|
|
22
|
+
The formula involves:
|
|
23
|
+
- ω = log_SO3(R) where R is the rotation quaternion
|
|
24
|
+
- ρ = V^(-1) * t where V is the left Jacobian inverse and t is the translation
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
se3: SE(3) transformation in [w, x, y, z, tx, ty, tz] format of shape (..., 7)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Tangent vector in se(3) as [ω, ρ] of shape (..., 6)
|
|
31
|
+
"""
|
|
32
|
+
xp = get_namespace(se3)
|
|
33
|
+
se3 = canonicalize(se3)
|
|
34
|
+
|
|
35
|
+
q = se3[..., :4]
|
|
36
|
+
t = se3[..., 4:7]
|
|
37
|
+
|
|
38
|
+
omega = so3_log(q)
|
|
39
|
+
omega_norm = xp.linalg.norm(omega, axis=-1, keepdims=True)
|
|
40
|
+
|
|
41
|
+
eps = xp.finfo(omega.dtype).eps
|
|
42
|
+
small_angle_threshold = xp.asarray(max(1e-6, float(eps)), dtype=omega.dtype)
|
|
43
|
+
small_angle_mask = omega_norm < small_angle_threshold
|
|
44
|
+
|
|
45
|
+
omega_cross = hat(omega)
|
|
46
|
+
omega_cross_sq = xp.matmul(omega_cross, omega_cross)
|
|
47
|
+
|
|
48
|
+
identity = xp.eye(3, dtype=omega.dtype)
|
|
49
|
+
identity = xp.broadcast_to(identity, omega.shape[:-1] + (3, 3))
|
|
50
|
+
|
|
51
|
+
V_inv_small = identity - 0.5 * omega_cross + (1.0 / 12.0) * omega_cross_sq
|
|
52
|
+
|
|
53
|
+
half_norm = omega_norm / 2.0
|
|
54
|
+
cos_half = xp.cos(half_norm)
|
|
55
|
+
sin_half = xp.sin(half_norm)
|
|
56
|
+
|
|
57
|
+
safe_sin_half = xp.where(small_angle_mask, xp.ones_like(sin_half), sin_half)
|
|
58
|
+
cot_half = cos_half / safe_sin_half
|
|
59
|
+
|
|
60
|
+
safe_norm = xp.where(small_angle_mask, xp.ones_like(omega_norm), omega_norm)
|
|
61
|
+
safe_norm_sq = xp.where(small_angle_mask, xp.ones_like(omega_norm), omega_norm**2)
|
|
62
|
+
B = (1.0 - 0.5 * safe_norm * cot_half) / safe_norm_sq
|
|
63
|
+
B = xp.reshape(B, B.shape[:-1])[..., None, None]
|
|
64
|
+
|
|
65
|
+
V_inv_large = identity - 0.5 * omega_cross + B * omega_cross_sq
|
|
66
|
+
|
|
67
|
+
mask = xp.reshape(small_angle_mask, small_angle_mask.shape[:-1])[..., None, None]
|
|
68
|
+
V_inv = xp.where(mask, V_inv_small, V_inv_large)
|
|
69
|
+
V_inv = xp.reshape(V_inv, omega.shape[:-1] + (3, 3))
|
|
70
|
+
|
|
71
|
+
rho = xp.matmul(V_inv, t[..., None])[..., 0]
|
|
72
|
+
|
|
73
|
+
tangent = xp.concatenate([omega, rho], axis=-1)
|
|
74
|
+
|
|
75
|
+
return tangent
|