# PyBulletFleet - Design Documentation ## Architecture Overview This package provides a modular, reusable PyBullet simulation framework designed for multi-agent scenarios. The architecture is organized into several key components, each with specific responsibilities. > **Note:** The methods, attributes, and parameters listed in this document are representative highlights — not exhaustive lists. Refer to the source code or API reference for the full interface of each class. ``` ┌─────────────────────────────────────────────────────────────┐ │ User Application │ └───────────────────┬─────────────────────────────────────────┘ │ ┌───────────────┴───────────────┐ │ MultiRobotSimulationCore │ ← Main simulation engine │ (core_simulation.py) │ └───────────────┬───────────────┘ │ ┌───────────────┼───────────────┬───────────────┬──────────────┐ │ │ │ │ │ ┌───▼───┐ ┌─────▼──────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ Agent │ │ Agent │ │ Action │ │ Tools │ │Visualizer│ │ │ │ Manager │ │ System │ │ (utils) │ │ Monitor │ └───────┘ └────────────┘ └─────────┘ └─────────┘ └─────────┘ ``` ## Core Components ### 1. core_simulation.py **Purpose**: Main simulation engine and foundational classes #### Key Classes: ##### MultiRobotSimulationCore The central orchestrator for PyBullet simulations. **Responsibilities:** - PyBullet engine initialization and configuration - Simulation loop management (timestep control, speed multiplier) - Camera setup (manual/automatic positioning) - Visualization control (visual shapes, collision shapes, transparency) - Performance monitoring integration - Structure body tracking - Callback management for user-defined updates - Keyboard event handling (SPACE, v, c, t keys) - Collision detection system integration **Key Methods:** - `from_dict(config)` / `from_yaml(path)`: Factory methods for initialization - `run_simulation(update_callback, final_callback)`: Main simulation loop - `step_once()`: Single simulation step - `setup_camera()`: Camera positioning - `configure_visualizer()`: Visual settings configuration - `register_static_body(body_id)`: Track static structure elements - `register_callback(callback, frequency)`: Register custom update callbacks - `_handle_keyboard_events()`: Process keyboard inputs **Associated Params:** - **`SimulationParams`** — Configuration dataclass holding all parameters for `MultiRobotSimulationCore`. Passed to the constructor to configure the simulation engine. - Attributes: `gui`, `timestep`, `target_rtf`, `duration` (core settings), `physics`, `monitor` (feature toggles), `enable_floor` (plane.urdf loading, default `True`), `camera_*` (camera config), `enable_*` (visualization), `spatial_hash_*` (collision detection) - Creation: `SimulationParams(gui=False, target_rtf=0, ...)`, `SimulationParams.from_dict(config)`, `SimulationParams.from_config("config/config.yaml")` - `enable_floor=False` skips loading `plane.urdf` in both `setup_pybullet()` and `reset()`, allowing custom floor handling (e.g., transparent floors, environment SDF meshes) ##### SimObject Base class for all simulation objects (single rigid body, no joints or links). SimObject represents a **single-body** object in PyBullet — it has only a base link and does **not** support joints, links, or URDF loading. For objects that require multi-link bodies with joint control (e.g., URDF robots), use Agent instead. **Responsibilities:** - Common interface for objects in simulation - Position and orientation management via Pose - Metadata storage - Object attachment system (base-link attachment only) - Shared shape caching for performance **Key Methods:** - `from_params(spawn_params)` / `from_mesh(...)`: Factory methods for creation - `get_pose()` / `set_pose(pose)`: Position and orientation management - `set_collision_mode(mode)`: Change collision detection mode - `attach_object(obj)` / `detach_object(obj)`: Parent-child attachment with constraints - `get_attached_objects()` / `is_attached()`: Query attachment state - `register_callback(callback, frequency)`: Register custom update callbacks **Key Features:** - Support for mesh and primitive shapes (created via `createMultiBody`) - Collision and visual shape separation - Pickable/non-pickable objects - Parent-child attachment with constraints - No joint/link support — single rigid body only **Associated Params:** - `SimObjectSpawnParams` — Parameters for spawning a SimObject (visual/collision shapes, initial pose, mass, pickable, collision mode, name, user_data). Pass to `SimObject.from_params()`. - `ShapeParams` — Visual or collision shape definition (shape type, mesh path, half extents, radius, colour, frame offset). Referenced by `SimObjectSpawnParams.visual_shape` and `.collision_shape`. ##### LogLevelManager Utility for managing PyBullet log verbosity. **Key Methods:** - `set_log_level(level)`: Control PyBullet logging output --- ### 2. agent.py **Purpose**: Agent with goal-based navigation and action system #### Key Classes: ##### Agent (extends SimObject) Agent extends SimObject to support **URDF loading with multi-link bodies and joint control**. While SimObject is limited to single rigid bodies, Agent can load URDF models that contain multiple links connected by joints, and provides joint state management and link-level object attachment via `update_attached_objects_kinematics()`. **Responsibilities:** - Goal-based navigation (move towards target pose) - Action execution (MoveTo, Pick, Drop, Wait) - Velocity and acceleration limiting - Path following - Object manipulation (supports link-level attachment for URDF robots) - Collision handling - URDF model loading with joint/link support - Model name resolution — `from_urdf()` calls `resolve_urdf()` internally, accepting both model names (e.g., `"panda"`) and direct file paths **Key Methods:** - `from_urdf(urdf_path, ...)`: Factory method — accepts a model name (resolved via `resolve_urdf()`) or a direct URDF path - `set_goal(pose)`: Set target destination - `update(dt)`: Update agent state per timestep - `execute_action(action)`: Execute high-level action - `is_goal_reached()`: Check if at destination - `pick(obj)`: Attach object to agent - `drop()`: Detach currently held object **Motion Modes:** - Omnidirectional: Move in any direction without rotation - Differential: Rotate towards goal then move forward **Control Algorithm:** - Proportional controller for position - Linear interpolation for smooth motion - Velocity clamping based on max_linear_vel and max_linear_accel **Joint Control Modes:** - **Physics mode** (`mass > 0`, `physics=True`): `setJointMotorControl2` — PyBullet motor control with torque limits - **Kinematic mode** (`mass=0.0` or `physics=False`): `resetJointState` with per-step interpolation — joints move at URDF `` rates, falling back to per-joint-type defaults when unspecified: `_KINEMATIC_JOINT_FALLBACK_VELOCITY` (2.0 rad/s) for revolute joints and `_KINEMATIC_PRISMATIC_FALLBACK_VELOCITY` (0.5 m/s) for prismatic joints. Mode selected once at init via `_compute_use_kinematic_joints()` and cached in `_use_kinematic_joints`. - **Kinematic joint cache** (`_kinematic_joint_positions`): Joint positions cached in a Python dict, initialized via batch `p.getJointStates()`, updated after each `resetJointState()`. `get_joint_state()` returns cached values for kinematic robots — zero PyBullet calls per step. **Key Joint Methods:** - `set_joint_target(index, position)`: Set single joint target (transparent mode switching) - `set_all_joints_targets(positions)`: Set all joint targets at once - `are_all_joints_at_targets(targets, tolerance)`: Check if all joints reached targets - `are_joints_at_targets(targets, tolerance)`: Unified check — accepts list, dict, or `None` (uses `_last_joint_targets`) - `_update_kinematic_joints(dt)`: Internal per-step interpolation (called from `update()`) **Inverse Kinematics (IK):** - `move_end_effector(target_position, target_orientation, end_effector_link)`: High-level EE position command. Solves IK internally, checks reachability, sets joint targets. Returns `True` if reachable, `False` if not (best-effort targets still set). **Associated Params:** - `AgentSpawnParams` — Configuration for agent initialization: motion limits (`max_linear_vel`, `max_linear_accel`, `max_angular_vel`, `max_angular_accel`), motion mode (`"omnidirectional"` / `"differential"`), orientation, mass, collision toggle. Immutable after creation. - `IKParams` — IK solver configuration dataclass: `max_outer_iterations`, `convergence_threshold`, `max_inner_iterations`, `residual_threshold`, `reachability_tolerance`, `seed_quartiles`, `ik_joint_names`. Passed to `Agent.from_urdf(ik_params=...)`. Default: 5 outer iterations, 0.01 m threshold. - `ik_joint_names` (optional `tuple[str, ...]`) — When set, only the named joints participate in IK; all other movable joints are locked at their current positions. When `None` (default), the solver auto-detects: `JOINT_FIXED` joints are skipped, and continuous joints (lower limit ≥ upper limit, e.g. wheels) are locked automatically. This makes IK work correctly on composite robots like mobile manipulators without manual configuration. **Agent-Level Tolerance:** - `Agent.joint_tolerance` — property (float, list, dict, or None) that provides a default tolerance for `JointAction` when `tolerance=None`. Supports dict keyed by joint name or list indexed by absolute joint index for per-joint thresholds (useful for mixed prismatic/revolute arms). Out-of-range list indices fall back to the class default (0.01). Fallback: instance value → class default (0.01). Can be set at construction via `Agent.from_urdf(joint_tolerance=...)` or updated via the property setter. --- ### 3. agent_manager.py **Purpose**: Multi-agent coordination and spawning #### Key Classes: ##### SimObjectManager Base manager for all simulation objects. Parametrised by an `object_class` (default `SimObject`) so that spawning methods automatically create the right type. **Key Methods:** - `spawn_objects_grid(num_objects, grid_params, spawn_params)`: Create objects in grid pattern - `spawn_grid_mixed(num_objects, grid_params, spawn_params_list)`: Mixed type spawning - `spawn_grid_counts(grid_params, spawn_params_count_list)`: Exact count spawning - `spawn_objects_batch(params_list)`: Batch spawn with explicit poses ##### AgentManager Extends SimObjectManager with `object_class=Agent`. **Additional Responsibilities:** - Goal management and update callbacks - Query moving/stopped agents **Convenience Aliases:** - `spawn_agents_grid(...)` → `spawn_objects_grid(...)` - `spawn_agents_grid_mixed(...)` → `spawn_grid_mixed(...)` - `spawn_agent_grid_counts(...)` → `spawn_grid_counts(...)` **Agent-Specific Methods:** - `register_callback(callback)`: Register custom goal logic - `set_goal_pose(agent_index, goal)`: Set goal for a specific agent - `get_moving_count()`: Count moving agents **Note:** - Agent.update() is automatically called by MultiRobotSimulationCore.step_once() - AgentManager focuses on goal management, not movement updates **Associated Params:** - `GridSpawnParams` — Grid layout configuration: boundaries (`x_min`/`x_max`, `y_min`/`y_max`, `z_min`/`z_max`), spacing, offset. Automatically distributes agents evenly using `ceil(sqrt(n))`. --- ### 4. action.py **Purpose**: High-level action system for agents #### Key Classes: ##### Action (Base Class) Abstract base class for all actions. **Key Methods:** - `start(agent)`: Initialize action - `update(agent, dt)`: Update action state - `is_complete()`: Check if action finished - `stop(agent)`: Clean up action ##### MoveTo Navigate agent to target pose. **Key Parameters:** - `target_pose`: Destination pose - `tolerance`: Distance threshold for completion ##### Pick Pick up an object and attach it to agent. **Key Parameters:** - `target_object_id`: Specific object body ID to pick (optional) - `target_position`: Pick from position — auto-select nearest pickable object (optional) - `search_radius`: Search radius when using `target_position` (default: 0.5m) - `attach_link`: Link index or name to attach to (default: -1 for base) - `attach_relative_pose`: Offset in link's frame as Pose - `use_approach`: Whether to execute the approach/retreat phases (default: `True`). When `True`, the agent navigates to an approach pose → moves forward to the pick position → picks → retreats. When `False`, the pick is executed immediately at the agent's current position — useful for arm robots and mobile manipulators where the EE is already positioned via IK. - `approach_offset`: Distance from target for auto-calculated approach pose (default: 1.0 m) ##### Drop Drop an attached object at a specified location. **Key Parameters:** - `drop_pose`: Where to drop the object (position and orientation) - `drop_relative_pose`: Optional `Pose` offset — when set, the object is placed at its current (pre-detach) position transformed by this offset instead of being teleported to `drop_pose`. Useful for EE-attached objects on mobile manipulators where the absolute world drop position is hard to predict. - `target_object_id`: Specific object to drop (None = first attached) - `place_gently`: Place at exact position vs drop from height (default: True) - `use_approach`: Whether to execute the approach/retreat phases (default: `True`). When `True`, the agent navigates to an approach pose near `drop_pose` → moves forward → drops → retreats. When `False`, the drop is executed immediately — useful for arm robots and mobile manipulators where the EE is already positioned. - `approach_offset`: Distance from drop pose for auto-calculated approach pose (default: 1.0 m) - `drop_offset`: Distance from `drop_pose` where the actual drop occurs (default: 0.0 = at `drop_pose`) ##### Wait Wait for specified duration. **Key Parameters:** - `duration`: Wait time in seconds ##### JointAction Move all joints to target positions. **Key Parameters:** - `target_joint_positions`: List of target positions for all controllable joints (radians for revolute, metres for prismatic), or dict keyed by joint name - `tolerance`: Completion threshold per joint — scalar `float`, `list` indexed by absolute joint index, `dict` keyed by joint name, or `None` (resolved from `agent.joint_tolerance` on first tick; default: 0.01). Out-of-range list indices fall back to the class default (0.01) - `max_force`: Motor force for physics mode (default: 500.0 N·m) **Tolerance resolution:** When `tolerance` is `None`, it is resolved once from `agent.joint_tolerance` at the first `execute()` call and written back to `action.tolerance`. Fallback chain: Action → Agent → class default (0.01). Dict tolerance enables per-joint thresholds for mixed prismatic/revolute arms. **Completion:** All joints within `tolerance` of their targets. Works transparently in both physics mode (motor control) and kinematic mode (interpolation). ##### PoseAction Move end-effector to a Cartesian target position via IK. **Key Parameters:** - `target_position`: EE target `[x, y, z]` in world frame - `target_orientation`: Optional quaternion `[x, y, z, w]` for orientation control - `end_effector_link`: Link index, name, or `None` (auto-detect last link) - `tolerance`: EE Cartesian distance threshold in metres (default: 0.02 m) - `max_force`: Motor force for physics mode (default: 500.0 N·m) **Completion:** Joints within default joint tolerance of the IK solution **and** EE within `tolerance` of the target position. Calls `move_end_effector()` on start, then monitors `are_joints_at_targets()` and `are_ee_at_target()` each step. **Unreachable targets:** If the IK solver determines the target is unreachable, the action does not fail immediately. Best-effort joint targets are set and joints move toward them. After settling, the action completes with `ActionStatus.FAILED` (not `COMPLETED`). A warning is logged at start and the `error_message` attribute is set to `"IK target was not reachable"`. **IK integration in Pick/Drop:** `PickAction` and `DropAction` accept an optional `ee_target_position` parameter. When set, the action delegates to a `PoseAction` sub-action to position the EE via IK before performing the pick/drop operation, as an alternative to `JointAction`-based positioning. A `continue_on_ik_failure` flag (default: `True`) controls whether the pick/drop proceeds even when the IK target is unreachable. --- ### 5. geometry.py **Purpose**: Geometric data structures (`Pose`, `Path`) used throughout the codebase for position/orientation representation and waypoint sequences. --- ### 6. tools.py **Purpose**: Utility functions for pose calculation (approach/offset poses for pick/drop actions). --- ### 7. data_monitor.py **Purpose**: Optional real-time GUI monitor (`DataMonitor`) displaying FPS and step-time metrics in a tkinter window. Enabled via `monitor: true` in config. --- ### 8. robot_models.py **Purpose**: Robot model resolution and introspection. Provides a tiered registry (`KNOWN_MODELS`) that maps model names to URDF paths across multiple sources, plus auto-detection of robot capabilities. #### Key Functions: ##### resolve_urdf(name_or_path) Resolves a model name to an absolute URDF file path by searching through tiers in order: | Tier | Source | Example | |------|--------|---------| | 0 — `local` | `robots/` directory in the project | `arm_robot`, `mobile_robot` | | 1 — `pybullet_data` | PyBullet's bundled data directory | `panda`, `kuka_iiwa`, `r2d2` | | 2 — `ros` | ROS install paths (`$AMENT_PREFIX_PATH`) | `ur5e`, `turtlebot3_burger` | | 3 — `robot_descriptions` | `robot_descriptions` pip package | `tiago`, `pr2` | Direct file paths (containing `/` or ending in `.urdf`/`.sdf`) pass through unchanged. Called internally by `Agent.from_urdf()`, so users can pass model names directly. `KNOWN_MODELS` is a curated subset — not every model from each tier is pre-registered. Unlisted models are resolved automatically via fallback scanning of `pybullet_data` and `robot_descriptions`. For name-based lookup, user search paths (registered via `add_search_path()`) are checked **before** the `KNOWN_MODELS` tiers. ##### register_model(name, path_or_entry) / unregister_model(name) Add or remove entries from `KNOWN_MODELS` at runtime. `path_or_entry` accepts an absolute path string (tier defaults to `"user"`) or a `ModelEntry` for full tier metadata. Prevents accidental overwrites by default (`force=True` to override). ##### discover_models(tier) Scan an entire tier (`"pybullet_data"` or `"robot_descriptions"`) and return all discoverable models as `{name: path}`. Unlike `KNOWN_MODELS`, this returns every model found in the installed package. Also used internally by `resolve_urdf()` as a fallback for unlisted models. ##### add_search_path(directory) / remove_search_path(directory) / get_search_paths() Register custom directories for name-based URDF lookup. User search paths take priority over `KNOWN_MODELS`, enabling users to shadow built-in models with custom versions. `add_search_path()` validates the directory exists and is idempotent. ##### auto_detect_profile(body_id_or_path, client) Inspects a loaded PyBullet body (or loads a URDF temporarily) and returns a `RobotProfile` dataclass: - `robot_type` — `"arm"`, `"mobile"`, `"mobile_manipulator"`, or `"static"` - `num_joints`, `movable_joint_names`, `movable_joint_indices` - `ee_link_name`, `ee_link_index` — end-effector detection - `joint_lower_limits`, `joint_upper_limits`, `joint_max_velocities` Accepts `Union[str, int]` — when given an `int` (body_id), skips load/removeBody overhead. ##### list_all_models() Returns a dict of all registered models with tier, availability, and resolved path or error message. ##### detect_robot_type(body_id, client) Lightweight type detection (arm/mobile/mobile_manipulator/static) without full profile analysis. #### Key Data: - **`KNOWN_MODELS`** — `dict[str, ModelEntry]` mapping model names to their tier and path resolver - **`RobotProfile`** — Frozen dataclass with all introspection results - **`ModelEntry`** — Named tuple: `(tier, path_func)` for each model --- ## Performance Considerations ### Bottlenecks: 1. **GUI Rendering**: 2-3x slower than headless mode 2. **Collision Detection**: O(N) with spatial hashing, O(N²) without 3. **Mesh Complexity**: High-poly meshes slow down rendering 4. **Monitor Updates**: tkinter GUI overhead 5. **Shape Creation**: Repeated shape creation for many objects ### Optimizations: 1. **Disable GUI**: `gui: false` for batch simulations 2. **Spatial Hashing**: Enabled by default for collision detection 3. **Shared Shapes**: Automatic shape caching reduces OpenGL overhead 4. **Increase Timestep**: Trade accuracy for speed (default: 1/240s) 5. **Simple Shapes**: Use boxes/cylinders instead of complex meshes 6. **Batch Operations**: Update all agents in single pass 7. **Disable Monitor**: `monitor: false` in production 8. **Cell Size Tuning**: Use `constant` mode with optimal cell_size for best performance See `docs/PERFORMANCE_ANALYSIS.md` and `docs/OPTIMIZATION_RESULTS.md` for detailed benchmarks.