nanomanifold 0.1.1__py3-none-any.whl
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/SE3/__init__.py +21 -0
- nanomanifold/SE3/canonicalize.py +25 -0
- nanomanifold/SE3/conversions/__init__.py +9 -0
- nanomanifold/SE3/conversions/matrix.py +57 -0
- nanomanifold/SE3/conversions/rt.py +30 -0
- nanomanifold/SE3/exp.py +73 -0
- nanomanifold/SE3/inverse.py +37 -0
- nanomanifold/SE3/log.py +75 -0
- nanomanifold/SE3/multiply.py +48 -0
- nanomanifold/SE3/transform_points.py +34 -0
- nanomanifold/SO3/__init__.py +35 -0
- nanomanifold/SO3/canonicalize.py +17 -0
- nanomanifold/SO3/conversions/__init__.py +14 -0
- nanomanifold/SO3/conversions/axis_angle.py +63 -0
- nanomanifold/SO3/conversions/euler.py +149 -0
- nanomanifold/SO3/conversions/matrix.py +79 -0
- nanomanifold/SO3/distance.py +52 -0
- nanomanifold/SO3/exp.py +25 -0
- nanomanifold/SO3/hat.py +36 -0
- nanomanifold/SO3/inverse.py +16 -0
- nanomanifold/SO3/log.py +25 -0
- nanomanifold/SO3/multiply.py +40 -0
- nanomanifold/SO3/rotate_points.py +50 -0
- nanomanifold/SO3/slerp.py +69 -0
- nanomanifold/SO3/vee.py +28 -0
- nanomanifold/SO3/weighted_mean.py +89 -0
- nanomanifold/__init__.py +3 -0
- nanomanifold/common.py +25 -0
- nanomanifold-0.1.1.dist-info/METADATA +112 -0
- nanomanifold-0.1.1.dist-info/RECORD +31 -0
- nanomanifold-0.1.1.dist-info/WHEEL +4 -0
|
@@ -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
|
nanomanifold/SE3/exp.py
ADDED
|
@@ -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)
|
nanomanifold/SE3/log.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from jaxtyping import Float
|
|
4
|
+
|
|
5
|
+
from nanomanifold.common import get_namespace
|
|
6
|
+
from nanomanifold.SO3 import multiply as so3_multiply
|
|
7
|
+
from nanomanifold.SO3 import rotate_points
|
|
8
|
+
|
|
9
|
+
from .canonicalize import canonicalize
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def multiply(se3_1: Float[Any, "... 7"], se3_2: Float[Any, "... 7"]) -> Float[Any, "... 7"]:
|
|
13
|
+
"""Multiply two SE(3) transformations.
|
|
14
|
+
|
|
15
|
+
The multiplication order matches transformation matrix multiplication:
|
|
16
|
+
multiply(se3_1, se3_2) represents the same composition as to_matrix(se3_1) @ to_matrix(se3_2)
|
|
17
|
+
|
|
18
|
+
This means se3_2 is applied first, then se3_1.
|
|
19
|
+
|
|
20
|
+
For SE(3) transformations [q1, t1] and [q2, t2], the result is:
|
|
21
|
+
- Quaternion: q1 * q2 (quaternion multiplication)
|
|
22
|
+
- Translation: R1 * t2 + t1 (where R1 rotates by q1)
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
se3_1: First SE(3) transformation in [w, x, y, z, tx, ty, tz] format
|
|
26
|
+
se3_2: Second SE(3) transformation in [w, x, y, z, tx, ty, tz] format
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Product SE(3) transformation representing the composed transformation
|
|
30
|
+
"""
|
|
31
|
+
xp = get_namespace(se3_1)
|
|
32
|
+
|
|
33
|
+
se3_1 = canonicalize(se3_1)
|
|
34
|
+
se3_2 = canonicalize(se3_2)
|
|
35
|
+
|
|
36
|
+
q1 = se3_1[..., :4]
|
|
37
|
+
t1 = se3_1[..., 4:7]
|
|
38
|
+
q2 = se3_2[..., :4]
|
|
39
|
+
t2 = se3_2[..., 4:7]
|
|
40
|
+
|
|
41
|
+
q_result = so3_multiply(q1, q2)
|
|
42
|
+
|
|
43
|
+
t2_rotated = rotate_points(q1, t2[..., None, :]).squeeze(-2)
|
|
44
|
+
t_result = t2_rotated + t1
|
|
45
|
+
|
|
46
|
+
result = xp.concatenate([q_result, t_result], axis=-1)
|
|
47
|
+
|
|
48
|
+
return canonicalize(result)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from jaxtyping import Float
|
|
4
|
+
|
|
5
|
+
from nanomanifold.SO3 import rotate_points
|
|
6
|
+
|
|
7
|
+
from .canonicalize import canonicalize
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def transform_points(se3: Float[Any, "... 7"], points: Float[Any, "... N 3"]) -> Float[Any, "... N 3"]:
|
|
11
|
+
"""Transform 3D points using SE(3) transformation.
|
|
12
|
+
|
|
13
|
+
Applies both rotation and translation: p' = R * p + t
|
|
14
|
+
where SE(3) = [q, t] with q being the quaternion and t being the translation.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
se3: SE(3) transformation in [w, x, y, z, tx, ty, tz] format of shape (..., 7)
|
|
18
|
+
points: Points to transform of shape (..., N, 3)
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Transformed points of shape (..., N, 3)
|
|
22
|
+
"""
|
|
23
|
+
se3 = canonicalize(se3)
|
|
24
|
+
|
|
25
|
+
q = se3[..., :4]
|
|
26
|
+
t = se3[..., 4:7]
|
|
27
|
+
|
|
28
|
+
rotated_points = rotate_points(q, points)
|
|
29
|
+
|
|
30
|
+
t_expanded = t[..., None, :]
|
|
31
|
+
|
|
32
|
+
transformed_points = rotated_points + t_expanded
|
|
33
|
+
|
|
34
|
+
return transformed_points
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from .canonicalize import canonicalize
|
|
2
|
+
from .conversions.axis_angle import from_axis_angle, to_axis_angle
|
|
3
|
+
from .conversions.euler import from_euler, to_euler
|
|
4
|
+
from .conversions.matrix import from_matrix, to_matrix
|
|
5
|
+
from .distance import distance
|
|
6
|
+
from .exp import exp
|
|
7
|
+
from .hat import hat
|
|
8
|
+
from .inverse import inverse
|
|
9
|
+
from .log import log
|
|
10
|
+
from .multiply import multiply
|
|
11
|
+
from .rotate_points import rotate_points
|
|
12
|
+
from .slerp import slerp
|
|
13
|
+
from .vee import vee
|
|
14
|
+
from .weighted_mean import mean, weighted_mean
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"to_axis_angle",
|
|
18
|
+
"from_axis_angle",
|
|
19
|
+
"to_euler",
|
|
20
|
+
"from_euler",
|
|
21
|
+
"to_matrix",
|
|
22
|
+
"from_matrix",
|
|
23
|
+
"canonicalize",
|
|
24
|
+
"rotate_points",
|
|
25
|
+
"inverse",
|
|
26
|
+
"multiply",
|
|
27
|
+
"distance",
|
|
28
|
+
"log",
|
|
29
|
+
"exp",
|
|
30
|
+
"hat",
|
|
31
|
+
"vee",
|
|
32
|
+
"slerp",
|
|
33
|
+
"weighted_mean",
|
|
34
|
+
"mean",
|
|
35
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from jaxtyping import Float
|
|
4
|
+
|
|
5
|
+
from nanomanifold.common import get_namespace
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def canonicalize(q: Float[Any, "... 4"]) -> Float[Any, "... 4"]:
|
|
9
|
+
xp = get_namespace(q)
|
|
10
|
+
|
|
11
|
+
norm = xp.sqrt(xp.sum(q**2, axis=-1, keepdims=True))
|
|
12
|
+
q_normalized = q / norm
|
|
13
|
+
|
|
14
|
+
mask = q_normalized[..., 0:1] < 0
|
|
15
|
+
q_canonical = xp.where(mask, -q_normalized, q_normalized)
|
|
16
|
+
|
|
17
|
+
return q_canonical
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""SO(3) conversion functions."""
|
|
2
|
+
|
|
3
|
+
from .axis_angle import from_axis_angle, to_axis_angle
|
|
4
|
+
from .euler import from_euler, to_euler
|
|
5
|
+
from .matrix import from_matrix, to_matrix
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"to_axis_angle",
|
|
9
|
+
"from_axis_angle",
|
|
10
|
+
"to_euler",
|
|
11
|
+
"from_euler",
|
|
12
|
+
"to_matrix",
|
|
13
|
+
"from_matrix",
|
|
14
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from jaxtyping import Float
|
|
4
|
+
|
|
5
|
+
from nanomanifold.common import get_namespace
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_axis_angle(q: Float[Any, "... 4"]) -> Float[Any, "... 3"]:
|
|
9
|
+
xp = get_namespace(q)
|
|
10
|
+
|
|
11
|
+
w = q[..., 0]
|
|
12
|
+
xyz = q[..., 1:4]
|
|
13
|
+
|
|
14
|
+
norm_xyz = xp.linalg.norm(xyz, axis=-1, keepdims=True)
|
|
15
|
+
|
|
16
|
+
# For small rotations, use the direct approximation: axis_angle ≈ 2*xyz
|
|
17
|
+
# This avoids numerical issues with acos when w ≈ 1
|
|
18
|
+
small_angle_threshold = 1e-6
|
|
19
|
+
small_angle_mask = norm_xyz < small_angle_threshold
|
|
20
|
+
|
|
21
|
+
axis_angle_small = 2.0 * xyz
|
|
22
|
+
|
|
23
|
+
w_clipped = xp.clip(xp.abs(w), 0.0, 1.0) # Use abs(w) to handle canonicalization
|
|
24
|
+
angle = 2 * xp.acos(w_clipped)
|
|
25
|
+
|
|
26
|
+
safe_norm = xp.where(norm_xyz < 1e-15, 1.0, norm_xyz) # Avoid true division by zero
|
|
27
|
+
axis = xyz / safe_norm
|
|
28
|
+
|
|
29
|
+
axis_angle_large = angle[..., None] * axis
|
|
30
|
+
|
|
31
|
+
axis_angle = xp.where(small_angle_mask, axis_angle_small, axis_angle_large)
|
|
32
|
+
|
|
33
|
+
return axis_angle
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def from_axis_angle(axis_angle: Float[Any, "... 3"]) -> Float[Any, "... 4"]:
|
|
37
|
+
xp = get_namespace(axis_angle)
|
|
38
|
+
|
|
39
|
+
angle = xp.linalg.norm(axis_angle, axis=-1)
|
|
40
|
+
|
|
41
|
+
small_angle_mask = angle < 1e-12
|
|
42
|
+
safe_angle = xp.where(small_angle_mask, 1.0, angle)
|
|
43
|
+
axis = axis_angle / safe_angle[..., None]
|
|
44
|
+
|
|
45
|
+
half_angle = angle / 2
|
|
46
|
+
|
|
47
|
+
# For small angles, use Taylor series: cos(x) ≈ 1 - x²/2, sin(x) ≈ x
|
|
48
|
+
cos_half = xp.cos(half_angle)
|
|
49
|
+
sin_half = xp.sin(half_angle)
|
|
50
|
+
|
|
51
|
+
w = cos_half[..., None]
|
|
52
|
+
|
|
53
|
+
xyz_normal = sin_half[..., None] * axis
|
|
54
|
+
xyz_small = axis_angle / 2.0
|
|
55
|
+
|
|
56
|
+
xyz = xp.where(small_angle_mask[..., None], xyz_small, xyz_normal)
|
|
57
|
+
|
|
58
|
+
q = xp.concat([w, xyz], axis=-1)
|
|
59
|
+
|
|
60
|
+
mask = q[..., 0:1] < 0
|
|
61
|
+
q = xp.where(mask, -q, q)
|
|
62
|
+
|
|
63
|
+
return q
|