bulletlab 0.1.0__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 +363 -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 +514 -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.0.dist-info/METADATA +271 -0
- bulletlab-0.1.0.dist-info/RECORD +35 -0
- bulletlab-0.1.0.dist-info/WHEEL +5 -0
- bulletlab-0.1.0.dist-info/licenses/LICENSE +21 -0
- bulletlab-0.1.0.dist-info/top_level.txt +1 -0
bulletlab/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BulletLab – A fast, extensible robotics experimentation framework built on PyBullet.
|
|
3
|
+
|
|
4
|
+
Developed by Ranasurya Ghosh (https://github.com/NuclearVenom/BulletLab)
|
|
5
|
+
|
|
6
|
+
BulletLab provides a high-level Python API over PyBullet, making robotics
|
|
7
|
+
experimentation significantly easier by exposing robots as structured Python
|
|
8
|
+
objects rather than raw physics engine primitives.
|
|
9
|
+
|
|
10
|
+
Quick Start::
|
|
11
|
+
|
|
12
|
+
from bulletlab import Simulation, Robot
|
|
13
|
+
|
|
14
|
+
sim = Simulation()
|
|
15
|
+
sim.start()
|
|
16
|
+
|
|
17
|
+
robot = Robot.load("robot.urdf", sim=sim)
|
|
18
|
+
robot.joints["motor"].velocity = 15
|
|
19
|
+
robot.links["wheel"].mass = 2.5
|
|
20
|
+
|
|
21
|
+
while True:
|
|
22
|
+
sim.step()
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from bulletlab.core.simulation import Simulation
|
|
26
|
+
from bulletlab.core.world import World
|
|
27
|
+
from bulletlab.robot.robot import Robot
|
|
28
|
+
from bulletlab.robot.joint import Joint
|
|
29
|
+
from bulletlab.robot.link import Link
|
|
30
|
+
from bulletlab.telemetry.manager import TelemetryManager
|
|
31
|
+
from bulletlab.logging.logger import DataLogger
|
|
32
|
+
from bulletlab.plotting.live_plot import LivePlot
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
35
|
+
__author__ = "Ranasurya Ghosh"
|
|
36
|
+
__url__ = "https://github.com/NuclearVenom/BulletLab"
|
|
37
|
+
__license__ = "MIT"
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Simulation",
|
|
41
|
+
"World",
|
|
42
|
+
"Robot",
|
|
43
|
+
"Joint",
|
|
44
|
+
"Link",
|
|
45
|
+
"TelemetryManager",
|
|
46
|
+
"DataLogger",
|
|
47
|
+
"LivePlot",
|
|
48
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BulletLab core subpackage.
|
|
3
|
+
|
|
4
|
+
Provides the Simulation and World classes — the foundation of every BulletLab session.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from bulletlab.core.simulation import Simulation
|
|
8
|
+
from bulletlab.core.world import World
|
|
9
|
+
|
|
10
|
+
__all__ = ["Simulation", "World"]
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulation – the central controller for a BulletLab physics session.
|
|
3
|
+
|
|
4
|
+
The Simulation class manages the PyBullet physics server connection, controls
|
|
5
|
+
the simulation time step, gravity, stepping, pausing, and tracks all robots
|
|
6
|
+
loaded into the scene.
|
|
7
|
+
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
from bulletlab import Simulation
|
|
11
|
+
|
|
12
|
+
sim = Simulation()
|
|
13
|
+
sim.start() # connects to PyBullet GUI
|
|
14
|
+
sim.gravity = (0, 0, -9.81)
|
|
15
|
+
sim.timestep = 1.0 / 240.0
|
|
16
|
+
|
|
17
|
+
while True:
|
|
18
|
+
sim.step()
|
|
19
|
+
|
|
20
|
+
sim.stop()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import time
|
|
26
|
+
from typing import TYPE_CHECKING, Optional
|
|
27
|
+
|
|
28
|
+
import pybullet as p
|
|
29
|
+
import pybullet_data
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from bulletlab.robot.robot import Robot
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Simulation:
|
|
36
|
+
"""Central controller for a BulletLab physics simulation session.
|
|
37
|
+
|
|
38
|
+
Wraps the PyBullet physics server and provides a high-level interface
|
|
39
|
+
for connecting, stepping, pausing, resetting, and configuring the
|
|
40
|
+
simulation environment.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
mode: PyBullet connection mode. Use ``"gui"`` for an interactive
|
|
44
|
+
window or ``"direct"`` for headless (testing/RL) mode.
|
|
45
|
+
gravity: Initial gravity vector as ``(gx, gy, gz)``.
|
|
46
|
+
Defaults to ``(0, 0, -9.81)``.
|
|
47
|
+
timestep: Physics timestep in seconds. Defaults to ``1/240``.
|
|
48
|
+
real_time: If ``True``, enable real-time simulation in GUI mode.
|
|
49
|
+
|
|
50
|
+
Example::
|
|
51
|
+
|
|
52
|
+
sim = Simulation(mode="gui")
|
|
53
|
+
sim.start()
|
|
54
|
+
sim.gravity = (0, 0, -9.81)
|
|
55
|
+
|
|
56
|
+
for _ in range(1000):
|
|
57
|
+
sim.step()
|
|
58
|
+
|
|
59
|
+
sim.stop()
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
GUI = p.GUI
|
|
63
|
+
DIRECT = p.DIRECT
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
mode: str = "gui",
|
|
68
|
+
gravity: tuple[float, float, float] = (0.0, 0.0, -9.81),
|
|
69
|
+
timestep: float = 1.0 / 240.0,
|
|
70
|
+
real_time: bool = False,
|
|
71
|
+
hide_gui: bool = True,
|
|
72
|
+
) -> None:
|
|
73
|
+
self._mode_str = mode.lower()
|
|
74
|
+
self._mode = p.GUI if self._mode_str == "gui" else p.DIRECT
|
|
75
|
+
self._gravity = gravity
|
|
76
|
+
self._timestep = timestep
|
|
77
|
+
self._real_time = real_time
|
|
78
|
+
self._hide_gui = hide_gui
|
|
79
|
+
self._client_id: int = -1
|
|
80
|
+
self._paused: bool = False
|
|
81
|
+
self._step_count: int = 0
|
|
82
|
+
self._robots: list["Robot"] = []
|
|
83
|
+
self._connected: bool = False
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# Connection lifecycle
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def start(self) -> "Simulation":
|
|
90
|
+
"""Connect to the PyBullet server and configure the environment.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
self, for method chaining.
|
|
94
|
+
|
|
95
|
+
Example::
|
|
96
|
+
|
|
97
|
+
sim = Simulation().start()
|
|
98
|
+
"""
|
|
99
|
+
if self._connected:
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
self._client_id = p.connect(self._mode)
|
|
103
|
+
p.setAdditionalSearchPath(pybullet_data.getDataPath(), physicsClientId=self._client_id)
|
|
104
|
+
p.setGravity(*self._gravity, physicsClientId=self._client_id)
|
|
105
|
+
p.setTimeStep(self._timestep, physicsClientId=self._client_id)
|
|
106
|
+
|
|
107
|
+
if self._mode == p.GUI:
|
|
108
|
+
if self._hide_gui:
|
|
109
|
+
# Remove all PyBullet built-in sidebar panels, sliders,
|
|
110
|
+
# and debug widgets. BulletLab provides its own ImGui UI.
|
|
111
|
+
p.configureDebugVisualizer(
|
|
112
|
+
p.COV_ENABLE_GUI, 0, physicsClientId=self._client_id
|
|
113
|
+
)
|
|
114
|
+
if self._real_time:
|
|
115
|
+
p.setRealTimeSimulation(1, physicsClientId=self._client_id)
|
|
116
|
+
|
|
117
|
+
self._connected = True
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def stop(self) -> None:
|
|
122
|
+
"""Disconnect from the PyBullet server.
|
|
123
|
+
|
|
124
|
+
Example::
|
|
125
|
+
|
|
126
|
+
sim.stop()
|
|
127
|
+
"""
|
|
128
|
+
if self._connected:
|
|
129
|
+
try:
|
|
130
|
+
p.disconnect(physicsClientId=self._client_id)
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
self._connected = False
|
|
134
|
+
self._client_id = -1
|
|
135
|
+
|
|
136
|
+
# Alias for stop()
|
|
137
|
+
disconnect = stop
|
|
138
|
+
|
|
139
|
+
def reset(self) -> None:
|
|
140
|
+
"""Reset the simulation to a clean state.
|
|
141
|
+
|
|
142
|
+
Removes all objects from the world, resets the step counter,
|
|
143
|
+
and reloads the data search path. Robot references in
|
|
144
|
+
``self.robots`` are cleared.
|
|
145
|
+
|
|
146
|
+
Example::
|
|
147
|
+
|
|
148
|
+
sim.reset()
|
|
149
|
+
"""
|
|
150
|
+
if not self._connected:
|
|
151
|
+
return
|
|
152
|
+
p.resetSimulation(physicsClientId=self._client_id)
|
|
153
|
+
p.setAdditionalSearchPath(pybullet_data.getDataPath(), physicsClientId=self._client_id)
|
|
154
|
+
p.setGravity(*self._gravity, physicsClientId=self._client_id)
|
|
155
|
+
p.setTimeStep(self._timestep, physicsClientId=self._client_id)
|
|
156
|
+
self._robots.clear()
|
|
157
|
+
self._step_count = 0
|
|
158
|
+
self._paused = False
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
# Stepping
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def step(self) -> None:
|
|
165
|
+
"""Advance the simulation by one timestep.
|
|
166
|
+
|
|
167
|
+
Does nothing if the simulation is paused or not connected.
|
|
168
|
+
|
|
169
|
+
Example::
|
|
170
|
+
|
|
171
|
+
for _ in range(1000):
|
|
172
|
+
sim.step()
|
|
173
|
+
"""
|
|
174
|
+
if not self._connected or self._paused:
|
|
175
|
+
return
|
|
176
|
+
p.stepSimulation(physicsClientId=self._client_id)
|
|
177
|
+
self._step_count += 1
|
|
178
|
+
|
|
179
|
+
def pause(self) -> None:
|
|
180
|
+
"""Pause the simulation. Calls to :meth:`step` are no-ops while paused.
|
|
181
|
+
|
|
182
|
+
Example::
|
|
183
|
+
|
|
184
|
+
sim.pause()
|
|
185
|
+
"""
|
|
186
|
+
self._paused = True
|
|
187
|
+
|
|
188
|
+
def resume(self) -> None:
|
|
189
|
+
"""Resume a paused simulation.
|
|
190
|
+
|
|
191
|
+
Example::
|
|
192
|
+
|
|
193
|
+
sim.resume()
|
|
194
|
+
"""
|
|
195
|
+
self._paused = False
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
# Properties
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def gravity(self) -> tuple[float, float, float]:
|
|
203
|
+
"""Gravity vector ``(gx, gy, gz)`` in m/s².
|
|
204
|
+
|
|
205
|
+
Example::
|
|
206
|
+
|
|
207
|
+
sim.gravity = (0, 0, -9.81) # Earth gravity
|
|
208
|
+
sim.gravity = (0, 0, -1.62) # Moon gravity
|
|
209
|
+
"""
|
|
210
|
+
return self._gravity
|
|
211
|
+
|
|
212
|
+
@gravity.setter
|
|
213
|
+
def gravity(self, value: tuple[float, float, float]) -> None:
|
|
214
|
+
self._gravity = tuple(float(v) for v in value) # type: ignore[assignment]
|
|
215
|
+
if self._connected:
|
|
216
|
+
p.setGravity(*self._gravity, physicsClientId=self._client_id)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def timestep(self) -> float:
|
|
220
|
+
"""Physics timestep in seconds.
|
|
221
|
+
|
|
222
|
+
Example::
|
|
223
|
+
|
|
224
|
+
sim.timestep = 1.0 / 480.0 # higher resolution
|
|
225
|
+
"""
|
|
226
|
+
return self._timestep
|
|
227
|
+
|
|
228
|
+
@timestep.setter
|
|
229
|
+
def timestep(self, value: float) -> None:
|
|
230
|
+
self._timestep = float(value)
|
|
231
|
+
if self._connected:
|
|
232
|
+
p.setTimeStep(self._timestep, physicsClientId=self._client_id)
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def is_paused(self) -> bool:
|
|
236
|
+
"""``True`` if the simulation is currently paused."""
|
|
237
|
+
return self._paused
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def is_connected(self) -> bool:
|
|
241
|
+
"""``True`` if connected to a PyBullet physics server."""
|
|
242
|
+
return self._connected
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def client_id(self) -> int:
|
|
246
|
+
"""The PyBullet physics server client ID.
|
|
247
|
+
|
|
248
|
+
This is an internal identifier. Most users should not need it.
|
|
249
|
+
"""
|
|
250
|
+
return self._client_id
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def step_count(self) -> int:
|
|
254
|
+
"""Total number of simulation steps taken since last reset."""
|
|
255
|
+
return self._step_count
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def elapsed_time(self) -> float:
|
|
259
|
+
"""Simulated time elapsed in seconds since last reset."""
|
|
260
|
+
return self._step_count * self._timestep
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def robots(self) -> list["Robot"]:
|
|
264
|
+
"""List of all robots currently registered in this simulation."""
|
|
265
|
+
return list(self._robots)
|
|
266
|
+
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
# Robot management
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
def add_robot(self, robot: "Robot") -> None:
|
|
272
|
+
"""Register a robot with this simulation.
|
|
273
|
+
|
|
274
|
+
This is called automatically by :meth:`Robot.load` when ``sim`` is
|
|
275
|
+
provided, so you rarely need to call this directly.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
robot: A :class:`~bulletlab.robot.robot.Robot` instance.
|
|
279
|
+
|
|
280
|
+
Example::
|
|
281
|
+
|
|
282
|
+
sim.add_robot(robot)
|
|
283
|
+
"""
|
|
284
|
+
if robot not in self._robots:
|
|
285
|
+
self._robots.append(robot)
|
|
286
|
+
|
|
287
|
+
def remove_robot(self, robot: "Robot") -> None:
|
|
288
|
+
"""Unregister a robot from this simulation.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
robot: The robot to remove.
|
|
292
|
+
"""
|
|
293
|
+
if robot in self._robots:
|
|
294
|
+
self._robots.remove(robot)
|
|
295
|
+
|
|
296
|
+
# ------------------------------------------------------------------
|
|
297
|
+
# Utilities
|
|
298
|
+
# ------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
def add_debug_text(
|
|
301
|
+
self,
|
|
302
|
+
text: str,
|
|
303
|
+
position: tuple[float, float, float],
|
|
304
|
+
color: tuple[float, float, float] = (1.0, 1.0, 1.0),
|
|
305
|
+
size: float = 1.5,
|
|
306
|
+
) -> int:
|
|
307
|
+
"""Add a text label to the PyBullet debug visualizer.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
text: Text to display.
|
|
311
|
+
position: 3D world position ``(x, y, z)``.
|
|
312
|
+
color: RGB color normalized to [0, 1].
|
|
313
|
+
size: Text size multiplier.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Debug item ID (can be used to remove later).
|
|
317
|
+
"""
|
|
318
|
+
if not self._connected:
|
|
319
|
+
return -1
|
|
320
|
+
return p.addUserDebugText(
|
|
321
|
+
text,
|
|
322
|
+
list(position),
|
|
323
|
+
textColorRGB=list(color),
|
|
324
|
+
textSize=size,
|
|
325
|
+
physicsClientId=self._client_id,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def remove_debug_item(self, item_id: int) -> None:
|
|
329
|
+
"""Remove a debug visualizer item by ID.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
item_id: ID returned by :meth:`add_debug_text` or similar.
|
|
333
|
+
"""
|
|
334
|
+
if self._connected:
|
|
335
|
+
p.removeUserDebugItem(item_id, physicsClientId=self._client_id)
|
|
336
|
+
|
|
337
|
+
def set_camera(
|
|
338
|
+
self,
|
|
339
|
+
distance: float = 3.0,
|
|
340
|
+
yaw: float = 50.0,
|
|
341
|
+
pitch: float = -35.0,
|
|
342
|
+
target: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Set the PyBullet debug camera position.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
distance: Camera distance from target.
|
|
348
|
+
yaw: Camera yaw angle in degrees.
|
|
349
|
+
pitch: Camera pitch angle in degrees.
|
|
350
|
+
target: Camera target position ``(x, y, z)``.
|
|
351
|
+
"""
|
|
352
|
+
if self._connected and self._mode == p.GUI:
|
|
353
|
+
p.resetDebugVisualizerCamera(
|
|
354
|
+
cameraDistance=distance,
|
|
355
|
+
cameraYaw=yaw,
|
|
356
|
+
cameraPitch=pitch,
|
|
357
|
+
cameraTargetPosition=list(target),
|
|
358
|
+
physicsClientId=self._client_id,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# ------------------------------------------------------------------
|
|
362
|
+
# Context manager support
|
|
363
|
+
# ------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
def __enter__(self) -> "Simulation":
|
|
366
|
+
self.start()
|
|
367
|
+
return self
|
|
368
|
+
|
|
369
|
+
def __exit__(self, *_: object) -> None:
|
|
370
|
+
self.stop()
|
|
371
|
+
|
|
372
|
+
def __repr__(self) -> str:
|
|
373
|
+
status = "connected" if self._connected else "disconnected"
|
|
374
|
+
return (
|
|
375
|
+
f"Simulation(mode={self._mode_str!r}, {status}, "
|
|
376
|
+
f"step={self._step_count}, t={self.elapsed_time:.3f}s)"
|
|
377
|
+
)
|
bulletlab/core/world.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
World – helper for populating a BulletLab simulation with environment objects.
|
|
3
|
+
|
|
4
|
+
The World class provides convenient methods for loading static environment
|
|
5
|
+
geometry (ground planes, terrain, obstacles) into the simulation.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
from bulletlab import Simulation
|
|
10
|
+
from bulletlab.core.world import World
|
|
11
|
+
|
|
12
|
+
sim = Simulation().start()
|
|
13
|
+
world = World(sim)
|
|
14
|
+
world.load_plane()
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
import pybullet as p
|
|
23
|
+
import pybullet_data
|
|
24
|
+
|
|
25
|
+
from bulletlab.core.simulation import Simulation
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class World:
|
|
29
|
+
"""Helper for populating the simulation environment with static geometry.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
sim: The :class:`~bulletlab.core.simulation.Simulation` instance to use.
|
|
33
|
+
|
|
34
|
+
Example::
|
|
35
|
+
|
|
36
|
+
sim = Simulation().start()
|
|
37
|
+
world = World(sim)
|
|
38
|
+
world.load_plane()
|
|
39
|
+
world.load_urdf("box.urdf", position=(2, 0, 0.5))
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, sim: Simulation) -> None:
|
|
43
|
+
self._sim = sim
|
|
44
|
+
self._bodies: list[int] = []
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# Loaders
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def load_plane(
|
|
51
|
+
self,
|
|
52
|
+
texture_scale: float = 1.0,
|
|
53
|
+
) -> int:
|
|
54
|
+
"""Load the standard flat ground plane.
|
|
55
|
+
|
|
56
|
+
Uses the ``plane.urdf`` asset bundled with pybullet_data.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
texture_scale: Not used currently; reserved for future texture
|
|
60
|
+
scaling support.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
PyBullet body ID of the plane.
|
|
64
|
+
|
|
65
|
+
Example::
|
|
66
|
+
|
|
67
|
+
world.load_plane()
|
|
68
|
+
"""
|
|
69
|
+
body_id = p.loadURDF(
|
|
70
|
+
"plane.urdf",
|
|
71
|
+
physicsClientId=self._sim.client_id,
|
|
72
|
+
)
|
|
73
|
+
self._bodies.append(body_id)
|
|
74
|
+
return body_id
|
|
75
|
+
|
|
76
|
+
def load_urdf(
|
|
77
|
+
self,
|
|
78
|
+
path: str | Path,
|
|
79
|
+
position: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
80
|
+
orientation: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0),
|
|
81
|
+
fixed: bool = True,
|
|
82
|
+
scale: float = 1.0,
|
|
83
|
+
) -> int:
|
|
84
|
+
"""Load an arbitrary URDF as a static or dynamic object.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
path: Path to the URDF file. Can be an absolute path or a
|
|
88
|
+
filename resolvable from the pybullet_data search path.
|
|
89
|
+
position: Initial position ``(x, y, z)`` in meters.
|
|
90
|
+
orientation: Initial orientation as a quaternion ``(x, y, z, w)``.
|
|
91
|
+
fixed: If ``True``, the base link is fixed to the world frame.
|
|
92
|
+
scale: Global scale factor for the loaded model.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
PyBullet body ID.
|
|
96
|
+
|
|
97
|
+
Example::
|
|
98
|
+
|
|
99
|
+
box_id = world.load_urdf("cube_small.urdf", position=(1, 0, 0.5))
|
|
100
|
+
"""
|
|
101
|
+
flags = p.URDF_USE_INERTIA_FROM_FILE
|
|
102
|
+
body_id = p.loadURDF(
|
|
103
|
+
str(path),
|
|
104
|
+
basePosition=list(position),
|
|
105
|
+
baseOrientation=list(orientation),
|
|
106
|
+
useFixedBase=fixed,
|
|
107
|
+
globalScaling=scale,
|
|
108
|
+
flags=flags,
|
|
109
|
+
physicsClientId=self._sim.client_id,
|
|
110
|
+
)
|
|
111
|
+
self._bodies.append(body_id)
|
|
112
|
+
return body_id
|
|
113
|
+
|
|
114
|
+
def load_sdf(
|
|
115
|
+
self,
|
|
116
|
+
path: str | Path,
|
|
117
|
+
) -> list[int]:
|
|
118
|
+
"""Load an SDF file and return all resulting body IDs.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
path: Path to the SDF file.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of PyBullet body IDs created.
|
|
125
|
+
"""
|
|
126
|
+
ids = p.loadSDF(str(path), physicsClientId=self._sim.client_id)
|
|
127
|
+
self._bodies.extend(ids)
|
|
128
|
+
return list(ids)
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Convenience factories
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def set_gravity(self, gx: float = 0.0, gy: float = 0.0, gz: float = -9.81) -> None:
|
|
135
|
+
"""Set simulation gravity.
|
|
136
|
+
|
|
137
|
+
Convenience shorthand for :attr:`Simulation.gravity`.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
gx: X component in m/s².
|
|
141
|
+
gy: Y component in m/s².
|
|
142
|
+
gz: Z component in m/s².
|
|
143
|
+
|
|
144
|
+
Example::
|
|
145
|
+
|
|
146
|
+
world.set_gravity(gz=-1.62) # Moon
|
|
147
|
+
"""
|
|
148
|
+
self._sim.gravity = (gx, gy, gz)
|
|
149
|
+
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
# Cleanup
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
def clear(self) -> None:
|
|
155
|
+
"""Remove all objects loaded by this World instance.
|
|
156
|
+
|
|
157
|
+
Does not reset the entire simulation — only removes objects
|
|
158
|
+
that this :class:`World` instance created.
|
|
159
|
+
"""
|
|
160
|
+
for body_id in self._bodies:
|
|
161
|
+
try:
|
|
162
|
+
p.removeBody(body_id, physicsClientId=self._sim.client_id)
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
self._bodies.clear()
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def body_ids(self) -> list[int]:
|
|
169
|
+
"""List of all PyBullet body IDs managed by this World."""
|
|
170
|
+
return list(self._bodies)
|
|
171
|
+
|
|
172
|
+
def __repr__(self) -> str:
|
|
173
|
+
return f"World(bodies={len(self._bodies)})"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CSV writer backend for DataLogger.
|
|
3
|
+
|
|
4
|
+
Internal module — used by :class:`~bulletlab.logging.logger.DataLogger`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import csv
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CsvWriter:
|
|
15
|
+
"""Writes tabular log data to a CSV file.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
filepath: Output file path.
|
|
19
|
+
fieldnames: Column names.
|
|
20
|
+
|
|
21
|
+
Example::
|
|
22
|
+
|
|
23
|
+
writer = CsvWriter("run1.csv", ["t", "speed", "roll"])
|
|
24
|
+
writer.write_row({"t": 0.0, "speed": 1.5, "roll": 0.01})
|
|
25
|
+
writer.close()
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, filepath: str | Path, fieldnames: list[str]) -> None:
|
|
29
|
+
self._filepath = Path(filepath)
|
|
30
|
+
self._fieldnames = fieldnames
|
|
31
|
+
self._file = open(self._filepath, "w", newline="", encoding="utf-8")
|
|
32
|
+
self._writer = csv.DictWriter(self._file, fieldnames=self._fieldnames)
|
|
33
|
+
self._writer.writeheader()
|
|
34
|
+
|
|
35
|
+
def write_row(self, row: dict[str, Any]) -> None:
|
|
36
|
+
"""Write a single row to the CSV file.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
row: Dictionary mapping column names to values.
|
|
40
|
+
"""
|
|
41
|
+
self._writer.writerow(row)
|
|
42
|
+
|
|
43
|
+
def flush(self) -> None:
|
|
44
|
+
"""Flush the write buffer to disk."""
|
|
45
|
+
self._file.flush()
|
|
46
|
+
|
|
47
|
+
def close(self) -> None:
|
|
48
|
+
"""Close the file handle."""
|
|
49
|
+
if not self._file.closed:
|
|
50
|
+
self._file.flush()
|
|
51
|
+
self._file.close()
|
|
52
|
+
|
|
53
|
+
def __del__(self) -> None:
|
|
54
|
+
self.close()
|