Skip to content

Lockstep Stepping#

In lockstep, the simulation advances only when a client requests physics steps. This tutorial steps the game from Python and from ROS 2, records an episode log and examines the determinism of two runs.

The flow of one lockstep step request: the client sends commands and a step request, the simulator releases the world for N frames with one physics step in each frame, holds the world, lets the sensors finish, sends a barrier on each data channel and then sends the reply. CLIENT GAME OR CORE SERVER DATA CHANNELS 1 Send the commands on the vehicle bridge, before the step request Vehicle bridge the commands go into the command queue of the agent commands 2 Send the step request {"op":"step","steps":N} 3 Start the request frame time = one physics step, 1/120 s wait until no step is in progress 4 Release the world for one frame exactly one physics step for each agent the queued commands apply apply at the next step 5 Hold the world while sensors work the sensors complete the scans and images that were due in the step steps 4 and 5: N times 6 Send a barrier on each channel the N steps are complete, the world is held and all sensors are idle Barrier: sensor stream frame type 6, payload step S Barrier: vehicle bridge {"type":"barrier","step":S} Barrier: CAN transport frame 0xFF1E, payload step S 7 Send the reply {"ok":true,"step":S,"barrier":S} 8 Receive the reply S is the physics step that the world reached the client waits 9 Read each channel to its barrier. All data of the N steps is before the barrier. sensor data and reports of the steps, then the barrier
One step request. The world advances one physics step in each frame. The reply comes after the barrier on each data channel. Open the diagram

Before You Start#

  • Build or install the packaged game: refer to Run the Packaged Game.
  • For the ROS 2 procedure, build the ROS 2 workspace: refer to Build the ROS 2 Workspace.
  • Use Python 3. The Python client uses only the standard library.
  • Do all commands in the repository root.

The procedures use three sockets of the game.

Socket Option Port
Simulator Control Channel -SimControl= 5600
Vehicle Bridge -RlPort= 5556
Sensor Stream -SensorStream= 5601

How a Step Request Operates#

One physics step is 1/120 s. A step request of 12 steps advances the simulation by 0.1 s.

Rule Description
Hold Between the requests, the simulator holds the world. The physics, the clock, the weather and the traffic do not advance.
One step for each frame During a request, the frame time is one physics step. The game does one physics step in each frame.
Commands A command that arrives before the request applies at the first step of the request.
Sensors With a sensor stream or a sensor recording, the game releases the next step only when the sensors are idle. A LiDAR scan or a camera image thus shows the world of its own step.
Barrier After the last step, the simulator sends a barrier on each vehicle bridge, each sensor stream and each CAN transport.
Reply The reply to the request comes after the barriers. The client then reads each channel as far as its barrier.

Note

The wall-clock limits of the vehicle bridge do not apply in lockstep. A controller can use as much time as necessary between two requests.

Step from Python#

  1. Start the game in lockstep with one Polaris and its three sockets.

    Packaged/Linux/Acres.sh -VehicleDemo -Vehicle=polaris \
        -SensorStream=5601 -RlPort=5556 -SimControl=5600 -Lockstep
    

    Expected Result

    The log contains these lines. The window shows the Polaris in front of the ICSC garage, and the clock of the HUD does not move.

    ACRES_SENSOR_STREAM_LISTENING agent=polaris port=5601
    ACRES_RL_LISTENING port=5556
    ACRES_SIM_CONTROL_LISTENING port=5600
    ACRES_LOCKSTEP on
    ACRES_SIM_CONTROL_READY port=5600 lockstep=1 replay=0 log=
    
  2. Start Python and connect to the simulator control channel and to the vehicle bridge.

    import sys
    sys.path.insert(0, "Tools/SimControl")
    from sim_control import JsonBridge, SimControl
    
    ctl = SimControl(5600)
    dbw = JsonBridge(5556)
    print(ctl.call("get_state"))
    

    Expected Result

    The state is 2 (paused) and the physics step is 0.

    {'state': 2, 'lockstep': True, 'step': 0, 'time_s': 0, 'id': 1, 'ok': True, 'result': 1}
    
  3. Send the commands for the next step: enable the drive-by-wire, select the low gear and command 2 m/s.

    dbw.send({"dbw": {"enable": True}})
    dbw.send({"gear_cmd": {"cmd": 5}})
    dbw.send({"ulc_cmd": {"cmd": 2.0, "cmd_type": 1, "enable": True}})
    
  4. Request 12 physics steps.

    reply = ctl.call("step", steps=12)
    print(reply)
    

    Expected Result

    The reply gives the step that the world reached and the time that the request used.

    {'step': 12, 'time_s': 0.10000000521540642, 'barrier': 12, 'wall_s': 0.38564583205152303,
     'id': 2, 'ok': True, 'result': 1}
    
  5. Read the vehicle bridge as far as the barrier of the request.

    dbw.wait_barrier(reply["barrier"])
    print(len(dbw.reports), dbw.report["t"], dbw.report["ulc_report"])
    

    Expected Result

    The bridge sent six reports before the barrier. The reports have a rate of 50 Hz in simulation time. The ULC started to increase its speed reference.

    6 0.1 {'cmd_type': 1, 'vel_ref': 0.12, 'vel_meas': 0.0, 'accel_ref': 0.1, 'accel_meas': 0.0,
     'enabled': True, 'timeout': False}
    
  6. Start an episode log and run a control loop at 10 Hz of simulation time for 10 s.

    ctl.call("record", action="start", path="/data/lockstep.mcap")
    for k in range(100):
        dbw.send({"dbw": {"enable": True}})
        dbw.send({"gear_cmd": {"cmd": 5}})
        dbw.send({"ulc_cmd": {"cmd": 2.0, "cmd_type": 1, "enable": True}})
        dbw.send({"steering_cmd": {"cmd": 60.0, "cmd_type": 2, "cmd_rate": 200.0, "enable": True}})
        reply = ctl.call("step", steps=12)
        dbw.wait_barrier(reply["barrier"])
    print(reply["time_s"], dbw.report["ulc_report"]["vel_meas"])
    print(ctl.call("record", action="stop"))
    

    Expected Result

    The Polaris accelerates in a left curve. After 10 s its speed is 1.54 m/s. The episode log contains one state for each of the 1200 physics steps.

    10.100000526756048 1.5373
    {'messages': 2412, 'duration_s': 10.000000522, 'path': '/data/lockstep.mcap', 'id': 104, 'ok': True, 'result': 1}
    
  7. Stop the game. If you continue with the procedure Step from ROS 2, do not do this step.

    ctl.call("set_state", state=3)
    

    Expected Result

    The log contains ACRES_SIM_CONTROL_QUIT and the game stops.

