BulletLab 0.1.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.
- bulletlab/__init__.py +48 -0
- bulletlab/core/__init__.py +10 -0
- bulletlab/core/simulation.py +377 -0
- bulletlab/core/world.py +173 -0
- bulletlab/logging/__init__.py +9 -0
- bulletlab/logging/csv_writer.py +54 -0
- bulletlab/logging/json_writer.py +70 -0
- bulletlab/logging/logger.py +258 -0
- bulletlab/plotting/__init__.py +9 -0
- bulletlab/plotting/live_plot.py +364 -0
- bulletlab/robot/__init__.py +12 -0
- bulletlab/robot/joint.py +402 -0
- bulletlab/robot/link.py +401 -0
- bulletlab/robot/robot.py +533 -0
- bulletlab/telemetry/__init__.py +10 -0
- bulletlab/telemetry/channel.py +124 -0
- bulletlab/telemetry/manager.py +227 -0
- bulletlab/ui/__init__.py +31 -0
- bulletlab/ui/app.py +543 -0
- bulletlab/ui/panels/__init__.py +16 -0
- bulletlab/ui/panels/console.py +189 -0
- bulletlab/ui/panels/explorer.py +144 -0
- bulletlab/ui/panels/plots.py +143 -0
- bulletlab/ui/panels/properties.py +265 -0
- bulletlab/ui/panels/telemetry.py +105 -0
- bulletlab/ui/widgets.py +348 -0
- bulletlab/utils/__init__.py +26 -0
- bulletlab/utils/math_utils.py +183 -0
- bulletlab/utils/timer.py +107 -0
- bulletlab/utils/urdf_utils.py +114 -0
- bulletlab-0.1.2.dist-info/METADATA +284 -0
- bulletlab-0.1.2.dist-info/RECORD +35 -0
- bulletlab-0.1.2.dist-info/WHEEL +5 -0
- bulletlab-0.1.2.dist-info/licenses/LICENSE +21 -0
- bulletlab-0.1.2.dist-info/top_level.txt +1 -0
bulletlab/robot/joint.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Joint – a single degree-of-freedom actuator in a robot model.
|
|
3
|
+
|
|
4
|
+
The Joint class wraps a single PyBullet joint and exposes its properties
|
|
5
|
+
(position, velocity, torque, limits, max_force, max_velocity) as Python
|
|
6
|
+
attributes. Setting any property automatically propagates to PyBullet.
|
|
7
|
+
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
joint = robot.joints["motor"]
|
|
11
|
+
print(joint.position) # read current angle
|
|
12
|
+
joint.velocity = 15.0 # apply velocity control
|
|
13
|
+
joint.torque = 20.0 # apply torque control
|
|
14
|
+
joint.reset(pos=0.0) # reset to zero position
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from enum import IntEnum
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
import pybullet as p
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from bulletlab.core.simulation import Simulation
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JointType(IntEnum):
|
|
29
|
+
"""Enumeration of PyBullet joint types."""
|
|
30
|
+
|
|
31
|
+
REVOLUTE = p.JOINT_REVOLUTE
|
|
32
|
+
PRISMATIC = p.JOINT_PRISMATIC
|
|
33
|
+
SPHERICAL = p.JOINT_SPHERICAL
|
|
34
|
+
PLANAR = p.JOINT_PLANAR
|
|
35
|
+
FIXED = p.JOINT_FIXED
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Joint:
|
|
39
|
+
"""A single joint in a robot model.
|
|
40
|
+
|
|
41
|
+
Joints are typically not instantiated directly — they are created by
|
|
42
|
+
:class:`~bulletlab.robot.robot.Robot` when loading a URDF/MJCF file.
|
|
43
|
+
Access them via ``robot.joints["joint_name"]``.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: Joint name (from URDF).
|
|
47
|
+
index: PyBullet joint index.
|
|
48
|
+
body_id: PyBullet body ID of the parent robot.
|
|
49
|
+
sim: The :class:`~bulletlab.core.simulation.Simulation` instance.
|
|
50
|
+
|
|
51
|
+
Example::
|
|
52
|
+
|
|
53
|
+
joint = robot.joints["wheel_left"]
|
|
54
|
+
joint.velocity = 10.0
|
|
55
|
+
joint.max_force = 50.0
|
|
56
|
+
print(joint.position)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
name: str,
|
|
62
|
+
index: int,
|
|
63
|
+
body_id: int,
|
|
64
|
+
sim: "Simulation",
|
|
65
|
+
) -> None:
|
|
66
|
+
self._name = name
|
|
67
|
+
self._index = index
|
|
68
|
+
self._body_id = body_id
|
|
69
|
+
self._sim = sim
|
|
70
|
+
self._max_force: float = 100.0
|
|
71
|
+
self._max_velocity: float = 10.0
|
|
72
|
+
self._control_mode: int = p.VELOCITY_CONTROL
|
|
73
|
+
self._enabled: bool = True
|
|
74
|
+
|
|
75
|
+
# Read limits and joint type from PyBullet
|
|
76
|
+
info = p.getJointInfo(
|
|
77
|
+
self._body_id,
|
|
78
|
+
self._index,
|
|
79
|
+
physicsClientId=self._sim.client_id,
|
|
80
|
+
)
|
|
81
|
+
self._joint_type = JointType(info[2]) if info[2] in [t.value for t in JointType] else info[2]
|
|
82
|
+
self._lower_limit: float = float(info[8])
|
|
83
|
+
self._upper_limit: float = float(info[9])
|
|
84
|
+
self._max_force = float(info[10]) if info[10] > 0 else 100.0
|
|
85
|
+
self._max_velocity = float(info[11]) if info[11] > 0 else 10.0
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
# Safety guard
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def _check_connected(self) -> bool:
|
|
92
|
+
"""Return True if the physics server is still live.
|
|
93
|
+
|
|
94
|
+
Uses ``p.isConnected()`` to query the actual PyBullet runtime rather
|
|
95
|
+
than BulletLab's internal flag, which only updates on explicit
|
|
96
|
+
``sim.stop()`` calls and misses cases where the 3D window is closed
|
|
97
|
+
by the user independently.
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
return bool(p.isConnected(physicsClientId=self._sim.client_id))
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
# Identity
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def name(self) -> str:
|
|
110
|
+
"""Joint name as defined in the URDF."""
|
|
111
|
+
return self._name
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def index(self) -> int:
|
|
115
|
+
"""PyBullet joint index (internal identifier)."""
|
|
116
|
+
return self._index
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def joint_type(self) -> JointType | int:
|
|
120
|
+
"""Type of this joint (revolute, prismatic, fixed, etc.)."""
|
|
121
|
+
return self._joint_type
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def is_fixed(self) -> bool:
|
|
125
|
+
"""``True`` if this is a fixed (non-actuated) joint."""
|
|
126
|
+
return self._joint_type == JointType.FIXED
|
|
127
|
+
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
# State reads
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def position(self) -> float:
|
|
134
|
+
"""Current joint position (angle in radians or displacement in meters).
|
|
135
|
+
|
|
136
|
+
Read-only. Use :meth:`reset` to set position directly.
|
|
137
|
+
|
|
138
|
+
Example::
|
|
139
|
+
|
|
140
|
+
print(robot.joints["elbow"].position)
|
|
141
|
+
"""
|
|
142
|
+
if not self._check_connected():
|
|
143
|
+
return 0.0
|
|
144
|
+
state = p.getJointState(
|
|
145
|
+
self._body_id,
|
|
146
|
+
self._index,
|
|
147
|
+
physicsClientId=self._sim.client_id,
|
|
148
|
+
)
|
|
149
|
+
return float(state[0])
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def velocity(self) -> float:
|
|
153
|
+
"""Current joint velocity (rad/s or m/s).
|
|
154
|
+
|
|
155
|
+
Setting this applies velocity control via PyBullet.
|
|
156
|
+
|
|
157
|
+
Example::
|
|
158
|
+
|
|
159
|
+
robot.joints["wheel"].velocity = 10.0
|
|
160
|
+
"""
|
|
161
|
+
if not self._check_connected():
|
|
162
|
+
return 0.0
|
|
163
|
+
state = p.getJointState(
|
|
164
|
+
self._body_id,
|
|
165
|
+
self._index,
|
|
166
|
+
physicsClientId=self._sim.client_id,
|
|
167
|
+
)
|
|
168
|
+
return float(state[1])
|
|
169
|
+
|
|
170
|
+
@velocity.setter
|
|
171
|
+
def velocity(self, value: float) -> None:
|
|
172
|
+
if not self._check_connected():
|
|
173
|
+
return
|
|
174
|
+
self._control_mode = p.VELOCITY_CONTROL
|
|
175
|
+
p.setJointMotorControl2(
|
|
176
|
+
self._body_id,
|
|
177
|
+
self._index,
|
|
178
|
+
controlMode=p.VELOCITY_CONTROL,
|
|
179
|
+
targetVelocity=float(value),
|
|
180
|
+
force=self._max_force,
|
|
181
|
+
physicsClientId=self._sim.client_id,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def torque(self) -> float:
|
|
186
|
+
"""Current joint applied torque (N·m or N).
|
|
187
|
+
|
|
188
|
+
Setting this applies torque control via PyBullet.
|
|
189
|
+
|
|
190
|
+
Example::
|
|
191
|
+
|
|
192
|
+
robot.joints["hip"].torque = 20.0
|
|
193
|
+
"""
|
|
194
|
+
if not self._check_connected():
|
|
195
|
+
return 0.0
|
|
196
|
+
state = p.getJointState(
|
|
197
|
+
self._body_id,
|
|
198
|
+
self._index,
|
|
199
|
+
physicsClientId=self._sim.client_id,
|
|
200
|
+
)
|
|
201
|
+
return float(state[3])
|
|
202
|
+
|
|
203
|
+
@torque.setter
|
|
204
|
+
def torque(self, value: float) -> None:
|
|
205
|
+
if not self._check_connected():
|
|
206
|
+
return
|
|
207
|
+
self._control_mode = p.TORQUE_CONTROL
|
|
208
|
+
# Disable velocity motor first so torque control can take over
|
|
209
|
+
p.setJointMotorControl2(
|
|
210
|
+
self._body_id,
|
|
211
|
+
self._index,
|
|
212
|
+
controlMode=p.VELOCITY_CONTROL,
|
|
213
|
+
targetVelocity=0,
|
|
214
|
+
force=0,
|
|
215
|
+
physicsClientId=self._sim.client_id,
|
|
216
|
+
)
|
|
217
|
+
p.setJointMotorControl2(
|
|
218
|
+
self._body_id,
|
|
219
|
+
self._index,
|
|
220
|
+
controlMode=p.TORQUE_CONTROL,
|
|
221
|
+
force=float(value),
|
|
222
|
+
physicsClientId=self._sim.client_id,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
# Position control
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def set_position(self, target: float, max_force: float | None = None) -> None:
|
|
230
|
+
"""Apply position control to move the joint to a target angle/displacement.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
target: Target position (radians or meters).
|
|
234
|
+
max_force: Maximum force/torque. Defaults to :attr:`max_force`.
|
|
235
|
+
|
|
236
|
+
Example::
|
|
237
|
+
|
|
238
|
+
robot.joints["shoulder"].set_position(1.57) # 90°
|
|
239
|
+
"""
|
|
240
|
+
if not self._check_connected():
|
|
241
|
+
return
|
|
242
|
+
force = float(max_force) if max_force is not None else self._max_force
|
|
243
|
+
self._control_mode = p.POSITION_CONTROL
|
|
244
|
+
p.setJointMotorControl2(
|
|
245
|
+
self._body_id,
|
|
246
|
+
self._index,
|
|
247
|
+
controlMode=p.POSITION_CONTROL,
|
|
248
|
+
targetPosition=float(target),
|
|
249
|
+
force=force,
|
|
250
|
+
physicsClientId=self._sim.client_id,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# ------------------------------------------------------------------
|
|
254
|
+
# Limits and tuning
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def limits(self) -> tuple[float, float]:
|
|
259
|
+
"""Joint position limits as ``(lower, upper)`` in radians or meters.
|
|
260
|
+
|
|
261
|
+
Returns ``(0.0, 0.0)`` for fixed joints or joints with no limits.
|
|
262
|
+
|
|
263
|
+
Example::
|
|
264
|
+
|
|
265
|
+
lo, hi = robot.joints["elbow"].limits
|
|
266
|
+
"""
|
|
267
|
+
return (self._lower_limit, self._upper_limit)
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def max_force(self) -> float:
|
|
271
|
+
"""Maximum motor force / torque in N or N·m.
|
|
272
|
+
|
|
273
|
+
Example::
|
|
274
|
+
|
|
275
|
+
robot.joints["motor"].max_force = 150.0
|
|
276
|
+
"""
|
|
277
|
+
return self._max_force
|
|
278
|
+
|
|
279
|
+
@max_force.setter
|
|
280
|
+
def max_force(self, value: float) -> None:
|
|
281
|
+
self._max_force = float(value)
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def max_velocity(self) -> float:
|
|
285
|
+
"""Maximum joint velocity in rad/s or m/s.
|
|
286
|
+
|
|
287
|
+
Example::
|
|
288
|
+
|
|
289
|
+
robot.joints["wheel"].max_velocity = 20.0
|
|
290
|
+
"""
|
|
291
|
+
return self._max_velocity
|
|
292
|
+
|
|
293
|
+
@max_velocity.setter
|
|
294
|
+
def max_velocity(self, value: float) -> None:
|
|
295
|
+
self._max_velocity = float(value)
|
|
296
|
+
|
|
297
|
+
# ------------------------------------------------------------------
|
|
298
|
+
# Enable / disable
|
|
299
|
+
# ------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
def enable(self) -> None:
|
|
302
|
+
"""Enable the joint motor (default state).
|
|
303
|
+
|
|
304
|
+
Example::
|
|
305
|
+
|
|
306
|
+
robot.joints["motor"].enable()
|
|
307
|
+
"""
|
|
308
|
+
self._enabled = True
|
|
309
|
+
if not self._check_connected():
|
|
310
|
+
return
|
|
311
|
+
p.setJointMotorControl2(
|
|
312
|
+
self._body_id,
|
|
313
|
+
self._index,
|
|
314
|
+
controlMode=p.VELOCITY_CONTROL,
|
|
315
|
+
targetVelocity=0,
|
|
316
|
+
force=self._max_force,
|
|
317
|
+
physicsClientId=self._sim.client_id,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def disable(self) -> None:
|
|
321
|
+
"""Disable the joint motor (free-spinning / passive joint).
|
|
322
|
+
|
|
323
|
+
Example::
|
|
324
|
+
|
|
325
|
+
robot.joints["motor"].disable()
|
|
326
|
+
"""
|
|
327
|
+
self._enabled = False
|
|
328
|
+
if not self._check_connected():
|
|
329
|
+
return
|
|
330
|
+
p.setJointMotorControl2(
|
|
331
|
+
self._body_id,
|
|
332
|
+
self._index,
|
|
333
|
+
controlMode=p.VELOCITY_CONTROL,
|
|
334
|
+
targetVelocity=0,
|
|
335
|
+
force=0,
|
|
336
|
+
physicsClientId=self._sim.client_id,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def is_enabled(self) -> bool:
|
|
341
|
+
"""``True`` if the joint motor is enabled."""
|
|
342
|
+
return self._enabled
|
|
343
|
+
|
|
344
|
+
# ------------------------------------------------------------------
|
|
345
|
+
# Reset
|
|
346
|
+
# ------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
def reset(self, pos: float = 0.0, vel: float = 0.0) -> None:
|
|
349
|
+
"""Teleport the joint to a specific state.
|
|
350
|
+
|
|
351
|
+
This bypasses physics and directly sets joint position and velocity.
|
|
352
|
+
Useful for initializing a robot pose.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
pos: Target position in radians or meters.
|
|
356
|
+
vel: Target velocity in rad/s or m/s.
|
|
357
|
+
|
|
358
|
+
Example::
|
|
359
|
+
|
|
360
|
+
robot.joints["shoulder"].reset(pos=1.0, vel=0.0)
|
|
361
|
+
"""
|
|
362
|
+
if not self._check_connected():
|
|
363
|
+
return
|
|
364
|
+
p.resetJointState(
|
|
365
|
+
self._body_id,
|
|
366
|
+
self._index,
|
|
367
|
+
targetValue=float(pos),
|
|
368
|
+
targetVelocity=float(vel),
|
|
369
|
+
physicsClientId=self._sim.client_id,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# ------------------------------------------------------------------
|
|
373
|
+
# Reaction forces
|
|
374
|
+
# ------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def reaction_forces(self) -> tuple[float, ...]:
|
|
378
|
+
"""Joint reaction forces and torques as a 6-element tuple ``(Fx, Fy, Fz, Mx, My, Mz)``.
|
|
379
|
+
|
|
380
|
+
Note:
|
|
381
|
+
Requires enabling joint force sensors via PyBullet first.
|
|
382
|
+
"""
|
|
383
|
+
if not self._check_connected():
|
|
384
|
+
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
|
385
|
+
state = p.getJointState(
|
|
386
|
+
self._body_id,
|
|
387
|
+
self._index,
|
|
388
|
+
physicsClientId=self._sim.client_id,
|
|
389
|
+
)
|
|
390
|
+
return tuple(float(v) for v in state[2])
|
|
391
|
+
|
|
392
|
+
# ------------------------------------------------------------------
|
|
393
|
+
# Repr
|
|
394
|
+
# ------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
def __repr__(self) -> str:
|
|
397
|
+
if self._check_connected():
|
|
398
|
+
return (
|
|
399
|
+
f"Joint({self._name!r}, type={self._joint_type}, "
|
|
400
|
+
f"pos={self.position:.3f}, vel={self.velocity:.3f})"
|
|
401
|
+
)
|
|
402
|
+
return f"Joint({self._name!r}, type={self._joint_type}, disconnected)"
|