Simulation Module User Guide
============================
The Simulation Module provides a comprehensive PyBullet-based environment for visualizing,
testing, and interacting with your ManipulaPy-powered robot models. It supports
real-time control, trajectory playback, collision checking, data logging, and GPU-accelerated
computations through CuPy integration.
.. note::
This guide assumes Python 3.10+, PyBullet 3.2.5+, CuPy 12.0+, and Matplotlib 3.5+.
For optimal performance, CUDA-capable GPU is recommended but not required.
.. contents:: Table of Contents
:depth: 3
:local:
:backlinks: none
Installation and Setup
----------------------
Prerequisites
~~~~~~~~~~~~~
Before using the Simulation module, ensure you have the following dependencies:
**Core Dependencies**
.. code-block:: bash
pip install ManipulaPy[simulation] pybullet>=3.2.5 matplotlib>=3.9 "numpy>=2.0,<3.0"
**GPU Acceleration (Optional but Recommended)**
.. code-block:: bash
pip install cupy-cuda11x # For CUDA 11.x
# or
pip install cupy-cuda12x # For CUDA 12.x
**URDF Processing**
The native URDF parser is included with ManipulaPy. Install
``ManipulaPy[urdf]`` only when you need optional ``trimesh`` mesh loading.
Verification
~~~~~~~~~~~~
Verify your installation with this simple test:
.. code-block:: python
import pybullet as p
import ManipulaPy.sim as sim
import cupy as cp
print("PyBullet version:", p.__version__)
print("CuPy version:", cp.__version__)
print("CUDA available:", cp.cuda.is_available())
print("Simulation module loaded:", hasattr(sim, "Simulation"))
.. tip::
If CUDA is not available, the simulation will automatically fall back to CPU computation
with slightly reduced performance.
Quick Start
-----------
Basic Trajectory Simulation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Launch a simple trajectory playback with your robot:
.. code-block:: python
from ManipulaPy.sim import Simulation
from ManipulaPy.ManipulaPy_data.xarm import urdf_file as xarm_urdf
import numpy as np
# Define joint limits for a 6-DOF manipulator
joint_limits = [(-3.14, 3.14)] * 6
torque_limits = [(-50, 50)] * 6 # Newton-meters
# Create Simulation instance
sim = Simulation(
urdf_file_path=xarm_urdf,
joint_limits=joint_limits,
torque_limits=torque_limits,
time_step=0.01, # 100 Hz simulation
real_time_factor=1.0 # Real-time playback
)
# Initialize robot dynamics and planning
sim.initialize_robot()
sim.initialize_planner_and_controller()
# Generate a smooth sinusoidal trajectory
t = np.linspace(0, 4*np.pi, 300)
trajectory = []
for t_i in t:
# Create smooth joint motion
joint_angles = 0.5 * np.sin(0.5 * t_i) * np.array([1, 0.8, 0.6, 0.4, 0.2, 0.1])
trajectory.append(joint_angles)
# Execute trajectory with visualization
final_ee_position = sim.run_trajectory(trajectory)
print(f"Final end-effector position: {final_ee_position}")
Interactive Manual Control
~~~~~~~~~~~~~~~~~~~~~~~~~~
Enter manual control mode for real-time robot manipulation:
.. code-block:: python
# Enter interactive mode with GUI sliders
sim.manual_control()
# This opens PyBullet GUI with:
# - Joint sliders for each degree of freedom
# - Gravity control (-20 to 20 m/s²)
# - Time step adjustment (0.001 to 0.1 s)
# - Reset button to return to home position
Module API Reference
--------------------
Simulation Class
~~~~~~~~~~~~~~~~
.. autoclass:: ManipulaPy.sim.Simulation
:no-members:
:no-index:
:undoc-members:
:show-inheritance:
:special-members: __init__
Constructor Parameters
^^^^^^^^^^^^^^^^^^^^^^
.. py:method:: __init__(urdf_file_path, joint_limits, torque_limits=None, time_step=0.01, real_time_factor=1.0, physics_client=None, enable_self_collision=False, disable_pairs=None)
:no-index:
Initialize the simulation environment.
:param str urdf_file_path: Path to the robot's URDF file
:param list joint_limits: List of (min, max) tuples for each joint in radians
:param list torque_limits: Optional list of (min, max) torques in N⋅m. If None, uses unlimited torques
:param float time_step: Physics simulation time step in seconds (default: 0.01)
:param float real_time_factor: Playback speed multiplier (1.0 = real-time, 0.5 = half-speed)
:param int physics_client: Existing PyBullet client ID, or None to create new GUI client
:param bool enable_self_collision: When True, passes PyBullet's ``URDF_USE_SELF_COLLISION`` flag to ``loadURDF``, enabling self-contact detection (default: False)
:param list disable_pairs: Optional list of ``(link_name_a, link_name_b)`` tuples whose collision response is disabled via ``setCollisionFilterPair`` after loading (default: None)
:raises FileNotFoundError: If URDF file doesn't exist
:raises ValueError: If joint_limits and URDF DOF don't match
Core Simulation Methods
^^^^^^^^^^^^^^^^^^^^^^^
**Trajectory Execution**
.. py:method:: run_trajectory(joint_trajectory) -> np.ndarray
:no-index:
Execute a sequence of joint configurations with physics simulation and visualization.
:param list joint_trajectory: List of joint angle arrays, each matching robot DOF
:return: Final end-effector position [x, y, z]
:rtype: np.ndarray
.. code-block:: python
# Example: Linear interpolation between two poses
start = np.zeros(6)
end = np.array([0.5, -0.3, 0.8, 0, -0.5, 0])
trajectory = []
for i in range(100):
alpha = i / 99.0
pose = start + alpha * (end - start)
trajectory.append(pose)
final_pos = sim.run_trajectory(trajectory)
**Open-Loop Position Tracking**
.. py:method:: run_controller(desired_positions) -> np.ndarray
:no-index:
Execute open-loop position tracking with real-time visualization. For closed-loop torque control, drive PyBullet's ``p.TORQUE_CONTROL`` mode directly. See ``CHANGELOG.md`` for the v1.3.2 migration notes.
:param array_like desired_positions: Sequence of joint configurations to visit in open-loop position control, shape (N, n_joints)
:return: Final end-effector position
:rtype: np.ndarray
**Manual Control and Interaction**
.. py:method:: manual_control()
:no-index:
Enter interactive manual control mode with GUI sliders.
Creates real-time sliders for:
- Each robot joint (within specified limits)
- Gravity magnitude and direction
- Physics time step
- Reset button for returning to home position
:raises KeyboardInterrupt: Exit manual mode with Ctrl+C
Initialization and Setup Methods
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. py:method:: initialize_robot()
:no-index:
Process URDF file and create robot dynamics model.
This method:
- Loads and parses the URDF file
- Extracts kinematic and dynamic parameters
- Creates SerialManipulator and ManipulatorDynamics instances
- Identifies non-fixed joints for control
.. py:method:: initialize_planner_and_controller()
:no-index:
Initialize trajectory planning and control modules.
Creates:
- TrajectoryPlanning instance for path generation
- ManipulatorController for closed-loop control
- Collision checking and potential field modules
State Management and Monitoring
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. py:method:: get_joint_positions() -> np.ndarray
:no-index:
Get current joint positions from simulation.
:return: Joint angles in radians
:rtype: np.ndarray
.. py:method:: set_joint_positions(joint_positions)
:no-index:
Set target joint positions for position control.
:param array_like joint_positions: Target angles in radians
.. py:method:: check_collisions()
:no-index:
Check for self-collisions and log contact points.
Queries PyBullet contact detection and logs warnings for any detected collisions
between robot links.
Data Logging and Analysis
^^^^^^^^^^^^^^^^^^^^^^^^^
.. py:method:: save_joint_states(filename="joint_states.csv")
:no-index:
Save current joint states to CSV file.
:param str filename: Output CSV filename
Creates CSV with columns: Position, Velocity for each joint.
.. py:method:: plot_trajectory_in_scene(joint_trajectory, end_effector_trajectory)
:no-index:
Create 3D visualization of end-effector trajectory.
:param list joint_trajectory: Joint angle trajectory
:param list end_effector_trajectory: End-effector position trajectory
Advanced Examples
-----------------
Open-Loop Position Tracking Simulation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demonstrate trajectory generation followed by open-loop position tracking:
.. code-block:: python
from ManipulaPy.sim import Simulation
from ManipulaPy.path_planning import TrajectoryPlanning
import numpy as np
# Setup simulation
sim = Simulation(urdf_file_path="robot.urdf",
joint_limits=[(-np.pi, np.pi)]*6,
torque_limits=[(-100, 100)]*6)
sim.initialize_robot()
sim.initialize_planner_and_controller()
# Generate reference trajectory
planner = TrajectoryPlanning(sim.robot, "robot.urdf", sim.dynamics,
sim.joint_limits, sim.torque_limits)
traj_data = planner.joint_trajectory(
thetastart=np.zeros(6),
thetaend=np.array([0.5, -0.3, 0.8, 0, -0.5, 0]),
Tf=5.0, # 5 second trajectory
N=500, # 500 waypoints
method=5 # Quintic time scaling
)
# Execute open-loop position tracking
final_pos = sim.run_controller(traj_data["positions"])
Multi-Phase Simulation Workflow
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Combine trajectory execution, manual control, and data logging:
.. code-block:: python
import time
from pathlib import Path
# Phase 1: Automated trajectory execution
print("Phase 1: Running automated trajectory...")
# Create complex multi-segment trajectory
segments = []
waypoints = [
np.array([0, 0, 0, 0, 0, 0]), # Home
np.array([0.8, -0.5, 0.3, 0.2, -0.1, 0]), # Intermediate
np.array([0.2, 0.7, -0.4, -0.3, 0.6, 0.8]) # Target
]
for i in range(len(waypoints)-1):
segment = []
for j in range(50):
alpha = j / 49.0
pose = waypoints[i] + alpha * (waypoints[i+1] - waypoints[i])
segment.append(pose)
segments.extend(segment)
sim.run_trajectory(segments)
# Phase 2: Manual inspection and adjustment
print("Phase 2: Manual control mode...")
print("Use GUI sliders to inspect robot configuration")
print("Press Reset button when done")
try:
sim.manual_control()
except KeyboardInterrupt:
print("Manual control ended")
# Phase 3: Data logging and analysis
print("Phase 3: Saving simulation data...")
# Save joint states
timestamp = int(time.time())
sim.save_joint_states(f"joint_states_{timestamp}.csv")
# Additional trajectory analysis
ee_positions = []
for joint_config in segments[-10:]: # Last 10 poses
sim.set_joint_positions(joint_config)
time.sleep(0.1)
ee_pos = sim.get_joint_positions() # Current state
ee_positions.append(ee_pos)
print(f"Trajectory analysis complete. Final EE positions saved.")
GPU-Accelerated Batch Simulation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Leverage CuPy for high-performance batch processing:
.. code-block:: python
import cupy as cp
import time
# Generate large batch of test trajectories
n_trajectories = 100
trajectory_length = 200
print(f"Generating {n_trajectories} test trajectories...")
# Create random but smooth trajectories
np.random.seed(42)
trajectories = []
for i in range(n_trajectories):
# Random waypoints
waypoints = np.random.uniform(-1, 1, (5, 6)) # 5 waypoints, 6 DOF
# Smooth interpolation
trajectory = []
for j in range(4): # 4 segments
for k in range(trajectory_length // 4):
alpha = k / (trajectory_length // 4 - 1)
pose = waypoints[j] + alpha * (waypoints[j+1] - waypoints[j])
trajectory.append(pose)
trajectories.append(trajectory)
# Batch execution with timing
execution_times = []
final_positions = []
print("Executing batch simulation...")
for i, traj in enumerate(trajectories):
start_time = time.time()
# Clip to joint limits
traj_clipped = []
for pose in traj:
clipped = np.clip(pose,
[limits[0] for limits in sim.joint_limits],
[limits[1] for limits in sim.joint_limits])
traj_clipped.append(clipped)
final_pos = sim.run_trajectory(traj_clipped)
execution_time = time.time() - start_time
execution_times.append(execution_time)
final_positions.append(final_pos)
if (i + 1) % 20 == 0:
print(f"Completed {i+1}/{n_trajectories} trajectories")
# Performance analysis
avg_time = np.mean(execution_times)
std_time = np.std(execution_times)
print(f"\nBatch Simulation Results:")
print(f"Average execution time: {avg_time:.3f} ± {std_time:.3f} seconds")
print(f"Total simulation time: {sum(execution_times):.1f} seconds")
print(f"Trajectories per minute: {60 * n_trajectories / sum(execution_times):.1f}")
Advanced Configuration
----------------------
Physics Parameter Tuning
~~~~~~~~~~~~~~~~~~~~~~~~
Fine-tune simulation physics for accuracy vs. performance:
.. code-block:: python
# High-precision configuration
sim_precise = Simulation(
urdf_file_path="robot.urdf",
joint_limits=joint_limits,
time_step=0.001, # 1000 Hz for high precision
real_time_factor=0.1 # Slow motion for detailed analysis
)
# Real-time configuration
sim_realtime = Simulation(
urdf_file_path="robot.urdf",
joint_limits=joint_limits,
time_step=0.02, # 50 Hz for real-time performance
real_time_factor=1.0 # Real-time execution
)
# After initialization, adjust physics parameters
import pybullet as p
# Set solver iterations for accuracy
p.setPhysicsEngineParameter(numSolverIterations=100)
# Enable additional collision margin
p.setPhysicsEngineParameter(contactBreakingThreshold=0.001)
# Configure constraint solving
p.setPhysicsEngineParameter(constraintSolverType=p.CONSTRAINT_SOLVER_LCP_PGS)
Custom Visualization and Debugging
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add custom visual elements and debugging information:
.. code-block:: python
import pybullet as p
def add_coordinate_frame(position, orientation, size=0.1):
"""Add coordinate frame visualization."""
# X-axis (red)
p.addUserDebugLine(position,
[position[0]+size, position[1], position[2]],
[1,0,0], lineWidth=3)
# Y-axis (green)
p.addUserDebugLine(position,
[position[0], position[1]+size, position[2]],
[0,1,0], lineWidth=3)
# Z-axis (blue)
p.addUserDebugLine(position,
[position[0], position[1], position[2]+size],
[0,0,1], lineWidth=3)
def add_workspace_visualization(sim, resolution=20):
"""Visualize robot workspace boundary."""
workspace_points = []
# Sample random joint configurations
for _ in range(1000):
joints = []
for limit in sim.joint_limits:
joints.append(np.random.uniform(limit[0], limit[1]))
# Get end-effector position
T = sim.robot.forward_kinematics(joints)
workspace_points.append(T[:3, 3])
# Draw workspace boundary points
for point in workspace_points[::10]: # Subsample for performance
p.addUserDebugLine(point, point, [0.5, 0.5, 0.5], lineWidth=1)
# Usage in simulation
sim.initialize_robot()
# Add visualization elements
add_coordinate_frame([0, 0, 0], [0, 0, 0, 1], 0.2) # World frame
add_workspace_visualization(sim)
Performance Optimization
------------------------
Memory Management
~~~~~~~~~~~~~~~~~
Optimize memory usage for long-running simulations:
.. code-block:: python
import gc
import psutil
import cupy as cp
def monitor_memory():
"""Monitor CPU and GPU memory usage."""
# CPU memory
cpu_memory = psutil.virtual_memory()
print(f"CPU Memory: {cpu_memory.percent:.1f}% used")
# GPU memory (if available)
if cp.cuda.is_available():
mempool = cp.get_default_memory_pool()
print(f"GPU Memory: {mempool.used_bytes() / 1024**3:.2f} GB used")
def cleanup_simulation():
"""Clean up memory after simulation."""
# Force garbage collection
gc.collect()
# Clear GPU memory pool
if cp.cuda.is_available():
mempool = cp.get_default_memory_pool()
mempool.free_all_blocks()
print("Memory cleanup completed")
# Example usage in long simulation loop
for i in range(100):
# Run trajectory
sim.run_trajectory(trajectory)
# Periodic cleanup
if i % 10 == 0:
monitor_memory()
cleanup_simulation()
Parallel Simulation Setup
~~~~~~~~~~~~~~~~~~~~~~~~~
Configure multiple simulation instances for parallel processing:
.. code-block:: python
import multiprocessing as mp
import pybullet as p
def run_simulation_worker(worker_id, urdf_path, joint_limits, trajectory_queue, result_queue):
"""Worker function for parallel simulation."""
# Create simulation in DIRECT mode (no GUI)
client = p.connect(p.DIRECT)
sim = Simulation(urdf_file_path=urdf_path,
joint_limits=joint_limits,
physics_client=client)
sim.initialize_robot() # redundant when physics_client= is passed; see note below
while True:
try:
trajectory = trajectory_queue.get(timeout=1)
if trajectory is None: # Shutdown signal
break
final_pos = sim.run_trajectory(trajectory)
result_queue.put((worker_id, final_pos))
except:
break
p.disconnect(client)
# Usage example
def parallel_simulation_example():
n_workers = 4
trajectory_queue = mp.Queue()
result_queue = mp.Queue()
# Start worker processes
workers = []
for i in range(n_workers):
worker = mp.Process(target=run_simulation_worker,
args=(i, "robot.urdf", joint_limits,
trajectory_queue, result_queue))
worker.start()
workers.append(worker)
# Submit trajectories
for traj in trajectories:
trajectory_queue.put(traj)
# Collect results
results = []
for _ in trajectories:
results.append(result_queue.get())
# Shutdown workers
for _ in workers:
trajectory_queue.put(None)
for worker in workers:
worker.join()
return results
.. note::
In the worker pattern above (calling only ``run_trajectory`` or
``run_controller``), ``sim.initialize_robot()`` is redundant — the
constructor already loads the robot when ``physics_client=`` is
passed. Workers that subsequently call
``initialize_planner_and_controller()`` still need ``initialize_robot()``
first, because the planner/controller setup reads ``self.robot`` and
``self.dynamics``.
Troubleshooting Guide
-------------------------
Common Issues and Solutions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**1. Black screen or no GUI**
.. code-block:: python
# Check PyBullet GUI availability
try:
client = p.connect(p.GUI)
print("GUI mode available")
except:
print("GUI mode not available, using DIRECT mode")
client = p.connect(p.DIRECT)
finally:
p.disconnect(client)
**Solutions:**
- Ensure X11 forwarding for remote systems: ``ssh -X username@hostname``
- Install GUI libraries: ``sudo apt-get install python3-tk``
- Use DIRECT mode for headless servers
**2. Joint limit violations**
.. code-block:: python
def validate_trajectory(trajectory, joint_limits):
"""Validate trajectory against joint limits."""
violations = []
for i, pose in enumerate(trajectory):
for j, (angle, (min_val, max_val)) in enumerate(zip(pose, joint_limits)):
if angle < min_val or angle > max_val:
violations.append((i, j, angle, min_val, max_val))
return violations
# Usage
violations = validate_trajectory(trajectory, sim.joint_limits)
if violations:
print(f"Found {len(violations)} joint limit violations")
for step, joint, angle, min_val, max_val in violations[:5]:
print(f"Step {step}, Joint {joint}: {angle:.3f} not in [{min_val:.3f}, {max_val:.3f}]")
**3. Simulation instability**
Reduce time step and increase solver iterations:
.. code-block:: python
# More stable configuration
sim = Simulation(urdf_file_path="robot.urdf",
joint_limits=joint_limits,
time_step=0.001) # Smaller time step
# Increase solver accuracy
p.setPhysicsEngineParameter(numSolverIterations=200)
p.setPhysicsEngineParameter(useSplitImpulse=True)
**4. Performance issues**
.. code-block:: python
# Profile simulation performance
import time
import cProfile
def profile_trajectory(sim, trajectory):
"""Profile trajectory execution."""
profiler = cProfile.Profile()
start_time = time.time()
profiler.enable()
sim.run_trajectory(trajectory)
profiler.disable()
execution_time = time.time() - start_time
print(f"Execution time: {execution_time:.3f} seconds")
print(f"FPS: {len(trajectory) / execution_time:.1f}")
# Print top time consumers
profiler.print_stats(sort='cumtime', lines=10)
**5. CUDA/CuPy errors**
.. code-block:: python
# Graceful fallback to CPU
try:
import cupy as cp
device = cp.cuda.Device()
print(f"Using GPU: {device.id}")
except ImportError:
print("CuPy not available, using NumPy")
import numpy as cp
except Exception as e:
print(f"CUDA error: {e}, falling back to NumPy")
import numpy as cp
Debugging Tools
~~~~~~~~~~~~~~~~~~~
Enable detailed logging and visualization:
.. code-block:: python
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# Add trajectory debugging
def debug_trajectory(sim, trajectory, step_interval=10):
"""Debug trajectory execution with detailed logging."""
for i, pose in enumerate(trajectory):
if i % step_interval == 0:
# Log joint positions
print(f"Step {i}: joints = {pose}")
# Check joint limits
for j, (angle, (min_val, max_val)) in enumerate(zip(pose, sim.joint_limits)):
if angle < min_val or angle > max_val:
print(f" WARNING: Joint {j} out of limits: {angle:.3f}")
# Visualize current pose
sim.set_joint_positions(pose)
time.sleep(0.1)
Performance Benchmarks
~~~~~~~~~~~~~~~~~~~~~~~~~
Typical performance metrics for reference:
.. list-table:: Performance Benchmarks
:header-rows: 1
:widths: 30 20 20 30
* - Configuration
- FPS (GUI)
- FPS (Headless)
- Notes
* - Default (dt=0.01)
- 60-100
- 200-500
- Standard real-time
* - High precision (dt=0.001)
- 10-20
- 50-100
- Detailed physics
* - Fast preview (dt=0.02)
- 100-200
- 500-1000
- Quick visualization
* - GPU accelerated
- +20-50%
- +30-70%
- With CuPy controller
Best Practices
--------------------
1. **Always validate trajectories** before execution
2. **Use appropriate time steps** for your application
3. **Monitor memory usage** in long simulations
4. **Enable collision checking** for safety-critical applications
5. **Save simulation data** for post-analysis
6. **Use GPU acceleration** for compute-intensive controllers
7. **Profile performance** to identify bottlenecks
Integration with Other Modules
----------------------------------
The Simulation module integrates seamlessly with other ManipulaPy components:
- **Kinematics**: Forward/inverse kinematics for trajectory generation
- **Dynamics**: Physics-based simulation and control
- **Path Planning**: Trajectory optimization and collision avoidance
- **Control**: Closed-loop feedback control implementation
- **Vision**: Sensor simulation and perception testing
See Also
--------
- :doc:`/user_guide/Control` — Controller Implementation Guide
- :doc:`/user_guide/Trajectory_Planning` — Path Planning and Optimization
- :doc:`/user_guide/Dynamics` — Robot Dynamics and Physics
- :doc:`/api/simulation` — Complete API Reference
- `PyBullet Documentation `_ — Physics Engine Reference
- `CuPy Documentation `_ — GPU Acceleration Reference