CAUTION

Do not use the method SimControl.call() for a step request with progress equal to true. The method uses the first progress event as the reply.

Step from ROS 2#

The ROS 2 bridge gives the step request as the service /step_simulation and as the action /simulate_steps. The bridge sends the commands that it holds to the game before it sends the request. The service returns after the bridge published all sensor data and reports of the steps.

WARNING

The drive-by-wire topics of the simulator have the same names as the topics of the real vehicle. Source ROS/Env/setup_env.sh in each terminal. It keeps all ROS 2 traffic on the loopback address.

  1. Use the game of the procedure Step from Python, but do not stop it. Stop only the Python client.

    ctl.close()
    exit()
    

    Expected Result

    The game stays in the paused state. Each socket of the game accepts one client, thus the Python client must disconnect first.

  2. In a second terminal, start the ROS 2 bridge.

    source ROS/Env/setup_env.sh
    ros2 launch acres_sim sim_bridge.launch.py
    

    Expected Result

    [sim_bridge]: publishing the polaris topics
    [sim_bridge]: connected to polaris (polaris): LiDAR 1800 x 32 at 10 Hz, camera 896 x 512 at 10 Hz, INS 100 Hz
    [sim_bridge]: simulator control: connected to 127.0.0.1:5600 (protocol 1, map V03ACRE, state 2,
        lockstep on, step 1212, 1 agents)
    
  3. In a third terminal, request 12 physics steps with the service.

    source ROS/Env/setup_env.sh
    ros2 service call /step_simulation simulation_interfaces/srv/StepSimulation "{steps: 12}"
    

    Expected Result

    The result code 1 means that the steps are complete.

    requester: making request: simulation_interfaces.srv.StepSimulation_Request(steps=12)
    
    response:
    simulation_interfaces.srv.StepSimulation_Response(
        result=simulation_interfaces.msg.Result(result=1, error_message=''))
    
  4. Request 36 steps with the action. The action gives feedback during the steps.

    ros2 action send_goal --feedback /simulate_steps \
        simulation_interfaces/action/SimulateSteps "{steps: 36}"
    

    Expected Result

    Sending goal:
         steps: 36
    
    Goal accepted with ID: 5447dcb1352e497393efaf0e5efff637
    
    Feedback:
        completed_steps: 12
    remaining_steps: 24
    
    ...
    
    Result:
        result:
      result: 1
      error_message: ''
    
    Goal finished with status: SUCCEEDED
    
  5. Run the worked example. It records an episode log and drives the Polaris for 20 s of simulation time.

    python Demo/lockstep_drive.py --log /data/lockstep-ros.mcap --seconds 20
    

    Expected Result

    Each 0.1 s, the example publishes the drive-by-wire commands and then requests 12 steps.

    recording: /data/lockstep-ros.mcap
    t   0.0 s  utm 500434.61 4479976.26  v 1.06 m/s
    t   5.0 s  utm 500431.65 4479982.85  v 1.98 m/s
    t  10.0 s  utm 500424.25 4479988.99  v 2.45 m/s
    t  15.0 s  utm 500415.99 4479993.98  v 2.07 m/s
    20 s simulated in 67.0 s of wall time; log /data/lockstep-ros.mcap: 5905 messages, 20.0 s
    
  6. Stop the game with the service of the simulation state. Then stop the ROS 2 bridge with Ctrl+C.

    ros2 service call /set_simulation_state \
        simulation_interfaces/srv/SetSimulationState "{state: {state: 3}}"
    

