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,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)"