Two-Phase Step
PyBulletFleet’s step_once() is internally a two-phase step. This page
explains what each phase does and what it means for callback / plugin authors.
If you only call high-level APIs (
Agent.add_action,controller.set_cmd_vel,agent.get_pose), the two-phase split is fully transparent — you do not need to change any code. This page is for callback / plugin / ROS-bridge authors who interact with poses or PyBullet directly inside a step.
Design rationale
Separating pure-Python compute from PyBullet C-API writes lets the simulator:
batch pose writes for N agents in one pass (
set_poses()),vectorize per-agent compute with NumPy across N agents,
guarantee that collision detection always sees a consistent world snapshot (all writes applied, all AABBs refreshed) within a single step.
The three internal phases
step_once()
├── PHASE 1 — UPDATE (pure Python / NumPy; pose writes are buffered)
│ ├── pre-step events
│ ├── for obj in sim_objects: obj.update(dt)
│ │ └── controller.compute → agent.set_pose(...) ← buffered
│ ├── user callbacks (registered via register_callback)
│ └── plugin on_step hooks
│
├── PHASE 2 — POSE FLUSH (tight C-API loop)
│ └── for obj_id in _pending_pose_ids:
│ p.resetBasePositionAndOrientation(...)
│
├── stepSimulation() (only when physics is enabled)
│
├── PHASE 3 — AABB + GRID FLUSH
│ └── for obj_id in _pending_pose_ids:
│ refresh AABB (p.getAABB)
│ update spatial-hash grid
│
├── check_collisions (frequency-gated; sees up-to-date AABBs/grid)
└── monitor / post-step events
The set of “objects that moved this step” (_pending_pose_ids) is built up
during Phase 1 and consumed (and cleared) by Phases 2 and 3.
set_pose() behaviour: buffered vs immediate
Calls to SimObject.set_pose() (and agent.set_pose_raw()) behave
differently depending on when they are made:
When |
Behaviour |
|---|---|
Outside |
Immediate — writes to PyBullet right away. |
Inside |
Buffered — only the cache is updated; the actual PyBullet pose is written at Phase 2 (a few microseconds later in the same step). |
Buffering applies to kinematic pose writes via set_pose() only. Physics-driven
rigid bodies are advanced by stepSimulation() itself between Phase 2 and Phase 3.
The cache is the source of truth mid-step, so:
✅
agent.get_pose()— always correct (reads the cache).⚠️
p.getBasePositionAndOrientation(object_id)— returns the previous step’s pose during Phase 1, because PyBullet hasn’t been written yet.⚠️
p.getAABB(object_id)— returns the previous step’s AABB during Phases 1 and 2. AABBs are refreshed in Phase 3.
Rule of thumb
Inside a callback or plugin
on_step, always use the framework getter (agent.get_pose(),sim_object.get_pose()), never the raw PyBullet API.
If you genuinely need PyBullet to reflect the latest pose mid-step (e.g. for a raycast against just-moved agents), do the work in a post-step callback or let it run in the next step — by then Phase 2 has flushed.
Profiling
The per-step profiling dict (returned by MultiRobotSimulationCore.get_profiling_stats())
exposes the phase split:
Key |
What it measures |
|---|---|
|
End-to-end Phase 1 wall time (events → |
|
Time spent writing buffered poses to PyBullet ( |
|
Time spent refreshing AABBs and spatial-hash grid entries for the objects flushed in Phase 2. |
|
Time spent in the per-object |
See also
PyBulletFleet - Design Documentation — overall architecture
Custom Class Profiling Guide — adding your own profiling metrics