pybullet-robot 0.0.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.
@@ -0,0 +1 @@
1
+ from .bullet_robot import BulletRobot
@@ -0,0 +1,1084 @@
1
+ from typing import List, Dict, Any, Tuple, TypeAlias
2
+ from threading import Lock
3
+ import copy
4
+ from numbers import Number
5
+ from pprint import pprint
6
+ import pybullet as pb
7
+ import numpy as np
8
+
9
+ QuatType: TypeAlias = np.ndarray
10
+ """Numpy array representating quaternion in format [x,y,z,w]"""
11
+ Vector3D: TypeAlias = np.ndarray
12
+ """Numpy array representating 3D cartesian vector in format [x,y,z]"""
13
+
14
+
15
+ def wrap_angle(angle: float | np.ndarray) -> float | np.ndarray:
16
+ """Wrap the provided angle(s) (radians) to be within -pi and pi.
17
+
18
+ Args:
19
+ angle (float | np.ndarray): Input angle (or array of angles) in radians.
20
+
21
+ Returns:
22
+ float | np.ndarray: Output after wrapping.
23
+ """
24
+ return (angle + np.pi) % (2 * np.pi) - np.pi
25
+
26
+
27
+ def get_pybullet_object_joint_info(
28
+ objectId: int, cid: int = 0, verbose: bool = False
29
+ ) -> Dict[str, Any]:
30
+ """Get dictionarised version of joint information from pybullet's getJointInfo for all joints.
31
+
32
+ Args:
33
+ objectId (int): Pybullet object ID
34
+ cid (int, optional): Pybullet physics client ID. Defaults to 0.
35
+ verbose (bool, optional): Verbosity flag. Defaults to False.
36
+
37
+ Returns:
38
+ Dict[str, Any]: Key-value pairs of joint info.
39
+ Keys:
40
+ "jointIndex",
41
+ "jointName",
42
+ "jointType",
43
+ "qIndex",
44
+ "uIndex",
45
+ "flags",
46
+ "jointDamping",
47
+ "jointFriction",
48
+ "jointLowerLimit",
49
+ "jointUpperLimit",
50
+ "jointMaxForce",
51
+ "jointMaxVelocity",
52
+ "linkName",
53
+ "jointAxis",
54
+ "parentFramePos",
55
+ "parentFrameOrn",
56
+ "parentIndex",
57
+ "numActuatedJoint",
58
+ "actuatedJointName",
59
+ "actuatedJointIndex",
60
+ "actuatedJointLowerLimit",
61
+ "actuatedJointUpperLimit"
62
+
63
+ """
64
+ PYBULLET_JOINT_INFO_KEYS = [
65
+ "jointIndex",
66
+ "jointName",
67
+ "jointType",
68
+ "qIndex",
69
+ "uIndex",
70
+ "flags",
71
+ "jointDamping",
72
+ "jointFriction",
73
+ "jointLowerLimit",
74
+ "jointUpperLimit",
75
+ "jointMaxForce",
76
+ "jointMaxVelocity",
77
+ "linkName",
78
+ "jointAxis",
79
+ "parentFramePos",
80
+ "parentFrameOrn",
81
+ "parentIndex",
82
+ ]
83
+
84
+ numJoints = pb.getNumJoints(objectId, physicsClientId=cid)
85
+ jointInfoValues = [[] for _ in range(len(PYBULLET_JOINT_INFO_KEYS))]
86
+ for i in range(numJoints):
87
+ jointInfoTuple = pb.getJointInfo(objectId, i, physicsClientId=cid)
88
+ for jointInfoValue, jointInfoItem in zip(jointInfoValues, jointInfoTuple):
89
+ if isinstance(jointInfoItem, bytes):
90
+ jointInfoItem = jointInfoItem.decode()
91
+ jointInfoValue.append(jointInfoItem)
92
+ jointInfo = dict(zip(PYBULLET_JOINT_INFO_KEYS, jointInfoValues))
93
+ jointInfo["numJoint"] = numJoints
94
+ actuated_joint_name = []
95
+ actuated_joint_index = []
96
+ actuated_joint_lower_limits = []
97
+ actuated_joint_upper_limits = []
98
+ continuous_joint_name = []
99
+ continuous_joint_index = []
100
+
101
+ for n, name in enumerate(jointInfo["jointName"]):
102
+ jtype = jointInfo["jointType"][n]
103
+ if jtype == pb.JOINT_FIXED:
104
+ continue
105
+
106
+ j_upper_lim = jointInfo["jointUpperLimit"][n]
107
+ j_lower_lim = jointInfo["jointLowerLimit"][n]
108
+ actuated_joint_index.append(n)
109
+ actuated_joint_name.append(name)
110
+ actuated_joint_lower_limits.append(j_lower_lim)
111
+ actuated_joint_upper_limits.append(j_upper_lim)
112
+
113
+ if jtype == pb.JOINT_REVOLUTE and j_upper_lim == 0 and j_lower_lim == 0:
114
+ continuous_joint_name.append(name)
115
+ continuous_joint_index.append(n)
116
+
117
+ jointInfo["numActuatedJoint"] = len(actuated_joint_name)
118
+ jointInfo["actuatedJointName"] = actuated_joint_name
119
+ jointInfo["actuatedJointIndex"] = actuated_joint_index
120
+ jointInfo["continuousJointName"] = continuous_joint_name
121
+ jointInfo["continuousJointIndex"] = continuous_joint_index
122
+ jointInfo["actuatedJointLowerLimit"] = actuated_joint_lower_limits
123
+ jointInfo["actuatedJointUpperLimit"] = actuated_joint_upper_limits
124
+
125
+ if verbose:
126
+ print("jointInfo:")
127
+ pprint(jointInfo)
128
+ return jointInfo
129
+
130
+
131
+ def get_pybullet_object_link_info(
132
+ objectId: int, cid: int = 0, verbose: bool = False
133
+ ) -> Dict[str, Any]:
134
+ """Get dictionarised version of link information from links of a pybullet object.
135
+
136
+ Args:
137
+ objectId (int): Pybullet object ID
138
+ cid (int, optional): Pybullet physics client ID. Defaults to 0.
139
+ verbose (bool, optional): Verbosity flag. Defaults to False.
140
+
141
+ Returns:
142
+ Dict[str, Any]: Key-value pairs of joint info.
143
+ Keys:
144
+ "mass",
145
+ "lateralFriction",
146
+ "localInertiaDiagonal",
147
+ "localInertialPos",
148
+ "localInertialOrn",
149
+ "restitution",
150
+ "rollingFriction",
151
+ "spinningFriction",
152
+ "contactDamping",
153
+ "contactStiffness",
154
+ "bodyType",
155
+ "collisionMargin",
156
+ "index",
157
+ "name"
158
+
159
+ """
160
+ PYBULLET_LINK_INFO_KEYS = [
161
+ "mass",
162
+ "lateralFriction",
163
+ "localInertiaDiagonal",
164
+ "localInertialPos",
165
+ "localInertialOrn",
166
+ "restitution",
167
+ "rollingFriction",
168
+ "spinningFriction",
169
+ "contactDamping",
170
+ "contactStiffness",
171
+ "bodyType",
172
+ "collisionMargin",
173
+ ]
174
+
175
+ numJoints = pb.getNumJoints(objectId, physicsClientId=cid)
176
+ baseName = pb.getBodyInfo(objectId, physicsClientId=cid)[0].decode()
177
+
178
+ linkIndexes = [-1] + list(range(numJoints))
179
+ linkNames = [baseName]
180
+ for i in range(numJoints):
181
+ jointInfoTuple = pb.getJointInfo(objectId, i, physicsClientId=cid)
182
+ linkNames.append(jointInfoTuple[12].decode())
183
+
184
+ linkInfoValues = [[] for _ in range(len(PYBULLET_LINK_INFO_KEYS))]
185
+ for linkIndex in linkIndexes:
186
+ linkInfoTuple = pb.getDynamicsInfo(objectId, linkIndex, physicsClientId=cid)
187
+ for linkInfoValue, linkInfoIterm in zip(linkInfoValues, linkInfoTuple):
188
+ linkInfoValue.append(linkInfoIterm)
189
+ linkInfo = dict(zip(PYBULLET_LINK_INFO_KEYS, linkInfoValues))
190
+
191
+ linkInfo["index"] = linkIndexes
192
+ linkInfo["name"] = linkNames
193
+
194
+ if verbose:
195
+ print("linkInfo:")
196
+ pprint(linkInfo)
197
+
198
+ return linkInfo
199
+
200
+
201
+ def get_pybullet_object_info(
202
+ objectId: int, cid: int = 0, verbose: bool = True
203
+ ) -> Dict[str, Any]:
204
+ """Get dictionarised version of information about a pybullet object.
205
+
206
+ Args:
207
+ objectId (int): Pybullet object ID
208
+ cid (int, optional): Pybullet physics client ID. Defaults to 0.
209
+ verbose (bool, optional): Verbosity flag. Defaults to False.
210
+
211
+ Returns:
212
+ Dict[str, Any]: Key-value pairs of joint info.
213
+ Keys:
214
+ "objectId",
215
+ "objectName",
216
+ "objectMass",
217
+ "baseName",
218
+ "jointInfo",
219
+ "linkInfo",
220
+
221
+ """
222
+ objectName = pb.getBodyInfo(objectId, physicsClientId=cid)[1].decode()
223
+ baseName = pb.getBodyInfo(objectId, physicsClientId=cid)[0].decode()
224
+ jointInfo = get_pybullet_object_joint_info(objectId, cid=cid)
225
+ linkInfo = get_pybullet_object_link_info(objectId, cid=cid)
226
+ objectMass = np.sum(linkInfo["mass"])
227
+ objectInfo = {
228
+ "objectId": objectId,
229
+ "objectName": objectName,
230
+ "objectMass": objectMass,
231
+ "baseName": baseName,
232
+ "jointInfo": jointInfo,
233
+ "linkInfo": linkInfo,
234
+ }
235
+
236
+ if verbose:
237
+ print("\n" + "*" * 100 + "\nPybullet Info " + "\u2193 " * 20 + "\n" + "*" * 100)
238
+ print("objectInfo:")
239
+ pprint(objectInfo)
240
+ print("*" * 100 + "\nPybullet Info " + "\u2191 " * 20 + "\n" + "*" * 100)
241
+ return objectInfo
242
+
243
+
244
+ def fix_pb_link_mass_inertia(
245
+ body_id: int,
246
+ cid: int = 0,
247
+ mass: float = 1e-9,
248
+ local_inertia_diagonal: float = (1e-9, 1e-9, 1e-9),
249
+ ):
250
+ """Fix pybullet virtual links.
251
+
252
+ When loading urdf, pybullet assigned default value for link with no inertial (default value mass=1,
253
+ localinertiadiagonal = 1,1,1; identity local inertial frame). This function fixes the link inertial
254
+ values to the given mass and local_inertia_diagonal; small values by default.
255
+
256
+ Args:
257
+ body_id (int): Pybullet object ID
258
+ cid (int, optional): Pybullet physics client ID. Defaults to 0.
259
+ mass (float, optional): Virtual mass to assign to virtual link. Defaults to 1e-9.
260
+ local_inertia_diagonal (float, optional): Values to assign to inertia matrix diagonals for virtual links.
261
+ Defaults to (1e-9, 1e-9, 1e-9).
262
+ """
263
+ link_ids = [-1] + [-1] + list(range(pb.getNumJoints(body_id, physicsClientId=cid)))
264
+ for link_index in link_ids:
265
+ link_info = pb.getDynamicsInfo(body_id, link_index, physicsClientId=cid)
266
+ if link_info[0] == 1.0 and link_info[2] == (1.0, 1.0, 1.0):
267
+ pb.changeDynamics(
268
+ bodyUniqueId=body_id,
269
+ linkIndex=link_index,
270
+ mass=mass,
271
+ localInertiaDiagonal=local_inertia_diagonal,
272
+ physicsClientId=cid,
273
+ )
274
+
275
+
276
+ class BulletRobot:
277
+ """Robot interface utility for a generic robot in pybullet."""
278
+
279
+ def __init__(
280
+ self,
281
+ urdf_path: str,
282
+ cid: int = None,
283
+ run_async: bool = True,
284
+ ee_names: List[str] = None,
285
+ default_joint_positions: List[float] = None,
286
+ place_on_ground: bool = False,
287
+ default_base_position: Vector3D = np.zeros(3),
288
+ default_base_orientation: QuatType = np.array([0, 0, 0, 1]),
289
+ enable_torque_mode: bool = False,
290
+ verbose: bool = True,
291
+ fake_feedback: bool = False,
292
+ load_ground_plane: bool = True,
293
+ use_fixed_base: bool = True,
294
+ ghost_mode: bool = False,
295
+ ):
296
+ """Robot interface utility for a generic robot in pybullet.
297
+
298
+ Args:
299
+ urdf_path (str): Path to urdf file of robot.
300
+ cid (int, optional): Physics client id of pybullet physics engine (if already running).
301
+ If None provided, will create a new physics GUI instance. Defaults to None.
302
+ run_async (bool, optional): Whether to run physics in a separate thread. If set to False,
303
+ step() method has to be called in the main thread. Defaults to True.
304
+ ee_names (List[str], optional): List of end-effectors for the robot. Defaults to None.
305
+ default_joint_positions (List[float], optional): Optional starting values for joints.
306
+ Defaults to None. NOTE: These values should be in the order of joints in pybullet.
307
+ place_on_ground (bool): If true, the base position height will automatically be adjusted
308
+ such that the robot is on the ground with the given joint positions. Defaults to False.
309
+ NOTE: This is a naive implementation. It does not actually check if the collision is
310
+ with the ground object only. It can be with any object in the world.
311
+ default_base_position (Vector3D, optional): Default position of the base of the robot
312
+ in the world frame during start-up. Note that the height value (z) is only used if
313
+ 'place_on_ground' is set to False. Defaults to np.zeros(3).
314
+ default_base_orientation (QuatType, optional): Default orientation quaternion in the
315
+ world frame for the base of the robot during start-up. Defaults to np.array([0, 0, 0, 1]).
316
+ enable_torque_mode (bool, optional): Flag to enable effort controlling of the robot (control commands
317
+ have to be sent continuously to keep the robot from falling). Defaults to True.
318
+ verbose (bool, optional): Verbosity flag for debugging robot info during construction. Defaults to True.
319
+ fake_feedback (bool, optional): If set to True, `get_robot_states` will return last set joint commands
320
+ and base pose, velocities as the current state. Only for debugging controllers. Defaults to False.
321
+ load_ground_plane (bool, optional): If set to True, will load a ground plane in the XY plane at origin.
322
+ Defaults to True.
323
+ """
324
+ self.cid = cid
325
+ if self.cid is None:
326
+ self.cid = pb.connect(pb.GUI_SERVER)
327
+
328
+ if load_ground_plane:
329
+ import pybullet_data
330
+
331
+ pb.setAdditionalSearchPath(pybullet_data.getDataPath())
332
+ pb.loadURDF("plane.urdf", physicsClientId=self.cid)
333
+
334
+ self.sync_mode = not run_async
335
+
336
+ self.default_start_pose = [default_base_position, default_base_orientation]
337
+
338
+ self.ee_names = ee_names
339
+ if self.ee_names is None:
340
+ self.ee_names = []
341
+
342
+ self.robot_id = pb.loadURDF(
343
+ urdf_path,
344
+ basePosition=self.default_start_pose[0],
345
+ baseOrientation=self.default_start_pose[1],
346
+ useFixedBase=use_fixed_base,
347
+ physicsClientId=self.cid,
348
+ )
349
+ pb.setGravity(0, 0, -9.81, physicsClientId=self.cid)
350
+ self._state_mutex = Lock()
351
+
352
+ fix_pb_link_mass_inertia(body_id=self.robot_id, cid=self.cid)
353
+
354
+ self._retrieve_robot_info()
355
+
356
+ if verbose:
357
+ self._print_robot_info()
358
+
359
+ self.default_joint_positions = (
360
+ default_joint_positions
361
+ if default_joint_positions is not None
362
+ else np.zeros_like(self.actuated_joint_ids)
363
+ )
364
+ self._fake_feedback = fake_feedback
365
+ self._fake_feedback_values = {
366
+ "base_position": copy.deepcopy(self.default_start_pose[0]),
367
+ "base_orientation": copy.deepcopy(self.default_start_pose[1]),
368
+ "base_linear_velocity": np.zeros(3),
369
+ "base_angular_velocity": np.zeros(3),
370
+ "joint_positions": dict(
371
+ zip(
372
+ self.actuated_joint_ids,
373
+ copy.deepcopy(self.default_joint_positions),
374
+ )
375
+ ),
376
+ "joint_velocities": dict(
377
+ zip(
378
+ self.actuated_joint_ids,
379
+ np.zeros(self.num_of_actuated_joints),
380
+ )
381
+ ),
382
+ }
383
+ self.reset_joints(
384
+ joint_ids=self.actuated_joint_ids,
385
+ joint_positions=self.default_joint_positions,
386
+ )
387
+
388
+ self.urdf_path = urdf_path
389
+ self._in_torque_mode = False
390
+
391
+ if place_on_ground:
392
+ self._place_robot_on_ground()
393
+
394
+ if enable_torque_mode:
395
+ self.set_torque_control_mode()
396
+
397
+ self._prev_torque_cmds = dict(
398
+ zip(self.actuated_joint_ids, np.zeros(self.num_of_actuated_joints))
399
+ )
400
+
401
+ pb.setRealTimeSimulation(0 if self.sync_mode else 1, physicsClientId=self.cid)
402
+
403
+ self._ghost_mode = ghost_mode
404
+ rgba = (0, 0, 0, 0.3)
405
+ if self._ghost_mode:
406
+ for link_id in self.link_ids:
407
+ if rgba is not None:
408
+ pb.changeVisualShape(
409
+ self.robot_id,
410
+ link_id,
411
+ rgbaColor=rgba,
412
+ physicsClientId=self.cid,
413
+ )
414
+ pb.setCollisionFilterGroupMask(
415
+ self.robot_id, link_id, 0, 0, physicsClientId=self.cid
416
+ )
417
+ pb.changeDynamics(
418
+ self.robot_id,
419
+ link_id,
420
+ mass=0, # non-dynamic objects have mass 0
421
+ lateralFriction=0,
422
+ spinningFriction=0,
423
+ rollingFriction=0,
424
+ restitution=0,
425
+ linearDamping=0,
426
+ angularDamping=0,
427
+ contactStiffness=0,
428
+ contactDamping=0,
429
+ jointDamping=0,
430
+ maxJointVelocity=0,
431
+ physicsClientId=self.cid,
432
+ )
433
+
434
+ def _retrieve_robot_info(self):
435
+ self.objectInfo = get_pybullet_object_info(
436
+ self.robot_id, cid=self.cid, verbose=False
437
+ )
438
+ self.name = self.objectInfo["objectName"]
439
+ self.mass = self.objectInfo["objectMass"]
440
+ self.base_name = self.objectInfo["baseName"]
441
+ self.link_names = self.objectInfo["linkInfo"]["name"]
442
+ self.link_ids = self.objectInfo["linkInfo"]["index"]
443
+ self.link_masses = self.objectInfo["linkInfo"]["mass"]
444
+ self.num_joints = self.objectInfo["jointInfo"]["numJoint"]
445
+ self.joint_names = self.objectInfo["jointInfo"]["jointName"]
446
+ self.joint_ids = self.objectInfo["jointInfo"]["jointIndex"]
447
+ self.joint_dampings = self.objectInfo["jointInfo"]["jointDamping"]
448
+ self.joint_frictions = self.objectInfo["jointInfo"]["jointFriction"]
449
+ self.joint_lower_limits = self.objectInfo["jointInfo"]["jointLowerLimit"]
450
+ self.joint_upper_limits = self.objectInfo["jointInfo"]["jointUpperLimit"]
451
+ self.joint_neutral_positions = (
452
+ np.array(self.joint_lower_limits) + np.array(self.joint_upper_limits)
453
+ ) / 2
454
+ self.num_of_actuated_joints = self.objectInfo["jointInfo"]["numActuatedJoint"]
455
+ self.actuated_joint_names = self.objectInfo["jointInfo"]["actuatedJointName"]
456
+ self.actuated_joint_ids = self.objectInfo["jointInfo"]["actuatedJointIndex"]
457
+ self.actuated_joint_lower_limits = self.objectInfo["jointInfo"][
458
+ "actuatedJointLowerLimit"
459
+ ]
460
+ self.actuated_joint_upper_limits = self.objectInfo["jointInfo"][
461
+ "actuatedJointUpperLimit"
462
+ ]
463
+ self.continuous_joint_ids = self.objectInfo["jointInfo"]["continuousJointIndex"]
464
+ self.continuous_joint_names = self.objectInfo["jointInfo"][
465
+ "continuousJointName"
466
+ ]
467
+
468
+ self.has_continuous_joints = len(self.continuous_joint_ids) > 0
469
+
470
+ self.actuated_joint_neutral_positions = (
471
+ np.array(self.actuated_joint_lower_limits)
472
+ + np.array(self.actuated_joint_upper_limits)
473
+ ) / 2
474
+
475
+ self.link_name_to_index = dict(zip(self.link_names, self.link_ids))
476
+ self.joint_name_to_index = dict(zip(self.joint_names, self.joint_ids))
477
+ self.ee_ids = [self.link_name_to_index[name] for name in self.ee_names]
478
+
479
+ def refresh_sim_data(self):
480
+ self._retrieve_robot_info()
481
+
482
+ def _print_robot_info(self):
483
+ print("\n")
484
+ print("*" * 100 + "\nSimRobot Info " + "\u2193 " * 20 + "\n" + "*" * 100)
485
+ print("robot ID: ", self.robot_id)
486
+ print("robot name: ", self.name)
487
+ print("robot mass: ", self.mass)
488
+ print("base link name: ", self.base_name)
489
+ print("link names: ", len(self.link_names), self.link_names)
490
+ print("link indexes: ", len(self.link_ids), self.link_ids)
491
+ print("link masses: ", len(self.link_masses), self.link_masses)
492
+ print("end-effectors ", self.ee_names)
493
+
494
+ print("num of joints: ", self.num_joints)
495
+ print("num of actuated joints:", self.num_of_actuated_joints)
496
+
497
+ print("joint names: ", len(self.joint_names), self.joint_names)
498
+ print("joint indexes: ", len(self.joint_ids), self.joint_ids)
499
+ print("joint dampings: ", len(self.joint_dampings), self.joint_dampings)
500
+ print(
501
+ "joint frictions: ", len(self.joint_frictions), self.joint_frictions
502
+ )
503
+ print(
504
+ "joint lower limits: ",
505
+ len(self.joint_lower_limits),
506
+ self.joint_lower_limits,
507
+ )
508
+ print(
509
+ "joint higher limits: ",
510
+ len(self.joint_upper_limits),
511
+ self.joint_upper_limits,
512
+ )
513
+
514
+ print(
515
+ "actuated joint names: ",
516
+ len(self.actuated_joint_names),
517
+ self.actuated_joint_names,
518
+ )
519
+ print(
520
+ "actuated joint indexes:",
521
+ len(self.actuated_joint_ids),
522
+ self.actuated_joint_ids,
523
+ )
524
+ print(
525
+ "continuous joint names: ",
526
+ len(self.continuous_joint_names),
527
+ self.continuous_joint_names,
528
+ )
529
+ print(
530
+ "continuous joint indexes:",
531
+ len(self.continuous_joint_ids),
532
+ self.continuous_joint_ids,
533
+ )
534
+
535
+ print("*" * 100 + "\nSimRobot Info " + "\u2191 " * 20 + "\n" + "*" * 100)
536
+ print("\n")
537
+
538
+ def step(self):
539
+ """Step simulation (if this object was created with `async=False` argument)."""
540
+ if self.sync_mode:
541
+ pb.stepSimulation(physicsClientId=self.cid)
542
+
543
+ def shutdown(self):
544
+ try:
545
+ pb.disconnect(physicsClientId=self.cid)
546
+ except (TypeError, pb.error):
547
+ # raised when pb already dead
548
+ pass
549
+
550
+ def toggle_ft_sensor_for_joints(self, joint_ids: List[int], enable: bool = True):
551
+ if isinstance(joint_ids, int):
552
+ joint_ids = [joint_ids]
553
+ for jid in joint_ids:
554
+ pb.enableJointForceTorqueSensor(
555
+ self.robot_id, jid, enable, physicsClientId=self.cid
556
+ )
557
+
558
+ def set_robot_transparency(self, alpha: float):
559
+ vis_data = pb.getVisualShapeData(self.robot_id, physicsClientId=self.cid)
560
+ for data in vis_data:
561
+ pb.changeVisualShape(
562
+ self.robot_id,
563
+ data[1],
564
+ rgbaColor=(data[7][0], data[7][1], data[7][2], alpha),
565
+ physicsClientId=self.cid,
566
+ )
567
+
568
+ def _place_robot_on_ground(
569
+ self,
570
+ move_resolution: float = 0.01,
571
+ default_position: Vector3D = None,
572
+ default_orientation: QuatType = None,
573
+ ):
574
+ pb.setRealTimeSimulation(0)
575
+ in_collision = True
576
+ if default_position is None:
577
+ default_position = [
578
+ self.default_start_pose[0][0],
579
+ self.default_start_pose[0][1],
580
+ -move_resolution,
581
+ ]
582
+ if default_orientation is None:
583
+ default_orientation = self.default_start_pose[1]
584
+
585
+ joint_pos = self.get_joint_states(joint_ids=self.actuated_joint_ids)[0].copy()
586
+ # print(f"{self.__class__.__name__}: Trying to place robot on the ground.")
587
+ while in_collision:
588
+ default_position[2] += move_resolution
589
+ self.reset_base_pose(
590
+ position=default_position, orientation=default_orientation
591
+ )
592
+ self.reset_joints(
593
+ joint_ids=self.actuated_joint_ids, joint_positions=joint_pos
594
+ )
595
+ pb.stepSimulation(physicsClientId=self.cid)
596
+ in_collision = np.any(self.get_contact_states_of_links(self.link_ids))
597
+ # print(f"{self.__class__.__name__}: Robot should be just above the ground now.")
598
+ self.default_start_pose[0] = np.array(default_position)
599
+
600
+ def set_position_control_mode(self):
601
+ """Set control mode to position-based (robot will stay in place if no commands sent)."""
602
+ pb.setJointMotorControlArray(
603
+ self.robot_id,
604
+ self.joint_ids,
605
+ pb.POSITION_CONTROL,
606
+ targetPositions=self.joint_neutral_positions,
607
+ physicsClientId=self.cid,
608
+ )
609
+ self._in_torque_mode = False
610
+ print("SimRobot position control mode enabled!")
611
+
612
+ def get_physics_parameters(self) -> Dict[str, Any]:
613
+ return pb.getPhysicsEngineParameters(physicsClientId=self.cid)
614
+
615
+ def set_torque_control_mode(self):
616
+ """Set control mode to effort-based (robot will NOT stay in place if no commands sent)."""
617
+ pb.setJointMotorControlArray(
618
+ self.robot_id,
619
+ self.actuated_joint_ids,
620
+ pb.VELOCITY_CONTROL,
621
+ forces=[0.0] * self.num_of_actuated_joints,
622
+ physicsClientId=self.cid,
623
+ )
624
+ self._in_torque_mode = True
625
+ print("SimRobot effort control mode enabled!")
626
+
627
+ def reset_base_pose(
628
+ self,
629
+ position: Vector3D = None,
630
+ orientation: QuatType = None,
631
+ base_linear_velocity: Vector3D = None,
632
+ base_angular_velocity: Vector3D = None,
633
+ ):
634
+ """Reset the robot's base pose to default pose.
635
+
636
+ Args:
637
+ position (Vector3D, optional): If specified, sets robot to this position. Defaults to None.
638
+ orientation (QuatType, optional): If specified, sets robot to this orientation.
639
+ Quaternion order: x,y,z,w. Defaults to None.
640
+ """
641
+ if position is None:
642
+ position = self.default_start_pose[0]
643
+ if orientation is None:
644
+ orientation = self.default_start_pose[1]
645
+ if base_linear_velocity is None:
646
+ base_linear_velocity = np.zeros(3)
647
+ if base_angular_velocity is None:
648
+ base_angular_velocity = np.zeros(3)
649
+ if self._fake_feedback:
650
+ self._fake_feedback_values["base_position"] = position
651
+ self._fake_feedback_values["base_orientation"] = orientation
652
+ self._fake_feedback_values["base_linear_velocity"] = base_linear_velocity
653
+ self._fake_feedback_values["base_angular_velocity"] = base_angular_velocity
654
+ pb.resetBasePositionAndOrientation(
655
+ self.robot_id, position, orientation, physicsClientId=self.cid
656
+ )
657
+
658
+ def reset_joints(
659
+ self,
660
+ joint_ids: List[int],
661
+ joint_positions: np.ndarray,
662
+ joint_velocities: np.ndarray = None,
663
+ ):
664
+ if joint_velocities is None:
665
+ joint_velocities = np.zeros(len(joint_ids))
666
+ for n, jid in enumerate(joint_ids):
667
+ if self._fake_feedback:
668
+ self._fake_feedback_values["joint_positions"][jid] = joint_positions[n]
669
+ self._fake_feedback_values["joint_velocities"][jid] = joint_velocities[
670
+ n
671
+ ]
672
+ pb.resetJointState(
673
+ self.robot_id,
674
+ jid,
675
+ joint_positions[n],
676
+ joint_velocities[n],
677
+ physicsClientId=self.cid,
678
+ )
679
+
680
+ def reset_actuated_joint_positions(
681
+ self,
682
+ joint_positions: np.ndarray = None,
683
+ joint_names: List[str] = None,
684
+ joint_velocities: np.ndarray = None,
685
+ ):
686
+ """Reset the joint positions of the robot.
687
+
688
+ Args:
689
+ joint_positions (np.ndarray, optional): If specified, sets joints to these values, otherwise
690
+ uses default. Defaults to None.
691
+ joint_names (List[str], optional): Joint names in the order the position values were given.
692
+ If None provided, uses default order of self.actuated_joint_names.
693
+ """
694
+ if joint_positions is None:
695
+ joint_positions = self.default_joint_positions
696
+ if joint_names is None:
697
+ joint_names = self.actuated_joint_names
698
+ joint_ids = self.get_joint_ids(joint_names=joint_names)
699
+ self.reset_joints(
700
+ joint_ids=joint_ids,
701
+ joint_positions=joint_positions,
702
+ joint_velocities=joint_velocities,
703
+ )
704
+
705
+ def get_object_id(self) -> int:
706
+ return self.robot_id
707
+
708
+ def get_base_link_name(self) -> str:
709
+ return pb.getBodyInfo(self.robot_id, physicsClientId=self.cid)[0].decode()
710
+
711
+ def get_joint_name(self, joint_id: int) -> str:
712
+ return pb.getJointInfo(self.robot_id, joint_id, physicsClientId=self.cid)[
713
+ 1
714
+ ].decode()
715
+
716
+ def get_joint_id(self, joint_name: str) -> int:
717
+ return self.joint_name_to_index[joint_name]
718
+
719
+ def get_joint_ids(self, joint_names: List[str]) -> List[int]:
720
+ return [self.joint_name_to_index[joint_name] for joint_name in joint_names]
721
+
722
+ def get_link_name(self, link_id: int) -> str:
723
+ if link_id == -1:
724
+ return self.get_base_link_name()
725
+ else:
726
+ return pb.getJointInfo(self.robot_id, link_id, physicsClientId=self.cid)[
727
+ 12
728
+ ].decode()
729
+
730
+ def get_link_index(self, link_name: str) -> int:
731
+ return self.link_name_to_index[link_name]
732
+
733
+ def get_joint_type(self, joint_id: int) -> str:
734
+ return pb.getJointInfo(self.robot_id, joint_id, physicsClientId=self.cid)[2]
735
+
736
+ def get_joint_damping(self, joint_id: int) -> float:
737
+ return pb.getJointInfo(self.robot_id, joint_id, physicsClientId=self.cid)[6]
738
+
739
+ def get_joint_friction(self, joint_id: int) -> float:
740
+ return pb.getJointInfo(self.robot_id, joint_id, physicsClientId=self.cid)[7]
741
+
742
+ def get_joint_lower_limit_position(self, joint_id: int) -> float:
743
+ return pb.getJointInfo(self.robot_id, joint_id, physicsClientId=self.cid)[8]
744
+
745
+ def get_joint_upper_limit_position(self, joint_id: int) -> float:
746
+ return pb.getJointInfo(self.robot_id, joint_id, physicsClientId=self.cid)[9]
747
+
748
+ def get_joint_max_force(self, joint_id: int) -> float:
749
+ return pb.getJointInfo(self.robot_id, joint_id, physicsClientId=self.cid)[10]
750
+
751
+ def get_joint_max_velocity(self, joint_id: int) -> float:
752
+ return pb.getJointInfo(self.robot_id, joint_id, physicsClientId=self.cid)[11]
753
+
754
+ def get_link_mass(self, link_id: int) -> float:
755
+ return pb.getDynamicsInfo(self.robot_id, link_id, physicsClientId=self.cid)[0]
756
+
757
+ def get_link_local_inertial_transform(
758
+ self, link_id: int
759
+ ) -> Tuple[np.ndarray, QuatType]:
760
+ return pb.getDynamicsInfo(self.robot_id, link_id, physicsClientId=self.cid)[3:5]
761
+
762
+ def get_link_local_inertial_position(self, link_id: int) -> np.ndarray:
763
+ return np.array(
764
+ pb.getDynamicsInfo(self.robot_id, link_id, physicsClientId=self.cid)[3]
765
+ )
766
+
767
+ def get_link_local_inertial_quaternion(self, link_id: int) -> QuatType:
768
+ return np.array(
769
+ pb.getDynamicsInfo(self.robot_id, link_id, physicsClientId=self.cid)[4]
770
+ )
771
+
772
+ # Get Base states
773
+ def get_base_local_inertia_transform(self) -> Tuple[Vector3D, QuatType]:
774
+ return self.get_link_local_inertial_transform(-1)
775
+
776
+ def get_base_local_inertia_position(self) -> Vector3D:
777
+ return self.get_link_local_inertial_position(-1)
778
+
779
+ def get_base_local_inertia_quaternion(self) -> QuatType:
780
+ return self.get_link_local_inertial_quaternion(-1)
781
+
782
+ def get_base_com_position(self) -> Vector3D:
783
+ worldTransCom = self.get_base_pose()
784
+
785
+ localTransCom = self.get_base_local_inertia_transform()
786
+ comTransLocal = pb.invertTransform(
787
+ position=localTransCom[0],
788
+ orientation=localTransCom[1],
789
+ physicsClientId=self.cid,
790
+ )
791
+ worldTransLocal = pb.multiplyTransforms(
792
+ positionA=worldTransCom[0],
793
+ orientationA=worldTransCom[1],
794
+ positionB=comTransLocal[0],
795
+ orientationB=comTransLocal[1],
796
+ physicsClientId=self.cid,
797
+ )
798
+ return np.array(worldTransLocal[0])
799
+
800
+ def get_base_pose(self) -> Tuple[Vector3D, QuatType]:
801
+ p, q = pb.getBasePositionAndOrientation(self.robot_id, physicsClientId=self.cid)
802
+ if np.isnan(q[0]):
803
+ # BUG: occasional nans in quaternion retrieved from pybullet
804
+ # https://github.com/bulletphysics/bullet3/issues/976
805
+ raise RuntimeError("Pybullet pose retrieval returned a nan quaternion")
806
+ return np.array(p), np.array(q)
807
+
808
+ def get_base_velocity(self) -> Tuple[Vector3D, Vector3D]:
809
+ lin, ang = pb.getBaseVelocity(self.robot_id, physicsClientId=self.cid)
810
+ return np.array(lin), np.array(ang)
811
+
812
+ def change_dynamics(self, link_name: str, **kwargs):
813
+ """Exposes the `pb.changeDynamics` pybullet API for this robot. Also
814
+ updates the robot, link and joint info (accessed via `objectInfo` dict,
815
+ and other exposed properties related to joints and links for this robot)
816
+ after making the change.
817
+
818
+ NOTE: Not to be used in high-speed loops as this operation can be heavy.
819
+ """
820
+ kwargs.pop("bodyUniqueId", None)
821
+ kwargs.pop("linkIndex", None)
822
+ kwargs.pop("physicsClientId", None)
823
+ pb.changeDynamics(
824
+ bodyUniqueId=self.robot_id,
825
+ linkIndex=self.get_link_index(link_name=link_name),
826
+ physicsClientId=self.cid,
827
+ **kwargs,
828
+ )
829
+ self.refresh_sim_data()
830
+
831
+ # Get actuated joint states
832
+ def get_actuated_joint_positions(
833
+ self, actuated_joint_names: List[str] = None
834
+ ) -> np.ndarray:
835
+ if actuated_joint_names is None:
836
+ actuated_joint_names = self.actuated_joint_names
837
+ return self.get_joint_states(
838
+ joint_ids=self.get_joint_ids(actuated_joint_names)
839
+ )[0]
840
+
841
+ def get_actuated_joint_velocities(
842
+ self, actuated_joint_names: List[str] = None
843
+ ) -> np.ndarray:
844
+ if actuated_joint_names is None:
845
+ actuated_joint_names = self.actuated_joint_names
846
+ return self.get_joint_states(
847
+ joint_ids=self.get_joint_ids(actuated_joint_names)
848
+ )[1]
849
+
850
+ def get_actuated_joint_torques(
851
+ self, actuated_joint_names: List[str] = None
852
+ ) -> np.ndarray:
853
+ if actuated_joint_names is None:
854
+ actuated_joint_names = self.actuated_joint_names
855
+ return self.get_joint_states(
856
+ joint_ids=self.get_joint_ids(actuated_joint_names)
857
+ )[2]
858
+
859
+ def get_actuated_joint_name_to_position(self) -> Dict[str, float]:
860
+ return dict(
861
+ zip(
862
+ self.actuated_joint_names,
863
+ self.get_joint_states(joint_ids=self.actuated_joint_ids)[0].tolist(),
864
+ )
865
+ )
866
+
867
+ def get_actuated_joint_name_to_velocity(self) -> Dict[str, float]:
868
+ return dict(
869
+ zip(
870
+ self.actuated_joint_names,
871
+ self.get_joint_states(joint_ids=self.actuated_joint_ids)[1].tolist(),
872
+ )
873
+ )
874
+
875
+ def get_actuated_joint_name_to_torque(self) -> Dict[str, float]:
876
+ return dict(
877
+ zip(
878
+ self.actuated_joint_names,
879
+ self.get_joint_states(joint_ids=self.actuated_joint_ids)[2].tolist(),
880
+ )
881
+ )
882
+
883
+ def get_link_pose(self, link_id: int) -> Tuple[Vector3D, QuatType]:
884
+ if link_id == -1:
885
+ return self.get_base_pose()
886
+ p, o = pb.getLinkState(self.robot_id, link_id, physicsClientId=self.cid)[4:6]
887
+ return np.array(p), np.array(o)
888
+
889
+ def get_link_com_pose(self, link_id: int) -> Tuple[Vector3D, QuatType]:
890
+ if link_id == -1:
891
+ # NOTE: probably not valid for base
892
+ return self.get_base_com_position(), self.get_base_pose()[1]
893
+ p, o = pb.getLinkState(self.robot_id, link_id, physicsClientId=self.cid)[:2]
894
+ return np.array(p), np.array(o)
895
+
896
+ def get_ee_contact_states(self, ee_names: List[str] = None) -> np.ndarray:
897
+ if ee_names is None:
898
+ ee_names = self.ee_names
899
+ return self.get_contact_states_of_links(
900
+ link_ids=[self.get_link_index(link_name=name) for name in ee_names]
901
+ )
902
+
903
+ def get_contact_states_of_links(self, link_ids: List[int]) -> np.ndarray:
904
+ return np.array(
905
+ [
906
+ int(
907
+ len(
908
+ pb.getContactPoints(
909
+ bodyA=self.robot_id,
910
+ linkIndexA=idx,
911
+ physicsClientId=self.cid,
912
+ )
913
+ )
914
+ > 0
915
+ )
916
+ for idx in link_ids
917
+ ]
918
+ )
919
+
920
+ def toggle_self_collision(self, enable: bool):
921
+ for n, l_id in enumerate(self.link_ids):
922
+ if n < len(self.link_ids) - 1:
923
+ for l2_id in self.link_ids[n:]:
924
+ pb.setCollisionFilterPair(
925
+ self.robot_id,
926
+ self.robot_id,
927
+ l_id,
928
+ l2_id,
929
+ int(enable),
930
+ physicsClientId=self.cid,
931
+ )
932
+
933
+ # Get robot states
934
+ def get_robot_states(
935
+ self, actuated_joint_names: List[str] = None, ee_names: List[str] = None
936
+ ) -> Dict[str, Any]:
937
+ if actuated_joint_names is None:
938
+ actuated_joint_names = self.actuated_joint_names
939
+ joint_ids = self.actuated_joint_ids
940
+ else:
941
+ joint_ids = self.get_joint_ids(joint_names=actuated_joint_names)
942
+ if ee_names is None:
943
+ ee_ids = self.ee_ids
944
+ ee_names = self.ee_names
945
+ else:
946
+ ee_ids = [self.get_link_index(link_name=name) for name in ee_names]
947
+
948
+ if self._fake_feedback:
949
+ base_pose = [
950
+ self._fake_feedback_values["base_position"],
951
+ self._fake_feedback_values["base_orientation"],
952
+ ]
953
+ base_vel = [
954
+ self._fake_feedback_values["base_linear_velocity"],
955
+ self._fake_feedback_values["base_angular_velocity"],
956
+ ]
957
+ jpos = np.array(
958
+ [self._fake_feedback_values["joint_positions"][i] for i in joint_ids]
959
+ )
960
+ jvel = np.array(
961
+ [self._fake_feedback_values["joint_velocities"][i] for i in joint_ids]
962
+ )
963
+ jtorque = np.zeros_like(jpos)
964
+ else:
965
+ base_pose = self.get_base_pose()
966
+ base_vel = self.get_base_velocity()
967
+ joint_states = self.get_joint_states(joint_ids=joint_ids)
968
+ jpos = joint_states[0]
969
+ jvel = joint_states[1]
970
+ jtorque = joint_states[2]
971
+ return {
972
+ "base_position": base_pose[0],
973
+ "base_com_position": self.get_base_com_position(),
974
+ "base_quaternion": base_pose[1],
975
+ "base_velocity_linear": base_vel[0],
976
+ "base_velocity_angular": base_vel[1],
977
+ "actuated_joint_positions": jpos,
978
+ "actuated_joint_velocities": jvel,
979
+ "actuated_joint_torques": jtorque,
980
+ "joint_order": actuated_joint_names,
981
+ "ee_order": ee_names,
982
+ "ee_contact_states": self.get_contact_states_of_links(ee_ids),
983
+ }
984
+
985
+ def get_joint_states(
986
+ self, joint_ids: List[int] = None
987
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
988
+ """Get joint positions, velocities and efforts.
989
+
990
+ Args:
991
+ joint_ids (List[int], Optional): List of joint ids (optional)
992
+
993
+ Returns:
994
+ Tuple[np.ndarray, np.ndarray, np.ndarray]: joint positions, joint velocities, joint efforts
995
+ """
996
+ if joint_ids is None:
997
+ joint_ids = self.actuated_joint_ids
998
+ dof = len(joint_ids)
999
+ q, v, tau = np.zeros(dof), np.zeros(dof), np.zeros(dof)
1000
+ for n, idx in enumerate(joint_ids):
1001
+ q[n], v[n], _, tau[n] = pb.getJointState(
1002
+ self.robot_id, idx, physicsClientId=self.cid
1003
+ )
1004
+ if idx in self.continuous_joint_ids:
1005
+ q[n] = wrap_angle(q[n])
1006
+ if self._in_torque_mode:
1007
+ tau[n] = self._prev_torque_cmds[idx]
1008
+ return q, v, tau
1009
+
1010
+ def compute_joint_pd_error(
1011
+ self,
1012
+ joint_ids: List[int],
1013
+ q_des: np.ndarray,
1014
+ dq_des: np.ndarray,
1015
+ Kp: float | np.ndarray,
1016
+ Kd: float | np.ndarray,
1017
+ ):
1018
+ curr_q, curr_dq, _ = self.get_joint_states(joint_ids=joint_ids)
1019
+
1020
+ p_term = np.zeros_like(curr_q)
1021
+ if q_des is not None:
1022
+ p_term = Kp * (q_des - curr_q)
1023
+ if self.has_continuous_joints:
1024
+ for n, jid in enumerate(joint_ids):
1025
+ if jid in self.continuous_joint_ids:
1026
+ p_term[n] = Kp[n] * wrap_angle(q_des[n] - curr_q[n])
1027
+
1028
+ d_term = np.zeros_like(curr_dq)
1029
+ if dq_des is not None:
1030
+ d_term = Kd * (dq_des - curr_dq)
1031
+
1032
+ return p_term + d_term
1033
+
1034
+ def set_actuated_joint_commands(
1035
+ self,
1036
+ actuated_joint_names: List[str] = None,
1037
+ q: float | np.ndarray = 0,
1038
+ Kp: float | np.ndarray = 0,
1039
+ dq: float | np.ndarray = 0,
1040
+ Kd: float | np.ndarray = 0,
1041
+ tau: float | np.ndarray = 0,
1042
+ ):
1043
+ # can be further optimised for speed by storing last used actuated joint names, e.g.
1044
+ # if using in a control loop
1045
+
1046
+ if actuated_joint_names is None:
1047
+ actuated_joint_names = self.actuated_joint_names
1048
+ jids = self.get_joint_ids(actuated_joint_names)
1049
+
1050
+ if self._fake_feedback:
1051
+ self.reset_joints(joint_ids=jids, joint_positions=q, joint_velocities=dq)
1052
+ return
1053
+
1054
+ if self._in_torque_mode:
1055
+ tau_cmd = (
1056
+ self.compute_joint_pd_error(
1057
+ joint_ids=jids, q_des=q, dq_des=dq, Kp=Kp, Kd=Kd
1058
+ )
1059
+ + tau
1060
+ )
1061
+ pb.setJointMotorControlArray(
1062
+ self.robot_id,
1063
+ jids,
1064
+ pb.TORQUE_CONTROL,
1065
+ forces=tau_cmd,
1066
+ physicsClientId=self.cid,
1067
+ )
1068
+ for n, jid in enumerate(jids):
1069
+ self._prev_torque_cmds[jid] = tau_cmd[n]
1070
+ else:
1071
+ # tau = [self._max_actuator_force] * len(actuated_joint_names)
1072
+ if isinstance(dq, Number):
1073
+ dq = [dq] * len(actuated_joint_names)
1074
+ pb.setJointMotorControlArray(
1075
+ self.robot_id,
1076
+ jids,
1077
+ pb.POSITION_CONTROL,
1078
+ targetPositions=q,
1079
+ targetVelocities=dq,
1080
+ # forces=tau,
1081
+ # positionGains=Kp,
1082
+ # velocityGains=Kd,
1083
+ physicsClientId=self.cid,
1084
+ )
File without changes
@@ -0,0 +1,45 @@
1
+ from typing import List
2
+ import logging
3
+ import importlib
4
+ from robot_descriptions._descriptions import DESCRIPTIONS as _AR_DESCRIPTIONS
5
+
6
+
7
+ AWESOME_ROBOTS: List[str] = _AR_DESCRIPTIONS.keys()
8
+ """List of all available robots from the Awesome Robots List that can be used."""
9
+
10
+
11
+ def get_robot_description_urdf(
12
+ robot_description_pkg_name: str,
13
+ ) -> str:
14
+ """Get robot urdf for the specified robot description package.
15
+
16
+ The specified package should be from the list of awesome robot descriptions
17
+ (https://github.com/robot-descriptions/robot_descriptions.py/tree/main?tab=readme-ov-file#descriptions).
18
+
19
+ The list of available robot descriptions can also be viewed in the variable `AWESOME_ROBOTS` imported
20
+ from `utils.urdf_utils`.
21
+
22
+ Downloads description package for the specified robot and caches it locally (only needs downloading once).
23
+
24
+ Args:
25
+ robot_description_pkg_name (str): The package name as specified in the Awesome Robot Descriptions list.
26
+
27
+ Returns:
28
+ str: Path to the robot's urdf file.
29
+ """
30
+ try:
31
+ return importlib.import_module(
32
+ f"robot_descriptions.{robot_description_pkg_name}"
33
+ ).URDF_PATH
34
+ except AttributeError as e:
35
+ msg = f"URDF has not been provided for robot {robot_description_pkg_name}."
36
+ logging.error(msg)
37
+ raise AttributeError(msg) from e
38
+ except (ModuleNotFoundError, KeyError) as e:
39
+ msg = (
40
+ f"No description package called {robot_description_pkg_name} in"
41
+ " Awesome Robot Descriptions list. Use on of the description names from "
42
+ "https://github.com/robot-descriptions/robot_descriptions.py/tree/main?tab=readme-ov-file#descriptions."
43
+ )
44
+ logging.error(msg)
45
+ raise ModuleNotFoundError(msg) from e
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.1
2
+ Name: pybullet-robot
3
+ Version: 0.0.1
4
+ Summary:
5
+ Author: JustaGist
6
+ Author-email: mail@saifsidhik.page
7
+ Requires-Python: >=3.10
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: numpy
13
+ Requires-Dist: pybullet (>3.1)
14
+ Requires-Dist: robot-descriptions (>=1.10.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Pybullet Robot
18
+
19
+
20
+ ## TODO:
21
+
22
+ - [ ] IK interface
23
+ - [ ] robot descriptions loader
24
+ - [ ] pybullet data robot loader
25
+ - [ ] doc
26
+ - [ ] tests
27
+
@@ -0,0 +1,7 @@
1
+ pybullet_robot/__init__.py,sha256=sfi7OVZAsaiIeBS4AftTLN7TdnUQOlMSubBgrdnF-CQ,38
2
+ pybullet_robot/bullet_robot.py,sha256=qs7t1yu2Qb4Yu5VO044TZsbeE6DA9Eheg4C4JCa9xik,41840
3
+ pybullet_robot/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pybullet_robot/utils/urdf_utils.py,sha256=kvOuvI4wj4x2XGFgrlmG2x4-F7w1UjMc0hVDIGMUj9E,1789
5
+ pybullet_robot-0.0.1.dist-info/METADATA,sha256=I_euMtwXQEFTOiYRly5m_ts-NOfwX5l6BtorOwEuT1o,620
6
+ pybullet_robot-0.0.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
7
+ pybullet_robot-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any