C++ API#
This page is the reference of the C++ library libacres_core, its command-line tools and the scripts in Core/Scripts.
The library is ACRES Core: the Polaris, the ACRE world, the ray-cast LiDAR and the episode log without Unreal Engine.
Build ACRES Core builds the library. All names are in the namespace AcresCore.
The model types, for example AcresSim::FDbwCommand, come from the engine-free model headers of the game.
C++ Headers lists each declaration of each header with its comment.
Index#
| Name | Kind | Description |
|---|---|---|
| CMake targets | build | The library targets and the include paths. |
| Minimal program | example | A complete program that steps four environments. |
FAcresBatch |
class | N environments on a thread pool. |
FAcresBatchOptions |
struct | The options of a batch. |
StepPhysics |
function | One physics step with complete drive-by-wire commands. |
FAcresWorld |
class | The shared, static world. |
FAcresWorldOptions |
struct | The options of the world. |
FAcresTerrain |
class | The height field of the tile and its ray cast. |
FAcresScene |
class | The obstacles for the LiDAR and the collisions. |
FAcresPolaris |
class | One Polaris: the model on a rigid body. |
FAcresFarmState |
class | The crop marks and the ruts of one environment. |
FAcresLidar |
class | The ray-cast LiDAR. |
FAcresEpisodeLog |
class | The writer of the episode log. |
FJson |
class | The reader of the configuration files. |
FVec3, FQuat, FMat3 |
structs | The vector, quaternion and matrix types. |
| Lifecycle and threads | table | The sequence of calls and the thread rules of each class. |
core_replay |
tool | Replays a recorded drive-by-wire log. |
lidar_compare |
tool | Makes Helios scans at a list of poses. |
bench.py |
script | Measures the speed of ACRES Core. |
replay_logs.py |
script | Compares ACRES Core with the Polaris test stand and the recorded logs. |
compare_unreal.py |
script | Compares ACRES Core with the game. |
unreal_dbw_replay.py |
script | Sends a recorded command log to the game. |
compare_lidar.py |
script | Compares the ray-cast LiDAR with the GPU LiDAR of the game. |
build_scene.py |
script | Makes the obstacle scene file. |
Use the Library#
CMake Targets#
Core/CMakeLists.txt defines the targets. The project has no install step and no package file.
Add the folder Core to your CMake project as a subdirectory, then link one of the two library targets.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
acres_core |
shared library | libacres_core.so. |
||
acres_core_static |
static library | libacres_core_static.a. The tools, the tests and acres_core_sim link this target. |
||
acres_core_py |
Python module | The module acres_core. It exists when ACRES_CORE_PYTHON is ON. |
||
ACRES_CORE_PYTHON |
option | ON |
Set it to OFF when your project does not need the Python module. |
The two library targets export their include paths: Core/Source and Acres/Source/Acres.
They also export the thread library. The sources need C++20.
The sources compile with -Wall -Wextra -Wshadow -Wpedantic -Werror.
Without CMake, give the two include paths and the library to the compiler.
# Build with CMake. ACRES_ROOT is the repository root.
ACRES_ROOT=$HOME/ACRES cmake -S . -B build -G Ninja
cmake --build build
build/first_core $HOME/ACRES
# Or compile directly against Core/Build.
cd $HOME/ACRES
g++ -std=c++20 -O2 -I Core/Source -I Acres/Source/Acres \
first_core.cpp -L Core/Build -lacres_core -pthread \
-o first_core
LD_LIBRARY_PATH=Core/Build ./first_core .
# CMakeLists.txt of a program that uses ACRES Core
cmake_minimum_required(VERSION 3.20)
project(first_core CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(ACRES_ROOT "$ENV{ACRES_ROOT}" CACHE PATH "ACRES root")
set(ACRES_CORE_PYTHON OFF CACHE BOOL "No Python module" FORCE)
add_subdirectory(${ACRES_ROOT}/Core
${CMAKE_BINARY_DIR}/acres_core
EXCLUDE_FROM_ALL)
add_executable(first_core first_core.cpp)
target_link_libraries(first_core PRIVATE acres_core_static)
Minimal Program#
The program loads the world, puts four environments at the ICSC garage and drives them for 10 s. Each environment gets a different curvature command.
The sequence of calls is the lifecycle of a batch.
- Fill
FAcresBatchOptions. Set the content folder. Set the scene file when you need the LiDAR or the collisions. - Call
Init. It loads the world and the Polaris parameters and starts the thread pool. - Call
Resetwith the start poses. - Call
Stepwith one command row for each environment. One call is 12 physics steps, that is 0.1 s. - Read the arrays
State,Wheels,Energy,EnergyStep,ZoneCrushedandZoneStep.
The columns of the arrays are the enumerations ECommandColumn, EStateColumn and EWheelColumn.
Python API gives the unit of each column.
#include "AcresCoreBatch.h"
#include <cstdio>
#include <limits>
#include <vector>
using namespace AcresCore;
int main(int Argc, char** Argv)
{
const std::string Root = Argc > 1 ? Argv[1] : ".";
FAcresBatchOptions Options;
Options.World.ContentDir =
Root + "/Acres/Content/Simulation";
Options.World.SceneFile =
Root + "/Core/Data/acre_scene.json";
Options.NumEnvs = 4;
FAcresBatch Batch;
std::string Error;
if (!Batch.Init(Options, &Error))
{
std::fprintf(stderr, "%s\n", Error.c_str());
return 1;
}
// Start poses: x, y (m), yaw (rad), speed (m/s).
const FAcresPlace* Home =
Batch.World.FindPlace("spawn-icsc-garage");
std::vector<int> Indices;
std::vector<double> Poses;
for (int I = 0; I < Batch.Size(); ++I)
{
Indices.push_back(I);
Poses.insert(Poses.end(),
{Home->X + 6. * I, Home->Y, Pi / 2, 0.});
}
Batch.Reset(Indices.data(), Batch.Size(), Poses.data());
// One command row for each environment. NaN: no message.
const double NaN =
std::numeric_limits<double>::quiet_NaN();
std::vector<double> Rows(
size_t(Batch.Size()) * CommandColumnCount, NaN);
for (int I = 0; I < Batch.Size(); ++I)
{
double* Row =
Rows.data() + size_t(I) * CommandColumnCount;
Row[CmdEnable] = 1;
Row[CmdSteerMode] = AcresSim::DbwSteerCurvature;
Row[CmdSteerValue] = .02 * I;
Row[CmdUlcMode] = AcresSim::UlcVelocity;
Row[CmdUlcValue] = 3;
}
for (int Step = 0; Step < 100; ++Step) // 100 x 0.1 s
Batch.Step(Rows.data());
for (int I = 0; I < Batch.Size(); ++I)
{
const double* S =
Batch.State.data() + size_t(I) * StateColumnCount;
const double* E = Batch.Energy.data() +
size_t(I) * EnergyColumnCount;
std::printf("env %d: t %.1f s, x %.2f m, y %.2f m, "
"yaw %.3f rad, speed %.2f m/s, "
"fuel %.0f kJ\n",
I, S[StTime], S[StX], S[StY], S[StYaw],
S[StSpeed], E[0] / 1e3);
}
return 0;
}
Output
env 0: t 10.0 s, x -37.04 m, y -115.85 m, yaw 1.431 rad, speed 3.55 m/s, fuel 266 kJ
env 1: t 10.0 s, x -36.12 m, y -116.69 m, yaw 1.858 rad, speed 3.55 m/s, fuel 270 kJ
env 2: t 10.0 s, x -35.06 m, y -117.99 m, yaw 2.289 rad, speed 3.51 m/s, fuel 271 kJ
env 3: t 10.0 s, x -32.40 m, y -121.32 m, yaw 2.665 rad, speed 3.50 m/s, fuel 277 kJ
Classes#
FAcresBatch#
Header AcresCoreBatch.h. N independent Polaris environments on one shared world.
Step and Reset divide the work between the threads of a pool that stays alive between calls.
One thread steps one environment.
| Function | Description |
|---|---|
Init(Options, Error) |
Loads the world and the parameters, makes the environments and the pool. Returns false with a message on a load error. |
Size() |
The number of environments. |
Reset(Indices, Count, Poses, bSettled, Gear, DriveMode) |
Puts environments at poses. One pose is x, y, yaw and speed. |
Step(Commands) |
Advances all environments by SubSteps physics steps. Commands is Size() rows of CommandColumnCount values, or null to hold. |
StepPhysics(Commands) |
Advances all environments by one physics step. |
SetConditions(...) |
Sets the soil-water scenario of environments. |
SetParameters(Indices, Count, Overrides, Error) |
Gives environments a different parameter set. The vehicles need a reset afterwards. |
StartLog(Index, Path, Metadata, AgentName, Error), StopLog(Index) |
Records one environment into an episode log. |
Scan(Indices, Count, Pattern, OutRanges, bNoise, Seed, OutMaterial) |
Makes LiDAR scans. Pattern is LidarHelios or LidarPlanar. |
AgentInfo, ConditionsMessage, FillAgentState |
The messages of the episode log for one environment. acres_core_sim uses them to write a log with more than one agent. |
ParallelFor(Count, Fn) |
Runs Fn(i) for each index on the pool and waits. |
| Member | Description |
|---|---|
State, Wheels, Energy, EnergyStep, ZoneCrushed, ZoneStep |
The output arrays as std::vector<double>, one row for each environment. |
World |
The shared FAcresWorld. |
Envs |
The environments (FAcresEnv): vehicle, farm state, conditions, held command and log. |
BaseParameters, Options |
The Polaris parameters and the options of Init. |
LidarPatterns |
The two LiDAR patterns of Scan. |
The functions CommandColumnName, StateColumnName, WheelColumnName and EnergyColumnName give the column names.
The Python class Batch is a thin layer on this class.
FAcresBatchOptions BatchOptions;
BatchOptions.World.ContentDir = Content;
BatchOptions.World.SceneFile =
Root + "/Core/Data/acre_scene.json";
BatchOptions.NumEnvs = 2;
FAcresBatch Batch;
if (!Batch.Init(BatchOptions, &Error)) return 1;
const int Indices[2] = {0, 1};
const double Poses[8] = {Home->X, Home->Y, Pi / 2, 0,
Home->X + 6, Home->Y, Pi / 2, 0};
Batch.Reset(Indices, 2, Poses);
// LiDAR: one planar scan for each environment.
const int Planar = FAcresBatch::LidarPlanar;
const int Beams = Batch.LidarPatterns[Planar].BeamCount();
std::vector<float> Ranges(size_t(2) * Beams);
Batch.Scan(Indices, 2, Planar, Ranges.data());
FAcresBatchOptions#
The options of FAcresBatch::Init.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
World |
FAcresWorldOptions |
The world. World.ContentDir is necessary. |
||
PolarisConfig |
string | <ContentDir>/polaris.json |
The parameter file of the Polaris. | |
PolarisOverrides |
map of string to double | empty | Parameters by their keys. | |
Occupants |
int | -1 |
The number of persons. -1 keeps the value of the file. |
|
NumEnvs |
int | 1 |
The number of environments. | |
Threads |
int | 0 |
The number of threads. 0 uses all hardware threads. |
|
PhysicsDt |
double | s | 1. / 120 |
The physics step. |
SubSteps |
int | 12 |
The number of physics steps in one Step. |
|
CommandResendSteps |
int | 6 |
Step sends a held command again after this number of physics steps. 0 sends it one time only. |
|
bFarm |
bool | true |
Keeps the crop marks and the ruts. | |
OffMapMarginM |
double | m | 10 |
An environment nearer than this to the edge of the tile is off the map. |
StepPhysics#
FAcresBatch::StepPhysics(Commands) advances each environment by one physics step.
The caller gives one complete AcresSim::FDbwCommand for each environment.
The flags bSteerFresh, bThrottleFresh, bBrakeFresh and bUlcFresh mark a message that arrives at this step.
A fresh flag starts the 0.1 s timer of the watchdog again.
This is the command path of the game. core_sim uses it for each physics step.
Step uses command rows and its own clock for held commands. StepPhysics does not use them.
The two functions give the same result bit for bit when the commands are the same.
AcresSim::FDbwCommand Command;
Command.bSystemEnable = true;
Command.bSteerEnable = Command.bUlcEnable = true;
Command.SteerMode = AcresSim::DbwSteerAngle;
Command.SteerValue = 12.5; // deg, straight
Command.UlcMode = AcresSim::UlcVelocity;
Command.UlcValue = 2; // m/s
std::vector<AcresSim::FDbwCommand> Commands(2, Command);
for (int K = 0; K < 600; ++K) // 5 s
{
// A controller at 20 Hz: a message each 6 steps.
for (AcresSim::FDbwCommand& C : Commands)
C.bSteerFresh = C.bUlcFresh = K % 6 == 0;
Batch.StepPhysics(Commands.data());
}
std::printf("t %.3f s, speed %.2f m/s\n",
Batch.State[StTime], Batch.State[StSpeed]);
Output
FAcresWorld#
Header AcresCoreWorld.h. The static world that all environments share.
It holds the terrain, the surface map, and the surface templates and the soil of tractor.json.
It also holds the soil units, the field raster, the crop patches, the places and the obstacle scene.
| Function | Description |
|---|---|
Load(Options, Error) |
Loads all data. Returns false with a message for a file that is missing or not correct. |
SurfaceClassAt(X, Y) |
The class of the surface map (AcresSim::ESurfaceClass). |
SurfaceAt(X, Y, Conditions, RutDepthM, OutClass) |
The surface below a wheel: the template, the soil water, the rut depth and the soil class. |
FieldAt(X, Y), FieldPolygonAt(X, Y) |
The field number from the 4 m raster, or from the field polygons. |
SoilTextureAt(X, Y) |
The texture group of the soil unit. |
CellTheta(Cell, Conditions) |
The water content of a 4 m cell for a scenario. |
FieldGrowth(Field) |
The growth of the crop of a field, 0 to 1. |
PatchBin(Bx, By, Begin, End) |
The crop patches in one 4 m bin. |
FindPlace(IdOrName) |
A named place, or null. |
SetZones(Data, Width, Height, X0, Y0, CellM), ZoneAt(X, Y) |
The zone raster of the crop accounts. |
The members Terrain, Scene, Patches and Places give direct access to the data.
FAcresConditions holds the soil-water scenario of one environment: GlobalFraction, FieldFraction[60] and SurfaceFilm.
FAcresWorldOptions Options;
Options.ContentDir = Root + "/Acres/Content/Simulation";
Options.SceneFile = Root + "/Core/Data/acre_scene.json";
Options.ObstaclesFile =
Options.ContentDir + "/ACRE/Scouting/obstacles.json";
FAcresWorld World;
std::string Error;
if (!World.Load(Options, &Error))
{
std::fprintf(stderr, "%s\n", Error.c_str());
return 1;
}
const FAcresPlace* Home =
World.FindPlace("spawn-icsc-garage");
std::printf("%zu crop patches, %zu places, field %d, "
"class %d\n",
World.Patches.size(), World.Places.size(),
World.FieldAt(195, 190),
World.SurfaceClassAt(Home->X, Home->Y));
Output
FAcresWorldOptions#
The options of FAcresWorld::Load.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
ContentDir |
string | The folder Acres/Content/Simulation. |
||
bFlat |
bool | false |
A level plane with one surface. It replaces the ACRE tile. | |
FlatZ |
double | m | 0 |
The height of the plane. |
FlatSurface |
string | "asphalt" |
A surface of tractor.json, or bench_asphalt, bench_grass, bench_gravel. |
|
CropSeason |
string | "2026" |
The crop plan ACRE/crops_<season>.json. "legacy" uses fields.json. |
|
InitialGrowth |
double | 1 |
The growth of each planted field, 0 to 1. | |
bGeneratedCrops |
bool | true |
Plants the fields that have no surveyed patches on the patch grid. | |
bFarmWater |
bool | true |
The wheels read the soil water of the farm grid. false uses the water of the surface templates. |
|
CropHeightM |
double[3] | m | 2.4, 0.5, 0.55 |
The height of mature corn, soybean and potato. These values are estimates. |
SceneFile |
string | empty | The obstacle scene, Core/Data/acre_scene.json. Empty: no scene. |
|
ObstaclesFile |
string | empty | The obstacles of the scouting map, ACRE/Scouting/obstacles.json. It needs SceneFile. |
|
GroundClassesFile |
string | empty | The ground classes of the scouting map, ACRE/Scouting/ground_classes.u8. They become the zone raster. |
Note
The Python class Batch sets the three file options itself. A C++ program must set them.
FAcresTerrain#
Header AcresCoreTerrain.h. The survey height field of the tile.
The file heights.f32 has 1001 × 1001 corner heights on the survey grid of 1.524 m.
Each cell is two triangles, the same triangles as the collision mesh of the game.
Heights, normals and ray casts are exact on this mesh.
| Function | Description |
|---|---|
Load(AcreDirectory, Error) |
Reads site.json and heights.f32 from the folder ACRE. |
MakeFlat(Z, HalfSizeM, StepM) |
Makes a level plane. |
HeightAt(X, Y) |
The height of the surface. |
NormalAt(X, Y) |
The unit normal of the triangle below a point. It points up. |
RayCast(Origin, Direction, MaxT, Hit) |
The first hit of a ray on the surface. FRayHit gives the distance, the point and the normal. |
Inside(X, Y, MarginM) |
true when the point is on the grid with this margin. |
ToGrid, FromGrid |
The conversion between the frame of the tile and grid cells. |
const FAcresTerrain& Terrain = World.Terrain;
FRayHit Hit;
const bool bHit = Terrain.RayCast({Home->X, Home->Y, 50},
{0, 0, -1}, 100, Hit);
const FVec3 Normal = Terrain.NormalAt(Home->X, Home->Y);
std::printf("height %.3f m, hit %d at z %.3f m, "
"normal z %.5f, inside %d\n",
Terrain.HeightAt(Home->X, Home->Y), bHit,
Hit.Point.Z, Normal.Z,
Terrain.Inside(Home->X, Home->Y, 10));
Output
FAcresScene#
Header AcresCoreScene.h. The static obstacles of the map: buildings, grain bins and trees as simple shapes.
The shapes are triangles, vertical cylinders and ellipsoids in one bounding volume hierarchy.
The collision outlines are polygons and circles on a grid of 8 m.
| Function | Description |
|---|---|
Load(Path, Error) |
Reads the scene file (schema acres-core-scene-1). |
LoadObstacles(Path, Terrain, Error) |
Replaces the collision obstacles with the obstacles of the scouting map. |
RayCast(Origin, Direction, MaxT, Hit, Material, MinT) |
The nearest hit of a ray and its material index. |
Occluded(Origin, Direction, MaxT) |
true when an object blocks the ray. |
Overlaps(Centre, Yaw, HalfLengthM, HalfWidthM, ZMin, ZMax, ObstacleId) |
true when a box on the ground touches an obstacle. |
NearestObstacleM(X, Y, MaxM) |
The horizontal distance to the nearest obstacle. |
ObstacleName(ObstacleId) |
The identifier of a collision obstacle. |
Material(Index), FindMaterial(Name), MaterialCount() |
The LiDAR materials. |
TriangleCount(), CylinderCount(), EllipsoidCount(), ObstacleCount(), NodeCount() |
The sizes of the scene. |
The ACRE Scene gives the sources of the obstacle data.
const FAcresScene& Scene = *World.Scene;
int Material = -1, Obstacle = -1;
const bool bWall =
Scene.RayCast({Home->X, Home->Y, 2}, {.82, -.57, 0}, 100,
Hit, Material);
const bool bTouch = Scene.Overlaps({-7.3, -162, 0}, 0, 1.9,
.8, .3, 2, &Obstacle);
std::printf("%d triangles, %d obstacles; ray hits %d at "
"%.2f m (%s); nearest obstacle %.2f m; "
"overlap %d\n",
Scene.TriangleCount(), Scene.ObstacleCount(),
bWall, Hit.T,
Scene.Material(Material).Name.c_str(),
Scene.NearestObstacleM(Home->X, Home->Y, 30),
bTouch);
Output
FAcresPolaris#
Header AcresCorePolaris.h. One Polaris: the drive-by-wire, powertrain, tyre and driveline model of the game on a rigid body.
LoadPolarisParameters reads polaris.json the same way as the game and applies overrides.
| Function | Description |
|---|---|
LoadPolarisParameters(Path, World, Overrides, Occupants, P, Error) |
Fills AcresSim::FUtvParameters. Returns false for a read error or a key that the model does not know. |
Init(Parameters) |
Copies the parameters and puts the vehicle at the origin. |
Reset(World, X, Y, YawRad, SpeedMps, bSettled, Gear, DriveMode) |
Puts the vehicle at a pose and resets the model state. |
Step(Dt, Command, World, Conditions, Farm, Info) |
Advances one physics step. Farm can be null. Info gets the crushed area and the surface below each wheel. |
BaseFootprint(), Yaw(), PitchUp(), Roll() |
The pose of base_footprint. |
VelocityBody(), AngularVelocityBody(), Speed() |
The motion in the body frame. |
KineticEnergy() |
The kinetic energy of the chassis. |
Parameters(), MutableParameters() |
The parameters in use. |
The members Com, Velocity, AngularVelocity and Orientation are the state of the rigid body in the frame of the tile.
The member S is the model state: the engine, the CVT, the wheels, the energy ledger and the drive-by-wire reports.
Time Stepping and Determinism gives the order of one step and the integration.
AcresSim::FUtvParameters Parameters;
if (!LoadPolarisParameters(Content + "/polaris.json", World,
{{"mass.occupants", 2}}, -1,
Parameters, &Error))
return 1;
FAcresPolaris Vehicle;
Vehicle.Init(Parameters);
Vehicle.Reset(World, 195, 190, 0); // field 24, to the east
FAcresConditions Conditions;
FAcresFarmState Farm;
AcresSim::FDbwCommand Command;
Command.bSystemEnable = true;
Command.bSteerEnable = Command.bUlcEnable = true;
Command.SteerMode = AcresSim::DbwSteerAngle;
Command.SteerValue = Parameters.SteeringCenterDeg;
Command.UlcMode = AcresSim::UlcVelocity;
Command.UlcValue = 2;
for (int K = 0; K < 1200; ++K) // 10 s
{
Command.bSteerFresh = Command.bUlcFresh = K % 6 == 0;
Vehicle.Step(1. / 120, Command, World, Conditions, &Farm);
}
const FVec3 Foot = Vehicle.BaseFootprint();
std::printf("mass %.0f kg, t %.2f s, x %.2f m, y %.2f m, "
"speed %.2f m/s, crushed %.2f m2\n",
Vehicle.Parameters().MassKg, Vehicle.TimeS,
Foot.X, Foot.Y, Vehicle.Speed(), Farm.CrushedM2);
Output
FAcresFarmState#
Header AcresCoreFarm.h. The changes that the vehicle of one environment makes on the farm: crushed crop and ruts.
The crop layout and the soil are in the shared world. This class holds only the changes, thus a reset is fast.
| Function | Description |
|---|---|
Reset() |
Removes all marks. |
RutAt(X, Y) |
The rut depth of the 25 cm cell below a point. |
Wheel(World, Stamp) |
Applies one tyre contact: crushes crop and makes the rut deeper. |
Body(World, Stamp) |
Applies the body: crushes crop that is taller than the ground clearance. |
| Member | Description |
|---|---|
CrushedM2 |
The crushed area from the last reset. |
CrushedByFieldM2[60], CrushedByZoneM2[8] |
The crushed area for each field and for each zone. |
Crushed |
The crushed sample masks by patch index. |
Ruts |
The rut depth by cell. |
FAcresFarmStamp describes one contact: the centre, the heading, the width, the length, the contact pressure and the sinkage.
Crops and Ground Classes gives the rules.
Output
FAcresLidar#
Header AcresCoreLidar.h. A ray-cast LiDAR on the terrain, the obstacle scene and the boxes of other vehicles.
FAcresLidarPattern holds the beam pattern, the mount and the signal model.
FromSensorsJson reads the Helios of sensors_polaris.json. Planar makes a level scan.
| Function | Description |
|---|---|
FAcresLidar(Terrain, Scene) |
Keeps references to the terrain and the scene. Scene can be null. |
Scan(Pattern, BasePosition, BaseOrientation, Boxes, Out, Options) |
Makes one scan from the pose of base_footprint. |
CastRay(...) |
The nearest hit of one ray. |
FAcresLidarPattern::FromSensorsJson(Path, Out, Error) |
Reads the block lidar of a sensor file. |
FAcresLidarPattern::Planar(Beams, MaxRangeM) |
A level pattern at the position of the Helios. |
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
bNoise |
bool | false |
Adds range noise and signal noise. The noise is a function of Seed and the beam. |
|
Seed |
uint64 | 0 |
The seed of the noise and of the returns from the body. | |
bFoliage |
bool | true |
A tree crown lets a part of the beams through. false makes the crowns solid. |
|
bSelfReturns |
bool | true |
Applies the table of returns from the body of the vehicle. | |
GroundMaterial |
int | -1 |
The scene material of the terrain. -1 uses grass. |
|
GroundMaterialAt |
function | empty | A function of the hit point that gives the material of the terrain. |
FAcresLidarScanOut gives the ranges, the points (x, y, z and intensity in the sensor frame), the material codes and the number of returns.
The cell index is column * Rings + ring. LiDAR gives the sensor model.
FAcresLidarPattern Helios;
if (!FAcresLidarPattern::FromSensorsJson(
Content + "/sensors_polaris.json", Helios, &Error))
return 1;
const FAcresLidar Lidar(World.Terrain, World.Scene.get());
FAcresLidarOptions ScanOptions;
ScanOptions.bNoise = true;
ScanOptions.Seed = 7;
FAcresLidarScanOut Scan;
Lidar.Scan(Helios, Vehicle.BaseFootprint(),
Vehicle.Orientation, {}, Scan, ScanOptions);
std::printf("%d x %d beams, %d returns\n", Scan.Columns,
Scan.Rings, Scan.Returns);
Output
FAcresEpisodeLog#
Header AcresCoreEpisodeLog.h. The episode log of ACRES Core.
The class feeds the writer of the game (AcresEpisodeLog.h), thus the file has the same format as a log of the game.
FAcresBatch::StartLog uses this class for one environment.
| Function | Description |
|---|---|
Open(Path, Metadata, Error) |
Opens the file and writes the metadata record with the source core. |
AddAgent(Info) |
Adds an agent. Returns its index. |
BeginEpisode(TimeS, Step, ResetEpoch, Conditions, Shifts) |
Starts an episode: at the start of the recording and after each reset. |
WriteStep(TimeS, Step, States, Stamps) |
Writes the agent states and the farm contacts of one physics step. |
WriteDbwCommand(Agent, TimeS, Command, FreshMask) |
Writes the drive-by-wire messages that arrived at this step. |
WriteTask(Status) |
Writes a task status. |
Close(), IsOpen(), MessageCount() |
Closes the file and gives its state. |
Episode Log gives the topics and the messages.
if (!Batch.StartLog(0, "/tmp/core-cpp.mcap", {{"seed", "7"}},
"polaris", &Error))
return 1;
for (int K = 0; K < 600; ++K) // 5 s
{
for (AcresSim::FDbwCommand& C : Commands)
C.bSteerFresh = C.bUlcFresh = K % 6 == 0;
Batch.StepPhysics(Commands.data());
}
const uint64_t Messages = Batch.Envs[0].Log->MessageCount();
Batch.StopLog(0);
std::printf("%llu log messages\n",
static_cast<unsigned long long>(Messages));
Output
FJson#
Header AcresCoreJson.h. A small reader for the JSON configuration files. It has no external dependency.
A number is a double. An object keeps the sequence of its keys.
| Function | Description |
|---|---|
FJson::Parse(Text, Out, Error), FJson::Load(Path, Out, Error) |
Reads a document from a string or from a file. |
Kind(), IsNull(), IsNumber(), IsString(), IsArray(), IsObject(), IsBool() |
The type of a value. |
AsNumber(Default), AsBool(Default), AsString() |
The value. |
Items(), Members(), Size() |
The elements of an array and the members of an object. |
operator[] |
An element by index or a member by key. A missing item gives a null value. |
Has(Key), Number(Key, Default), Str(Key, Default) |
Access to the members of an object. |
ReadFileBytes(Path, Out) |
Reads a binary file. |
Output
FVec3, FQuat, FMat3#
Header AcresCoreMath.h. Vector, quaternion and matrix types in double precision. The header has no source file.
ACRES Core uses a right-handed frame: x east, y north, z up for the world and x forward, y left, z up for the body. The game uses the left-handed frame of Unreal Engine: X east, Y south, Z up, in centimetres. The two frames are mirror images. A conversion changes the sign of y and the unit.
| Name | Description |
|---|---|
FVec3 |
A vector with Dot, Cross, Length, LengthSquared, Normalized and the arithmetic operators. |
FQuat |
A rotation quaternion (W, X, Y, Z) with Rotate, Unrotate, Forward, Left, Up, Conjugate, Normalized. |
FQuat::FromAxisAngle, FQuat::FromYawPitchRoll, ToYawPitchRoll |
Conversions. The Euler angles use the sequence yaw, pitch, roll, as tf2. |
FMat3 |
A 3 × 3 matrix. FMat3::FromQuat makes the rotation matrix. |
QuatFromAxes(Fx, Fy, Fz) |
The quaternion of three body axes. |
WrapPi(A) |
An angle in the range from -π to π. |
Pi |
The constant π. |
Output
Lifecycle and Threads#
| Class | Lifecycle | Threads |
|---|---|---|
FAcresWorld |
Call Load one time for each process. |
Read-only after Load. Each thread can query it. Do not call SetZones during a step. |
FAcresTerrain |
Load or MakeFlat. |
Read-only afterwards. Each thread can query it. |
FAcresScene |
Load, then LoadObstacles when necessary. |
Read-only afterwards. Each thread can query it. |
FAcresLidar |
Make it from a terrain and a scene. These objects must live longer than the LiDAR. | Read-only. Scan can run on many threads at the same time. |
FAcresPolaris |
Init, Reset, then Step for each physics step. |
One thread for each vehicle. |
FAcresFarmState |
One object for each environment. Reset with the vehicle. |
The thread of its environment. No lock. |
FAcresEpisodeLog |
Open, AddAgent, BeginEpisode, WriteStep for each step, Close. |
The thread of its environment. |
FAcresBatch |
Init, Reset, then Step or StepPhysics. The destructor stops the pool. |
Call Step, StepPhysics, Reset and Scan from one thread at a time. |
FJson |
Parse or Load. |
Not safe during the parse. Read-only afterwards. |
The model functions in the namespace AcresSim have no global state.
The result of a batch does not change with the number of threads.
Time Stepping and Determinism gives the measurements.
Command-Line Tools#
Core/build.sh builds the tools into Core/Build. The sources are in Core/Tools.
core_replay#
Replays a recorded drive-by-wire log of the real Polaris through two models on the same level ground.
The first model is the Polaris test stand of Tools/PolarisModel. The second model is ACRES Core.
The tool writes one CSV file for each model, with one row for each physics step.
replay_logs.py reads the two files.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
--root |
path | . |
The repository root. | |
--run |
name | The name of the log. The tool reads <run>_commands.csv and <run>_reports.csv. This option is necessary. |
||
--ground |
name | bench_asphalt |
The surface: bench_asphalt, bench_grass, bench_gravel or a surface of tractor.json. |
|
--data |
path | <root>/Tools/PolarisModel/Data |
The folder of the logs. | |
--out |
path | the name of the log | The prefix of the output files <prefix>_core.csv and <prefix>_bench.csv. |
The motion columns are t_s, x_m, y_m, yaw_rad, yaw_rate_rad_s and speed_mps.
The steering columns are swa_deg, swa_ref_deg, steer_enabled and road_wheel_rad.
The other columns are throttle_cmd_pct, throttle_pedal_pct, brake_bar, gear, ulc_vel_ref_mps, ulc_enabled,
speed_brake_mps, rpm and fuel_lph.
The exit code is 2 without --run and 1 for a file that the tool cannot read.
Output
lidar_compare#
Makes one Helios scan for each pose of a list. compare_lidar.py uses the tool.
The tool loads the terrain and Core/Data/acre_scene.json.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
--root |
path | . |
The repository root. | |
--poses |
path | The CSV file of the poses. This option is necessary. | ||
--out |
path | The output folder. It must exist. This option is necessary. | ||
--noise |
flag | off | Adds range noise. |
The pose file has a header line, then one line for each pose: name,x,y,z,qx,qy,qz,qw,yaw_rad.
The pose is the pose of base_footprint. A z of nan puts the vehicle on the terrain with the heading yaw_rad.
The file <out>/<name>.scan contains two int32 values (columns, rings), then one float32 range for each cell,
then one uint8 material code for each cell. The cell index is column * rings + ring.
mkdir -p /tmp/scans
printf 'name,x,y,z,qx,qy,qz,qw,yaw_rad\n' > /tmp/poses.csv
printf 'garage,-38.71,-140.513,nan,0,0,0,1,1.5708\n' \
>> /tmp/poses.csv
printf 'lot,210.312,-249.936,nan,0,0,0,1,1.5708\n' \
>> /tmp/poses.csv
Core/Build/lidar_compare --root . --poses /tmp/poses.csv \
--out /tmp/scans
ls -l /tmp/scans
Output
Scripts#
The scripts are in Core/Scripts. Run them in the repository root after the build.
The scripts that import acres_core need PYTHONPATH=Core/Build. The scripts that make figures need matplotlib.
Tools and Scripts lists the scripts of the other folders.
bench.py#
Measures the speed of ACRES Core on the ACRE tile. The environments start near the two spawn places and get a new curvature and speed command each 2 s. The script measures four cases: with and without the farm marks, with one thread and with all threads. It also measures the two LiDAR patterns.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
--root |
path | the repository | The repository root. | |
--envs |
int | 64 |
The number of environments. | |
--seconds |
float | s | 20 |
The simulated time of each case. |
--json |
path | Writes the results into this file. |
The output is from the test workstation: an AMD Ryzen 7 9700X with 8 cores and 16 threads. Other jobs used a part of the processor during the measurement. Three runs gave these ranges.
| Case | Times Real Time | Physics Steps for Each Second |
|---|---|---|
| Farm on, 1 thread | 440 to 560 | 53,000 to 67,000 |
| Farm on, 16 threads | 4,100 to 4,900 | 492,000 to 590,000 |
| Farm off, 1 thread | 590 to 620 | 71,000 to 74,000 |
| Farm off, 16 threads | 4,400 to 4,760 | 530,000 to 571,000 |
| Helios scans, 1 thread | 90 to 94 scans, 5.2 to 5.4 million rays | |
| Helios scans, 16 threads | 770 to 805 scans, 44 to 46 million rays | |
| Planar scans, 1 thread | 16,000 to 16,600 scans |
"Times real time" is the sum of the simulated time of all environments divided by the wall time.
Output
farm_1_thread 558 x real time (67,014 physics steps/s)
farm_all_threads 4173 x real time (500,816 physics steps/s)
no_farm_1_thread 617 x real time (74,054 physics steps/s)
no_farm_all_threads 4756 x real time (570,757 physics steps/s)
lidar helios 1 thread: 94.3 scans/s, 5,429,158 rays/s
lidar planar 1 thread: 16035.6 scans/s, 5,772,808 rays/s
lidar helios all threads: 804.8 scans/s, 46,354,550 rays/s
replay_logs.py#
Runs core_replay for the four recorded logs of Tools/PolarisModel/Data.
It then compares ACRES Core with the Polaris test stand, and the two models with the reports of the real vehicle.
The script prints two tables and writes one figure for each log.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
--root |
path | the repository | The repository root. | |
--build |
path | <root>/Core/Build |
The folder that contains core_replay. |
|
--out |
path | $TMPDIR/acres-core-replay |
The folder of the traces and of the summary. | |
--json |
path | <out>/replay_logs.json |
The summary file. | |
--figures |
path | ~/UnrealEngine/Demo/26-acres-core |
The folder of the figures. |
On the three logs in which the vehicle stands, ACRES Core and the test stand give identical traces.
On the log grass_diag, a drive of 30 m, the speeds differ by 0.012 m/s at most and the paths end 3.4 mm apart.
The cause of this difference is the chassis: ACRES Core has a rigid body in three dimensions and the test stand has a planar body.
Calibrate against Real Logs gives the logs and the fit.
Output
(a) Core against the Polaris test stand (same commands, same ground)
| Run | Steps | Speed max / RMS (m/s) | Yaw max (rad) | ... | Path end / max (m) |
| dbw_direct_test_01 | 2426 | 0 / 0 | 0 | ... | 0 / 0 |
| grass_diag_20260731_174757 | 4986 | 0.0117 / 0.00147 | 7e-05 | ... | 0.00337 / 0.00343 |
| human_20260813_172445 | 543 | 0 / 0 | 0 | ... | 0 / 0 |
| human_20260813_192910 | 272 | 0 / 0 | 0 | ... | 0 / 0 |
(b) Against the recorded reports (Core | test stand)
| Run | Metric | Core | Test Stand | Reference |
| dbw_direct_test_01 | swa_rmse_deg | 0.7497 | 0.7497 | |
| dbw_direct_test_01 | steer_off_s | 15.85 | 15.85 | |
| grass_diag_20260731_174757 | speed_rmse_mps | 0.107 | 0.1062 | |
| grass_diag_20260731_174757 | curvature_rmse_per_m | 0.001275 | 0.001281 | |
...
summary: /tmp/replay/replay_logs.json
compare_unreal.py#
Drives ACRES Core with the commands of a recording of the game and compares the two trajectories. ACRES Core starts at the pose of the game and gets each command at the same simulation time. The script makes two comparisons.
- Open loop. ACRES Core replays the full command stream one time. The script reports the speed, the yaw rate and the distance between the two paths.
- Windows. ACRES Core starts again from the pose of the game at regular intervals. The script reports the position error and the heading error 2 s, 5 s and 10 s after the start of each window.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
--root |
path | the repository | The repository root. | |
--bag |
path | A recording: a ROS 2 bag folder, a .db3 file or an .mcap file. An episode log is also a valid input. |
||
--session |
path | A session folder of the game that unreal_dbw_replay.py drove. |
||
--commands |
path | With --session: the file commands_sent.csv. |
||
--session2, --commands2 |
path | A second session of the same log. The script then also reports the difference between the two sessions of the game. | ||
--follower-all |
flag | off | Compares each recording that $POLARIS_FOLLOWER/sim/configs.json lists. |
|
--only |
names | With --follower-all: only these recordings. |
||
--content-commit |
commit | Uses the configuration folder of this Git commit. | ||
--content-dir |
path | Uses this configuration folder. | ||
--overrides |
text | empty | Polaris parameters as key=value;key=value. |
|
--game-drop |
flag | off | Starts ACRES Core with the 0.2 m drop of the game. | |
--farm-water, --no-farm-water |
flag | from the session | Selects the source of the soil water. | |
--every |
float | s | 10 |
The interval between two windows. |
--warmup |
float | s | 3 |
The time before a window in which ACRES Core runs the logged commands. |
--horizon |
float | s | 10 |
The length of a window. |
--json |
path | Writes the results into this file. | ||
--figure-dir |
path | Writes figures into this folder. | ||
--selftest |
flag | off | Compares ACRES Core with itself. Core/build.sh runs this test. |
The script writes temporary files into /tmp/acres-core-compare. The variable ACRES_CORE_SCRATCH changes this folder.
Headless Core Runs gives the procedure. Time Stepping and Determinism gives the results.
Output
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 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
unreal_dbw_replay.py#
Sends the commands of a recorded log to the game through the vehicle bridge.
The script resets the Polaris to a start pose and sends each command when the physics clock of the game gets to its time.
It writes the commands that it sent, with the physics time of the game, for compare_unreal.py.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
--run |
name | dbw_direct_test_01 |
A log of Tools/PolarisModel/Data. |
|
--commands |
path | A different command file in the same format. | ||
--port |
int | 5556 |
The port of the vehicle bridge. | |
--place |
name | spawn-icsc-garage |
The start place. | |
--start |
text | m, m, deg | The start pose as east,north,heading. It replaces --place. |
|
--speed |
float | m/s | 0 |
The speed at the start. |
--settle |
float | s | 3 |
For a start at rest: the time in which the ULC holds the vehicle before the first command. |
--tail |
float | s | 2 |
The time that the script records after the last command. |
--out |
path | replay |
The output folder: commands_sent.csv, reports.jsonl and replay.json. |
|
--selftest |
flag | off | Tests the script with a substitute for the game. |
WARNING
Use this script only with the game on the same workstation. Do not send its commands to the real vehicle.
compare_lidar.py#
Compares the ray-cast Helios of ACRES Core with the GPU LiDAR of the game at the same vehicle poses.
The input is the work folder of the LiDAR calibration: the scans of the game and their poses.
This folder is not part of the repository. The variable POLARIS_LIDAR_WORK gives its path.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
--work |
path | $POLARIS_LIDAR_WORK |
The work folder of the LiDAR calibration. | |
--label |
name | after |
The set of renders in the work folder. | |
--poses |
int | 60 |
The maximum number of scans. | |
--bin |
path | Core/Build/lidar_compare |
The tool that makes the scans of ACRES Core. | |
--scratch |
path | a temporary folder | The folder of the poses and of the scans. | |
--summary |
path | The summary file (JSON). | ||
--figure |
path | ~/UnrealEngine/Demo/26-acres-core/lidar_compare.png |
The figure. |
The script hides the body of the vehicle in the two scans. ACRES Core has no crops and no ground cover.
Output
build_scene.py#
Makes the obstacle scene Core/Data/acre_scene.json for the LiDAR and the collisions of ACRES Core.
The repository contains the result. Run the script only after a change of the map layers.
The buildings and the bins come from Calibration/Map/Layers/building_models.json.
The trees come from a list of the tree instances of the level. This list is not part of the repository.
The LiDAR materials come from sensors_polaris.json.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
--episodes |
path | $POLARIS_EPISODES |
The folder that contains the tree list reference/t20_work/trees_before.json. |
|
--out |
path | Core/Data/acre_scene.json |
The output file. |
The output has the schema acres-core-scene-1: materials, triangles, cylinders, ellipsoids and collision outlines.
The ACRE Scene gives the source data.