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.

Files changed (55) hide show
  1. nanomanifold-0.1.1/.github/workflows/release.yml +65 -0
  2. nanomanifold-0.1.1/.gitignore +10 -0
  3. nanomanifold-0.1.1/.python-version +1 -0
  4. nanomanifold-0.1.1/PKG-INFO +112 -0
  5. nanomanifold-0.1.1/README.md +101 -0
  6. nanomanifold-0.1.1/pyproject.toml +22 -0
  7. nanomanifold-0.1.1/src/nanomanifold/SE3/__init__.py +21 -0
  8. nanomanifold-0.1.1/src/nanomanifold/SE3/canonicalize.py +25 -0
  9. nanomanifold-0.1.1/src/nanomanifold/SE3/conversions/__init__.py +9 -0
  10. nanomanifold-0.1.1/src/nanomanifold/SE3/conversions/matrix.py +57 -0
  11. nanomanifold-0.1.1/src/nanomanifold/SE3/conversions/rt.py +30 -0
  12. nanomanifold-0.1.1/src/nanomanifold/SE3/exp.py +73 -0
  13. nanomanifold-0.1.1/src/nanomanifold/SE3/inverse.py +37 -0
  14. nanomanifold-0.1.1/src/nanomanifold/SE3/log.py +75 -0
  15. nanomanifold-0.1.1/src/nanomanifold/SE3/multiply.py +48 -0
  16. nanomanifold-0.1.1/src/nanomanifold/SE3/transform_points.py +34 -0
  17. nanomanifold-0.1.1/src/nanomanifold/SO3/__init__.py +35 -0
  18. nanomanifold-0.1.1/src/nanomanifold/SO3/canonicalize.py +17 -0
  19. nanomanifold-0.1.1/src/nanomanifold/SO3/conversions/__init__.py +14 -0
  20. nanomanifold-0.1.1/src/nanomanifold/SO3/conversions/axis_angle.py +63 -0
  21. nanomanifold-0.1.1/src/nanomanifold/SO3/conversions/euler.py +149 -0
  22. nanomanifold-0.1.1/src/nanomanifold/SO3/conversions/matrix.py +79 -0
  23. nanomanifold-0.1.1/src/nanomanifold/SO3/distance.py +52 -0
  24. nanomanifold-0.1.1/src/nanomanifold/SO3/exp.py +25 -0
  25. nanomanifold-0.1.1/src/nanomanifold/SO3/hat.py +36 -0
  26. nanomanifold-0.1.1/src/nanomanifold/SO3/inverse.py +16 -0
  27. nanomanifold-0.1.1/src/nanomanifold/SO3/log.py +25 -0
  28. nanomanifold-0.1.1/src/nanomanifold/SO3/multiply.py +40 -0
  29. nanomanifold-0.1.1/src/nanomanifold/SO3/rotate_points.py +50 -0
  30. nanomanifold-0.1.1/src/nanomanifold/SO3/slerp.py +69 -0
  31. nanomanifold-0.1.1/src/nanomanifold/SO3/vee.py +28 -0
  32. nanomanifold-0.1.1/src/nanomanifold/SO3/weighted_mean.py +89 -0
  33. nanomanifold-0.1.1/src/nanomanifold/__init__.py +3 -0
  34. nanomanifold-0.1.1/src/nanomanifold/common.py +25 -0
  35. nanomanifold-0.1.1/tests/conftest.py +137 -0
  36. nanomanifold-0.1.1/tests/test_se3_conversions_matrix.py +78 -0
  37. nanomanifold-0.1.1/tests/test_se3_conversions_rt.py +111 -0
  38. nanomanifold-0.1.1/tests/test_se3_inverse.py +330 -0
  39. nanomanifold-0.1.1/tests/test_se3_log_exp.py +329 -0
  40. nanomanifold-0.1.1/tests/test_se3_multiply.py +358 -0
  41. nanomanifold-0.1.1/tests/test_se3_singularities.py +376 -0
  42. nanomanifold-0.1.1/tests/test_se3_transform_points.py +318 -0
  43. nanomanifold-0.1.1/tests/test_so3_conversions_axis_angle.py +83 -0
  44. nanomanifold-0.1.1/tests/test_so3_conversions_euler.py +113 -0
  45. nanomanifold-0.1.1/tests/test_so3_conversions_matrix.py +84 -0
  46. nanomanifold-0.1.1/tests/test_so3_distance.py +242 -0
  47. nanomanifold-0.1.1/tests/test_so3_hat_vee.py +235 -0
  48. nanomanifold-0.1.1/tests/test_so3_inverse.py +82 -0
  49. nanomanifold-0.1.1/tests/test_so3_log_exp.py +344 -0
  50. nanomanifold-0.1.1/tests/test_so3_mean.py +393 -0
  51. nanomanifold-0.1.1/tests/test_so3_multiply.py +267 -0
  52. nanomanifold-0.1.1/tests/test_so3_rotate_points.py +196 -0
  53. nanomanifold-0.1.1/tests/test_so3_singularities.py +289 -0
  54. nanomanifold-0.1.1/tests/test_so3_slerp.py +342 -0
  55. 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,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -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,9 @@
1
+ from .matrix import from_matrix, to_matrix
2
+ from .rt import from_rt, to_rt
3
+
4
+ __all__ = [
5
+ "from_matrix",
6
+ "to_matrix",
7
+ "from_rt",
8
+ "to_rt",
9
+ ]
@@ -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