pyroboplan 1.0.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.
- pyroboplan/__init__.py +1 -0
- pyroboplan/core/__init__.py +5 -0
- pyroboplan/core/planning_context.py +18 -0
- pyroboplan/core/utils.py +358 -0
- pyroboplan/ik/__init__.py +4 -0
- pyroboplan/ik/differential_ik.py +253 -0
- pyroboplan/ik/nullspace_components.py +76 -0
- pyroboplan/models/__init__.py +1 -0
- pyroboplan/models/panda.py +143 -0
- pyroboplan/models/panda_description/LICENSE +176 -0
- pyroboplan/models/panda_description/README.md +4 -0
- pyroboplan/models/panda_description/meshes/collision/finger.stl +0 -0
- pyroboplan/models/panda_description/meshes/collision/hand.stl +0 -0
- pyroboplan/models/panda_description/meshes/collision/link0.stl +0 -0
- pyroboplan/models/panda_description/meshes/collision/link1.stl +0 -0
- pyroboplan/models/panda_description/meshes/collision/link2.stl +0 -0
- pyroboplan/models/panda_description/meshes/collision/link3.stl +0 -0
- pyroboplan/models/panda_description/meshes/collision/link4.stl +0 -0
- pyroboplan/models/panda_description/meshes/collision/link5.stl +0 -0
- pyroboplan/models/panda_description/meshes/collision/link6.stl +0 -0
- pyroboplan/models/panda_description/meshes/collision/link7.stl +0 -0
- pyroboplan/models/panda_description/meshes/visual/finger.dae +63 -0
- pyroboplan/models/panda_description/meshes/visual/hand.dae +63 -0
- pyroboplan/models/panda_description/meshes/visual/link0.dae +63 -0
- pyroboplan/models/panda_description/meshes/visual/link1.dae +63 -0
- pyroboplan/models/panda_description/meshes/visual/link2.dae +63 -0
- pyroboplan/models/panda_description/meshes/visual/link3.dae +63 -0
- pyroboplan/models/panda_description/meshes/visual/link4.dae +63 -0
- pyroboplan/models/panda_description/meshes/visual/link5.dae +63 -0
- pyroboplan/models/panda_description/meshes/visual/link6.dae +63 -0
- pyroboplan/models/panda_description/meshes/visual/link7.dae +63 -0
- pyroboplan/models/panda_description/urdf/panda.urdf +280 -0
- pyroboplan/models/utils.py +16 -0
- pyroboplan/planning/__init__.py +4 -0
- pyroboplan/planning/cartesian_planner.py +231 -0
- pyroboplan/planning/graph.py +137 -0
- pyroboplan/planning/rrt.py +417 -0
- pyroboplan/planning/utils.py +29 -0
- pyroboplan/trajectory/__init__.py +5 -0
- pyroboplan/trajectory/polynomial.py +308 -0
- pyroboplan/trajectory/trapezoidal_velocity.py +353 -0
- pyroboplan/visualization/__init__.py +4 -0
- pyroboplan/visualization/meshcat_utils.py +124 -0
- pyroboplan-1.0.0.dist-info/METADATA +69 -0
- pyroboplan-1.0.0.dist-info/RECORD +47 -0
- pyroboplan-1.0.0.dist-info/WHEEL +4 -0
- pyroboplan-1.0.0.dist-info/licenses/LICENSE +21 -0
pyroboplan/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""" Educational Python library for manipulator motion planning. """
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pinocchio
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PlanningContext:
|
|
5
|
+
"""
|
|
6
|
+
Defines a planning context, which contains Pinocchio models and necessary utilities for motion planning.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, model, visual_model=None, collision_model=None):
|
|
10
|
+
"""
|
|
11
|
+
Creates a planning context instance given a set of Pinocchio models.
|
|
12
|
+
"""
|
|
13
|
+
self.model = model
|
|
14
|
+
self.visual_model = visual_model
|
|
15
|
+
self.collision_model = collision_model
|
|
16
|
+
|
|
17
|
+
self.data = model.createData()
|
|
18
|
+
self.collision_data = collision_model.createData()
|
pyroboplan/core/utils.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
""" Core utilities for robot modeling. """
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pinocchio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def check_collisions_at_state(model, collision_model, q):
|
|
9
|
+
"""
|
|
10
|
+
Checks whether a specified joint configuration is collision-free.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
model : `pinocchio.Model`
|
|
15
|
+
The model from which to generate a random state.
|
|
16
|
+
collision_model : `pinocchio.Model`
|
|
17
|
+
The model to use for collision checking.
|
|
18
|
+
q : array-like
|
|
19
|
+
The joint configuration of the model.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
bool
|
|
24
|
+
True is there are any collisions, otherwise False.
|
|
25
|
+
"""
|
|
26
|
+
data = model.createData()
|
|
27
|
+
collision_data = collision_model.createData()
|
|
28
|
+
stop_at_first_collision = True # For faster computation
|
|
29
|
+
|
|
30
|
+
pinocchio.computeCollisions(
|
|
31
|
+
model, data, collision_model, collision_data, q, stop_at_first_collision
|
|
32
|
+
)
|
|
33
|
+
return np.any([cr.isCollision() for cr in collision_data.collisionResults])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_collisions_along_path(model, collision_model, q_path):
|
|
37
|
+
"""
|
|
38
|
+
Checks whether a path consisting of multiple joint configurations is collision-free.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
model : `pinocchio.Model`
|
|
43
|
+
The model from which to generate a random state.
|
|
44
|
+
collision_model : `pinocchio.Model`
|
|
45
|
+
The model to use for collision checking.
|
|
46
|
+
q_path : list[array-like]
|
|
47
|
+
A list of joint configurations describing the path.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
bool
|
|
52
|
+
True is there are any collisions, otherwise False.
|
|
53
|
+
"""
|
|
54
|
+
data = model.createData()
|
|
55
|
+
collision_data = collision_model.createData()
|
|
56
|
+
stop_at_first_collision = True # For faster computation
|
|
57
|
+
|
|
58
|
+
for q in q_path:
|
|
59
|
+
pinocchio.computeCollisions(
|
|
60
|
+
model, data, collision_model, collision_data, q, stop_at_first_collision
|
|
61
|
+
)
|
|
62
|
+
if np.any([cr.isCollision() for cr in collision_data.collisionResults]):
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def configuration_distance(q_start, q_end):
|
|
69
|
+
"""
|
|
70
|
+
Returns the distance between two joint configurations.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
q_start : array-like
|
|
75
|
+
The start joint configuration.
|
|
76
|
+
q_end : array-like
|
|
77
|
+
The end joint configuration.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
float
|
|
82
|
+
The distance between the two joint configurations.
|
|
83
|
+
"""
|
|
84
|
+
return np.linalg.norm(q_end - q_start)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_path_length(q_path):
|
|
88
|
+
"""
|
|
89
|
+
Returns the configuration distance of a path.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
q_path : list[array-like]
|
|
94
|
+
A list of joint configurations describing a path.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
float
|
|
99
|
+
The total configuration distance of the entire path.
|
|
100
|
+
"""
|
|
101
|
+
total_distance = 0.0
|
|
102
|
+
for idx in range(1, len(q_path)):
|
|
103
|
+
total_distance += configuration_distance(q_path[idx - 1], q_path[idx])
|
|
104
|
+
return total_distance
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_random_state(model, padding=0.0):
|
|
108
|
+
"""
|
|
109
|
+
Returns a random state that is within the model's joint limits.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
model : `pinocchio.Model`
|
|
114
|
+
The model from which to generate a random state.
|
|
115
|
+
padding : float or array-like, optional
|
|
116
|
+
The padding to use around the sampled joint limits.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
array-like
|
|
121
|
+
A set of randomly generated joint variables.
|
|
122
|
+
"""
|
|
123
|
+
return np.random.uniform(
|
|
124
|
+
model.lowerPositionLimit + padding, model.upperPositionLimit - padding
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_random_collision_free_state(model, collision_model, padding=0.0, max_tries=100):
|
|
129
|
+
"""
|
|
130
|
+
Returns a random state that is within the model's joint limits and is collision-free according to the collision model.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
model : `pinocchio.Model`
|
|
135
|
+
The model from which to generate a random state.
|
|
136
|
+
collision_model : `pinocchio.Model`
|
|
137
|
+
The model to use for collision checking.
|
|
138
|
+
padding : float or array-like, optional
|
|
139
|
+
The padding to use around the sampled joint limits.
|
|
140
|
+
max_tries : int, optional
|
|
141
|
+
The maximum number of tries for sampling a collision-free state.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
array-like
|
|
146
|
+
A set of randomly generated collision-free joint variables, or None if one cannot be found.
|
|
147
|
+
"""
|
|
148
|
+
num_tries = 0
|
|
149
|
+
while num_tries < max_tries:
|
|
150
|
+
state = get_random_state(model, padding=padding)
|
|
151
|
+
if not check_collisions_at_state(model, collision_model, state):
|
|
152
|
+
return state
|
|
153
|
+
num_tries += 1
|
|
154
|
+
|
|
155
|
+
print(f"Could not generate collision-free state after {max_tries} tries.")
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_random_transform(model, target_frame, joint_padding=0.0):
|
|
160
|
+
"""
|
|
161
|
+
Returns a random transform for a target frame that is within the model's joint limits.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
model : `pinocchio.Model`
|
|
166
|
+
The model from which to generate a random transform.
|
|
167
|
+
target_frame : str
|
|
168
|
+
The name of the frame for which to generate a random transform.
|
|
169
|
+
joint_padding : float or array-like, optional
|
|
170
|
+
The padding to use around the sampled joint limits.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
`pinocchio.SE3`
|
|
175
|
+
A randomly generated transform for the specified target frame.
|
|
176
|
+
"""
|
|
177
|
+
q_target = get_random_state(model, joint_padding)
|
|
178
|
+
data = model.createData()
|
|
179
|
+
target_frame_id = model.getFrameId(target_frame)
|
|
180
|
+
pinocchio.framesForwardKinematics(model, data, q_target)
|
|
181
|
+
return data.oMf[target_frame_id]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_random_collision_free_transform(
|
|
185
|
+
model, collision_model, target_frame, joint_padding=0.0, max_tries=100
|
|
186
|
+
):
|
|
187
|
+
"""
|
|
188
|
+
Returns a random transform for a target frame that is within the model's joint limits and is collision-free.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
model : `pinocchio.Model`
|
|
193
|
+
The model from which to generate a random transform.
|
|
194
|
+
collision_model : `pinocchio.Model`
|
|
195
|
+
The model to use for collision checking.
|
|
196
|
+
target_frame : str
|
|
197
|
+
The name of the frame for which to generate a random transform.
|
|
198
|
+
joint_padding : float or array-like, optional
|
|
199
|
+
The padding to use around the sampled joint limits.
|
|
200
|
+
max_tries : int, optional
|
|
201
|
+
The maximum number of tries for sampling a collision-free state.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
`pinocchio.SE3`
|
|
206
|
+
A randomly generated transform for the specified target frame.
|
|
207
|
+
"""
|
|
208
|
+
q_target = get_random_collision_free_state(
|
|
209
|
+
model, collision_model, joint_padding, max_tries
|
|
210
|
+
)
|
|
211
|
+
data = model.createData()
|
|
212
|
+
target_frame_id = model.getFrameId(target_frame)
|
|
213
|
+
pinocchio.framesForwardKinematics(model, data, q_target)
|
|
214
|
+
return data.oMf[target_frame_id]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def check_within_limits(model, q):
|
|
218
|
+
"""
|
|
219
|
+
Checks whether a particular joint configuration is within the model's joint limits.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
model : `pinocchio.Model`
|
|
224
|
+
The model from which to generate a random state.
|
|
225
|
+
q : array-like
|
|
226
|
+
The joint configuration for the model.
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
bool
|
|
231
|
+
True if the configuration is within joint limits, otherwise False.
|
|
232
|
+
"""
|
|
233
|
+
return np.all(q >= model.lowerPositionLimit) and np.all(
|
|
234
|
+
q <= model.upperPositionLimit
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def extract_cartesian_pose(model, target_frame, q, data=None):
|
|
239
|
+
"""
|
|
240
|
+
Extracts the Cartesian pose of a specified model frame given a joint configuration.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
model : `pinocchio.Model`
|
|
245
|
+
The model from which to perform forward kinematics.
|
|
246
|
+
target_frame : str
|
|
247
|
+
The name of the target frame.
|
|
248
|
+
q : array-like
|
|
249
|
+
The joint configuration values describing the robot state.
|
|
250
|
+
data : `pinocchio.Data`, optional
|
|
251
|
+
The model data to use. If not set, one will be created.
|
|
252
|
+
|
|
253
|
+
Returns
|
|
254
|
+
-------
|
|
255
|
+
`pinocchio.SE3`
|
|
256
|
+
The transform describing the Cartesian pose of the specified frame at the provided joint configuration.
|
|
257
|
+
"""
|
|
258
|
+
if data is None:
|
|
259
|
+
data = model.createData()
|
|
260
|
+
target_frame_id = model.getFrameId(target_frame)
|
|
261
|
+
pinocchio.framesForwardKinematics(model, data, q)
|
|
262
|
+
return copy.deepcopy(data.oMf[target_frame_id])
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def extract_cartesian_poses(model, target_frame, q_vec, data=None):
|
|
266
|
+
"""
|
|
267
|
+
Extracts the Cartesian poses of a specified model frame given a list of joint configurations.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
model : `pinocchio.Model`
|
|
272
|
+
The model from which to perform forward kinematics.
|
|
273
|
+
target_frame : str
|
|
274
|
+
The name of the target frame.
|
|
275
|
+
q_vec : array-like
|
|
276
|
+
A list of joint configuration values describing the path.
|
|
277
|
+
data : `pinocchio.Data`, optional
|
|
278
|
+
The model data to use. If not set, one will be created.
|
|
279
|
+
|
|
280
|
+
Returns
|
|
281
|
+
-------
|
|
282
|
+
list[`pinocchio.SE3`]
|
|
283
|
+
A list of transforms describing the Cartesian poses of the specified frame at the provided joint configurations.
|
|
284
|
+
"""
|
|
285
|
+
if data is None:
|
|
286
|
+
data = model.createData()
|
|
287
|
+
target_frame_id = model.getFrameId(target_frame)
|
|
288
|
+
tforms = []
|
|
289
|
+
for q in q_vec:
|
|
290
|
+
pinocchio.framesForwardKinematics(model, data, q)
|
|
291
|
+
tforms.append(copy.deepcopy(data.oMf[target_frame_id]))
|
|
292
|
+
return tforms
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def get_collision_geometry_ids(model, collision_model, body):
|
|
296
|
+
"""
|
|
297
|
+
Gets a list of collision geometry model IDs for a specified body name.
|
|
298
|
+
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
model : `pinocchio.Model`
|
|
302
|
+
The model to use for getting frame IDs.
|
|
303
|
+
collision_model : `pinocchio.Model`
|
|
304
|
+
The model to use for collision checking.
|
|
305
|
+
body : str
|
|
306
|
+
The name of the body.
|
|
307
|
+
This can be directly the name of a geometry in the collision model,
|
|
308
|
+
or it can be the name of the frame in the main model.
|
|
309
|
+
|
|
310
|
+
Return
|
|
311
|
+
------
|
|
312
|
+
list[int]
|
|
313
|
+
A list of collision geometry IDs corresponding to the body name.
|
|
314
|
+
"""
|
|
315
|
+
body_collision_geom_ids = []
|
|
316
|
+
|
|
317
|
+
# First, check if this is directly a collision geometry.
|
|
318
|
+
body_collision_geom_id = collision_model.getGeometryId(body)
|
|
319
|
+
if body_collision_geom_id < collision_model.ngeoms:
|
|
320
|
+
body_collision_geom_ids.append(body_collision_geom_id)
|
|
321
|
+
else:
|
|
322
|
+
# Otherwise, look for the frame name in the model and return its associated collision objects.
|
|
323
|
+
body_frame_id = model.getFrameId(body)
|
|
324
|
+
if body_frame_id < model.nframes:
|
|
325
|
+
for id, obj in enumerate(collision_model.geometryObjects):
|
|
326
|
+
if obj.parentFrame == body_frame_id:
|
|
327
|
+
body_collision_geom_ids.append(id)
|
|
328
|
+
|
|
329
|
+
return body_collision_geom_ids
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def set_collisions(model, collision_model, body1, body2, enable):
|
|
333
|
+
"""
|
|
334
|
+
Sets collision checking between two bodies by searching for their corresponding geometry objects in the collision model.
|
|
335
|
+
|
|
336
|
+
Parameters
|
|
337
|
+
----------
|
|
338
|
+
model : `pinocchio.Model`
|
|
339
|
+
The model to use for getting frame IDs.
|
|
340
|
+
collision_model : `pinocchio.Model`
|
|
341
|
+
The model to use for collision checking.
|
|
342
|
+
body1 : str
|
|
343
|
+
The name of the first body.
|
|
344
|
+
body2 : str
|
|
345
|
+
The name of the second body.
|
|
346
|
+
enable : bool
|
|
347
|
+
If True, enables collisions. If False, disables collisions.
|
|
348
|
+
"""
|
|
349
|
+
body1_collision_ids = get_collision_geometry_ids(model, collision_model, body1)
|
|
350
|
+
body2_collision_ids = get_collision_geometry_ids(model, collision_model, body2)
|
|
351
|
+
|
|
352
|
+
for id1 in body1_collision_ids:
|
|
353
|
+
for id2 in body2_collision_ids:
|
|
354
|
+
pair = pinocchio.CollisionPair(id1, id2)
|
|
355
|
+
if enable:
|
|
356
|
+
collision_model.addCollisionPair(pair)
|
|
357
|
+
else:
|
|
358
|
+
collision_model.removeCollisionPair(pair)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
""" Utilities for differential IK. """
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pinocchio
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from ..core.utils import (
|
|
8
|
+
check_collisions_at_state,
|
|
9
|
+
check_within_limits,
|
|
10
|
+
get_random_state,
|
|
11
|
+
)
|
|
12
|
+
from ..visualization.meshcat_utils import visualize_frame
|
|
13
|
+
|
|
14
|
+
VIZ_INITIAL_RENDER_TIME = 0.5
|
|
15
|
+
VIZ_SLEEP_TIME = 0.05
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DifferentialIkOptions:
|
|
19
|
+
"""Options for differential IK."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
max_iters=200,
|
|
24
|
+
max_retries=10,
|
|
25
|
+
max_translation_error=1e-3,
|
|
26
|
+
max_rotation_error=1e-3,
|
|
27
|
+
damping=1e-3,
|
|
28
|
+
min_step_size=0.1,
|
|
29
|
+
max_step_size=0.5,
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
Initializes a set of differential IK options.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
max_iters : int
|
|
37
|
+
Maximum number of iterations per try.
|
|
38
|
+
max_retries : int
|
|
39
|
+
Maximum number of retries with random restarts.
|
|
40
|
+
If set to 0, only the initial state provided will be used.
|
|
41
|
+
max_translation_error : float
|
|
42
|
+
Maximum translation error, in meters, to consider IK solved.
|
|
43
|
+
max_rotation_error : float
|
|
44
|
+
Maximum rotation error, in radians, to consider IK solved.
|
|
45
|
+
damping : float
|
|
46
|
+
Damping value, between 0 and 1, for the Jacobian pseudoinverse.
|
|
47
|
+
Setting this to a nonzero value is using Levenberg-Marquardt.
|
|
48
|
+
min_step_size : float
|
|
49
|
+
Minimum gradient step, between 0 and 1, based on ratio of current distance to target to initial distance to target.
|
|
50
|
+
To use a fixed step size, set both minimum and maximum values to be equal.
|
|
51
|
+
max_step_size : float
|
|
52
|
+
Maximum gradient step, between 0 and 1, based on ratio of current distance to target to initial distance to target.
|
|
53
|
+
To use a fixed step size, set both minimum and maximum values to be equal.
|
|
54
|
+
"""
|
|
55
|
+
self.max_iters = max_iters
|
|
56
|
+
self.max_retries = max_retries
|
|
57
|
+
self.max_translation_error = max_translation_error
|
|
58
|
+
self.max_rotation_error = max_rotation_error
|
|
59
|
+
self.damping = damping
|
|
60
|
+
self.min_step_size = min_step_size
|
|
61
|
+
self.max_step_size = max_step_size
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DifferentialIk:
|
|
65
|
+
"""Differential IK solver.
|
|
66
|
+
|
|
67
|
+
This is a numerical IK solver that uses the manipulator's Jacobian to take first-order steps towards a solution.
|
|
68
|
+
It contains several of the common options such as damped least squares (Levenberg-Marquardt), random restarts, and nullspace projection.
|
|
69
|
+
|
|
70
|
+
Some good resources:
|
|
71
|
+
* https://motion.cs.illinois.edu/RoboticSystems/InverseKinematics.html
|
|
72
|
+
* https://homes.cs.washington.edu/~todorov/courses/cseP590/06_JacobianMethods.pdf
|
|
73
|
+
* https://www.cs.cmu.edu/~15464-s13/lectures/lecture6/iksurvey.pdf
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
model,
|
|
79
|
+
collision_model=None,
|
|
80
|
+
data=None,
|
|
81
|
+
visualizer=None,
|
|
82
|
+
options=DifferentialIkOptions(),
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Creates an instance of a DifferentialIk solver.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
model : `pinocchio.Model`
|
|
90
|
+
The model to use for this solver.
|
|
91
|
+
collision_model : `pinocchio.Model`, optional
|
|
92
|
+
The model to use for collision checking. If None, no collision checking takes place.
|
|
93
|
+
data : `pinocchio.Data`, optional
|
|
94
|
+
The model data to use for this solver. If None, data is created automatically.
|
|
95
|
+
visualizer : `pinocchio.visualize.meshcat_visualizer.MeshcatVisualizer`, optional
|
|
96
|
+
The visualizer to use for this solver.
|
|
97
|
+
options : `DifferentialIkOptions`, optional
|
|
98
|
+
The options to use for solving IK. If not specified, default options are used.
|
|
99
|
+
"""
|
|
100
|
+
self.model = model
|
|
101
|
+
self.collision_model = collision_model
|
|
102
|
+
|
|
103
|
+
if not data:
|
|
104
|
+
data = model.createData()
|
|
105
|
+
self.data = data
|
|
106
|
+
|
|
107
|
+
self.visualizer = visualizer
|
|
108
|
+
self.options = options
|
|
109
|
+
|
|
110
|
+
def solve(
|
|
111
|
+
self,
|
|
112
|
+
target_frame,
|
|
113
|
+
target_tform,
|
|
114
|
+
init_state=None,
|
|
115
|
+
nullspace_components=[],
|
|
116
|
+
verbose=False,
|
|
117
|
+
):
|
|
118
|
+
"""
|
|
119
|
+
Solves an IK query.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
target_frame : str
|
|
124
|
+
The name of the target frame in the model.
|
|
125
|
+
target_tform : `pinocchio.SE3`
|
|
126
|
+
The desired transformation of the target frame in the model.
|
|
127
|
+
init_state : array-like, optional
|
|
128
|
+
The initial state to solve from. If not specified, a random initial state will be selected.
|
|
129
|
+
nullspace_components : list[function], optional
|
|
130
|
+
An optional list of nullspace components to use when solving.
|
|
131
|
+
These components must take the form `lambda model, q: component(model, q, <other_args>)`.
|
|
132
|
+
verbose : bool, optional
|
|
133
|
+
If True, prints additional information to the console.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
array-like or None
|
|
138
|
+
A list of joint configuration values with the solution, if one was found. Otherwise, returns None.
|
|
139
|
+
"""
|
|
140
|
+
target_frame_id = self.model.getFrameId(target_frame)
|
|
141
|
+
|
|
142
|
+
# Create a random initial state, if not specified
|
|
143
|
+
if init_state is None:
|
|
144
|
+
init_state = get_random_state(self.model)
|
|
145
|
+
if self.visualizer:
|
|
146
|
+
self.visualizer.displayFrames(True, frame_ids=[target_frame_id])
|
|
147
|
+
visualize_frame(self.visualizer, "ik_target_pose", target_tform)
|
|
148
|
+
|
|
149
|
+
# Initialize IK
|
|
150
|
+
solved = False
|
|
151
|
+
n_tries = 0
|
|
152
|
+
q_cur = init_state
|
|
153
|
+
initial_error_norm = None
|
|
154
|
+
|
|
155
|
+
if self.visualizer:
|
|
156
|
+
self.visualizer.display(q_cur)
|
|
157
|
+
time.sleep(VIZ_INITIAL_RENDER_TIME) # Needed to render
|
|
158
|
+
|
|
159
|
+
while n_tries <= self.options.max_retries:
|
|
160
|
+
n_iters = 0
|
|
161
|
+
while n_iters < self.options.max_iters:
|
|
162
|
+
# Compute forward kinematics at the current state
|
|
163
|
+
pinocchio.framesForwardKinematics(self.model, self.data, q_cur)
|
|
164
|
+
cur_tform = self.data.oMf[target_frame_id]
|
|
165
|
+
|
|
166
|
+
# Check the error using actInv
|
|
167
|
+
error = target_tform.actInv(cur_tform)
|
|
168
|
+
error = -pinocchio.log(error).vector
|
|
169
|
+
if (
|
|
170
|
+
np.linalg.norm(error[:3]) < self.options.max_translation_error
|
|
171
|
+
and np.linalg.norm(error[3:]) < self.options.max_rotation_error
|
|
172
|
+
):
|
|
173
|
+
# Wrap to the range -/+ pi, and then check joint limits and collision.
|
|
174
|
+
q_cur = (q_cur + np.pi) % (2 * np.pi) - np.pi
|
|
175
|
+
if check_within_limits(self.model, q_cur):
|
|
176
|
+
if self.collision_model is not None:
|
|
177
|
+
if check_collisions_at_state(
|
|
178
|
+
self.model, self.collision_model, q_cur
|
|
179
|
+
):
|
|
180
|
+
if verbose:
|
|
181
|
+
print(
|
|
182
|
+
"Solved and within joint limits, but in collision."
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
solved = True
|
|
186
|
+
if verbose:
|
|
187
|
+
print(
|
|
188
|
+
"Solved, within joint limits, and collision-free!"
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
solved = True
|
|
192
|
+
if verbose:
|
|
193
|
+
print("Solved and within joint limits!")
|
|
194
|
+
else:
|
|
195
|
+
if verbose:
|
|
196
|
+
print("Solved, but outside joint limits.")
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
# Calculate the Jacobian
|
|
200
|
+
J = pinocchio.computeFrameJacobian(
|
|
201
|
+
self.model,
|
|
202
|
+
self.data,
|
|
203
|
+
q_cur,
|
|
204
|
+
target_frame_id,
|
|
205
|
+
pinocchio.ReferenceFrame.LOCAL,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Solve for the gradient using damping and nullspace components,
|
|
209
|
+
# as specified
|
|
210
|
+
jjt = J.dot(J.T) + self.options.damping**2 * np.eye(6)
|
|
211
|
+
|
|
212
|
+
# Compute the gradient descent step size
|
|
213
|
+
error_norm = np.linalg.norm(error)
|
|
214
|
+
if not initial_error_norm:
|
|
215
|
+
initial_error_norm = error_norm
|
|
216
|
+
alpha = self.options.min_step_size + (
|
|
217
|
+
1.0 - error_norm / initial_error_norm
|
|
218
|
+
) * (self.options.max_step_size - self.options.min_step_size)
|
|
219
|
+
|
|
220
|
+
# Gradient descent step
|
|
221
|
+
if not nullspace_components:
|
|
222
|
+
q_cur += alpha * J.T @ np.linalg.solve(jjt, error)
|
|
223
|
+
else:
|
|
224
|
+
nullspace_term = sum(
|
|
225
|
+
[comp(self.model, q_cur) for comp in nullspace_components]
|
|
226
|
+
)
|
|
227
|
+
q_cur += alpha * (
|
|
228
|
+
J.T @ (np.linalg.solve(jjt, error - J @ (nullspace_term)))
|
|
229
|
+
+ nullspace_term
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
n_iters += 1
|
|
233
|
+
|
|
234
|
+
if self.visualizer:
|
|
235
|
+
self.visualizer.display(q_cur)
|
|
236
|
+
time.sleep(VIZ_SLEEP_TIME)
|
|
237
|
+
|
|
238
|
+
# Check results at the end of this try
|
|
239
|
+
if solved:
|
|
240
|
+
if verbose:
|
|
241
|
+
print(f"Solved in {n_tries+1} tries.")
|
|
242
|
+
break
|
|
243
|
+
else:
|
|
244
|
+
q_cur = get_random_state(self.model)
|
|
245
|
+
n_tries += 1
|
|
246
|
+
if verbose:
|
|
247
|
+
print(f"Retry {n_tries}")
|
|
248
|
+
|
|
249
|
+
# Check final results
|
|
250
|
+
if solved:
|
|
251
|
+
return q_cur
|
|
252
|
+
else:
|
|
253
|
+
return None
|