Skip to content

Python API#

This page is the reference of the Python module acres_core, the interface of ACRES Core for Python programs. The module has one class, Batch, that steps N Polaris environments on the ACRE tile with NumPy arrays.

Build ACRES Core builds the module. Run each example in the repository root with PYTHONPATH=Core/Build python <file>. The source is Core/Python/acres_core_module.cpp.

Index#

Name Kind Description
PHYSICS_DT constant The length of one physics step.
COMMAND_COLUMNS, STATE_COLUMNS, WHEEL_COLUMNS, ENERGY_COLUMNS constants The column names of the arrays.
SURFACE_CLASSES, GROUND_CLASSES constants The names of the surface classes and of the ground classes.
Batch class Loads the world and makes the environments.
num_envs property The number of environments.
reset method Puts environments at start poses.
step method Advances all environments by one control step with drive-by-wire command rows.
step_curvature_speed method Advances all environments with a curvature command and a speed command.
state property The state array.
wheels property The wheel array.
energy, energy_step properties The energy ledger, in total and for the last control step.
zone_crushed, zone_crushed_step properties The crushed crop area for each zone.
set_conditions method Sets the soil-water scenario of environments.
set_parameters method Changes Polaris parameters of environments.
start_log, stop_log methods Records one environment into an episode log.
lidar method Makes LiDAR scans from the current poses.
set_zones method Replaces the zone raster of the crop accounts.
height_at, surface_class_at, field_at methods Queries of the terrain, the surface map and the field raster.
nearest_obstacle method The distance to the nearest obstacle.
places method The named places of the map.
num_crop_patches, crop_patches, crushed_patches property, methods The crop patches and the crushed samples.
Command columns table The 18 columns of a command row.
State columns table The 49 columns of the state array.
Wheel columns table The 13 columns of the wheel array.
Energy columns table The 24 columns of the energy arrays.
Zone columns table The 8 columns of the zone arrays.
LiDAR arrays table The beam order and the material codes.
Threads section The rules for threads.
Determinism section The results that repeat bit for bit.

Frames and Units#

All positions are in the frame of the tile: x east, y north, z up, in metres from the centre of the tile. The pose of a vehicle is the pose of base_footprint. This point is the middle of the rear axle on the ground. The yaw angle is in radians, counter-clockwise from east. A compass heading \(h\) in degrees gives the yaw \(90° - h\).

Velocities and accelerations of the state array are in the body frame: x forward, y left, z up. The drive-by-wire columns use the units of the ds_dbw_msgs messages: degrees, percent and bar.

One control step of the ACRES Core batch: the command rows go to N environments on the thread pool. Each environment reads its row and runs 12 physics steps: drive-by-wire, steering and powertrain, wheel contacts, driveline solve, farm stamps, rigid body. Then the batch publishes the state arrays. The episode log and the LiDAR scan are optional. One control step of the Core batch, 0.1 s INPUTS EACH ENVIRONMENT, ON ONE THREAD OF THE POOL OUTPUTS Command rows shape (N, 18), ds_dbw units NaN mode: no message Shared world terrain, surface map, soil units, crop patches, obstacle scene read-only for all threads Environment settings soil-water scenario, Polaris parameters FAcresBatch::Step 1 Read the command row new messages, subsystems that the caller holds 12 physics steps of 1/120 s A held command arrives again each 6 steps (20 Hz). 2 Drive-by-wire watchdog, actuators, ULC StepDbw 3 Steering and powertrain road wheels, engine, CVT StepUtvPowertrain 4 Four wheel contacts ray cast, surface, soil water PrepareUtvWheel 5 Driveline solve wheel speeds, tyre forces, fuel SolveUtvDriveline 6 Farm stamps tyre and body marks: crushed crop, ruts 7 Rigid body and energy ledger forces, semi-implicit Euler step AccountChassis 8 Publish the arrays one row for each environment Episode log, optional MCAP, one state message for each physics step State arrays state (N, 49) wheels (N, 4, 13) energy (N, 24) zone_crushed (N, 8) LiDAR scan, optional ray casts from the new poses lidar() The thread pool runs all environments at the same time. One environment uses one thread. The result does not change with the number of threads.
One call of step: each environment reads its command row and runs 12 physics steps on one thread of the pool. Open the diagram

Module Attributes#

PHYSICS_DT#

The length of one physics step in seconds. The value is 1/120. One control step of a batch is substeps physics steps. The default control step is 12 steps, that is 0.1 s.

Example

import acres_core as ac

print(ac.PHYSICS_DT)

Output

0.008333333333333333

Column Name Lists#

Four lists of strings give the column names of the arrays. The index of a name in its list is the column index.

Name Type Unit Default Description
COMMAND_COLUMNS list of 18 str The columns of a command row. See Command columns.
STATE_COLUMNS list of 49 str The columns of state. See State columns.
WHEEL_COLUMNS list of 13 str The columns of wheels. See Wheel columns.
ENERGY_COLUMNS list of 24 str The columns of energy and energy_step. See Energy columns.

Make a dictionary from a list to address a column by its name.

Example

import acres_core as ac

print(len(ac.COMMAND_COLUMNS), len(ac.STATE_COLUMNS),
      len(ac.WHEEL_COLUMNS), len(ac.ENERGY_COLUMNS))
