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,26 @@
1
+ """
2
+ BulletLab utils subpackage.
3
+
4
+ Provides mathematical helpers, URDF discovery utilities, and simulation timers.
5
+ """
6
+
7
+ from bulletlab.utils.math_utils import (
8
+ quaternion_to_euler,
9
+ euler_to_quaternion,
10
+ normalize,
11
+ clamp,
12
+ lerp,
13
+ )
14
+ from bulletlab.utils.timer import SimTimer
15
+ from bulletlab.utils.urdf_utils import find_urdf, list_available_urdfs
16
+
17
+ __all__ = [
18
+ "quaternion_to_euler",
19
+ "euler_to_quaternion",
20
+ "normalize",
21
+ "clamp",
22
+ "lerp",
23
+ "SimTimer",
24
+ "find_urdf",
25
+ "list_available_urdfs",
26
+ ]
@@ -0,0 +1,183 @@
1
+ """
2
+ Mathematical utilities for BulletLab.
3
+
4
+ Provides quaternion/Euler conversion, vector normalization, clamping,
5
+ and linear interpolation — all without external dependencies beyond NumPy.
6
+
7
+ Example::
8
+
9
+ from bulletlab.utils.math_utils import quaternion_to_euler, euler_to_quaternion
10
+
11
+ roll, pitch, yaw = quaternion_to_euler((0, 0, 0, 1))
12
+ q = euler_to_quaternion(0.1, 0.2, 0.3)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import Sequence
19
+
20
+ import numpy as np
21
+
22
+
23
+ def quaternion_to_euler(
24
+ q: Sequence[float],
25
+ ) -> tuple[float, float, float]:
26
+ """Convert a quaternion to Euler angles (roll, pitch, yaw).
27
+
28
+ Uses the ZYX (yaw-pitch-roll) convention, matching PyBullet's
29
+ ``getEulerFromQuaternion``.
30
+
31
+ Args:
32
+ q: Quaternion ``(x, y, z, w)``.
33
+
34
+ Returns:
35
+ Euler angles ``(roll, pitch, yaw)`` in radians.
36
+
37
+ Example::
38
+
39
+ roll, pitch, yaw = quaternion_to_euler(robot.base_orientation)
40
+ """
41
+ x, y, z, w = float(q[0]), float(q[1]), float(q[2]), float(q[3])
42
+
43
+ # Roll (rotation around X)
44
+ sinr_cosp = 2.0 * (w * x + y * z)
45
+ cosr_cosp = 1.0 - 2.0 * (x * x + y * y)
46
+ roll = math.atan2(sinr_cosp, cosr_cosp)
47
+
48
+ # Pitch (rotation around Y)
49
+ sinp = 2.0 * (w * y - z * x)
50
+ sinp = max(-1.0, min(1.0, sinp)) # clamp for numerical safety
51
+ pitch = math.asin(sinp)
52
+
53
+ # Yaw (rotation around Z)
54
+ siny_cosp = 2.0 * (w * z + x * y)
55
+ cosy_cosp = 1.0 - 2.0 * (y * y + z * z)
56
+ yaw = math.atan2(siny_cosp, cosy_cosp)
57
+
58
+ return (roll, pitch, yaw)
59
+
60
+
61
+ def euler_to_quaternion(
62
+ roll: float,
63
+ pitch: float,
64
+ yaw: float,
65
+ ) -> tuple[float, float, float, float]:
66
+ """Convert Euler angles (roll, pitch, yaw) to a quaternion.
67
+
68
+ Uses the ZYX (yaw-pitch-roll) convention, matching PyBullet's
69
+ ``getQuaternionFromEuler``.
70
+
71
+ Args:
72
+ roll: Rotation around X axis in radians.
73
+ pitch: Rotation around Y axis in radians.
74
+ yaw: Rotation around Z axis in radians.
75
+
76
+ Returns:
77
+ Quaternion ``(x, y, z, w)``.
78
+
79
+ Example::
80
+
81
+ q = euler_to_quaternion(0.0, 0.0, math.pi / 2)
82
+ """
83
+ cr = math.cos(roll * 0.5)
84
+ sr = math.sin(roll * 0.5)
85
+ cp = math.cos(pitch * 0.5)
86
+ sp = math.sin(pitch * 0.5)
87
+ cy = math.cos(yaw * 0.5)
88
+ sy = math.sin(yaw * 0.5)
89
+
90
+ w = cr * cp * cy + sr * sp * sy
91
+ x = sr * cp * cy - cr * sp * sy
92
+ y = cr * sp * cy + sr * cp * sy
93
+ z = cr * cp * sy - sr * sp * cy
94
+
95
+ return (x, y, z, w)
96
+
97
+
98
+ def normalize(v: Sequence[float]) -> np.ndarray:
99
+ """Return the unit vector of a vector.
100
+
101
+ Args:
102
+ v: Input vector (any length).
103
+
104
+ Returns:
105
+ Unit vector as a NumPy array, or the zero vector if norm is zero.
106
+
107
+ Example::
108
+
109
+ direction = normalize([1.0, 2.0, 3.0])
110
+ """
111
+ arr = np.asarray(v, dtype=np.float64)
112
+ norm = np.linalg.norm(arr)
113
+ if norm == 0.0:
114
+ return arr
115
+ return arr / norm
116
+
117
+
118
+ def clamp(value: float, lo: float, hi: float) -> float:
119
+ """Clamp a scalar value to the range ``[lo, hi]``.
120
+
121
+ Args:
122
+ value: Input value.
123
+ lo: Lower bound.
124
+ hi: Upper bound.
125
+
126
+ Returns:
127
+ Clamped value.
128
+
129
+ Example::
130
+
131
+ safe_speed = clamp(desired_speed, -10.0, 10.0)
132
+ """
133
+ return max(float(lo), min(float(hi), float(value)))
134
+
135
+
136
+ def lerp(a: float, b: float, t: float) -> float:
137
+ """Linear interpolation between two scalar values.
138
+
139
+ Args:
140
+ a: Start value.
141
+ b: End value.
142
+ t: Interpolation parameter in ``[0, 1]``.
143
+
144
+ Returns:
145
+ Interpolated value ``a + t * (b - a)``.
146
+
147
+ Example::
148
+
149
+ mid = lerp(0.0, 10.0, 0.5) # → 5.0
150
+ """
151
+ return float(a) + float(t) * (float(b) - float(a))
152
+
153
+
154
+ def wrap_angle(angle: float) -> float:
155
+ """Wrap an angle to the range ``[-π, π]``.
156
+
157
+ Args:
158
+ angle: Angle in radians.
159
+
160
+ Returns:
161
+ Wrapped angle in radians.
162
+
163
+ Example::
164
+
165
+ wrapped = wrap_angle(4.0) # → 4.0 - 2π ≈ -2.28
166
+ """
167
+ return math.atan2(math.sin(angle), math.cos(angle))
168
+
169
+
170
+ def vec3_magnitude(v: Sequence[float]) -> float:
171
+ """Return the magnitude (L2 norm) of a 3D vector.
172
+
173
+ Args:
174
+ v: Vector ``(x, y, z)``.
175
+
176
+ Returns:
177
+ Scalar magnitude.
178
+
179
+ Example::
180
+
181
+ speed = vec3_magnitude(robot.base_velocity)
182
+ """
183
+ return math.sqrt(v[0] ** 2 + v[1] ** 2 + v[2] ** 2)
@@ -0,0 +1,107 @@
1
+ """
2
+ SimTimer – a utility for tracking simulation time and wall-clock time.
3
+
4
+ Example::
5
+
6
+ from bulletlab.utils.timer import SimTimer
7
+
8
+ timer = SimTimer(timestep=1/240)
9
+ for _ in range(1000):
10
+ sim.step()
11
+ timer.tick()
12
+ print(f"Sim time: {timer.sim_time:.3f}s Wall time: {timer.wall_time:.3f}s")
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import time
18
+
19
+
20
+ class SimTimer:
21
+ """Tracks simulation step count, simulated time, and wall-clock time.
22
+
23
+ Args:
24
+ timestep: Simulation timestep in seconds (should match
25
+ :attr:`Simulation.timestep`).
26
+
27
+ Example::
28
+
29
+ timer = SimTimer(timestep=1/240)
30
+ timer.tick() # call each sim step
31
+ print(timer.sim_time) # time in seconds
32
+ print(timer.wall_time) # real-world elapsed seconds
33
+ print(timer.step_count) # total ticks
34
+ """
35
+
36
+ def __init__(self, timestep: float = 1.0 / 240.0) -> None:
37
+ self._timestep = timestep
38
+ self._step_count: int = 0
39
+ self._start_time: float = time.monotonic()
40
+ self._last_tick_time: float = self._start_time
41
+
42
+ def tick(self) -> None:
43
+ """Advance the timer by one simulation step.
44
+
45
+ Call this once per call to :meth:`Simulation.step`.
46
+
47
+ Example::
48
+
49
+ timer.tick()
50
+ """
51
+ self._step_count += 1
52
+ self._last_tick_time = time.monotonic()
53
+
54
+ def reset(self) -> None:
55
+ """Reset all counters to zero.
56
+
57
+ Example::
58
+
59
+ timer.reset()
60
+ """
61
+ self._step_count = 0
62
+ self._start_time = time.monotonic()
63
+ self._last_tick_time = self._start_time
64
+
65
+ @property
66
+ def step_count(self) -> int:
67
+ """Total number of :meth:`tick` calls since creation or last :meth:`reset`."""
68
+ return self._step_count
69
+
70
+ @property
71
+ def sim_time(self) -> float:
72
+ """Simulated elapsed time in seconds (``step_count * timestep``)."""
73
+ return self._step_count * self._timestep
74
+
75
+ @property
76
+ def wall_time(self) -> float:
77
+ """Real-world elapsed time in seconds since creation or last :meth:`reset`."""
78
+ return time.monotonic() - self._start_time
79
+
80
+ @property
81
+ def timestep(self) -> float:
82
+ """The configured simulation timestep in seconds."""
83
+ return self._timestep
84
+
85
+ @timestep.setter
86
+ def timestep(self, value: float) -> None:
87
+ self._timestep = float(value)
88
+
89
+ @property
90
+ def real_time_factor(self) -> float:
91
+ """Ratio of simulated time to wall-clock time.
92
+
93
+ A value of 1.0 means the simulation runs in real-time.
94
+ A value > 1.0 means the simulation is running faster than real-time.
95
+ """
96
+ wt = self.wall_time
97
+ if wt == 0.0:
98
+ return 0.0
99
+ return self.sim_time / wt
100
+
101
+ def __repr__(self) -> str:
102
+ return (
103
+ f"SimTimer(step={self._step_count}, "
104
+ f"sim_t={self.sim_time:.3f}s, "
105
+ f"wall_t={self.wall_time:.3f}s, "
106
+ f"rtf={self.real_time_factor:.2f})"
107
+ )
@@ -0,0 +1,114 @@
1
+ """
2
+ URDF discovery utilities for BulletLab.
3
+
4
+ Provides helpers for finding URDF files in the pybullet_data asset library
5
+ and scanning user-provided search paths.
6
+
7
+ Example::
8
+
9
+ from bulletlab.utils.urdf_utils import find_urdf, list_available_urdfs
10
+
11
+ path = find_urdf("kuka_iiwa/model.urdf")
12
+ print(list_available_urdfs())
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+ import pybullet_data
21
+
22
+
23
+ def get_pybullet_data_path() -> Path:
24
+ """Return the path to the pybullet_data asset directory.
25
+
26
+ Returns:
27
+ :class:`pathlib.Path` pointing to pybullet_data.
28
+
29
+ Example::
30
+
31
+ data_path = get_pybullet_data_path()
32
+ """
33
+ return Path(pybullet_data.getDataPath())
34
+
35
+
36
+ def find_urdf(
37
+ filename: str,
38
+ extra_search_paths: list[str | Path] | None = None,
39
+ ) -> Path:
40
+ """Search for a URDF file by name.
41
+
42
+ Searches in the following order:
43
+ 1. Absolute path (if ``filename`` is absolute and exists)
44
+ 2. Extra user-provided search paths
45
+ 3. pybullet_data directory (recursively)
46
+
47
+ Args:
48
+ filename: Filename or relative path (e.g. ``"kuka_iiwa/model.urdf"``).
49
+ extra_search_paths: Additional directories to search.
50
+
51
+ Returns:
52
+ Resolved :class:`pathlib.Path` to the URDF.
53
+
54
+ Raises:
55
+ FileNotFoundError: If the file cannot be found in any search location.
56
+
57
+ Example::
58
+
59
+ path = find_urdf("plane.urdf")
60
+ path = find_urdf("kuka_iiwa/model.urdf")
61
+ """
62
+ # 1. Absolute path
63
+ p = Path(filename)
64
+ if p.is_absolute() and p.exists():
65
+ return p
66
+
67
+ # 2. Extra search paths
68
+ search_roots: list[Path] = []
69
+ if extra_search_paths:
70
+ search_roots.extend(Path(sp) for sp in extra_search_paths)
71
+
72
+ # 3. pybullet_data
73
+ search_roots.append(get_pybullet_data_path())
74
+
75
+ for root in search_roots:
76
+ # Direct join
77
+ candidate = root / filename
78
+ if candidate.exists():
79
+ return candidate
80
+ # Recursive search by basename
81
+ basename = Path(filename).name
82
+ for found in root.rglob(basename):
83
+ if found.is_file():
84
+ return found
85
+
86
+ raise FileNotFoundError(
87
+ f"URDF/MJCF file not found: {filename!r}. "
88
+ f"Searched: {[str(r) for r in search_roots]}"
89
+ )
90
+
91
+
92
+ def list_available_urdfs(max_results: int = 200) -> list[str]:
93
+ """List URDF and MJCF files available in pybullet_data.
94
+
95
+ Args:
96
+ max_results: Maximum number of results to return.
97
+
98
+ Returns:
99
+ List of relative paths (relative to pybullet_data root).
100
+
101
+ Example::
102
+
103
+ for name in list_available_urdfs():
104
+ print(name)
105
+ """
106
+ root = get_pybullet_data_path()
107
+ results: list[str] = []
108
+ for ext in ("*.urdf", "*.xml", "*.sdf"):
109
+ for path in root.rglob(ext):
110
+ rel = str(path.relative_to(root))
111
+ results.append(rel)
112
+ if len(results) >= max_results:
113
+ return sorted(results)
114
+ return sorted(results)
@@ -0,0 +1,284 @@
1
+ Metadata-Version: 2.4
2
+ Name: BulletLab
3
+ Version: 0.1.2
4
+ Summary: A high-level robotics simulation and experimentation framework built on PyBullet.
5
+ Home-page: https://github.com/NuclearVenom/BulletLab
6
+ Author: Ranasurya Ghosh
7
+ Author-email: Ranasurya Ghosh <ranasuryaghosh@gmail.com>
8
+ Maintainer-email: Ranasurya Ghosh <ranasuryaghosh@gmail.com>
9
+ License-Expression: MIT
10
+ Project-URL: Homepage, https://github.com/NuclearVenom/BulletLab
11
+ Project-URL: Documentation, https://nuclearvenom.bulletlab.docs
12
+ Project-URL: Repository, https://github.com/NuclearVenom/BulletLab
13
+ Project-URL: Issues, https://github.com/NuclearVenom/BulletLab/issues
14
+ Keywords: robotics,simulation,pybullet,reinforcement-learning,telemetry,imgui,physics,research,robot-control,robotics-framework
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Education
18
+ Classifier: Intended Audience :: Science/Research
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Scientific/Engineering
25
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: pybullet>=3.2.6
31
+ Requires-Dist: numpy>=1.24.0
32
+ Requires-Dist: pandas>=2.0.0
33
+ Requires-Dist: pyyaml>=6.0
34
+ Requires-Dist: imgui[glfw]>=2.0.0
35
+ Requires-Dist: pyqtgraph>=0.13.3
36
+ Requires-Dist: PyQt5>=5.15.0
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
39
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
40
+ Requires-Dist: pytest-mock>=3.11.0; extra == "dev"
41
+ Requires-Dist: mkdocs>=1.5.0; extra == "dev"
42
+ Requires-Dist: mkdocstrings[python]>=0.23.0; extra == "dev"
43
+ Requires-Dist: mkdocs-material>=9.4.0; extra == "dev"
44
+ Dynamic: author
45
+ Dynamic: home-page
46
+ Dynamic: license-file
47
+
48
+ <h1>
49
+ <img src="https://raw.githubusercontent.com/NuclearVenom/BulletLab/main/assets/logo.png" width="40" align="center" alt="[logo]">
50
+ BulletLab
51
+ </h1>
52
+
53
+ Developed by [Ranasurya Ghosh](https://github.com/NuclearVenom)
54
+
55
+
56
+ >**A robotics experimentation framework that transforms PyBullet robots into intuitive Python objects, with modern ImGui-based controls, telemetry, visualization, and reinforcement learning workflows.**
57
+
58
+ [![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/)
59
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
60
+
61
+ **Install BulletLab library:** `pip install bulletlab`<br><br>
62
+ [Read Documentation](https://nuclearvenom.github.io/BulletLab/)
63
+ <br><br>
64
+
65
+ ![BulletLab example UI](https://raw.githubusercontent.com/NuclearVenom/BulletLab/main/assets/bulletlab_ui.png)
66
+
67
+ ---
68
+
69
+ ## What is BulletLab?
70
+
71
+ BulletLab provides a high-level object-oriented interface to [PyBullet](https://pybullet.org/wordpress/) that simplifies robotics experimentation by exposing joints, links, sensors, and environments as intuitive Python objects instead of raw physics engine IDs. It combines real-time simulation with a [ImGui](https://www.dearimgui.com/)-powered modern interface for interactive control, parameter tuning, telemetry visualization, and experiment management, while also offering reinforcement learning integration for training and evaluating autonomous robotic systems within a unified workflow.
72
+
73
+ **Instead of this:**
74
+ ```python
75
+ p.setJointMotorControl2(
76
+ robot_id, joint_index,
77
+ controlMode=p.VELOCITY_CONTROL,
78
+ targetVelocity=15,
79
+ force=100
80
+ )
81
+ ```
82
+
83
+ **You write this:**
84
+ ```python
85
+ robot.joints["motor"].velocity = 15
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Architecture
91
+
92
+ BulletLab uses a **two-window architecture**:
93
+
94
+ | Window | Purpose |
95
+ |--------|---------|
96
+ | PyBullet Native Window | Physics simulation, 3D rendering, camera |
97
+ | BulletLab ImGui Window | Control panels, telemetry, live plots, console |
98
+
99
+ These windows communicate through Python objects. BulletLab does **not** attempt to replace PyBullet's renderer or embed ImGui inside the simulation viewport.
100
+
101
+ ---
102
+
103
+ ## Quick Start
104
+
105
+ ### Installation
106
+
107
+ ```bash
108
+ pip install bulletlab
109
+ # or from source:
110
+ pip install -e .
111
+ ```
112
+
113
+ ### Basic Example
114
+
115
+ ```python
116
+ from bulletlab import Simulation, Robot
117
+ from bulletlab.ui import BulletLabUI
118
+
119
+ # Create simulation
120
+ sim = Simulation()
121
+ sim.start()
122
+
123
+ # Load robot
124
+ robot = Robot.load("path/to/robot.urdf", sim=sim)
125
+
126
+ # Control joints by name
127
+ robot.joints["wheel_left"].velocity = 10
128
+ robot.joints["wheel_right"].velocity = 10
129
+
130
+ # Modify physics parameters
131
+ robot.links["chassis"].mass = 5.0
132
+ robot.links["wheel_fl"].friction = 1.2
133
+
134
+ # Get robot state
135
+ state = robot.get_state()
136
+ print(f"Position: {robot.base_position}")
137
+ print(f"Roll: {robot.roll:.2f}°")
138
+
139
+ # Build UI
140
+ ui = BulletLabUI(sim=sim)
141
+ ui.register_panel(...)
142
+ ui.run()
143
+ ```
144
+
145
+ ### Telemetry & Logging
146
+
147
+ ```python
148
+ from bulletlab.telemetry import TelemetryManager
149
+ from bulletlab.logging import DataLogger
150
+
151
+ telemetry = TelemetryManager()
152
+ telemetry.watch("Speed", lambda: robot.base_velocity[0])
153
+ telemetry.watch("Roll", lambda: robot.roll)
154
+
155
+ logger = DataLogger()
156
+ logger.watch("speed", lambda: robot.base_velocity[0])
157
+ logger.start("run1.csv")
158
+
159
+ for _ in range(1000):
160
+ sim.step()
161
+ telemetry.update()
162
+ logger.step()
163
+
164
+ logger.stop()
165
+ ```
166
+
167
+ ### Live Plotting
168
+
169
+ ```python
170
+ from bulletlab.plotting import LivePlot
171
+
172
+ plot = LivePlot(title="Robot Speed")
173
+ plot.watch("Speed", lambda: robot.base_velocity[0], color="#00ff88")
174
+ plot.start()
175
+
176
+ for _ in range(1000):
177
+ sim.step()
178
+ plot.update()
179
+ ```
180
+
181
+ ### ImGui Control Panel
182
+
183
+ ```python
184
+ from bulletlab.ui import BulletLabUI
185
+ from bulletlab.ui import widgets as ui
186
+
187
+ app = BulletLabUI(sim=sim, robots=[robot])
188
+
189
+ @app.custom_panel("My Controls")
190
+ def my_panel():
191
+ ui.button("Reset", robot.reset)
192
+ ui.slider("Wheel Mass", robot.links["wheel"].mass, 0.1, 20,
193
+ setter=lambda v: setattr(robot.links["wheel"], "mass", v))
194
+ ui.checkbox("Motors Enabled", lambda: motors_on,
195
+ setter=lambda v: toggle_motors(v))
196
+
197
+ app.run()
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Supported Robot Types
203
+
204
+ BulletLab is completely generic — no code assumes a specific robot type:
205
+
206
+ - Cars & rovers
207
+ - Drones & quadrotors
208
+ - Robotic arms
209
+ - Self-balancing robots
210
+ - Quadrupeds
211
+ - Humanoids
212
+ - Custom mechanisms
213
+
214
+ ---
215
+
216
+ ## Reinforcement Learning
217
+
218
+ BulletLab exposes clean state/action interfaces without depending on any ML framework:
219
+
220
+ ```python
221
+ # Compatible with any RL approach
222
+ state = robot.get_state() # → numpy array
223
+ action = my_policy(state) # → numpy array
224
+ robot.apply_action(action) # → updates joints
225
+
226
+ # Manual Q-learning, SARSA, evolutionary algorithms — all supported
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Examples
232
+
233
+ | Example | Description |
234
+ |---------|-------------|
235
+ | `examples/01_differential_drive_rover.py` | Rover with wheel velocity control |
236
+ | `examples/02_robotic_arm.py` | Joint position control with ImGui sliders |
237
+ | `examples/03_self_balancing_robot.py` | PD controller for balance |
238
+ | `examples/04_drone_parameter_tuning.py` | Thrust/mass parameter exploration |
239
+ | `examples/05_generic_robot_inspector.py` | Load any URDF and inspect it |
240
+
241
+ Run any example:
242
+ ```bash
243
+ python examples/01_differential_drive_rover.py
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Documentation
249
+
250
+ ```bash
251
+ pip install -e ".[dev]"
252
+ mkdocs serve
253
+ ```
254
+
255
+ Then visit http://localhost:8000
256
+
257
+ ---
258
+
259
+ ## Testing
260
+
261
+ ```bash
262
+ pip install -e ".[dev]"
263
+ pytest tests/ -v --cov=bulletlab --cov-report=term-missing
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Technology Stack
269
+
270
+ | Component | Library |
271
+ |-----------|---------|
272
+ | Physics | PyBullet |
273
+ | UI | Dear ImGui (pyimgui) |
274
+ | Data | NumPy, Pandas |
275
+ | Config | PyYAML |
276
+ | Plotting | PyQtGraph |
277
+ | Testing | PyTest |
278
+ | Docs | MkDocs + mkdocstrings |
279
+
280
+ ---
281
+
282
+ ## License
283
+
284
+ MIT License — see [LICENSE](LICENSE) for details.