Skip to content

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.

  1. Fill FAcresBatchOptions. Set the content folder. Set the scene file when you need the LiDAR or the collisions.
  2. Call Init. It loads the world and the Polaris parameters and starts the thread pool.
  3. Call Reset with the start poses.
  4. Call Step with one command row for each environment. One call is 12 physics steps, that is 0.1 s.
  5. Read the arrays State, Wheels, Energy, EnergyStep, ZoneCrushed and ZoneStep.

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.
FAcresBatchOptions Options;
Options.World.ContentDir = Root + "/Acres/Content/Simulation";
Options.NumEnvs = 1024;
Options.Threads = 8;
Options.bFarm = false;            // no crop marks, faster
Options.PolarisOverrides = {{"mass.occupants", 2}};

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

t 5.000 s, speed 1.64 m/s

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

706104 crop patches, 7 places, field 24, class 2

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.

// A plane of grass for a replay of a recorded log.
FAcresWorldOptions Options;
Options.ContentDir = Root + "/Acres/Content/Simulation";
Options.bFlat = true;
Options.FlatSurface = "bench_grass";
FAcresWorld World;
World.Load(Options, &Error);

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

height 0.639 m, hit 1 at z 0.639 m, normal z 0.99976, inside 1

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

2182 triangles, 7100 obstacles; ray hits 1 at 9.44 m (building); nearest obstacle 3.20 m; overlap 1

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

mass 1318 kg, t 10.00 s, x 210.02 m, y 190.00 m, speed 2.33 m/s, crushed 32.11 m2

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.

// After the drive of the FAcresPolaris example:
std::printf("crushed %.2f m2, field 24: %.2f m2, "
            "rut behind the left wheels %.3f m\n",
            Farm.CrushedM2, Farm.CrushedByFieldM2[24],
            Farm.RutAt(Foot.X - 2, Foot.Y + .655));
Farm.Reset();

Output

crushed 32.11 m2, field 24: 32.11 m2, rut behind the left wheels 0.004 m

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

1800 x 32 beams, 33752 returns

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

1405 log messages

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.
#include "AcresCoreJson.h"

FJson Site;
if (!FJson::Load(Content + "/ACRE/site.json", Site, &Error))
    return 1;
std::printf("grid_step_m %.3f, surface_width %.0f, "
            "%zu members\n",
            Site.Number("grid_step_m"),
            Site.Number("surface_width"), Site.Size());

Output

grid_step_m 1.524, surface_width 1000, 25 members

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 π.
const FQuat Q = FQuat::FromYawPitchRoll(Pi / 2, 0, 0);
const FVec3 Forward = Q.Forward();
double Yaw, Pitch, Roll;
Q.ToYawPitchRoll(Yaw, Pitch, Roll);
std::printf("forward (%.3f, %.3f, %.3f), yaw %.4f rad, "
            "WrapPi(4) %.4f\n",
            Forward.X, Forward.Y, Forward.Z, Yaw, WrapPi(4));

Output

forward (0.000, 1.000, 0.000), yaw 1.5708 rad, WrapPi(4) -2.2832

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.

Core/Build/core_replay --root . \
    --run grass_diag_20260731_174757 \
    --ground bench_grass --out /tmp/grass
head -2 /tmp/grass_core.csv | cut -c1-74

Output

grass_diag_20260731_174757: 4986 steps, start 0.000 s, initial speed 0.785 m/s, ground bench_grass -> /tmp/grass_{core,bench}.csv
t_s,x_m,y_m,yaw_rad,yaw_rate_rad_s,speed_mps,swa_deg,swa_ref_deg,steer_enab
0.00833,1.24978,0.01387,0.000000,0.000000,0.78509,12.5314,12.5314,0,19.3295

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

2 scans, 10.3 ms per scan, 5.61 M rays/s (one thread)
-rw-r--r--. 1 user user 288008 garage.scan
-rw-r--r--. 1 user user 288008 lot.scan

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.

PYTHONPATH=Core/Build python Core/Scripts/bench.py

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.

python Core/Scripts/replay_logs.py --out /tmp/replay \
    --figures /tmp/replay/figures

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.

S=$HOME/acres-sessions/grass_1
PYTHONPATH=Core/Build python Core/Scripts/compare_unreal.py \
    --session $S/session \
    --commands $S/replay/commands_sent.csv

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.

python Core/Scripts/unreal_dbw_replay.py \
    --run grass_diag_20260731_174757 --speed 0.785 \
    --start 203.32,138.01,90 --port 5556 \
    --out $HOME/acres-sessions/grass_1/replay

Output

{
 "run": "grass_diag_20260731_174757",
 ...
 "speed_mps": 0.785
}

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.

python Core/Scripts/compare_lidar.py \
    --summary /tmp/lidar_compare.json \
    --figure /tmp/lidar_compare.png

Output

  "ground_lower": {
   "n": 475237,
   "median_diff_m": -0.0001,
   "p50_abs_m": 0.0054,
   "p90_abs_m": 0.105,
   "within_0.1_m": 0.899,
   "within_0.5_m": 0.923,
   "within_2_m": 0.995
  },
...
summary: /tmp/lidar_compare.json
figure: /tmp/lidar_compare.png

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.

python Core/Scripts/build_scene.py --out /tmp/acre_scene.json
cmp /tmp/acre_scene.json Core/Data/acre_scene.json \
    && echo identical

Output

/tmp/acre_scene.json: 63 buildings, 26 bins, 2072 triangles, 7010 cylinders (6984 trunks), 6984 crowns (342 gone trees left out), 0.66 MB
identical