S = {name: i for i, name in enumerate(ac.STATE_COLUMNS)}
print(S["speed"], S["collision"])

Output

18 49 13 24
20 46

Class Name Lists#

Name Type Unit Default Description
SURFACE_CLASSES list of 7 str The classes of the surface map. The index is the value of surface_under and of the wheel column surface_class.
GROUND_CLASSES list of 6 str The ground classes of the scouting map. The index is the column of zone_crushed on the ACRE tile.

Example

import acres_core as ac

print(ac.SURFACE_CLASSES)
print(ac.GROUND_CLASSES)

Output

['field', 'asphalt', 'concrete', 'gravel', 'grass_lane', 'grass', 'dirt']
['crop', 'lane', 'verge', 'edge_band', 'obstacle', 'other']

Batch Class#

Batch#

Batch(root, num_envs=1, threads=0, ...) loads the world one time and makes the environments. Each environment is one Polaris with its own farm marks and its own soil-water scenario. All environments share the terrain, the surface map, the soil data, the crop patches and the obstacle scene. After the constructor each vehicle stands at the origin. Call reset before the first step.

Name Type Unit Default Description
root str The repository root. The module reads Acres/Content/Simulation and Core/Data below it.
num_envs int 1 The number of environments. The minimum is 1.
threads int 0 The number of threads of the pool. 0 uses all hardware threads.
flat bool False True makes a level plane with one surface. It replaces the ACRE tile.
flat_surface str "asphalt" The surface of the plane: a surface of tractor.json, or bench_asphalt, bench_grass, bench_gravel.
crop_season str "2026" The crop plan ACRE/crops_<season>.json. "legacy" uses the land cover of fields.json.
initial_growth float 1.0 The growth of each planted field from 0 to 1. The value 1 is a mature crop.
farm bool True False keeps no crop marks and no ruts. The step is then faster.
scene bool True Loads the obstacle scene for the LiDAR and the collisions. A plane has no scene.
polaris_overrides dict of str to float {} Parameters of polaris.json by their keys, for example {"mass.occupants": 2}.
occupants int -1 The number of persons, 0 to 6. -1 keeps the value of polaris.json.
substeps int 12 The number of physics steps in one control step.
resend_steps int 6 The batch sends a held command again after this number of physics steps. 0 sends it one time only.
content_dir str "" A different configuration folder. The default is <root>/Acres/Content/Simulation.
farm_water bool True The wheels read the soil water of the 4 m farm grid. False uses the water of the surface templates in tractor.json.

The surfaces of tractor.json are asphalt, concrete, gravel, dry_soil, wet_soil, mud, sod_lane, sod and dirt_track. A load error raises RuntimeError with the name of the file.

Example

import acres_core as ac

b = ac.Batch(".", num_envs=64)
print(b.num_envs, b.state.shape)

flat = ac.Batch(".", flat=True, flat_surface="bench_grass",
                polaris_overrides={"mass.occupants": 2})
print(flat.num_envs, flat.state.shape)

Output

64 (64, 49)
1 (1, 49)

num_envs#

A read-only property. It gives the number of environments of the batch. The first dimension of each array has this length.

Example

import acres_core as ac

b = ac.Batch(".", num_envs=8)
print(b.num_envs)

Output

8

reset#

reset(indices, poses, settled=True, gear=5, drive_mode=2) puts environments at start poses. The reset clears the farm marks, the held commands, the energy ledger and the time of these environments. The drive-by-wire is off after a reset. The first command row must set enable to 1. The function then writes the arrays again. The wheel contacts are valid after the first step.

Name Type Unit Default Description
indices array of int, or None The environments. None selects all environments.
poses array (K, 4) m, m, rad, m/s One row for each index: x, y, yaw and the forward speed.
settled bool True True puts the vehicle on the terrain at its static ride height. False drops it from 0.2 m, as the reset of the game does.
gear int 5 The gear at the start: 1 P, 2 R, 3 N, 4 H, 5 L.
drive_mode int 2 The position of the AWD switch: 0 Turf, 1 2WD, 2 AWD.

An index out of range raises IndexError. A poses array with a different shape raises ValueError.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".", num_envs=4)
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")
yaw = np.radians(90.0 - home[4])     # compass heading to yaw
b.reset(None, np.tile([home[2], home[3], yaw, 0.0], (4, 1)))
print(b.state[0, [S["x"], S["y"], S["z"], S["yaw"]]].round(3))

b.reset([2, 3], np.array([[210.3, -249.9, 0.0, 2.0],
                          [210.3, -244.0, 0.0, 2.0]]),
        settled=False, gear=4)
columns = [S["speed"], S["gear"], S["time_s"]]
print(b.state[2, columns].round(3))

Output

[ -38.71  -140.513    0.639    1.571]
[2. 4. 0.]

step#

step(commands=None) advances all environments by one control step and returns the state array. A control step is substeps physics steps of 1/120 s.

A command row has the semantics of the Dataspeed drive-by-wire messages. Each row has four subsystems: steering, throttle, brake and ULC. Each subsystem has a mode column.

  • A mode that is a number is a message for this subsystem. The message arrives at the first physics step.
  • The batch sends the message again each resend_steps physics steps while the caller holds it.
  • A mode that is NaN is no message. The subsystem then gets no more messages and the watchdog disengages it after 0.1 s.
  • A mode of 0 is a message that releases the subsystem at once.
  • NaN in the columns enable, gear and drive_mode keeps the last value.

