litearm-python 1.2.0__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.
litearm/_rot.py ADDED
@@ -0,0 +1,245 @@
1
+ """旋转/位姿小工具 —— **纯 Python, 不依赖 numpy**。
2
+
3
+ 约定与**固件 `kin.c` 完全一致**(ZYX 内旋), 也与 pylitearm 的 `control/cartesian.py`
4
+ 一致。三条互不相同的表示在本包来回转, 别搞混:
5
+
6
+ =========== ================================== ==============================
7
+ 表示 形态 谁在用
8
+ =========== ================================== ==============================
9
+ 6 向量 ``(x, y, z, roll, pitch, yaw)`` 固件 / 本包 ``get_tcp`` / ``move_p``
10
+ 位姿对 ``(position[3], rotation[3x3])`` pylitearm 的 ``movel/movec/movep``
11
+ 旋转矩阵 ``R[3][3]`` 行主序嵌套 list 内部统一用这个算
12
+ =========== ================================== ==============================
13
+
14
+ ``as_pose()`` 是唯一的入口, 上面前两种都吃, 免得调用方自己去想该传什么。
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import math
19
+ from typing import List, Sequence, Tuple
20
+
21
+ Mat3 = List[List[float]]
22
+ Vec3 = List[float]
23
+ #: 归一化后的位姿: (位置[3], 旋转矩阵[3x3])
24
+ Pose = Tuple[Vec3, Mat3]
25
+
26
+ _EPS = 1e-12
27
+
28
+
29
+ # --------------------------------------------------------------------------
30
+ # 基本 3x3 运算
31
+ # --------------------------------------------------------------------------
32
+ def eye3() -> Mat3:
33
+ return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
34
+
35
+
36
+ def mat_mul(A: Sequence[Sequence[float]], B: Sequence[Sequence[float]]) -> Mat3:
37
+ return [[A[i][0] * B[0][j] + A[i][1] * B[1][j] + A[i][2] * B[2][j]
38
+ for j in range(3)] for i in range(3)]
39
+
40
+
41
+ def mat_T(A: Sequence[Sequence[float]]) -> Mat3:
42
+ return [[A[j][i] for j in range(3)] for i in range(3)]
43
+
44
+
45
+ def mat_vec(A: Sequence[Sequence[float]], v: Sequence[float]) -> Vec3:
46
+ return [A[i][0] * v[0] + A[i][1] * v[1] + A[i][2] * v[2] for i in range(3)]
47
+
48
+
49
+ def dot(a: Sequence[float], b: Sequence[float]) -> float:
50
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
51
+
52
+
53
+ def cross(a: Sequence[float], b: Sequence[float]) -> Vec3:
54
+ return [a[1] * b[2] - a[2] * b[1],
55
+ a[2] * b[0] - a[0] * b[2],
56
+ a[0] * b[1] - a[1] * b[0]]
57
+
58
+
59
+ def norm(v: Sequence[float]) -> float:
60
+ return math.sqrt(dot(v, v))
61
+
62
+
63
+ def sub(a: Sequence[float], b: Sequence[float]) -> Vec3:
64
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
65
+
66
+
67
+ def unit(v: Sequence[float]) -> Vec3:
68
+ n = norm(v)
69
+ if n < _EPS:
70
+ raise ValueError("零向量无法归一化")
71
+ return [v[0] / n, v[1] / n, v[2] / n]
72
+
73
+
74
+ # --------------------------------------------------------------------------
75
+ # RPY <-> 旋转矩阵 (ZYX 内旋, 与固件 kin_rpy_to_rot / kin_rot_to_rpy 同约定 ——
76
+ # `kin.c:343-358` / `kin.c:360-385`; 固件里没有 `kin_rpy_to_rpy` 这个名字)
77
+ # --------------------------------------------------------------------------
78
+ def rpy_to_mat(rpy: Sequence[float]) -> Mat3:
79
+ """``(roll, pitch, yaw)`` -> R, 即 ``R = Rz(yaw) @ Ry(pitch) @ Rx(roll)``。
80
+
81
+ 与固件 `kin_rpy_to_rot` (`kin.c:343-358`) 逐元素同式 —— 别再自己推一遍,
82
+ 推错方向不会报错, 只会让所有姿态静默反着转。
83
+ """
84
+ r, p, y = (float(v) for v in rpy)
85
+ cr, sr = math.cos(r), math.sin(r)
86
+ cp, sp = math.cos(p), math.sin(p)
87
+ cy, sy = math.cos(y), math.sin(y)
88
+ return [[cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr],
89
+ [sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr],
90
+ [-sp, cp * sr, cp * cr]]
91
+
92
+
93
+ def mat_to_rpy(R: Sequence[Sequence[float]]) -> Vec3:
94
+ """R -> ``(roll, pitch, yaw)``。万向锁 (|pitch|≈π/2) 时**强制 yaw=0** —— 与固件
95
+ `kin_rot_to_rpy` 的锁分支同约定 (`kin.c:380-384`: `rpy[2] = 0.0f`)。
96
+ ⚠ 只保证"锁上强制 yaw=0"这条**约定**一致 —— 两侧的**锁带宽度不同**: 本函数用
97
+ `|pitch|` 距 `π/2` 小于 `1e-6`, 固件用 `|cos(pitch)| <= 1e-4` (`kin.c:362`/`:376`),
98
+ 即固件的锁带约宽 100 倍。两者之间的窄带里, 两边走的分支可以不同。
99
+
100
+ ⚠ 这条强制是 `move_p` 到位判定里那条「旋转等价」补丁的由来: 同一个旋转在万向锁
101
+ 附近可以给出差很远的 rpy 分量, 拿分量逐个比会**永远判不到位**。
102
+ """
103
+ p = math.atan2(-R[2][0], math.hypot(R[0][0], R[1][0]))
104
+ if abs(abs(p) - math.pi / 2) < 1e-6:
105
+ return [math.atan2(-R[1][2], R[1][1]), p, 0.0]
106
+ return [math.atan2(R[2][1], R[2][2]), p, math.atan2(R[1][0], R[0][0])]
107
+
108
+
109
+ # --------------------------------------------------------------------------
110
+ # SO(3) 上的指数/对数 —— slerp 与姿态误差的基础
111
+ # --------------------------------------------------------------------------
112
+ def rot_log(R: Sequence[Sequence[float]]) -> Vec3:
113
+ """R -> 旋转向量 (轴×角)。``acos`` 的入参必须夹住: 数值噪声会让它越界到
114
+ ±(1+ε) 然后抛 `ValueError: math domain error`。"""
115
+ tr = R[0][0] + R[1][1] + R[2][2]
116
+ c = max(-1.0, min(1.0, (tr - 1.0) / 2.0))
117
+ ang = math.acos(c)
118
+ if ang < 1e-9:
119
+ return [0.0, 0.0, 0.0]
120
+ v = [R[2][1] - R[1][2], R[0][2] - R[2][0], R[1][0] - R[0][1]]
121
+ k = ang / (2.0 * math.sin(ang))
122
+ return [v[0] * k, v[1] * k, v[2] * k]
123
+
124
+
125
+ def rot_exp(w: Sequence[float]) -> Mat3:
126
+ """旋转向量 -> R (Rodrigues)。"""
127
+ ang = norm(w)
128
+ if ang < 1e-12:
129
+ return eye3()
130
+ k = [w[0] / ang, w[1] / ang, w[2] / ang]
131
+ K = [[0.0, -k[2], k[1]], [k[2], 0.0, -k[0]], [-k[1], k[0], 0.0]]
132
+ s, c = math.sin(ang), 1.0 - math.cos(ang)
133
+ KK = mat_mul(K, K)
134
+ return [[(1.0 if i == j else 0.0) + s * K[i][j] + c * KK[i][j]
135
+ for j in range(3)] for i in range(3)]
136
+
137
+
138
+ def rot_slerp(R0: Sequence[Sequence[float]], R1: Sequence[Sequence[float]],
139
+ t: float) -> Mat3:
140
+ """姿态球面插值。``R0 @ exp(t·log(R0ᵀ R1))`` —— 走**最短弧**, 也是 pylitearm
141
+ `plan_movel` 里那行 `_kin.rot_slerp` 的同式。"""
142
+ w = rot_log(mat_mul(mat_T(R0), R1))
143
+ if norm(w) < 1e-12:
144
+ return [list(row) for row in R0]
145
+ return mat_mul(R0, rot_exp([w[0] * t, w[1] * t, w[2] * t]))
146
+
147
+
148
+ def rot_angle(R: Sequence[Sequence[float]]) -> float:
149
+ """旋转矩阵对应的转角 (rad)。"""
150
+ return norm(rot_log(R))
151
+
152
+
153
+ # --------------------------------------------------------------------------
154
+ # 位姿
155
+ # --------------------------------------------------------------------------
156
+ def pose_err(pos: Sequence[float], R: Sequence[Sequence[float]],
157
+ pos_d: Sequence[float], R_d: Sequence[Sequence[float]]
158
+ ) -> Tuple[float, float]:
159
+ """``(位置误差 m, 姿态误差 rad)``。语义同 pylitearm `pose_error`。
160
+
161
+ ⚠ 姿态差必须是 ``rot_log(R_d @ Rᵀ)``, **不能**写成 ``rot_log(R @ R_dᵀ)`` —— 后者
162
+ 是复合不是差, 符号相反; 曾因此把 0.59mm 位置误差的位姿报成 153° 姿态误差
163
+ (那个 153.359° 恰好是该位姿转角的 2 倍: 153.359/2 = 76.68°)。
164
+ """
165
+ ep = norm(sub(pos_d, pos))
166
+ er = rot_angle(mat_mul(R_d, mat_T(R)))
167
+ return ep, er
168
+
169
+
170
+ def is_rotation(R: Sequence[Sequence[float]], atol: float = 1e-5) -> bool:
171
+ """是不是合法的 SO(3) 成员 (正交 + det≈+1)。照 pylitearm `_validate_pose` 的判据。"""
172
+ Rt = mat_T(R)
173
+ I3 = eye3()
174
+ for i in range(3):
175
+ for j in range(3):
176
+ if abs(sum(Rt[i][k] * R[k][j] for k in range(3)) - I3[i][j]) > atol:
177
+ return False
178
+ det = (R[0][0] * (R[1][1] * R[2][2] - R[1][2] * R[2][1])
179
+ - R[0][1] * (R[1][0] * R[2][2] - R[1][2] * R[2][0])
180
+ + R[0][2] * (R[1][0] * R[2][1] - R[1][1] * R[2][0]))
181
+ return det > 0.999
182
+
183
+
184
+ def _is_sequence(x) -> bool:
185
+ return isinstance(x, (list, tuple)) and not isinstance(x, (str, bytes))
186
+
187
+
188
+ def as_pose(pose) -> Pose:
189
+ """把调用方给的东西归一化成 ``(position[3], R[3x3])``。
190
+
191
+ 接受(顺序即判定顺序):
192
+
193
+ * ``(pos[3], R[3x3])`` —— pylitearm 原生形式, 与 `_validate_pose` 同判据
194
+ * ``[x, y, z, roll, pitch, yaw]`` —— 固件/本包 `get_tcp` 的形式
195
+ * ``R[4][4]`` 齐次矩阵 —— 取左上 3x3 与右上平移
196
+
197
+ **不接受**扁平 9 元素 (按行主序的旋转矩阵) —— 它没有位置, 而位置不可省
198
+ (见下面"形式 4"那个分支的注释); 也不接受任何缺分量的写法。
199
+
200
+ 不合法一律 `InvalidCommandError`, 文案里给出**实际收到的形状** —— 只说
201
+ "pose 非法" 会让调用方在 6 向量和位姿对之间反复猜。
202
+
203
+ ⚠ 但**数值转换**里的畸形 (`float("x")` 的 `ValueError`、容器类型不对的 `TypeError`)
204
+ 是从这里**原样冒出去**的 (本函数的契约只覆盖形状判定), 由调用方负责收进
205
+ `LiteArmError` 体系 —— 见 `cart._as_pose6` (它把这两类一并转包成
206
+ `InvalidCommandError`)。
207
+ """
208
+ from litearm.errors import InvalidCommandError
209
+
210
+ if not _is_sequence(pose):
211
+ raise InvalidCommandError(
212
+ f"pose 需为 (pos[3], R[3x3]) 或 [x,y,z,r,p,y]; 收到 {type(pose).__name__}")
213
+
214
+ items = list(pose)
215
+ # 形式 1: (pos[3], R[3x3])
216
+ if len(items) == 2 and _is_sequence(items[0]) and _is_sequence(items[1]):
217
+ p, R = items
218
+ if len(list(p)) != 3:
219
+ raise InvalidCommandError(f"pose 位置需 3 个分量, 收到 {len(list(p))}")
220
+ Rl = [list(row) for row in R]
221
+ if len(Rl) != 3 or any(len(row) != 3 for row in Rl):
222
+ raise InvalidCommandError(
223
+ f"pose 旋转需 3x3, 收到 {len(Rl)}x{len(Rl[0]) if Rl else 0}")
224
+ Rf = [[float(v) for v in row] for row in Rl]
225
+ if not is_rotation(Rf):
226
+ raise InvalidCommandError("pose 旋转矩阵不是有效 SO(3) (正交且 det≈+1)")
227
+ return [float(v) for v in p], Rf
228
+
229
+ # 形式 2: 6 向量 xyz+rpy
230
+ if len(items) == 6 and not any(_is_sequence(v) for v in items):
231
+ v = [float(x) for x in items]
232
+ return v[:3], rpy_to_mat(v[3:])
233
+
234
+ # 形式 3: 齐次 4x4
235
+ if len(items) == 4 and all(_is_sequence(r) and len(list(r)) == 4 for r in items):
236
+ M = [[float(v) for v in row] for row in items]
237
+ Rf = [row[:3] for row in M[:3]]
238
+ if not is_rotation(Rf):
239
+ raise InvalidCommandError("pose 4x4 的旋转块不是有效 SO(3)")
240
+ return [M[0][3], M[1][3], M[2][3]], Rf
241
+
242
+ # 形式 4: 扁平 9 元素旋转矩阵 (无位置) —— 不接受, 位置不可省
243
+ raise InvalidCommandError(
244
+ f"pose 形状无法识别: len={len(items)}; 支持 (pos[3], R[3x3]) / "
245
+ f"[x,y,z,r,p,y] / 4x4 齐次矩阵")