Real-time Synchronization Design

Overview

PyBulletFleet implements a real-time synchronization system to ensure smooth simulation playback at configurable speeds. This document explains the design, implementation, and best practices.


Design Philosophy

Absolute Time-based Control

The synchronization system uses absolute time calculation for stable, predictable behavior:

elapsed_time = current_time - start_time
target_sim_time = elapsed_time * speed
time_diff = target_sim_time - current_sim_time

Key Benefits:

  • Stable control: Fixed reference point (start_time) prevents oscillation

  • Simple logic: No complex history management or filtering

  • Predictable: Easy to reason about and debug

  • Efficient: Minimal computational overhead


Core Algorithm

1. Time Difference Calculation

# Calculate how much simulation should have progressed
target_sim_time = elapsed_real_time * speed_multiplier

# Compare with actual simulation progress
time_diff = target_sim_time - current_sim_time

Interpretation:

  • time_diff > 0: Simulation is behind → Execute multiple steps to catch up

  • time_diff < 0: Simulation is ahead → Sleep until next frame

  • time_diff 0: Simulation is synchronized → Continue at current pace

2. Adaptive Step Execution

When behind target:

if time_diff > 0:
    steps_needed = int(time_diff / timestep)
    steps_needed = max(1, min(steps_needed, max_steps_per_frame))
    for _ in range(steps_needed):
        step_once()

Saturation limit (max_steps_per_frame):

  • Default: 10 steps per frame

  • Prevents sudden jumps from large time_diff

  • Ensures GUI responsiveness

3. Sleep Strategy

When ahead of target:

if time_diff < 0:
    sleep_time = abs(time_diff) - last_step_process_time

    if sleep_time > 0:
        time.sleep(sleep_time)  # Exact sleep
    elif sleep_time > -min_sleep:
        time.sleep(min_sleep)   # Minimum for GUI responsiveness

GUI responsiveness:

  • min_sleep = 1.0 / gui_min_fps (default: 33ms for 30 FPS)

  • Ensures smooth rendering even when simulation is ahead


Pause/Resume Handling

Reset start_time on Resume

if last_pause_state and not self._simulation_paused:
    # Just resumed from pause
    start_time = current_time - current_sim_time / speed
    logger.info("Resumed from pause: start_time reset for smooth continuation")

Timeline visualization:

Real time:    100s -------- 105s [Pause 10s] 115s -------- 120s
Sim time:     0s ----------- 5s   (stopped)   5s ---------- 10s
start_time:   100s (initial) ──────────────→ 110s (reset) ────→

After reset:  target = (115 - 110) * 1.0 = 5s,  actual = 5s → smooth ✅

RTF Multiplier Support

Variable RTF Control

target_rtf = 0.5   # Half speed (slow motion)
target_rtf = 1.0   # Real-time (1:1)
target_rtf = 2.0   # Double speed (fast forward)
target_rtf = 0     # Maximum speed (no sleep, as fast as possible)

Example with target_rtf=2.0:

# After 1 second of real time
elapsed_real = 1.0
target_sim_time = 1.0 * 2.0 = 2.0 seconds

# Simulation should have progressed 2 seconds
# If current_sim_time = 1.8, then time_diff = 0.2
# → Execute extra steps to catch up

Best Practices

1. Spawn Objects Before run_simulation()

Recommended pattern:

# Setup phase (timing doesn't matter)
sim = MultiRobotSimulationCore.from_yaml("config.yaml")

# Spawn can take seconds - no impact on simulation time
for i in range(100):
    sim.spawn_large_structure()  # May take 10 seconds total

# Simulation starts HERE (start_time = time.time())
sim.run_simulation(duration=60.0)  # Smooth, no spawn delay impact

Why it works:

  • start_time is initialized in run_simulation()

  • Spawn time is completely excluded from synchronization

  • No accumulated time_diff from spawn overhead

2. Use Pause/Resume for Dynamic Spawning of large/complex objects ✅

Correct pattern:

# Programmatic pause/resume
sim.pause()
# spawn objects here. Won't affect time_diff
sim.resume()  # Auto-resets start_time