step() without an argument holds the commands of the last call.

Name Type Unit Default Description
commands array (num_envs, 18), or None None One command row for each environment. See Command columns.
returns array (num_envs, 49) A copy of the state array after the step.

An array with a different shape raises ValueError.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".", num_envs=2, flat=True)
b.reset(None, np.zeros((2, 4)))
C = {name: i for i, name in enumerate(ac.COMMAND_COLUMNS)}
S = {name: i for i, name in enumerate(ac.STATE_COLUMNS)}

rows = np.full((2, len(C)), np.nan)   # NaN mode: no message
rows[:, C["enable"]] = 1
rows[:, C["steer_mode"]] = 2          # steering wheel, deg
rows[:, C["steer_value"]] = [0.0, 90.0]
rows[:, C["ulc_mode"]] = 1            # speed, m/s
rows[:, C["ulc_value"]] = 3.0
for _ in range(100):                  # 10 s
    state = b.step(rows)
print(state[:, S["speed_measured"]].round(2),
      state[:, S["steering_wheel_deg"]].round(1))

state = b.step()                      # hold for 0.1 s
print(state[:, S["time_s"]].round(3),
      state[:, S["ulc_enabled"]])

Output

[3.26 3.24] [ 0. 90.]
[10.1 10.1] [1. 1.]

Example: The Watchdog

import numpy as np
import acres_core as ac

b = ac.Batch(".", flat=True)
b.reset(None, np.zeros((1, 4)))
C = {name: i for i, name in enumerate(ac.COMMAND_COLUMNS)}
S = {name: i for i, name in enumerate(ac.STATE_COLUMNS)}
on = np.full((1, len(C)), np.nan)
on[0, C["enable"]] = 1
on[0, [C["ulc_mode"], C["ulc_value"]]] = [1, 2.0]
silent = np.full((1, len(C)), np.nan)

print(b.step(on)[0, S["ulc_enabled"]])      # a message
print(b.step()[0, S["ulc_enabled"]])        # held
print(b.step(silent)[0, S["ulc_enabled"]])  # no message

Output

1.0
1.0
0.0

step_curvature_speed#

step_curvature_speed(curvature, speed) advances all environments by one control step. It sends a steering command in curvature mode and a ULC command in speed mode to each environment. The function sets enable to 1 and sends no throttle, brake or gear command. It returns the state array.

Name Type Unit Default Description
curvature array (num_envs) 1/m The path curvature. A positive value turns to the left. The drive-by-wire limits it to ±0.2.
speed array (num_envs) m/s The speed command of the ULC.
returns array (num_envs, 49) A copy of the state array after the step.

The ULC controls the measured wheel speed, which is 0.93 times the wheel speed of the model. The path curvature is smaller than the command. The steering centre has an offset of 12.5° and the AWD mode locks the rear axle. Polaris Ranger Dynamics and Drive-by-Wire and ULC give the models.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".", num_envs=3, flat=True)
b.reset(None, np.zeros((3, 4)))
S = {name: i for i, name in enumerate(ac.STATE_COLUMNS)}
curvature = np.array([0.0, 0.05, -0.1])   # 1/m
speed = np.array([2.0, 3.0, 4.0])         # m/s
for _ in range(150):                      # 15 s
    state = b.step_curvature_speed(curvature, speed)
print(state[:, S["speed_measured"]].round(2))
# path curvature = yaw rate / speed, 1/m
print((state[:, S["wz"]] / state[:, S["vx"]]).round(3))

Output

[2.01 3.21 4.1 ]
[-0.006  0.038 -0.093]

state#

A read-only property. It gives a copy of the state array with the shape (num_envs, 49) and the type float64. reset, step and step_curvature_speed write the array. State columns gives the columns.

Each access makes a new copy. Read the property one time after a step and keep the result.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".", num_envs=2)
home = next(p for p in b.places()
            if p[0] == "spawn-icsc-garage")
pose = [home[2], home[3], np.pi / 2, 0.0]
b.reset(None, np.tile(pose, (2, 1)))
for _ in range(50):
    b.step_curvature_speed(np.zeros(2), np.full(2, 3.0))
S = {name: i for i, name in enumerate(ac.STATE_COLUMNS)}
state = b.state
print(state.shape, state.dtype)
for name in ("time_s", "x", "y", "yaw", "speed", "rpm",
             "gear", "grounded", "surface_under"):
    print(f"{name:14s} {state[0, S[name]]:10.3f}")

Output

(2, 49) float64
time_s              5.000
x                 -38.499
y                -131.542
yaw                 1.522
speed               3.173
rpm              2774.625
gear                5.000
grounded            4.000
surface_under       1.000

wheels#

A read-only property. It gives a copy of the wheel array with the shape (num_envs, 4, 13). The second index is the wheel: 0 front left, 1 front right, 2 rear left, 3 rear right. Wheel columns gives the columns.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".")
home = next(p for p in b.places()
            if p[0] == "spawn-icsc-garage")
b.reset(None, np.array([[home[2], home[3], np.pi / 2, 0.0]]))
for _ in range(50):
    b.step_curvature_speed(np.zeros(1), np.full(1, 3.0))
