Headless Core Runs#
This tutorial runs ACRES Core without the game: a batch of environments from Python, the LiDAR, an episode log and ROS 2. The last part compares ACRES Core with the game on the same commands.
Before You Start#
- Build ACRES Core. Build ACRES Core gives the procedure.
- Do all commands in the repository root.
- Put the Python scripts of this page into the repository root. The scripts use
"."as the repository root. - Start each Python script with
PYTHONPATH=Core/Build python <file>. - For the ROS 2 part, build the ROS 2 workspace. Build the ROS 2 Workspace gives the procedure.
- For the comparison with the game, package the game. Build the Game gives the procedure.
The measured values of this page are from the test workstation: an AMD Ryzen 7 9700X with 16 threads.
Run a Batch with a Controller#
This procedure drives 256 environments to 256 goals. A controller in Python computes the commands for all environments at the same time.
-
Write this script into the file
batch_drive.py."""Drive 256 Polaris environments to goals with a simple steering law, and measure the speed.""" import time import numpy as np import acres_core as ac N = 256 b = ac.Batch(".", num_envs=N) S = {name: i for i, name in enumerate(ac.STATE_COLUMNS)} # All environments start at one point in field 14. Each goal is on a circle of 50 m around the start. start = np.array([-585.0, 75.0]) bearing = np.linspace(0.0, 2.0 * np.pi, N, endpoint=False) goal = start + 50.0 * np.stack([np.cos(bearing), np.sin(bearing)], axis=1) b.reset(None, np.tile([start[0], start[1], np.pi / 2, 0.0], (N, 1))) state = b.state arrival = np.full(N, np.nan) t0 = time.perf_counter() for k in range(600): # 60 s at most to_goal = goal - state[:, [S["x"], S["y"]]] distance = np.hypot(to_goal[:, 0], to_goal[:, 1]) alpha = np.arctan2(to_goal[:, 1], to_goal[:, 0]) - state[:, S["yaw"]] alpha = (alpha + np.pi) % (2.0 * np.pi) - np.pi # the angle to the goal, -pi to pi # Steer to a point 6 m ahead in the direction of the goal. Turn fully when the goal is behind. curvature = np.where(np.abs(alpha) < np.pi / 2, 2.0 * np.sin(alpha) / 6.0, 0.15 * np.sign(alpha)) curvature = np.clip(curvature, -0.15, 0.15) speed = np.where(distance > 2.0, 3.0, 0.0) arrival = np.where(np.isnan(arrival) & (distance <= 2.0), k * 0.1, arrival) if not np.isnan(arrival).any(): break state = b.step_curvature_speed(curvature, speed) wall = time.perf_counter() - t0 simulated = N * state[0, S["time_s"]] print(f"{np.isfinite(arrival).sum()} of {N} environments arrived, mean time {np.nanmean(arrival):.1f} s") print(f"crushed crop: mean {state[:, S['crop_crushed_m2']].mean():.1f} m2, collisions {int(state[:, S['collision']].sum())}") print(f"{simulated:.0f} s of simulation in {wall:.2f} s of wall time: {simulated / wall:.0f} times real time") -
Run the script.
The loop has three parts: read the state, compute the commands with NumPy, call step_curvature_speed.
One call advances each environment by 0.1 s. The batch uses all threads of the processor.
The environments do not see each other. Each environment has its own vehicle, its own crop marks and its own ruts.
The speed changes with the number of environments. One environment uses one thread, thus a small batch does not use all threads.
bench.py measures the speed without a controller: 440 to 560 times real time on one thread.
Note
Set farm=False in the constructor when the task does not use the crop marks. The step is then faster.
Use the LiDAR#
The function lidar makes scans from the poses after the last step. This procedure stops the Polaris before a tree.
-
Write this script into the file
lidar_stop.py."""Drive to a tree and stop with the planar LiDAR scan. Then make one Helios scan.""" import numpy as np import acres_core as ac b = ac.Batch(".", num_envs=1) S = {name: i for i, name in enumerate(ac.STATE_COLUMNS)} home = next(p for p in b.places() if p[0] == "spawn-icsc-garage") b.reset(None, np.array([[home[2], home[3], np.radians(45.0), 0.0]])) # heading north-east ahead = np.r_[0:21, 340:360] # the beams within 20 degrees of forward stop = False for k in range(300): ranges, material = b.lidar() # planar: (1, 360), beam k is k degrees to the left near = ranges[0, ahead] nearest = np.nanmin(near) if np.isfinite(near).any() else np.inf if k == 0: print(f"start: obstacle {nearest:.2f} m ahead of the sensor, material {material[0, ahead][np.nanargmin(near)]}") stop = stop or nearest <= 6.0 state = b.step_curvature_speed(np.zeros(1), np.array([0.0 if stop else 2.0])) if stop and abs(state[0, S["speed"]]) < 0.02: break print(f"stopped after {state[0, S['time_s']]:.1f} s, obstacle {nearest:.2f} m ahead of the sensor, " f"collision {int(state[0, S['collision']])}") ranges, material = b.lidar(None, "helios", noise=True, seed=1) cloud = ranges.reshape(1800, 32) # [column, ring] print(f"helios: {np.isfinite(cloud).sum()} returns of {cloud.size} beams, " f"terrain {(material == 254).sum()}, vehicle body {(material == 252).sum()}") -
Run the script.
Expected Result
The first scan shows a tree trunk (material 11,
bark) at 23.9 m. The Polaris stops 3.9 m before it.
The planar pattern has 360 level beams with a range of 30 m. It is the cheap scan for a learner. The Helios pattern is the sensor of the real Polaris with 57600 beams. A NaN range shows that the beam has no return. One thread makes approximately 16,000 planar scans or 90 Helios scans in one second. Python API gives the beam order and the material codes.
The LiDAR of ACRES Core sees the terrain, the buildings, the bins, the trees and the props. It does not see the crops. Limitations lists the differences from the game.
Record an Episode Log and Examine It#
ACRES Core writes the same episode log as the game. The game can show the log later with all cameras. The glossary names this method render-later.
-
Write this script into the file
record_log.py."""Record a Core episode into an episode log and examine the file.""" import numpy as np import acres_core as ac from mcap.reader import make_reader from mcap_ros2.decoder import DecoderFactory b = ac.Batch(".", num_envs=1) b.reset(None, np.array([[150.0, 190.0, 0.0, 0.0]])) # field 23, heading east into field 24 b.start_log(0, "core-episode.mcap", {"seed": "1", "note": "headless tutorial"}) for k in range(300): # 30 s b.step_curvature_speed(np.array([0.02 * np.sin(k / 20.0)]), np.array([3.0])) b.stop_log(0) with open("core-episode.mcap", "rb") as stream: reader = make_reader(stream, decoder_factories=[DecoderFactory()]) summary = reader.get_summary() for channel_id, count in sorted(summary.statistics.channel_message_counts.items()): if count: print(f"{count:5d} {summary.channels[channel_id].topic}") last = None for _, channel, _, message in reader.iter_decoded_messages(topics=["/sim/agent_states"]): last = message agent = last.agents[0] print(f"step {last.step}: {agent.name} at x {agent.pose.position.x:.2f} m, y {agent.pose.position.y:.2f} m, " f"crushed crop {agent.crop_crushed_m2:.1f} m2") -
Run the script.
Expected Result
The log has one state message for each physics step: 3600 messages for 30 s. The file size is 5.9 MB.
-
Optional: examine the file with the ROS 2 tools. This step needs the ROS 2 environment.
Expected Result
Bag size: 5.7 MiB Storage id: mcap Duration: 30.000000000s Messages: 8405 Topic information: Topic: /polaris/vehicle/dbw_enabled | Type: std_msgs/msg/Bool | Count: 1 | Serialization Format: cdr Topic: /sim/agent_states | Type: acres_interfaces/msg/AgentStates | Count: 3600 | Serialization Format: cdr Topic: /sim/farm_stamps | Type: acres_interfaces/msg/FarmStamps | Count: 3600 | Serialization Format: cdr -
Show the log in the game. The option
-CaptureVideo=writes a video of the replay.Packaged/Linux/Acres.sh -VehicleDemo -EpisodeReplay=$PWD/core-episode.mcap -EpisodeReplayExit \ -RenderOffscreen -ResX=1920 -ResY=1080 -CaptureVideo=$PWD/core-episode.mp4Expected Result
The Polaris follows the logged poses. The crop of the game goes down where the log has farm contacts. The game stops at the end of the log. The log of the game contains these lines.
The log contains the poses, the wheel states, the drive-by-wire state, the energy ledger and each farm contact. It contains no images. Replay gives the options of the episode replay. Episode Log gives the format.
Run ACRES Core on ROS 2#
The program core_sim of the package acres_core_sim puts ACRES Core behind the three local sockets of the game.
The ROS 2 bridge connects to these sockets. A ROS 2 node then sees the same topics and services as with the game.
ACRES Core has no camera, thus the camera topics have no messages.
CAUTION
core_sim uses the default ports of the game. Do not start the game and core_sim at the same time with the default ports.
-
Open a shell with the ROS 2 environment and the DDS loopback fence.
-
Start ACRES Core and the ROS 2 bridge in real time.
Expected Result
core_simloads the world in less than 1 s and prints its ports and the spawn pose. -
Open a second shell with the ROS 2 environment. Measure the rates of the clock, the odometry and the LiDAR.
source ROS/Env/setup_env.sh ros2 topic hz /clock --window 600 ros2 topic hz /vehicle/odom --window 600 ros2 topic hz /lidar/points --window 50Expected Result
The clock has one message for each physics step. Stop each command with Ctrl+C.
WARNING
The next step sends drive-by-wire commands on the topic names of the real Polaris. Send them only from a shell with the DDS loopback fence. Without the fence, the real vehicle can move.
-
Enable the drive-by-wire and command a speed of 2 m/s for 8 s. Read the speed in a third shell.
-
Stop the launch with Ctrl+C. Start it again at five times real time.
Expected Result
The rates are five times the rates of step 3: 600 Hz for
/clock, 500 Hz for/vehicle/odomand 50 Hz for/lidar/points. At the stopcore_simprints its speed.CAUTION
The watchdog of the drive-by-wire counts simulation time. A command stays valid for 0.1 s of simulation time. At a rate factor of 5, a controller must send its commands at more than 50 Hz of wall time. The command of step 4 at 20 Hz does not move the Polaris at this rate factor.
-
Stop the launch. Start it again in lockstep. The world then advances only when a client requests steps.
-
In the second shell, read the state and request 120 steps, that is 1 s.
ros2 service call /get_simulation_state simulation_interfaces/srv/GetSimulationState "{}" ros2 service call /step_simulation simulation_interfaces/srv/StepSimulation "{steps: 120}"Expected Result
The state 2 is "paused". The step service answers after the 120 steps and their sensor data are on ROS 2.
-
Record an episode log through the ROS 2 service: start, step 5 s, stop.
-
Stop the launch with Ctrl+C.
The table gives the arguments of the launch file.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
lockstep |
bool | false |
Starts with the world held. Advance it with /step_simulation or /simulate_steps. |
|
rate |
float | 1.0 |
The rate factor of a free run. 1 is real time. 0 runs as fast as the processor permits. |
|
spawn |
text | the ICSC garage | place:<id>, utm:<e>:<n>:<heading deg> or enu:<x>:<y>:<yaw deg>. One value for each agent, with commas between them. |
|
polaris_set |
text | empty | Polaris parameters as key=value;key=value. |
|
agents |
text | polaris |
The names of the agents, with commas between them. | |
sensor_port |
int | 5601 |
The port of the sensor stream. | |
rl_port |
int | 5556 |
The port of the vehicle bridge. | |
control_port |
int | 5600 |
The port of the simulator control channel. | |
lidar |
bool | true |
The ray-cast Helios at 10 Hz. | |
lidar_threads |
int | 4 |
The number of threads that make the scans. | |
noise |
bool | true |
The noise of the INS and of the LiDAR. | |
episode_log |
path | empty | Records an episode log from the start. | |
rviz |
bool | false |
Starts RViz. | |
stats_file |
path | empty | The bridge writes its transport statistics into this file at the stop. |
In a free run, core_sim runs 12 physics steps at most in one pass, as the game does in one frame.
When the processor is too slow for the rate factor, the simulation becomes slower. It does not skip time.
When the LiDAR threads are too slow, core_sim drops scans and counts them in lidar_dropped. In lockstep it drops no scan.
Lockstep Stepping gives the lockstep procedure for the game. It applies to core_sim without changes.
Nodes and Launch Files gives the nodes of the two packages.
Compare ACRES Core with the Game#
The script Core/Scripts/compare_unreal.py drives ACRES Core with the commands that the game got.
This procedure sends a recorded log of the real Polaris to the game, then gives the same commands to ACRES Core.
The log is grass_diag: 43 s on grass with a start speed of 0.785 m/s.
-
Start the game with the Polaris, the vehicle bridge and a session log.
-
In a second shell, send the commands of the log to the game. The start pose is on a grass lane.
-
Stop the game. The session folder now contains
tractor.csv, which has the pose of each physics step. -
Compare ACRES Core with the session.
PYTHONPATH=Core/Build python Core/Scripts/compare_unreal.py \ --session $HOME/acres-sessions/grass_1/session \ --commands $HOME/acres-sessions/grass_1/replay/commands_sent.csvExpected Result
session: open loop 43 s, speed RMSE 0.044 m/s (2 s means 0.035; limit cycle std game 0.053, Core 0.042; mean game 1.206, Core 1.210), yaw rate RMSE 0.0010 rad/s, 1 m apart after inf s, 5 m after inf s; apart 0.206 m at 10 s, 0.195 m at 20 s, 0.191 m at 30 s, 0.191 m at 40 s, at most 0.378 m windows 2s: n 3, position median 0.164 m, p90 0.174 m (across 0.001 m, p90 0.001; along 0.164 m); heading median 0.03 deg, p90 0.07 deg; speed median 0.031 m/s windows 5s: n 3, position median 0.166 m, p90 0.197 m (across 0.002 m, p90 0.005; along 0.166 m); heading median 0.03 deg, p90 0.07 deg; speed median 0.011 m/s windows 10s: n 3, position median 0.196 m, p90 0.263 m (across 0.011 m, p90 0.019; along 0.196 m); heading median 0.01 deg, p90 0.08 deg; speed median 0.006 m/s
In this run the two simulators stay less than 0.38 m apart during 43 s. The mean speeds are 1.206 m/s and 1.210 m/s. The difference across the track is 0.011 m after 10 s. Almost all of the difference is along the track.
The game does not repeat this run exactly in real time, because the frame times change.
Do steps 1 to 3 a second time with a different session folder to measure this effect.
The options --session2 and --commands2 then add the difference between the two sessions of the game.
In the test record the two sessions of the game were 0.008 m apart at most.
Time Stepping and Determinism gives all measured results and their causes.
The script also reads an episode log or a ROS 2 bag of the game with the option --bag.
C++ API gives all options.
Next Steps#
- Python API: all functions and all array columns of
acres_core. - C++ API: the library, the tools and the scripts.
- Time Stepping and Determinism: the step, the watchdog and the agreement with the game.
- Replay: show an episode log in the game.
- Lockstep Stepping: step the game or
core_simfrom a client. - Learning: the learning stack that uses the batch.