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