W = {name: i for i, name in enumerate(ac.WHEEL_COLUMNS)}
wheels = b.wheels
print(wheels.shape)
print(wheels[0, :, W["normal_n"]].round(0))
print(wheels[0, :, W["slip"]].round(4))
print(wheels[0, :, W["surface_class"]])

Output

(1, 4, 13)
[2931. 2739. 3270. 3143.]
[-0.0028 -0.0029 -0.0267 -0.0192]
[1. 1. 1. 1.]

energy, energy_step#

Two read-only properties with the shape (num_envs, 24). The unit is the joule. energy is the energy ledger from the last reset. energy_step is the change during the last control step. Energy columns gives the columns.

The ledger closes: the fuel energy is equal to the sum of the sinks and the stores. The column ground_loss is the sum of slip, lateral and soil. Energy and Fuel gives the model.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".", flat=True)
b.reset(None, np.zeros((1, 4)))
for _ in range(200):                  # 20 s at 3 m/s
    b.step_curvature_speed(np.full(1, 0.05), np.full(1, 3.0))
E = {name: i for i, name in enumerate(ac.ENERGY_COLUMNS)}
total, last = b.energy[0], b.energy_step[0]
print(f"fuel {total[E['fuel']] / 1e3:.1f} kJ, "
      f"ground loss {total[E['ground_loss']] / 1e3:.2f} kJ")
print(f"last control step: fuel {last[E['fuel']]:.1f} J, "
      f"aero {last[E['aero']]:.2f} J")
sinks = ("engine_loss", "parasitic", "pto", "hydraulic",
         "accessory_loss", "engine_kinetic", "clutch",
         "driveline", "wheel_kinetic", "brake", "hysteresis",
         "slip", "soil", "traction")
residual = total[E["fuel"]] - sum(total[E[k]] for k in sinks)
print(f"powertrain residual {residual:.3f} J")

Output

fuel 545.7 kJ, ground loss 3.95 kJ
last control step: fuel 3.6 J, aero 6.09 J
powertrain residual -0.000 J

zone_crushed, zone_crushed_step#

Two read-only properties with the shape (num_envs, 8). The unit is the square metre. zone_crushed is the crushed crop area in each zone from the last reset. zone_crushed_step is the area of the last control step. Zone columns gives the zones. The sum of the columns is the state column crop_crushed_m2.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".", num_envs=2)
# field 24 (corn), heading east
b.reset(None, np.tile([195.0, 190.0, 0.0, 0.0], (2, 1)))
speed = np.full(2, 2.0)
for _ in range(100):
    state = b.step_curvature_speed(np.zeros(2), speed)
S = {name: i for i, name in enumerate(ac.STATE_COLUMNS)}
Z = {name: i for i, name in enumerate(ac.GROUND_CLASSES)}
print(b.zone_crushed.shape)
print(b.zone_crushed[0, :6].round(2),
      state[0, S["crop_crushed_m2"]].round(2))
print(b.zone_crushed_step[0, Z["crop"]].round(3))

Output

(2, 8)
[30.3  0.   0.   0.   0.   0. ] 30.3
0.095

set_conditions#

set_conditions(indices=None, global_fraction=nan, field_fractions=None, surface_film=0.0) sets the soil-water scenario of environments. The scenario is constant during an episode. ACRES Core does not simulate rain or drying. A call replaces the full scenario of the environments. The next step uses the new values.

A fraction \(f\) sets the water content \(\theta\) of the surface layer of each soil cell. The soil unit of the cell gives the wilting point \(\theta_{wp}\), the field capacity \(\theta_{fc}\) and the saturation \(\theta_{s}\).

\[ \theta = \begin{cases} \theta_{wp} + f\,(\theta_{fc} - \theta_{wp}) & 0 \le f \le 1 \\ \theta_{fc} + (f - 1)\,(\theta_{s} - \theta_{fc}) & 1 < f \le 1.3 \end{cases} \]
Name Type Unit Default Description
indices array of int, or None None The environments. None selects all environments.
global_fraction float nan The fraction \(f\) of all soil cells, 0 to 1.3. NaN uses the value of farm.json, 0.5.
field_fractions array (K, 60), or None None A fraction for each field. Column \(n\) is field \(n\). Column 0 has no function. NaN uses global_fraction.
surface_film float 0.0 The rain film on the soil surface, 0 to 1.

Soil Water gives the model of the game.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".", num_envs=2)
b.set_conditions([0], global_fraction=0.0)  # wilting point
b.set_conditions([1], global_fraction=1.3)  # wet
# field 24 (corn), heading east
b.reset(None, np.tile([195.0, 190.0, 0.0, 0.0], (2, 1)))
for _ in range(100):
    b.step_curvature_speed(np.zeros(2), np.full(2, 2.0))
E = {name: i for i, name in enumerate(ac.ENERGY_COLUMNS)}
W = {name: i for i, name in enumerate(ac.WHEEL_COLUMNS)}
# ground loss, kJ: dry, wet
print((b.energy[:, E["ground_loss"]] / 1e3).round(2))
# mean sinkage, mm: dry, wet
sinkage = b.wheels[:, :, W["sinkage_m"]].mean(axis=1)
print((sinkage * 1e3).round(1))

