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/link.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Link – a rigid body segment in a robot model.
|
|
3
|
+
|
|
4
|
+
The Link class wraps a single PyBullet link and exposes its physical
|
|
5
|
+
properties (mass, friction, restitution, damping) as Python attributes.
|
|
6
|
+
Setting any property automatically calls PyBullet's changeDynamics.
|
|
7
|
+
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
link = robot.links["wheel_fl"]
|
|
11
|
+
link.mass = 2.5
|
|
12
|
+
link.friction = 0.8
|
|
13
|
+
link.restitution = 0.1
|
|
14
|
+
print(link.position) # world position
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
import pybullet as p
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from bulletlab.core.simulation import Simulation
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Link:
|
|
28
|
+
"""A single rigid body link in a robot model.
|
|
29
|
+
|
|
30
|
+
Links are typically not instantiated directly — they are created by
|
|
31
|
+
:class:`~bulletlab.robot.robot.Robot` when loading a URDF/MJCF file.
|
|
32
|
+
Access them via ``robot.links["link_name"]``.
|
|
33
|
+
|
|
34
|
+
The base link (index -1 in PyBullet) is also exposed as ``robot.links``
|
|
35
|
+
under its name.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
name: Link name (from URDF).
|
|
39
|
+
index: PyBullet link index (``-1`` for base link).
|
|
40
|
+
body_id: PyBullet body ID of the parent robot.
|
|
41
|
+
sim: The :class:`~bulletlab.core.simulation.Simulation` instance.
|
|
42
|
+
|
|
43
|
+
Example::
|
|
44
|
+
|
|
45
|
+
link = robot.links["chassis"]
|
|
46
|
+
link.mass = 10.0
|
|
47
|
+
link.friction = 0.8
|
|
48
|
+
print(link.position)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
name: str,
|
|
54
|
+
index: int,
|
|
55
|
+
body_id: int,
|
|
56
|
+
sim: "Simulation",
|
|
57
|
+
) -> None:
|
|
58
|
+
self._name = name
|
|
59
|
+
self._index = index
|
|
60
|
+
self._body_id = body_id
|
|
61
|
+
self._sim = sim
|
|
62
|
+
# Cache damping values: PyBullet's getDynamicsInfo returns 0.0 for the
|
|
63
|
+
# base link (index -1) even after changeDynamics is called, so we track
|
|
64
|
+
# the last written value ourselves.
|
|
65
|
+
self._linear_damping_cache: float | None = None
|
|
66
|
+
self._angular_damping_cache: float | None = None
|
|
67
|
+
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
# Identity
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def name(self) -> str:
|
|
74
|
+
"""Link name as defined in the URDF."""
|
|
75
|
+
return self._name
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def index(self) -> int:
|
|
79
|
+
"""PyBullet link index (``-1`` for base link)."""
|
|
80
|
+
return self._index
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# Physical properties (get/set → changeDynamics)
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def mass(self) -> float:
|
|
88
|
+
"""Link mass in kilograms.
|
|
89
|
+
|
|
90
|
+
Setting this value calls ``pybullet.changeDynamics`` immediately.
|
|
91
|
+
|
|
92
|
+
Example::
|
|
93
|
+
|
|
94
|
+
robot.links["chassis"].mass = 5.0
|
|
95
|
+
"""
|
|
96
|
+
info = p.getDynamicsInfo(
|
|
97
|
+
self._body_id,
|
|
98
|
+
self._index,
|
|
99
|
+
physicsClientId=self._sim.client_id,
|
|
100
|
+
)
|
|
101
|
+
return float(info[0])
|
|
102
|
+
|
|
103
|
+
@mass.setter
|
|
104
|
+
def mass(self, value: float) -> None:
|
|
105
|
+
p.changeDynamics(
|
|
106
|
+
self._body_id,
|
|
107
|
+
self._index,
|
|
108
|
+
mass=float(value),
|
|
109
|
+
physicsClientId=self._sim.client_id,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def friction(self) -> float:
|
|
114
|
+
"""Lateral friction coefficient.
|
|
115
|
+
|
|
116
|
+
Setting this value calls ``pybullet.changeDynamics`` immediately.
|
|
117
|
+
|
|
118
|
+
Example::
|
|
119
|
+
|
|
120
|
+
robot.links["wheel_fl"].friction = 1.2
|
|
121
|
+
"""
|
|
122
|
+
info = p.getDynamicsInfo(
|
|
123
|
+
self._body_id,
|
|
124
|
+
self._index,
|
|
125
|
+
physicsClientId=self._sim.client_id,
|
|
126
|
+
)
|
|
127
|
+
return float(info[1])
|
|
128
|
+
|
|
129
|
+
@friction.setter
|
|
130
|
+
def friction(self, value: float) -> None:
|
|
131
|
+
p.changeDynamics(
|
|
132
|
+
self._body_id,
|
|
133
|
+
self._index,
|
|
134
|
+
lateralFriction=float(value),
|
|
135
|
+
physicsClientId=self._sim.client_id,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def restitution(self) -> float:
|
|
140
|
+
"""Coefficient of restitution (bounciness), in range [0, 1].
|
|
141
|
+
|
|
142
|
+
Setting this value calls ``pybullet.changeDynamics`` immediately.
|
|
143
|
+
|
|
144
|
+
Example::
|
|
145
|
+
|
|
146
|
+
robot.links["bumper"].restitution = 0.5
|
|
147
|
+
"""
|
|
148
|
+
info = p.getDynamicsInfo(
|
|
149
|
+
self._body_id,
|
|
150
|
+
self._index,
|
|
151
|
+
physicsClientId=self._sim.client_id,
|
|
152
|
+
)
|
|
153
|
+
return float(info[5])
|
|
154
|
+
|
|
155
|
+
@restitution.setter
|
|
156
|
+
def restitution(self, value: float) -> None:
|
|
157
|
+
p.changeDynamics(
|
|
158
|
+
self._body_id,
|
|
159
|
+
self._index,
|
|
160
|
+
restitution=float(value),
|
|
161
|
+
physicsClientId=self._sim.client_id,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def linear_damping(self) -> float:
|
|
166
|
+
"""Linear velocity damping coefficient.
|
|
167
|
+
|
|
168
|
+
Setting this value calls ``pybullet.changeDynamics`` immediately.
|
|
169
|
+
|
|
170
|
+
Example::
|
|
171
|
+
|
|
172
|
+
robot.links["chassis"].linear_damping = 0.1
|
|
173
|
+
"""
|
|
174
|
+
if self._linear_damping_cache is not None:
|
|
175
|
+
return self._linear_damping_cache
|
|
176
|
+
info = p.getDynamicsInfo(
|
|
177
|
+
self._body_id,
|
|
178
|
+
self._index,
|
|
179
|
+
physicsClientId=self._sim.client_id,
|
|
180
|
+
)
|
|
181
|
+
return float(info[6])
|
|
182
|
+
|
|
183
|
+
@linear_damping.setter
|
|
184
|
+
def linear_damping(self, value: float) -> None:
|
|
185
|
+
self._linear_damping_cache = float(value)
|
|
186
|
+
p.changeDynamics(
|
|
187
|
+
self._body_id,
|
|
188
|
+
self._index,
|
|
189
|
+
linearDamping=float(value),
|
|
190
|
+
physicsClientId=self._sim.client_id,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def angular_damping(self) -> float:
|
|
195
|
+
"""Angular velocity damping coefficient.
|
|
196
|
+
|
|
197
|
+
Setting this value calls ``pybullet.changeDynamics`` immediately.
|
|
198
|
+
|
|
199
|
+
Example::
|
|
200
|
+
|
|
201
|
+
robot.links["chassis"].angular_damping = 0.1
|
|
202
|
+
"""
|
|
203
|
+
if self._angular_damping_cache is not None:
|
|
204
|
+
return self._angular_damping_cache
|
|
205
|
+
info = p.getDynamicsInfo(
|
|
206
|
+
self._body_id,
|
|
207
|
+
self._index,
|
|
208
|
+
physicsClientId=self._sim.client_id,
|
|
209
|
+
)
|
|
210
|
+
return float(info[7])
|
|
211
|
+
|
|
212
|
+
@angular_damping.setter
|
|
213
|
+
def angular_damping(self, value: float) -> None:
|
|
214
|
+
self._angular_damping_cache = float(value)
|
|
215
|
+
p.changeDynamics(
|
|
216
|
+
self._body_id,
|
|
217
|
+
self._index,
|
|
218
|
+
angularDamping=float(value),
|
|
219
|
+
physicsClientId=self._sim.client_id,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def damping(self) -> float:
|
|
224
|
+
"""Combined linear damping (shorthand for :attr:`linear_damping`).
|
|
225
|
+
|
|
226
|
+
Setting this updates both linear and angular damping symmetrically.
|
|
227
|
+
|
|
228
|
+
Example::
|
|
229
|
+
|
|
230
|
+
robot.links["chassis"].damping = 0.05
|
|
231
|
+
"""
|
|
232
|
+
return self.linear_damping
|
|
233
|
+
|
|
234
|
+
@damping.setter
|
|
235
|
+
def damping(self, value: float) -> None:
|
|
236
|
+
self._linear_damping_cache = float(value)
|
|
237
|
+
self._angular_damping_cache = float(value)
|
|
238
|
+
p.changeDynamics(
|
|
239
|
+
self._body_id,
|
|
240
|
+
self._index,
|
|
241
|
+
linearDamping=float(value),
|
|
242
|
+
angularDamping=float(value),
|
|
243
|
+
physicsClientId=self._sim.client_id,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def spinning_friction(self) -> float:
|
|
248
|
+
"""Spinning friction coefficient.
|
|
249
|
+
|
|
250
|
+
Example::
|
|
251
|
+
|
|
252
|
+
robot.links["wheel"].spinning_friction = 0.01
|
|
253
|
+
"""
|
|
254
|
+
info = p.getDynamicsInfo(
|
|
255
|
+
self._body_id,
|
|
256
|
+
self._index,
|
|
257
|
+
physicsClientId=self._sim.client_id,
|
|
258
|
+
)
|
|
259
|
+
return float(info[2])
|
|
260
|
+
|
|
261
|
+
@spinning_friction.setter
|
|
262
|
+
def spinning_friction(self, value: float) -> None:
|
|
263
|
+
p.changeDynamics(
|
|
264
|
+
self._body_id,
|
|
265
|
+
self._index,
|
|
266
|
+
spinningFriction=float(value),
|
|
267
|
+
physicsClientId=self._sim.client_id,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def rolling_friction(self) -> float:
|
|
272
|
+
"""Rolling friction coefficient.
|
|
273
|
+
|
|
274
|
+
Example::
|
|
275
|
+
|
|
276
|
+
robot.links["wheel"].rolling_friction = 0.01
|
|
277
|
+
"""
|
|
278
|
+
info = p.getDynamicsInfo(
|
|
279
|
+
self._body_id,
|
|
280
|
+
self._index,
|
|
281
|
+
physicsClientId=self._sim.client_id,
|
|
282
|
+
)
|
|
283
|
+
return float(info[3])
|
|
284
|
+
|
|
285
|
+
@rolling_friction.setter
|
|
286
|
+
def rolling_friction(self, value: float) -> None:
|
|
287
|
+
p.changeDynamics(
|
|
288
|
+
self._body_id,
|
|
289
|
+
self._index,
|
|
290
|
+
rollingFriction=float(value),
|
|
291
|
+
physicsClientId=self._sim.client_id,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# ------------------------------------------------------------------
|
|
295
|
+
# State reads (world-frame)
|
|
296
|
+
# ------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def position(self) -> tuple[float, float, float]:
|
|
300
|
+
"""World-frame position of this link's COM in meters ``(x, y, z)``.
|
|
301
|
+
|
|
302
|
+
For the base link (index -1), queries the base position directly.
|
|
303
|
+
|
|
304
|
+
Example::
|
|
305
|
+
|
|
306
|
+
x, y, z = robot.links["chassis"].position
|
|
307
|
+
"""
|
|
308
|
+
if self._index == -1:
|
|
309
|
+
pos, _ = p.getBasePositionAndOrientation(
|
|
310
|
+
self._body_id,
|
|
311
|
+
physicsClientId=self._sim.client_id,
|
|
312
|
+
)
|
|
313
|
+
return tuple(float(v) for v in pos) # type: ignore[return-value]
|
|
314
|
+
state = p.getLinkState(
|
|
315
|
+
self._body_id,
|
|
316
|
+
self._index,
|
|
317
|
+
physicsClientId=self._sim.client_id,
|
|
318
|
+
)
|
|
319
|
+
return tuple(float(v) for v in state[0]) # type: ignore[return-value]
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def orientation(self) -> tuple[float, float, float, float]:
|
|
323
|
+
"""World-frame orientation as a quaternion ``(x, y, z, w)``.
|
|
324
|
+
|
|
325
|
+
Example::
|
|
326
|
+
|
|
327
|
+
q = robot.links["chassis"].orientation
|
|
328
|
+
"""
|
|
329
|
+
if self._index == -1:
|
|
330
|
+
_, orn = p.getBasePositionAndOrientation(
|
|
331
|
+
self._body_id,
|
|
332
|
+
physicsClientId=self._sim.client_id,
|
|
333
|
+
)
|
|
334
|
+
return tuple(float(v) for v in orn) # type: ignore[return-value]
|
|
335
|
+
state = p.getLinkState(
|
|
336
|
+
self._body_id,
|
|
337
|
+
self._index,
|
|
338
|
+
physicsClientId=self._sim.client_id,
|
|
339
|
+
)
|
|
340
|
+
return tuple(float(v) for v in state[1]) # type: ignore[return-value]
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def velocity(self) -> tuple[float, float, float]:
|
|
344
|
+
"""World-frame linear velocity of this link in m/s ``(vx, vy, vz)``.
|
|
345
|
+
|
|
346
|
+
Example::
|
|
347
|
+
|
|
348
|
+
vx, vy, vz = robot.links["chassis"].velocity
|
|
349
|
+
"""
|
|
350
|
+
if self._index == -1:
|
|
351
|
+
vel, _ = p.getBaseVelocity(
|
|
352
|
+
self._body_id,
|
|
353
|
+
physicsClientId=self._sim.client_id,
|
|
354
|
+
)
|
|
355
|
+
return tuple(float(v) for v in vel) # type: ignore[return-value]
|
|
356
|
+
state = p.getLinkState(
|
|
357
|
+
self._body_id,
|
|
358
|
+
self._index,
|
|
359
|
+
computeLinkVelocity=1,
|
|
360
|
+
physicsClientId=self._sim.client_id,
|
|
361
|
+
)
|
|
362
|
+
return tuple(float(v) for v in state[6]) # type: ignore[return-value]
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def angular_velocity(self) -> tuple[float, float, float]:
|
|
366
|
+
"""World-frame angular velocity of this link in rad/s ``(wx, wy, wz)``.
|
|
367
|
+
|
|
368
|
+
Example::
|
|
369
|
+
|
|
370
|
+
wx, wy, wz = robot.links["chassis"].angular_velocity
|
|
371
|
+
"""
|
|
372
|
+
if self._index == -1:
|
|
373
|
+
_, avel = p.getBaseVelocity(
|
|
374
|
+
self._body_id,
|
|
375
|
+
physicsClientId=self._sim.client_id,
|
|
376
|
+
)
|
|
377
|
+
return tuple(float(v) for v in avel) # type: ignore[return-value]
|
|
378
|
+
state = p.getLinkState(
|
|
379
|
+
self._body_id,
|
|
380
|
+
self._index,
|
|
381
|
+
computeLinkVelocity=1,
|
|
382
|
+
physicsClientId=self._sim.client_id,
|
|
383
|
+
)
|
|
384
|
+
return tuple(float(v) for v in state[7]) # type: ignore[return-value]
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def inertia(self) -> tuple[float, float, float]:
|
|
388
|
+
"""Inertia diagonal ``(Ixx, Iyy, Izz)`` in kg·m²."""
|
|
389
|
+
info = p.getDynamicsInfo(
|
|
390
|
+
self._body_id,
|
|
391
|
+
self._index,
|
|
392
|
+
physicsClientId=self._sim.client_id,
|
|
393
|
+
)
|
|
394
|
+
return tuple(float(v) for v in info[2]) # type: ignore[return-value]
|
|
395
|
+
|
|
396
|
+
# ------------------------------------------------------------------
|
|
397
|
+
# Repr
|
|
398
|
+
# ------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
def __repr__(self) -> str:
|
|
401
|
+
return f"Link({self._name!r}, index={self._index}, mass={self.mass:.3f}kg)"
|