roboapi 0.4.2__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.
- roboapi/__init__.py +2 -0
- roboapi/calibration/__init__.py +6 -0
- roboapi/calibration/profile.py +139 -0
- roboapi/calibration/session.py +350 -0
- roboapi/cli.py +215 -0
- roboapi/config/__init__.py +22 -0
- roboapi/config/loader.py +119 -0
- roboapi/config/models.py +191 -0
- roboapi/embodiment.py +251 -0
- roboapi/errors.py +37 -0
- roboapi/geometry.py +337 -0
- roboapi/hardware/__init__.py +5 -0
- roboapi/hardware/base.py +113 -0
- roboapi/hardware/camera.py +167 -0
- roboapi/hardware/franka/__init__.py +5 -0
- roboapi/hardware/franka/arm.py +476 -0
- roboapi/hardware/franka/desk.py +555 -0
- roboapi/hardware/franka/feedback.py +196 -0
- roboapi/hardware/franka/guiding.py +104 -0
- roboapi/hardware/franka/ik.py +543 -0
- roboapi/hardware/franka/limits.py +110 -0
- roboapi/hardware/franka/settings.py +116 -0
- roboapi/hardware/robotiq.py +350 -0
- roboapi/hardware/ur/__init__.py +16 -0
- roboapi/hardware/ur/build.py +92 -0
- roboapi/hardware/ur/compliant.py +128 -0
- roboapi/hardware/ur/dashboard.py +103 -0
- roboapi/hardware/ur/feedback.py +159 -0
- roboapi/hardware/ur/ipc.py +206 -0
- roboapi/hardware/ur/native/CMakeLists.txt +37 -0
- roboapi/hardware/ur/native/dependencies.lock.json +13 -0
- roboapi/hardware/ur/native/include/ur_cpp/cartesian_speedl.hpp +170 -0
- roboapi/hardware/ur/native/include/ur_cpp/control_loop.hpp +126 -0
- roboapi/hardware/ur/native/include/ur_cpp/geom.hpp +72 -0
- roboapi/hardware/ur/native/include/ur_cpp/motion_primitives.hpp +350 -0
- roboapi/hardware/ur/native/include/ur_cpp/robot_io.hpp +26 -0
- roboapi/hardware/ur/native/include/ur_cpp/streaming_pose.hpp +369 -0
- roboapi/hardware/ur/native/src/control_loop.cpp +512 -0
- roboapi/hardware/ur/native/src/robot_io_urcl.cpp +143 -0
- roboapi/hardware/ur/native/tests/cartesian_speedl_unit.cpp +113 -0
- roboapi/hardware/ur/native/tests/control_modes_unit.cpp +137 -0
- roboapi/hardware/ur/native/tests/streaming_pose_unit.cpp +348 -0
- roboapi/hardware/ur/native/tests/velocity_governor_unit.cpp +65 -0
- roboapi/hardware/ur/native/worker.cpp +147 -0
- roboapi/hardware/ur/position.py +253 -0
- roboapi/hardware/ur/settings.py +95 -0
- roboapi/images.py +16 -0
- roboapi/initialize.py +137 -0
- roboapi/mcp.py +497 -0
- roboapi/motion.py +125 -0
- roboapi/naming.py +7 -0
- roboapi/operations.py +45 -0
- roboapi/policy/__init__.py +7 -0
- roboapi/policy/client.py +277 -0
- roboapi/policy/protocol.py +166 -0
- roboapi/policy/settings.py +79 -0
- roboapi/product-names.json +12 -0
- roboapi/records.py +149 -0
- roboapi/runtime.py +911 -0
- roboapi/semantics.py +6 -0
- roboapi/tasks.py +337 -0
- roboapi/web/__init__.py +5 -0
- roboapi/web/presets.py +44 -0
- roboapi/web/server.py +419 -0
- roboapi/web/static/assets/Bundler-LICENSE.txt +48 -0
- roboapi/web/static/assets/CalibrationScene-BO_spRoL.js +4220 -0
- roboapi/web/static/assets/Inter-OFL.txt +93 -0
- roboapi/web/static/assets/Inter-Variable-c8O0ljhh.ttf +0 -0
- roboapi/web/static/assets/Lucide-LICENSE.txt +43 -0
- roboapi/web/static/assets/Meshoptimizer-LICENSE.txt +23 -0
- roboapi/web/static/assets/MiSans-Regular-ES4DrbaF.woff2 +0 -0
- roboapi/web/static/assets/MiSans-Semibold-B34HIk9C.woff2 +0 -0
- roboapi/web/static/assets/PokeBrand-NOTICE.txt +20 -0
- roboapi/web/static/assets/React-LICENSE.txt +23 -0
- roboapi/web/static/assets/Three-LICENSE.txt +23 -0
- roboapi/web/static/assets/home-reference-CTpf8-oV.png +0 -0
- roboapi/web/static/assets/index-CGLhpUm0.js +28 -0
- roboapi/web/static/assets/index-QXqV-fDC.css +1 -0
- roboapi/web/static/assets/robotiq-ace-pro-2-EJuoOXTy.glb +0 -0
- roboapi/web/static/index.html +15 -0
- roboapi-0.4.2.dist-info/METADATA +205 -0
- roboapi-0.4.2.dist-info/RECORD +94 -0
- roboapi-0.4.2.dist-info/WHEEL +5 -0
- roboapi-0.4.2.dist-info/entry_points.txt +2 -0
- roboapi-0.4.2.dist-info/licenses/LICENSE +202 -0
- roboapi-0.4.2.dist-info/licenses/NOTICE +16 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Bundler-LICENSE.txt +48 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Inter-OFL.txt +93 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Lucide-LICENSE.txt +43 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Meshoptimizer-LICENSE.txt +23 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/PokeBrand-NOTICE.txt +20 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/React-LICENSE.txt +23 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Three-LICENSE.txt +23 -0
- roboapi-0.4.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
"""Feedback-leashed inverse kinematics for Franka Cartesian control."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Sequence
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy.optimize import Bounds, LinearConstraint, minimize
|
|
11
|
+
|
|
12
|
+
from .settings import IKConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class IKResult:
|
|
17
|
+
qd: np.ndarray
|
|
18
|
+
dq: np.ndarray
|
|
19
|
+
pose_error: np.ndarray
|
|
20
|
+
minimum_joint_margin: float
|
|
21
|
+
task_scale: float
|
|
22
|
+
joint_velocity_limited: bool
|
|
23
|
+
posture_limited: bool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _vector(value: Sequence[float], size: int, name: str) -> np.ndarray:
|
|
27
|
+
result = np.asarray(value, dtype=np.float64)
|
|
28
|
+
if result.shape != (size,) or not np.all(np.isfinite(result)):
|
|
29
|
+
raise ValueError(f"{name} must contain {size} finite values")
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize_quaternion(value: Sequence[float]) -> np.ndarray:
|
|
34
|
+
quaternion = _vector(value, 4, "quaternion")
|
|
35
|
+
norm = float(np.linalg.norm(quaternion))
|
|
36
|
+
if norm < 1e-12:
|
|
37
|
+
raise ValueError("quaternion norm is zero")
|
|
38
|
+
return quaternion / norm
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def quaternion_error_rotvec(target_xyzw: Sequence[float], current_xyzw: Sequence[float]) -> np.ndarray:
|
|
42
|
+
"""Shortest-path world-frame rotation vector from current to target."""
|
|
43
|
+
target = normalize_quaternion(target_xyzw)
|
|
44
|
+
current = normalize_quaternion(current_xyzw)
|
|
45
|
+
if float(np.dot(target, current)) < 0.0:
|
|
46
|
+
target = -target
|
|
47
|
+
tx, ty, tz, tw = target
|
|
48
|
+
cx, cy, cz, cw = current
|
|
49
|
+
# target * inverse(current)
|
|
50
|
+
vector = np.array(
|
|
51
|
+
[tw * -cx + tx * cw + ty * -cz - tz * -cy,
|
|
52
|
+
tw * -cy - tx * -cz + ty * cw + tz * -cx,
|
|
53
|
+
tw * -cz + tx * -cy - ty * -cx + tz * cw],
|
|
54
|
+
dtype=np.float64,
|
|
55
|
+
)
|
|
56
|
+
scalar = tw * cw + tx * cx + ty * cy + tz * cz
|
|
57
|
+
vector_norm = float(np.linalg.norm(vector))
|
|
58
|
+
if vector_norm < 1e-12:
|
|
59
|
+
return np.zeros(3, dtype=np.float64)
|
|
60
|
+
angle = 2.0 * np.arctan2(vector_norm, max(0.0, float(scalar)))
|
|
61
|
+
return vector * (angle / vector_norm)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _clip_norm(vector: np.ndarray, limit: float) -> np.ndarray:
|
|
65
|
+
norm = float(np.linalg.norm(vector))
|
|
66
|
+
if norm <= limit or norm < 1e-12:
|
|
67
|
+
return vector.copy()
|
|
68
|
+
return vector * (limit / norm)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _adaptive_regularization(
|
|
72
|
+
min_singular_value: float, config: IKConfig
|
|
73
|
+
) -> float:
|
|
74
|
+
"""Keep normal-space response while damping only the singular region."""
|
|
75
|
+
if config.singularity_hard_threshold >= config.singularity_soft_threshold:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"singularity_hard_threshold must be less than "
|
|
78
|
+
"singularity_soft_threshold"
|
|
79
|
+
)
|
|
80
|
+
if config.singularity_max_regularization < config.regularization:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"singularity_max_regularization must be at least regularization"
|
|
83
|
+
)
|
|
84
|
+
if min_singular_value >= config.singularity_soft_threshold:
|
|
85
|
+
return float(config.regularization)
|
|
86
|
+
if min_singular_value <= config.singularity_hard_threshold:
|
|
87
|
+
return float(config.singularity_max_regularization)
|
|
88
|
+
ratio = (
|
|
89
|
+
config.singularity_soft_threshold - min_singular_value
|
|
90
|
+
) / (
|
|
91
|
+
config.singularity_soft_threshold - config.singularity_hard_threshold
|
|
92
|
+
)
|
|
93
|
+
# Smoothstep avoids a damping derivative discontinuity at either threshold.
|
|
94
|
+
blend = ratio * ratio * (3.0 - 2.0 * ratio)
|
|
95
|
+
return float(
|
|
96
|
+
config.regularization
|
|
97
|
+
+ blend * (config.singularity_max_regularization - config.regularization)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _uniform_task_scale(
|
|
102
|
+
velocity: np.ndarray, lower: np.ndarray, upper: np.ndarray
|
|
103
|
+
) -> float:
|
|
104
|
+
"""Largest [0, 1] scale that preserves the differential-IK direction."""
|
|
105
|
+
scale = 1.0
|
|
106
|
+
for value, low, high in zip(velocity, lower, upper):
|
|
107
|
+
if value > high:
|
|
108
|
+
scale = min(scale, float(high / value))
|
|
109
|
+
elif value < low:
|
|
110
|
+
scale = min(scale, float(low / value))
|
|
111
|
+
return float(np.clip(scale, 0.0, 1.0))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _solve_bounded_qp(
|
|
115
|
+
hessian: np.ndarray,
|
|
116
|
+
rhs: np.ndarray,
|
|
117
|
+
lower: np.ndarray,
|
|
118
|
+
upper: np.ndarray,
|
|
119
|
+
*,
|
|
120
|
+
projection_direction: np.ndarray | None = None,
|
|
121
|
+
max_projection: float | None = None,
|
|
122
|
+
) -> np.ndarray:
|
|
123
|
+
"""Solve the seven-dimensional IK QP with box and nullspace bounds."""
|
|
124
|
+
if np.any(lower > upper):
|
|
125
|
+
raise ValueError("joint velocity constraints are infeasible")
|
|
126
|
+
constraints = ()
|
|
127
|
+
if projection_direction is not None:
|
|
128
|
+
if (
|
|
129
|
+
max_projection is None
|
|
130
|
+
or not np.isfinite(max_projection)
|
|
131
|
+
or max_projection <= 0
|
|
132
|
+
):
|
|
133
|
+
raise ValueError("max_projection must be positive")
|
|
134
|
+
direction = _vector(projection_direction, rhs.size, "projection_direction")
|
|
135
|
+
minimum = float(
|
|
136
|
+
np.sum(np.where(direction >= 0.0, direction * lower, direction * upper))
|
|
137
|
+
)
|
|
138
|
+
maximum = float(
|
|
139
|
+
np.sum(np.where(direction >= 0.0, direction * upper, direction * lower))
|
|
140
|
+
)
|
|
141
|
+
if minimum > max_projection or maximum < -max_projection:
|
|
142
|
+
raise np.linalg.LinAlgError(
|
|
143
|
+
"nullspace velocity cap is incompatible with the joint velocity box"
|
|
144
|
+
)
|
|
145
|
+
constraints = (
|
|
146
|
+
LinearConstraint(direction, -max_projection, max_projection),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def objective(value: np.ndarray) -> float:
|
|
150
|
+
return float(0.5 * value @ hessian @ value - rhs @ value)
|
|
151
|
+
|
|
152
|
+
with warnings.catch_warnings():
|
|
153
|
+
warnings.filterwarnings(
|
|
154
|
+
"ignore",
|
|
155
|
+
message="Values in x were outside bounds during a minimize step",
|
|
156
|
+
category=RuntimeWarning,
|
|
157
|
+
module="scipy.optimize._optimize",
|
|
158
|
+
)
|
|
159
|
+
result = minimize(
|
|
160
|
+
objective,
|
|
161
|
+
np.clip(np.linalg.solve(hessian, rhs), lower, upper),
|
|
162
|
+
jac=lambda value: hessian @ value - rhs,
|
|
163
|
+
bounds=Bounds(lower, upper),
|
|
164
|
+
constraints=constraints,
|
|
165
|
+
method="SLSQP",
|
|
166
|
+
options={"ftol": 1e-12, "maxiter": 100},
|
|
167
|
+
)
|
|
168
|
+
solution = np.clip(np.asarray(result.x, dtype=np.float64), lower, upper)
|
|
169
|
+
finite = bool(np.all(np.isfinite(solution)))
|
|
170
|
+
projection_ok = projection_direction is None or (
|
|
171
|
+
abs(float(direction @ solution)) <= float(max_projection) + 1e-7
|
|
172
|
+
)
|
|
173
|
+
if not result.success or not finite or not projection_ok:
|
|
174
|
+
raise np.linalg.LinAlgError(f"bounded IK QP failed: {result.message}")
|
|
175
|
+
return solution
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _strict_nullspace_posture(
|
|
179
|
+
*,
|
|
180
|
+
primary_dq: np.ndarray,
|
|
181
|
+
direction: np.ndarray,
|
|
182
|
+
preferred_dq: np.ndarray,
|
|
183
|
+
lower: np.ndarray,
|
|
184
|
+
upper: np.ndarray,
|
|
185
|
+
max_velocity: float,
|
|
186
|
+
) -> tuple[np.ndarray, bool]:
|
|
187
|
+
"""Set the final feasible posture velocity in the exact 7-DoF nullspace.
|
|
188
|
+
|
|
189
|
+
A full-row-rank 6x7 Jacobian always has one structural null direction. By
|
|
190
|
+
parameterising the correction as ``z * alpha`` with ``J z == 0``, the
|
|
191
|
+
secondary posture objective cannot alter the Cartesian velocity already
|
|
192
|
+
chosen by the bounded primary QP. Crucially, ``max_velocity`` limits the
|
|
193
|
+
*final* structural-nullspace component, not the correction added this tick.
|
|
194
|
+
Otherwise a primary solution constrained around the previous command can
|
|
195
|
+
carry old nullspace velocity which is then added again on every cycle.
|
|
196
|
+
|
|
197
|
+
The scalar correction interval is the intersection of all seven joint
|
|
198
|
+
velocity boxes, so acceleration and joint limits remain authoritative
|
|
199
|
+
without another optimisation dependency.
|
|
200
|
+
"""
|
|
201
|
+
if not np.isfinite(max_velocity) or max_velocity <= 0.0:
|
|
202
|
+
raise ValueError("posture_max_velocity must be positive")
|
|
203
|
+
if np.any(primary_dq < lower - 1e-9) or np.any(primary_dq > upper + 1e-9):
|
|
204
|
+
raise ValueError("primary IK velocity is outside posture bounds")
|
|
205
|
+
|
|
206
|
+
primary_alpha = float(np.dot(direction, primary_dq))
|
|
207
|
+
desired_alpha = float(np.dot(direction, preferred_dq))
|
|
208
|
+
target_alpha = float(np.clip(desired_alpha, -max_velocity, max_velocity))
|
|
209
|
+
desired_correction_alpha = target_alpha - primary_alpha
|
|
210
|
+
|
|
211
|
+
alpha_lower = -np.inf
|
|
212
|
+
alpha_upper = np.inf
|
|
213
|
+
for value, low, high, component in zip(
|
|
214
|
+
primary_dq, lower, upper, direction
|
|
215
|
+
):
|
|
216
|
+
if abs(component) < 1e-12:
|
|
217
|
+
continue
|
|
218
|
+
first = float((low - value) / component)
|
|
219
|
+
second = float((high - value) / component)
|
|
220
|
+
alpha_lower = max(alpha_lower, min(first, second))
|
|
221
|
+
alpha_upper = min(alpha_upper, max(first, second))
|
|
222
|
+
if alpha_lower > alpha_upper + 1e-10:
|
|
223
|
+
raise np.linalg.LinAlgError(
|
|
224
|
+
"nullspace posture constraints are infeasible"
|
|
225
|
+
)
|
|
226
|
+
correction_alpha = float(
|
|
227
|
+
np.clip(desired_correction_alpha, alpha_lower, alpha_upper)
|
|
228
|
+
)
|
|
229
|
+
limited = bool(
|
|
230
|
+
abs(target_alpha - desired_alpha) > 1e-9
|
|
231
|
+
or abs(correction_alpha - desired_correction_alpha) > 1e-9
|
|
232
|
+
)
|
|
233
|
+
return primary_dq + direction * correction_alpha, limited
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _posture_motion_activity(
|
|
237
|
+
*,
|
|
238
|
+
translation_speed: float,
|
|
239
|
+
rotation_speed: float,
|
|
240
|
+
translation_threshold: float,
|
|
241
|
+
rotation_threshold: float,
|
|
242
|
+
) -> float:
|
|
243
|
+
"""Smoothly enable posture motion from 1x to 2x each task threshold."""
|
|
244
|
+
|
|
245
|
+
def ramp(speed: float, threshold: float) -> float:
|
|
246
|
+
if threshold == 0.0:
|
|
247
|
+
return float(speed > 0.0)
|
|
248
|
+
ratio = float(np.clip((speed - threshold) / threshold, 0.0, 1.0))
|
|
249
|
+
return ratio * ratio * (3.0 - 2.0 * ratio)
|
|
250
|
+
|
|
251
|
+
return max(
|
|
252
|
+
ramp(translation_speed, translation_threshold),
|
|
253
|
+
ramp(rotation_speed, rotation_threshold),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def solve_feedback_leashed_ik(
|
|
258
|
+
*,
|
|
259
|
+
q: Sequence[float],
|
|
260
|
+
current_pose7: Sequence[float],
|
|
261
|
+
target_pose7: Sequence[float],
|
|
262
|
+
target_twist: Sequence[float] | None = None,
|
|
263
|
+
jacobian: Sequence[Sequence[float]],
|
|
264
|
+
lower_limits: Sequence[float],
|
|
265
|
+
upper_limits: Sequence[float],
|
|
266
|
+
dt: float,
|
|
267
|
+
config: IKConfig,
|
|
268
|
+
previous_dq: Sequence[float] | None = None,
|
|
269
|
+
joint_velocity_bounds: tuple[np.ndarray, np.ndarray] | None = None,
|
|
270
|
+
) -> IKResult:
|
|
271
|
+
"""Compute one bounded q target based only on the latest measured q."""
|
|
272
|
+
qv = _vector(q, 7, "q")
|
|
273
|
+
current = _vector(current_pose7, 7, "current_pose7")
|
|
274
|
+
target = _vector(target_pose7, 7, "target_pose7")
|
|
275
|
+
lower = _vector(lower_limits, 7, "lower_limits")
|
|
276
|
+
upper = _vector(upper_limits, 7, "upper_limits")
|
|
277
|
+
jac = np.asarray(jacobian, dtype=np.float64)
|
|
278
|
+
if jac.shape != (6, 7) or not np.all(np.isfinite(jac)):
|
|
279
|
+
raise ValueError("jacobian must be a finite 6x7 matrix")
|
|
280
|
+
if not np.all(lower < upper):
|
|
281
|
+
raise ValueError("joint limits are invalid")
|
|
282
|
+
if np.any(qv < lower) or np.any(qv > upper):
|
|
283
|
+
raise ValueError("measured joints are outside model limits")
|
|
284
|
+
if not np.isfinite(config.joint_limit_margin) or config.joint_limit_margin <= 0.0:
|
|
285
|
+
raise ValueError("joint_limit_margin must be positive")
|
|
286
|
+
if not np.isfinite(config.nullspace_gain) or config.nullspace_gain < 0.0:
|
|
287
|
+
raise ValueError("nullspace_gain must be non-negative")
|
|
288
|
+
if not np.isfinite(config.nominal_dt) or config.nominal_dt <= 0.0:
|
|
289
|
+
raise ValueError("nominal_dt must be positive")
|
|
290
|
+
if not np.isfinite(config.target_lead_time) or config.target_lead_time <= 0.0:
|
|
291
|
+
raise ValueError("target_lead_time must be positive")
|
|
292
|
+
if (
|
|
293
|
+
not np.isfinite(config.translation_feedback_gain)
|
|
294
|
+
or config.translation_feedback_gain < 0.0
|
|
295
|
+
):
|
|
296
|
+
raise ValueError("translation_feedback_gain must be non-negative")
|
|
297
|
+
if (
|
|
298
|
+
not np.isfinite(config.rotation_feedback_gain)
|
|
299
|
+
or config.rotation_feedback_gain < 0.0
|
|
300
|
+
):
|
|
301
|
+
raise ValueError("rotation_feedback_gain must be non-negative")
|
|
302
|
+
posture_reference = _vector(
|
|
303
|
+
config.posture_reference, 7, "posture_reference"
|
|
304
|
+
)
|
|
305
|
+
posture_weights = _vector(config.posture_weights, 7, "posture_weights")
|
|
306
|
+
if (
|
|
307
|
+
config.posture_gain > 0.0
|
|
308
|
+
and (
|
|
309
|
+
np.any(posture_reference < lower)
|
|
310
|
+
or np.any(posture_reference > upper)
|
|
311
|
+
)
|
|
312
|
+
):
|
|
313
|
+
raise ValueError("posture_reference is outside model limits")
|
|
314
|
+
if np.any(posture_weights < 0.0):
|
|
315
|
+
raise ValueError("posture_weights must be non-negative")
|
|
316
|
+
if not np.isfinite(config.posture_gain) or config.posture_gain < 0.0:
|
|
317
|
+
raise ValueError("posture_gain must be non-negative")
|
|
318
|
+
if (
|
|
319
|
+
not np.isfinite(config.posture_task_velocity_threshold)
|
|
320
|
+
or config.posture_task_velocity_threshold < 0.0
|
|
321
|
+
):
|
|
322
|
+
raise ValueError("posture_task_velocity_threshold must be non-negative")
|
|
323
|
+
if (
|
|
324
|
+
not np.isfinite(config.posture_rotation_velocity_threshold)
|
|
325
|
+
or config.posture_rotation_velocity_threshold < 0.0
|
|
326
|
+
):
|
|
327
|
+
raise ValueError(
|
|
328
|
+
"posture_rotation_velocity_threshold must be non-negative"
|
|
329
|
+
)
|
|
330
|
+
max_joint_velocity = _vector(
|
|
331
|
+
config.max_joint_velocity, 7, "max_joint_velocity"
|
|
332
|
+
)
|
|
333
|
+
max_joint_acceleration = _vector(
|
|
334
|
+
config.max_joint_acceleration, 7, "max_joint_acceleration"
|
|
335
|
+
)
|
|
336
|
+
if np.any(max_joint_velocity <= 0.0):
|
|
337
|
+
raise ValueError("max_joint_velocity must contain positive values")
|
|
338
|
+
if np.any(max_joint_acceleration <= 0.0):
|
|
339
|
+
raise ValueError("max_joint_acceleration must contain positive values")
|
|
340
|
+
previous_velocity = (
|
|
341
|
+
np.zeros(7, dtype=np.float64)
|
|
342
|
+
if previous_dq is None
|
|
343
|
+
else _vector(previous_dq, 7, "previous_dq")
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
budget_dt = min(max(float(dt), 0.0), config.nominal_dt)
|
|
347
|
+
if not np.isfinite(budget_dt) or budget_dt <= 0.0:
|
|
348
|
+
budget_dt = config.nominal_dt
|
|
349
|
+
error = np.concatenate(
|
|
350
|
+
[target[:3] - current[:3], quaternion_error_rotvec(target[3:], current[3:])]
|
|
351
|
+
)
|
|
352
|
+
twist = (
|
|
353
|
+
np.zeros(6, dtype=np.float64)
|
|
354
|
+
if target_twist is None
|
|
355
|
+
else _vector(target_twist, 6, "target_twist")
|
|
356
|
+
)
|
|
357
|
+
max_translation_velocity = float(config.max_translation_velocity)
|
|
358
|
+
max_rotation_velocity = float(config.max_rotation_velocity)
|
|
359
|
+
if (not np.isfinite(max_translation_velocity) or max_translation_velocity <= 0.0
|
|
360
|
+
or not np.isfinite(max_rotation_velocity) or max_rotation_velocity <= 0.0):
|
|
361
|
+
raise ValueError("Cartesian velocity limits must be positive and finite")
|
|
362
|
+
if (not np.isfinite(config.max_translation_step) or config.max_translation_step <= 0.0
|
|
363
|
+
or not np.isfinite(config.max_rotation_step) or config.max_rotation_step <= 0.0):
|
|
364
|
+
raise ValueError("Cartesian step limits must be positive and finite")
|
|
365
|
+
max_translation_velocity = min(
|
|
366
|
+
max_translation_velocity, config.max_translation_step / config.nominal_dt,
|
|
367
|
+
)
|
|
368
|
+
max_rotation_velocity = min(
|
|
369
|
+
max_rotation_velocity, config.max_rotation_step / config.nominal_dt,
|
|
370
|
+
)
|
|
371
|
+
feedback_velocity = np.concatenate(
|
|
372
|
+
[
|
|
373
|
+
_clip_norm(
|
|
374
|
+
config.translation_feedback_gain * error[:3],
|
|
375
|
+
max_translation_velocity,
|
|
376
|
+
),
|
|
377
|
+
_clip_norm(
|
|
378
|
+
config.rotation_feedback_gain * error[3:],
|
|
379
|
+
max_rotation_velocity,
|
|
380
|
+
),
|
|
381
|
+
]
|
|
382
|
+
)
|
|
383
|
+
feedforward_velocity = np.concatenate(
|
|
384
|
+
[
|
|
385
|
+
_clip_norm(twist[:3], max_translation_velocity),
|
|
386
|
+
_clip_norm(twist[3:], max_rotation_velocity),
|
|
387
|
+
]
|
|
388
|
+
)
|
|
389
|
+
cartesian_velocity = np.concatenate(
|
|
390
|
+
[
|
|
391
|
+
_clip_norm(
|
|
392
|
+
feedback_velocity[:3] + feedforward_velocity[:3],
|
|
393
|
+
max_translation_velocity,
|
|
394
|
+
),
|
|
395
|
+
_clip_norm(
|
|
396
|
+
feedback_velocity[3:] + feedforward_velocity[3:],
|
|
397
|
+
max_rotation_velocity,
|
|
398
|
+
),
|
|
399
|
+
]
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
singular_values = np.linalg.svd(jac, compute_uv=False)
|
|
403
|
+
min_singular_value = float(singular_values[-1])
|
|
404
|
+
effective_regularization = _adaptive_regularization(
|
|
405
|
+
min_singular_value, config
|
|
406
|
+
)
|
|
407
|
+
damping2 = effective_regularization**2
|
|
408
|
+
hessian = jac.T @ jac + damping2 * np.eye(7)
|
|
409
|
+
task_rhs = jac.T @ cartesian_velocity
|
|
410
|
+
unconstrained_task_dq = np.linalg.solve(hessian, task_rhs)
|
|
411
|
+
|
|
412
|
+
lower_distance = qv - lower
|
|
413
|
+
upper_distance = upper - qv
|
|
414
|
+
centre = 0.5 * (lower + upper)
|
|
415
|
+
proximity = np.clip(
|
|
416
|
+
1.0 - np.minimum(lower_distance, upper_distance) / config.joint_limit_margin,
|
|
417
|
+
0.0, 1.0,
|
|
418
|
+
)
|
|
419
|
+
# Smoothly bias the redundant joint direction away from a nearby limit.
|
|
420
|
+
proximity = proximity * proximity * (3.0 - 2.0 * proximity)
|
|
421
|
+
limit_bias = config.nullspace_gain * proximity * np.sign(centre - qv)
|
|
422
|
+
|
|
423
|
+
posture_error = (
|
|
424
|
+
posture_reference - qv
|
|
425
|
+
if config.posture_gain > 0.0
|
|
426
|
+
else np.zeros(7, dtype=np.float64)
|
|
427
|
+
)
|
|
428
|
+
# Feed-forward twist is the trajectory's motion intent. A smooth activity
|
|
429
|
+
# ramp keeps coordinated Hold exactly still and avoids a posture-velocity
|
|
430
|
+
# step as a segment starts or settles. Translation and rotation use their
|
|
431
|
+
# own physical-unit thresholds; either kind of task motion can enable the
|
|
432
|
+
# secondary objective.
|
|
433
|
+
posture_activity = _posture_motion_activity(
|
|
434
|
+
# Motion intent is the complete Cartesian request. Teleop deliberately
|
|
435
|
+
# suppresses trajectory twist, but its pose-feedback motion must still
|
|
436
|
+
# keep posture control active. A settled Hold has neither component.
|
|
437
|
+
translation_speed=float(np.linalg.norm(cartesian_velocity[:3])),
|
|
438
|
+
rotation_speed=float(np.linalg.norm(cartesian_velocity[3:])),
|
|
439
|
+
translation_threshold=config.posture_task_velocity_threshold,
|
|
440
|
+
rotation_threshold=config.posture_rotation_velocity_threshold,
|
|
441
|
+
)
|
|
442
|
+
posture_preference = (
|
|
443
|
+
posture_activity
|
|
444
|
+
* config.posture_gain
|
|
445
|
+
* posture_weights
|
|
446
|
+
* posture_error
|
|
447
|
+
)
|
|
448
|
+
posture_preference += limit_bias
|
|
449
|
+
|
|
450
|
+
# Apply step budgets and directional slowdown to a symmetric velocity box.
|
|
451
|
+
# Intersect the official box afterwards: it may require inward motion and
|
|
452
|
+
# must never be relaxed by multiplying one of its bounds toward zero.
|
|
453
|
+
max_step = _vector(config.max_joint_step, 7, "max_joint_step")
|
|
454
|
+
if np.any(max_step <= 0.0):
|
|
455
|
+
raise ValueError("max_joint_step must contain positive values")
|
|
456
|
+
step_velocity = max_step * (budget_dt / config.nominal_dt) / config.nominal_dt
|
|
457
|
+
velocity_limit = np.minimum(max_joint_velocity, step_velocity)
|
|
458
|
+
base_lower = -velocity_limit * np.clip(lower_distance / config.joint_limit_margin, 0.0, 1.0)
|
|
459
|
+
base_upper = velocity_limit * np.clip(upper_distance / config.joint_limit_margin, 0.0, 1.0)
|
|
460
|
+
if joint_velocity_bounds is not None:
|
|
461
|
+
velocity_lower = _vector(joint_velocity_bounds[0], 7, "joint_velocity_bounds lower")
|
|
462
|
+
velocity_upper = _vector(joint_velocity_bounds[1], 7, "joint_velocity_bounds upper")
|
|
463
|
+
base_lower = np.maximum(base_lower, velocity_lower)
|
|
464
|
+
base_upper = np.minimum(base_upper, velocity_upper)
|
|
465
|
+
position_horizon = max(config.nominal_dt, config.target_lead_time)
|
|
466
|
+
base_lower = np.maximum(base_lower, (lower - qv) / position_horizon)
|
|
467
|
+
base_upper = np.minimum(base_upper, (upper - qv) / position_horizon)
|
|
468
|
+
|
|
469
|
+
acceleration_delta = max_joint_acceleration * budget_dt
|
|
470
|
+
lower_velocity = np.maximum(
|
|
471
|
+
base_lower, previous_velocity - acceleration_delta
|
|
472
|
+
)
|
|
473
|
+
upper_velocity = np.minimum(
|
|
474
|
+
base_upper, previous_velocity + acceleration_delta
|
|
475
|
+
)
|
|
476
|
+
if np.any(lower_velocity > upper_velocity):
|
|
477
|
+
raise ValueError("joint position, velocity and acceleration constraints are infeasible")
|
|
478
|
+
|
|
479
|
+
# First scale the complete Cartesian task uniformly. This preserves its
|
|
480
|
+
# direction when a velocity, acceleration, or position constraint becomes
|
|
481
|
+
# active instead of letting independent joint clips bend the requested
|
|
482
|
+
# path. During a reversal zero may be outside the acceleration window; in
|
|
483
|
+
# that transient the QP performs the required bounded deceleration.
|
|
484
|
+
zero_is_feasible = np.all(lower_velocity <= 0.0) and np.all(
|
|
485
|
+
upper_velocity >= 0.0
|
|
486
|
+
)
|
|
487
|
+
base_contains_zero = np.all(base_lower <= 0.0) and np.all(base_upper >= 0.0)
|
|
488
|
+
task_scale = 1.0
|
|
489
|
+
if base_contains_zero:
|
|
490
|
+
task_scale = _uniform_task_scale(
|
|
491
|
+
unconstrained_task_dq,
|
|
492
|
+
lower_velocity if zero_is_feasible else base_lower,
|
|
493
|
+
upper_velocity if zero_is_feasible else base_upper,
|
|
494
|
+
)
|
|
495
|
+
# A position-dependent official box can require inward motion and exclude
|
|
496
|
+
# zero. Scaling from zero is invalid there; the QP handles that box directly.
|
|
497
|
+
scaled_cartesian_velocity = task_scale * cartesian_velocity
|
|
498
|
+
|
|
499
|
+
# First solve the bounded Cartesian task with no posture trade-off. Then
|
|
500
|
+
# use only its exact structural nullspace for the continuous secondary
|
|
501
|
+
# posture target. This is a strict-priority hierarchy, not a weighted
|
|
502
|
+
# objective that can smooth or bend the requested end-effector velocity.
|
|
503
|
+
rhs = jac.T @ scaled_cartesian_velocity
|
|
504
|
+
_, _, vh = np.linalg.svd(jac, full_matrices=True)
|
|
505
|
+
null_direction = np.asarray(vh[-1], dtype=np.float64)
|
|
506
|
+
null_direction /= max(float(np.linalg.norm(null_direction)), 1e-12)
|
|
507
|
+
primary_dq = _solve_bounded_qp(
|
|
508
|
+
hessian,
|
|
509
|
+
rhs,
|
|
510
|
+
lower_velocity,
|
|
511
|
+
upper_velocity,
|
|
512
|
+
projection_direction=null_direction,
|
|
513
|
+
max_projection=config.posture_max_velocity,
|
|
514
|
+
)
|
|
515
|
+
dq, posture_limited = _strict_nullspace_posture(
|
|
516
|
+
primary_dq=primary_dq,
|
|
517
|
+
direction=null_direction,
|
|
518
|
+
preferred_dq=posture_preference,
|
|
519
|
+
lower=lower_velocity,
|
|
520
|
+
upper=upper_velocity,
|
|
521
|
+
max_velocity=config.posture_max_velocity,
|
|
522
|
+
)
|
|
523
|
+
if abs(float(null_direction @ dq)) > config.posture_max_velocity + 1e-7:
|
|
524
|
+
raise np.linalg.LinAlgError("final structural-nullspace velocity exceeded its hard cap")
|
|
525
|
+
# qd is a bounded local position lead, rebuilt from measured q every tick.
|
|
526
|
+
# The same constrained dq is also the damping reference, so the position
|
|
527
|
+
# and velocity terms agree and no target debt accumulates.
|
|
528
|
+
lead_time = float(config.target_lead_time)
|
|
529
|
+
qd = qv + dq * lead_time
|
|
530
|
+
joint_velocity_limited = bool(
|
|
531
|
+
task_scale < 1.0 - 1e-6
|
|
532
|
+
or np.any(np.isclose(dq, lower_velocity, atol=1e-6))
|
|
533
|
+
or np.any(np.isclose(dq, upper_velocity, atol=1e-6))
|
|
534
|
+
)
|
|
535
|
+
return IKResult(
|
|
536
|
+
qd=qd,
|
|
537
|
+
dq=dq,
|
|
538
|
+
pose_error=error,
|
|
539
|
+
minimum_joint_margin=float(np.min(np.minimum(lower_distance, upper_distance))),
|
|
540
|
+
task_scale=task_scale,
|
|
541
|
+
joint_velocity_limited=joint_velocity_limited,
|
|
542
|
+
posture_limited=posture_limited,
|
|
543
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Official model limits and FR3 position-dependent velocity bounds.
|
|
2
|
+
|
|
3
|
+
FR3 5.9+ supplies its parameters in the robot's URDF. Earlier FR3 systems use
|
|
4
|
+
the libfranka 0.13.3 formula; Panda uses fixed nominal limits.
|
|
5
|
+
|
|
6
|
+
Sources:
|
|
7
|
+
https://frankarobotics.github.io/docs/robot_specifications.html
|
|
8
|
+
https://github.com/frankarobotics/libfranka/blob/0.21.2/src/joint_velocity_limits.cpp
|
|
9
|
+
https://github.com/frankarobotics/libfranka/blob/0.13.3/include/franka/rate_limiting.h
|
|
10
|
+
https://github.com/frankarobotics/docs/blob/73f863bb76c6fc1f9ecdb3c6838569e552f3bad0/source/control-parameters-joint-fr3.csv
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
import xml.etree.ElementTree as ET
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class FrankaLimits:
|
|
23
|
+
joint_lower: tuple[float, ...]
|
|
24
|
+
joint_upper: tuple[float, ...]
|
|
25
|
+
joint_velocity: tuple[float, ...]
|
|
26
|
+
joint_acceleration: tuple[float, ...]
|
|
27
|
+
cartesian_velocity: tuple[float, float]
|
|
28
|
+
_velocity_lower: tuple[float, ...] | None = field(default=None, repr=False)
|
|
29
|
+
_velocity_upper: tuple[float, ...] | None = field(default=None, repr=False)
|
|
30
|
+
_velocity_offset: tuple[float, ...] | None = field(default=None, repr=False)
|
|
31
|
+
_deceleration: tuple[float, ...] | None = field(default=None, repr=False)
|
|
32
|
+
|
|
33
|
+
def velocity_bounds(self, q) -> tuple[np.ndarray, np.ndarray]:
|
|
34
|
+
positions = np.asarray(q, dtype=np.float64)
|
|
35
|
+
if positions.shape != (7,) or not np.all(np.isfinite(positions)):
|
|
36
|
+
raise ValueError("joint positions must contain seven finite values")
|
|
37
|
+
maximum = np.asarray(self.joint_velocity)
|
|
38
|
+
if self._velocity_offset is None:
|
|
39
|
+
return -maximum, maximum.copy()
|
|
40
|
+
offset = np.asarray(self._velocity_offset)
|
|
41
|
+
deceleration = np.asarray(self._deceleration)
|
|
42
|
+
upper = np.minimum(maximum, np.maximum(
|
|
43
|
+
0.0, -offset + np.sqrt(np.maximum(
|
|
44
|
+
0.0, 2.0 * deceleration * (np.asarray(self._velocity_upper) - positions),
|
|
45
|
+
)),
|
|
46
|
+
))
|
|
47
|
+
lower = np.maximum(-maximum, np.minimum(
|
|
48
|
+
0.0, offset - np.sqrt(np.maximum(
|
|
49
|
+
0.0, 2.0 * deceleration * (positions - np.asarray(self._velocity_lower)),
|
|
50
|
+
)),
|
|
51
|
+
))
|
|
52
|
+
# libfranka's kJointVelocityLimitsTolerance is 0.001 for both FR3 versions.
|
|
53
|
+
return lower + 0.001, upper - 0.001
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_FR3_VELOCITY = (2.62, 2.62, 2.62, 2.62, 5.26, 4.18, 5.26)
|
|
57
|
+
_FR3_ACCELERATION = (10.0,) * 7
|
|
58
|
+
_FR3_56 = FrankaLimits(
|
|
59
|
+
joint_lower=(-2.7437, -1.7837, -2.9007, -3.0421, -2.8065, 0.5445, -3.0159),
|
|
60
|
+
joint_upper=(2.7437, 1.7837, 2.9007, -0.1518, 2.8065, 4.5169, 3.0159),
|
|
61
|
+
joint_velocity=_FR3_VELOCITY,
|
|
62
|
+
joint_acceleration=_FR3_ACCELERATION,
|
|
63
|
+
cartesian_velocity=(3.0, 2.5),
|
|
64
|
+
# These curve endpoints differ from the older system's joint-position limits.
|
|
65
|
+
_velocity_lower=(-2.7501, -1.7918, -2.9065, -3.0481, -2.8101, 0.54092, -3.0196),
|
|
66
|
+
_velocity_upper=(2.7501, 1.7918, 2.9065, -0.1458, 2.8101, 4.5205, 3.0196),
|
|
67
|
+
_velocity_offset=(0.30, 0.20, 0.20, 0.30, 0.35, 0.35, 0.35),
|
|
68
|
+
_deceleration=(6.0, 2.585, 3.5, 4.0, 17.0, 5.5, 17.0),
|
|
69
|
+
)
|
|
70
|
+
_PANDA = FrankaLimits(
|
|
71
|
+
joint_lower=(-2.8973, -1.7628, -2.8973, -3.0718, -2.8973, -0.0175, -2.8973),
|
|
72
|
+
joint_upper=(2.8973, 1.7628, 2.8973, -0.0698, 2.8973, 3.7525, 2.8973),
|
|
73
|
+
joint_velocity=(2.175, 2.175, 2.175, 2.175, 2.61, 2.61, 2.61),
|
|
74
|
+
joint_acceleration=(15.0, 7.5, 10.0, 12.5, 15.0, 20.0, 20.0),
|
|
75
|
+
cartesian_velocity=(1.7, 2.5),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_limits(model: str, model_urdf: str | None) -> FrankaLimits:
|
|
80
|
+
nominal = {"fr3": _FR3_56, "panda": _PANDA}[model]
|
|
81
|
+
if model_urdf is None or model == "panda":
|
|
82
|
+
return nominal
|
|
83
|
+
root = ET.fromstring(model_urdf)
|
|
84
|
+
|
|
85
|
+
joints = {}
|
|
86
|
+
names = {name: index for index in range(7)
|
|
87
|
+
for name in (f"joint{index + 1}", f"fr3_joint{index + 1}")}
|
|
88
|
+
for joint in root.findall("joint"):
|
|
89
|
+
name = joint.get("name", "")
|
|
90
|
+
index = names.get(name)
|
|
91
|
+
if index is None:
|
|
92
|
+
continue
|
|
93
|
+
limit = joint.find("limit")
|
|
94
|
+
curve = joint.find("position_based_velocity_limits")
|
|
95
|
+
lower, upper, velocity = (float(limit.attrib[key]) for key in ("lower", "upper", "velocity"))
|
|
96
|
+
offset = float(curve.attrib["velocity_offset"])
|
|
97
|
+
deceleration = float(curve.attrib["deceleration_limit"])
|
|
98
|
+
joints[index] = lower, upper, velocity, offset, deceleration
|
|
99
|
+
lower, upper, velocity, offset, deceleration = zip(*(joints[i] for i in range(7)))
|
|
100
|
+
return FrankaLimits(
|
|
101
|
+
joint_lower=lower,
|
|
102
|
+
joint_upper=upper,
|
|
103
|
+
joint_velocity=velocity,
|
|
104
|
+
joint_acceleration=_FR3_ACCELERATION,
|
|
105
|
+
cartesian_velocity=(3.0, 2.5),
|
|
106
|
+
_velocity_lower=lower,
|
|
107
|
+
_velocity_upper=upper,
|
|
108
|
+
_velocity_offset=offset,
|
|
109
|
+
_deceleration=deceleration,
|
|
110
|
+
)
|