fractions = np.full((1, 60), np.nan)
fractions[0, 39] = 1.0                      # field 39
b.set_conditions([0], global_fraction=0.5,
                 field_fractions=fractions, surface_film=0.2)

Output

[ 2.38 10.83]
[ 2.2 16. ]

set_parameters#

set_parameters(indices, overrides) gives environments a different set of Polaris parameters. Use it for hardware shifts and for domain randomisation. The new set is the parameter set of the batch with these overrides. Overrides of an earlier call do not stay.

CAUTION

The function initialises the vehicles of these environments again. Call reset for them before the next step.

Name Type Unit Default Description
indices array of int, or None The environments. None selects all environments.
overrides dict of str to float Parameters by their keys in polaris.json. An array element has the key name[i].

A key that the model does not know raises KeyError. Configuration Files gives the keys of polaris.json.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".", num_envs=2, flat=True)
b.set_parameters([1], {"mass.occupants": 4,
                       "steering.center_deg": 15.5})
b.reset(None, np.zeros((2, 4)))
W = {name: i for i, name in enumerate(ac.WHEEL_COLUMNS)}
b.step()
# total wheel load, N: 1 occupant and 4 occupants
print(b.wheels[:, :, W["normal_n"]].sum(axis=1).round(0))

try:
    b.set_parameters([0], {"no.such.key": 1.0})
except KeyError as error:
    print("KeyError:", error)

Output

[12092. 14592.]
KeyError: 'unknown Polaris parameter no.such.key'

start_log, stop_log#

start_log(index, path, metadata={}, agent="polaris") starts the episode log of one environment. stop_log(index) closes the file.

The file has the format of the episode log of the game. The game can show it with an episode replay. The log gets one state message and one farm message for each physics step. It gets the episode, agent, condition and shift messages at the start and after each reset. It also contains the drive-by-wire commands at the physics steps that applied them.

Name Type Unit Default Description
index int The environment.
path str The path of the MCAP file.
metadata dict of str to str {} More keys for the metadata record acres_episode, for example a seed.
agent str "polaris" The name of the agent in the log. The topics of the commands start with this name.

A file that the module cannot create raises RuntimeError. Episode Log gives the format. Replay shows the log in the game.

Example

import numpy as np
import acres_core as ac
from mcap.reader import make_reader

b = ac.Batch(".", num_envs=2)
home = next(p for p in b.places()
            if p[0] == "spawn-icsc-garage")
pose = [home[2], home[3], np.pi / 2, 0.0]
b.reset(None, np.tile(pose, (2, 1)))
b.start_log(0, "/tmp/core-episode.mcap", {"seed": "7"})
for _ in range(100):                  # 10 s
    b.step_curvature_speed(np.full(2, 0.02), np.full(2, 3.0))
b.stop_log(0)

with open("/tmp/core-episode.mcap", "rb") as stream:
    summary = make_reader(stream).get_summary()
counts = summary.statistics.channel_message_counts
for channel_id, count in sorted(counts.items()):
    print(f"{count:5d}  {summary.channels[channel_id].topic}")

Output

    1  /sim/episode
    1  /sim/agents
 1200  /sim/agent_states
    0  /sim/farm_events
 1200  /sim/farm_stamps
    1  /sim/conditions
    1  /sim/shifts
    0  /sim/task
  200  /polaris/vehicle/steering/cmd
    0  /polaris/vehicle/throttle/cmd
    0  /polaris/vehicle/brake/cmd
  200  /polaris/vehicle/ulc/cmd
    0  /polaris/vehicle/gear/cmd
    1  /polaris/vehicle/dbw_enabled

lidar#

lidar(indices=None, pattern="planar", noise=False, seed=0) makes one LiDAR scan for each selected environment. The rays start at the pose of the vehicle after the last step. They hit the terrain and the obstacle scene. The function returns the tuple (ranges, material).

Name Type Unit Default Description
indices array of int, or None None The environments. None selects all environments.
pattern str "planar" "planar": 360 level beams with a range of 30 m. "helios": the RoboSense Helios of the Polaris, 1800 × 32 beams.
noise bool False Adds range noise and signal noise.
seed int 0 The seed of the noise and of the returns from the body of the vehicle.
returns ranges array (K, beams), float32 m The range of each beam. NaN shows that the beam has no return.
returns material array (K, beams), uint8 The code of the object that each beam hit.

A pattern with a different name raises ValueError. LiDAR arrays gives the beam order and the codes. LiDAR gives the sensor model.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".", num_envs=2)
home = next(p for p in b.places()
            if p[0] == "spawn-icsc-garage")
x, y = home[2], home[3]
b.reset(None, np.array([[x, y, np.pi / 2, 0.0],
                        [x + 6.0, y, np.pi / 2, 0.0]]))

ranges, material = b.lidar()          # planar
print(ranges.shape, ranges.dtype, material.dtype)
print(f"returns {np.isfinite(ranges[0]).sum()} of 360, "
      f"nearest {np.nanmin(ranges[0]):.2f} m")

ranges, material = b.lidar([0], "helios", noise=True, seed=7)
share = np.isfinite(ranges).mean()
print(ranges.shape, f"returns {share:.3f}")
codes, counts = np.unique(material, return_counts=True)
print(dict(zip(codes.tolist(), counts.tolist())))
cloud = ranges.reshape(1800, 32)      # [column, ring]