The sensor messages of a request are on their topics when the service returns. This table shows the messages that a ROS 2 node received for each of five requests of 12 steps.

Request Simulation Time after the Request /clock /vehicle/odom Stamp of /lidar/points Stamp of /camera/image_raw
1 10.6000 s 12 messages 10 messages 10.5083 s 10.5083 s
2 10.7000 s 12 messages 10 messages 10.6083 s 10.6083 s
3 10.8000 s 12 messages 10 messages 10.7083 s 10.7083 s
4 10.9000 s 12 messages 10 messages 10.8083 s 10.8083 s
5 11.0000 s 12 messages 10 messages 10.9083 s 10.9083 s

Each request gives one clock message for each physics step, ten INS epochs, one LiDAR scan and one camera image. The stamp of the scan and of the image is the time of the physics step at which the sensor took its sample.

The services and the action are on the page Services and Actions.

Examine the Determinism#

Two runs with the same seed and the same commands at the same physics steps give the same trajectory. The tool Tools/SimControl/lockstep_determinism.py starts the game two times, drives the same command program and compares the poses.

  1. Close all instances of the game. The tool starts the game itself, one instance at a time.

  2. Run the tool with two runs of 30 s.

    python Tools/SimControl/lockstep_determinism.py --out /data/determinism --runs 2 --seconds 30
    

    Expected Result

    The tool prints the summary and writes it to summary.json. The exit code is 0 when the largest position difference is 1 cm or less. In this test the two runs of 56.7 m are equal: the difference is 0.

    {
     "runs": [
      {"steps": 3720, "wall_s": 65.72795515193138, "realtime_factor": 0.47164102288505594,
       "log_messages": 8563, "start_step": 0},
      {"steps": 3720, "wall_s": 65.85412743897177, "realtime_factor": 0.4707373889165606,
       "log_messages": 8563, "start_step": 0}
     ],
     "comparisons": [
      {"samples": 310, "max_position_diff_m": 0.0, "at_time_s": null, "max_yaw_diff_deg": 0.0,
       "path_length_m": 56.72085725036201, "steps_equal": true}
     ],
     "headless": false,
     "npc": false,
     "pass_1cm": true
    }
    
  3. Compare the two episode logs step by step.

    source ROS/Env/setup_env.sh
    python Tools/SimControl/compare_episodes.py \
        /data/determinism/run0.mcap /data/determinism/run1.mcap
    

    Expected Result

    polaris: 3720 common steps, max position difference 0.000e+00 m (step None),
    max heading difference 0.000e+00 deg
    steps only in a: 0, only in b: 0
    
  4. Do the same test with ACRES Core. The option --game core starts the Core server in place of the game. This option needs the ROS 2 workspace.

    python Tools/SimControl/lockstep_determinism.py --out /data/determinism-core \
        --runs 2 --seconds 30 --game core
    

    Expected Result

    The two runs of ACRES Core are equal to the last bit.

    {"comparisons": [{"samples": 310, "max_position_diff_m": 0.0, "max_yaw_diff_deg": 0.0,
      "path_length_m": 56.66187431527365, "steps_equal": true}], "pass_1cm": true}
    

Obey these rules to get a run that you can repeat.

Rule Reason
Send commands only between step requests. A command that arrives during a request applies at a step that changes with the timing.
Use the same options and the same seed. The sensor noise and the weather use the seed.
Start each run with a new session. A reset does not set the physics clock or the soil water back to the start.
Start the game with -Lockstep. The soil water then follows the simulation time and not the wall clock.

The model of the time steps is on the page Time Stepping and Determinism.

Stepping Rate#

In the game, one physics step uses one frame, thus the frame rate limits the stepping rate. These rates are measurements on a workstation with one RTX 5060 Ti (16 GB) and the high render tier.

Simulator and Session Client Steps for Each Second Ratio to Real Time
Game, 1280 × 720, one Polaris, no sensor stream One request of 600 steps. 63 0.53
Game, 640 × 360, one Polaris, no sensor stream, episode log One request of 12 steps for each 0.1 s. 56.5 0.47
Game, 640 × 360, one Polaris with the sensor stream The Python procedure of this page. 36.8 0.31
Game, 640 × 360, one Polaris with the sensor stream The ROS 2 service, 12 steps for each request. 36.8 0.31
Game, 1280 × 720, episode replay in lockstep One request of 1200 steps. 63 0.53
Core server, one Polaris One request of 12 steps and one pose request for each 0.1 s. 6800 57
Core server, one Polaris, no client on the sensor stream One request of 600 steps. 100 000 850

The sensor stream makes the game record the camera and the LiDAR. The game then waits for the sensors after each step, thus the rate decreases. The Core server has no frames. Its rate is the rate of the physics model and of the client.

Next Steps#