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.

@@ -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
@@ -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