Output

(2, 360) float32 uint8
returns 132 of 360, nearest 8.00 m
(1, 57600) returns 0.682
{9: 489, 11: 159, 12: 5325, 15: 24, 252: 15949, 254: 17315, 255: 18339}

set_zones#

set_zones(zones, x0, y0, cell_m) replaces the zone raster of the world. The crop accounts then put each crushed sample into the zone of its cell. All environments share the raster. Call the function between two steps.

The cell zones[j, i] covers x from x0 + i * cell_m and y from y0 + j * cell_m. The rows go north. A value larger than 7 counts as 7. A point outside the raster is in zone 0.

Name Type Unit Default Description
zones array (rows, columns), uint8 The zone of each cell, 0 to 7.
x0 float m The west edge of the raster.
y0 float m The south edge of the raster.
cell_m float m The size of one cell.

An array that does not have two dimensions raises ValueError.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".")
zones = np.zeros((2, 2), np.uint8)    # rows north
zones[:, 1] = 3                       # east half: zone 3
b.set_zones(zones, -800.0, -800.0, 800.0)
b.reset(None, np.array([[195.0, 190.0, 0.0, 0.0]]))
for _ in range(100):
    b.step_curvature_speed(np.zeros(1), np.full(1, 2.0))
print(b.zone_crushed[0].round(2))

Output

[ 0.   0.   0.  30.3  0.   0.   0.   0. ]

height_at, surface_class_at, field_at#

Three queries of the shared world at a point. The arguments are positional.

Name Type Unit Default Description
height_at(x, y) float m The height of the terrain. A plane gives its height, 0.
surface_class_at(x, y) int The class of the surface map, an index into SURFACE_CLASSES.
field_at(x, y) int The field number of the 4 m field raster, 1 to 59. 0 shows that the point is in no field.
x, y float m The point in the frame of the tile.

Example

import acres_core as ac

b = ac.Batch(".")
x, y = -38.71, -140.513               # the ICSC garage spawn
print(round(b.height_at(x, y), 3))
klass = b.surface_class_at(x, y)
print(klass, ac.SURFACE_CLASSES[klass])
print(b.field_at(x, y), b.field_at(195.0, 190.0))

Output

0.639
2 concrete
0 24

nearest_obstacle#

nearest_obstacle(x, y, max_m=30.0) gives the horizontal distance from a point to the nearest obstacle. The obstacles are the outlines of the buildings, the bins, the props and the tree trunks. The function returns max_m when no obstacle is nearer, or when the batch has no obstacle scene.

Name Type Unit Default Description
x, y float m The point in the frame of the tile.
max_m float m 30.0 The search radius.
returns float m The distance, max_m at most.

Example

import acres_core as ac

b = ac.Batch(".")
print(round(b.nearest_obstacle(-38.71, -140.513), 2))
print(b.nearest_obstacle(195.0, 190.0, max_m=20.0))

Output

3.2
20.0

places#

places() returns the named places of ACRE/places.json as a list of tuples. Each tuple is (id, name, x, y, heading_deg). The heading is a compass heading in degrees. The heading is NaN for a place that has no heading. A plane has no places.

Example

import acres_core as ac

b = ac.Batch(".")
for place in b.places()[:4]:
    print(place)

Output

('pmumbs9bp8b53v', 'Beck Agricultural Center', 148.011, -255.569, nan)
('pmumbr7olz33s6', 'Indiana Corn and Soybean Center', -13.259, -161.058, nan)
('spawn-beck-lot', "Spawn: Beck's parking lot", 210.312, -249.936, 0.0)
('spawn-icsc-garage', 'Spawn: ICSC garage', -38.71, -140.513, 0.0)

num_crop_patches, crop_patches, crushed_patches#

The crop of the world is a set of patches of 1.52 m × 1 m. Each patch has 4 × 4 samples of 0.095 m².

Name Type Unit Default Description
num_crop_patches int A read-only property: the number of patches of the world.
crop_patches() dict The patches of the world. See the keys below.
key xyzh array (N, 4), float32 m The origin of each patch (x, y, z) and the height of the mature plants.
key field array (N), uint8 The field number of each patch.
key kind array (N), uint8 The crop: 0 corn, 1 soybean, 2 potato.
crushed_patches(index) tuple of 2 arrays The crushed patches of one environment: the patch indices (uint32) and the sample masks (uint16).

Each bit of a mask is one crushed sample of the patch. An index out of range raises IndexError. Crops and Ground Classes gives the crop model.

Example

import numpy as np
import acres_core as ac

b = ac.Batch(".")
print(b.num_crop_patches)
patches = b.crop_patches()
print(patches["xyzh"].shape, patches["field"].dtype,
      np.bincount(patches["kind"]))

b.reset(None, np.array([[195.0, 190.0, 0.0, 0.0]]))
for _ in range(100):
    b.step_curvature_speed(np.zeros(1), np.full(1, 2.0))
index, mask = b.crushed_patches(0)
samples = sum(bin(m).count("1") for m in mask.tolist())
# patches, samples, area in m2
print(len(index), samples, round(samples * 1.52 / 16, 2))

Output

706104
(706104, 4) uint8 [292173 413931]
34 319 30.3

Array Columns#

Command Columns#

