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.
@@ -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)"