One command row has 18 columns of the type float64. The row has the fields of the ds_dbw_msgs commands of the Polaris.

Index Name Unit Description
0 enable The system enable: 0 or 1. NaN keeps the last value.
1 steer_mode The type of the steering command: 0 none, 2 angle, 3 curvature, 4 yaw rate, 14 percent. NaN: no message.
2 steer_value deg, 1/m, rad/s or % The steering command in the unit of its mode. The angle is the steering wheel angle.
3 steer_rate deg/s The rate limit of the steering wheel. 0 selects the default of polaris.json, 100.
4 steer_accel deg/s² The acceleration limit of the steering wheel. 0 selects the default, 500.
5 throttle_mode The type of the throttle command: 0 none, 13 raw percent, 14 percent. NaN: no message.
6 throttle_value % The throttle command.
7 brake_mode The type of the brake command: 0 none, 1 pressure, 14 percent. NaN: no message.
8 brake_value bar or % The brake command in the unit of its mode.
9 gear The gear command: 1 P, 2 R, 3 N, 4 H, 5 L. 0 or NaN: no new gear command.
10 ulc_mode The type of the ULC command: 0 none, 1 speed, 2 acceleration. NaN: no message.
11 ulc_value m/s or m/s² The ULC command in the unit of its mode.
12 ulc_limit_accel m/s² The acceleration limit of the ULC. 0 selects the default.
13 ulc_limit_decel m/s² The deceleration limit of the ULC. 0 selects the default.
14 ulc_jerk_throttle m/s³ The jerk limit while the acceleration increases. 0 selects the default.
15 ulc_jerk_brake m/s³ The jerk limit while the acceleration decreases. 0 selects the default.
16 ulc_coast 1 makes the ULC decelerate without the brakes.
17 drive_mode The AWD switch: 0 Turf, 1 2WD, 2 AWD. NaN keeps the last value.

A value column that is NaN counts as 0 when its mode is a number. A throttle command or a brake command overrides the ULC, as on the vehicle. Drive-by-Wire and ULC gives the model of each command.

State Columns#

The state array has 49 columns of the type float64. Flags have the values 0 and 1.

Index Name Unit Description
0 time_s s The time from the last reset.
1 to 3 x, y, z m The position of base_footprint.
4 to 7 qw, qx, qy, qz The orientation of the body as a quaternion.
8 yaw rad The heading of the forward axis, counter-clockwise from east.
9 pitch rad The pitch angle. A positive value has the nose up.
10 roll rad The roll angle. A positive value has the left side up.
11 to 13 vx, vy, vz m/s The velocity of the centre of mass in the body frame.
14 to 16 wx, wy, wz rad/s The angular velocity in the body frame. wz is the yaw rate.
17 to 19 ax, ay, az m/s² The acceleration of the centre of mass in the body frame during the last physics step. It does not contain gravity.
20 speed m/s The forward speed. It is equal to vx.
21 steering_wheel_deg deg The steering wheel angle.
22 steer_ref_deg deg The reference angle of the steering controller.
23 road_wheel_rad rad The steering angle at the front axle, as in a bicycle model.
24 throttle_pct % The throttle pedal output.
25 brake_bar bar The brake line pressure.
26 gear The gear: 1 P, 2 R, 3 N, 4 H, 5 L.
27 ulc_vel_ref m/s The speed reference of the ULC.
28 ulc_accel_ref m/s² The acceleration reference of the ULC.
29 speed_measured m/s The speed that the drive-by-wire reports from the wheel speeds. It is negative in reverse.
30 rpm 1/min The engine speed.
31 cvt_ratio The ratio of the CVT belt.
32 fuel_rate_lph L/h The fuel rate.
33 fuel_used_l L The fuel used from the last reset.
34 dbw_enabled flag The system enable that the last command row set.
35 to 38 steer_enabled, throttle_enabled, brake_enabled, ulc_enabled flag The subsystem is under drive-by-wire control.
39 grounded The number of wheels on the ground, 0 to 4.
40 rear_slip The mean slip ratio of the rear wheels that are on the ground.
41 crop_crushed_m2 m² The crushed crop area from the last reset.
42 crop_crushed_step_m2 m² The crushed crop area of the last control step.
43 crushed_wheels_step_m2 m² The part of the last control step that the tyres crushed.
44 crushed_body_step_m2 m² The part of the last control step that the body crushed.
45 off_map flag base_footprint is less than 10 m from the edge of the tile, or outside. Always 0 on a plane.
46 collision flag The outline of the vehicle touches an obstacle.
47 field_under The field number below base_footprint, 0 for no field.
48 surface_under The surface class below base_footprint, an index into SURFACE_CLASSES. -1 on a plane.

The collision test uses a box of 1.6 m width from 0.5 m behind the rear axle to 0.55 m ahead of the front axle. ACRES Core finds a collision but does not stop the vehicle. A task must end the episode.

Wheel Columns#

The wheel array has 13 columns for each wheel.

Index Name Unit Description
0 omega rad/s The spin rate of the wheel.
1 slip The longitudinal slip ratio.
2 normal_n N The normal load.
3 fx_n N The longitudinal force on the chassis.
4 fy_n N The lateral force. A positive value points to the right of the wheel.
5 sinkage_m m The sinkage of the tyre, or the depth of the rut.
6 contact flag The wheel touches the ground with a load of 1 N or more.
7 surface_class The surface class below the wheel, an index into SURFACE_CLASSES. -1 for a wheel in the air and on a plane.
8 field The field number below the wheel, 0 for no field.
9 steer_rad rad The steering angle of the wheel. A positive value is to the left.
10 drive_torque_nm N m The torque of the driveline on the wheel shaft.
11 suspension_m m The extension of the suspension from the mount to the wheel centre.
12 pressure_pa Pa The mean contact pressure on the ground.

Energy Columns#

The energy arrays have 24 columns. The unit of each column is the joule.

Index Name Description
0 fuel The chemical energy of the burnt fuel.
1 engine_loss The fuel energy that the engine does not deliver as work.
2 parasitic The drag of the fan, the alternator and the pumps.
3 pto The work on the PTO shaft. The Polaris has none.
4 hydraulic The work of the hydraulic consumers. The Polaris has none.
5 accessory_loss The losses of the PTO driveline and the hydraulic pump.
6 engine_kinetic The change of the rotational energy of the engine.
7 clutch The slip heat of the clutch.
8 driveline The losses of the belt, the gearbox and the axles.
9 wheel_kinetic The change of the rotational energy of the wheels.
10 brake The heat of the brakes.
11 hysteresis The rolling resistance of the tyres.
12 slip The slip loss of the tyres.
13 soil The work that makes ruts: compaction and bulldozing.
14 traction The net longitudinal work of the tyres on the chassis.
15 axle The work of the driveline on the wheel shafts. It is not part of the balance.
16 drawbar The work that pulls an implement. The Polaris has none.
17 aero The air drag.
18 water The drag of standing water.
19 lateral The side-slip loss of the tyres.
20 suspension The work that the springs, the dampers and the soil absorb.
21 chassis_kinetic The change of the kinetic energy of the chassis.
22 potential The change of the potential energy of the chassis.
23 ground_loss The sum of slip, lateral and soil.

The ledger has two balances. The fuel is equal to the sum of columns 1 to 14. The traction is equal to the sum of columns 16 to 22.

Zone Columns#

The zone arrays have 8 columns, one for each zone of the zone raster. On the ACRE tile the default raster is the ground classes of the scouting map, with cells of 0.5 m.

Index Name in GROUND_CLASSES Description
0 crop Crop that is not in the edge band. Also all points outside the raster.
1 lane A lane.
2 verge A verge.
3 edge_band The band along the edge of a field.
4 obstacle An obstacle.
5 other All other ground.
6, 7 Not used by the default raster. set_zones can use them.

A plane has no raster. All crushed crop is then in zone 0. Scouting Map gives the ground classes.

LiDAR Arrays#

Pattern Beams Beam Order Range
planar 360 Beam \(k\) points \(k\) degrees counter-clockwise from forward. The beams are level in the vehicle frame. 30 m at most. Each hit gives a return.
helios 57600 The index is column * 32 + ring. Column \(c\) is at \(-360\,c/1800\) degrees, clockwise. 0.2 m to 161.3 m. The signal model decides if a hit gives a return.

The two patterns have their origin at the mount of the Helios: 2.5 m ahead of base_footprint and 2.039 m above it. A range of the helios pattern starts at the centre of the lens.

Code in material Meaning
0 to 15 A material of the obstacle scene: asphalt, concrete, gravel, soil, grass, mud, corn_leaf, soybean_leaf, potato_leaf, tree_foliage, shrub, bark, building, metal_bin, vehicle, other.
252 The body of the vehicle itself. Only the helios pattern has these returns.
253 A different vehicle. The batch does not make this code.
254 The terrain.
255 No return.

Threads#

One batch has a pool of threads. reset, step, step_curvature_speed and lidar divide their work between these threads. One thread steps one environment. The functions release the Python lock while they work.

  • Call the functions of one batch from one Python thread at a time.
  • The properties make copies. A different thread can read a copy while the batch steps.
  • Two batches are independent. Each batch loads its own copy of the world, which takes approximately 0.5 s.
  • set_zones changes data that all environments read. Do not call it during a step.

Determinism#

A batch gives the same result bit for bit when it gets the same commands from the same start. The result does not change with the number of threads or with the number of environments. The module has no random generator. The LiDAR noise is a hash of the seed, the environment index, the step number and the beam. Time Stepping and Determinism gives the measurements and the comparison with the game.

Check with Two Thread Counts#

The example runs 8 environments for 30 s with one thread and with all threads. The three arrays are equal in all bits.

Example

import numpy as np
import acres_core as ac

def run(threads):
    b = ac.Batch(".", num_envs=8, threads=threads)
    home = next(p for p in b.places()
                if p[0] == "spawn-icsc-garage")
    x, y = home[2], home[3]
    poses = np.array([[x + 6.0 * i, y, np.pi / 2, 0.0]
                      for i in range(8)])
    b.reset(None, poses)
    for k in range(300):              # 30 s
        b.step_curvature_speed(np.linspace(-0.08, 0.08, 8),
                               np.full(8, 2.0 + 0.01 * k))
    return b.state, b.wheels, b.energy

one, many = run(1), run(0)
print([bool(np.array_equal(a, b)) for a, b in zip(one, many)])

Output

[True, True, True]