Skip to content

Time Stepping and Determinism#

This page gives the model of time in ACRES: the physics step, the clocks, the time of each command and of each sensor sample. It also gives the conditions for a result that repeats, and the measured agreement between ACRES Core and the game.

Scope and Assumptions#

The page covers the two simulators.

  • The game runs the vehicle model on the asynchronous physics tick of Unreal Engine with a fixed step of 1/120 s.
  • ACRES Core runs the same model sources with its own loop. The step is also 1/120 s.

Assumptions:

  • The physics step is constant. No model changes the step during a session.
  • A model gets the step as an argument. The model code has no clock of its own and no global state.
  • Simulation time is a count of physics steps. Wall time has an effect only at the interfaces: frames, sockets and threads.
  • A sensor sample or a command belongs to one physics step. No model interpolates between two steps. The rolling LiDAR sweep is the one exception.

The page does not give the equations of the vehicle, the soil or the sensors. The other pages of this section give them.

Symbols#

Symbol Quantity Unit
\(h\) Physics step, 1/120 s
\(k\) Index of a physics step
\(t_k = k\,h\) Simulation time at the start of step \(k\) s
\(n_s\) Number of physics steps in one control step of ACRES Core (SubSteps)
\(T_c = n_s\,h\) Control step of ACRES Core s
\(n_r\) Period at which the batch sends a held command again (CommandResendSteps) steps
\(\Delta t_f\) Wall time of one game frame s
\(\Delta t_{max}\) Largest solver time of one frame (MaxPhysicsDeltaTime) s
\(F\) Solver time that one frame adds s
\(S\) Solver time that the game thread gave to the solver before the frame s
\(P, L\) Wall time of the previous read and of the last read of the sockets s
\(a\) Wall time at which a command arrived s
\(t_a\) Solver time at which a command applies s
\(\varepsilon\) Tolerance of the command queue, 0.002 s
\(A\) Age of the last message of a drive-by-wire subsystem s
\(T_w\) Command timeout of the drive-by-wire, 0.1 s
\(f\) Rate of a sensor stream Hz
\(R\) Rate factor of core_sim
\(m\), \(\mathbf{I}\) Mass and inertia tensor of the chassis kg, kg m²
\(\mathbf{x}\), \(\mathbf{q}\) Position of the centre of mass and orientation quaternion of the chassis m,
\(\mathbf{v}\), \(\vec{\omega}\) Velocity and angular velocity of the chassis m/s, rad/s
\(\mathbf{F}\), \(\mathbf{M}\) Sum of the forces and of the moments about the centre of mass N, N m
\(\mathbf{g}\) Gravity, (0, 0, -9.80665) m/s²
\(c_\omega\) Angular damping of the chassis, 0.08 1/s
\(J\), \(\Omega\), \(r\) Spin inertia, spin rate and radius of a wheel kg m², rad/s, m
\(j\), \(\ell\) Shear displacement of a tyre and its relaxation length m, m
\(\tau\) Time constant of a first-order lag s

Clocks#

ACRES has five clocks. The first four clocks count physics steps. Only the wall time changes between two runs.

Clock Definition Source
Physics time The sum of the steps of one vehicle. It is \(k\,h\). PhysicsTime in AAcresVehiclePawn, TimeS in FAcresPolaris
World time The clock of the Chaos solver. All agents share it. Each pawn keeps a fixed offset to its own physics time. WorldTimeS, WorldTimeOffsetS
Episode time The time from the last reset of a vehicle. FVehicleState::TimeS, the state column time_s
Farm time The clock of the weather, the soil water and the crops. It advances by \(h\) times the time scale. FAcresEnvironmentRuntime::Advance, FAcresFarmRuntime::Advance
Wall time The clock of the workstation. It sets the frames and the arrival time of each command. FPlatformTime::Seconds

The time scale is 60 divided by seconds_per_game_minute of environment.json. The default is 1. The message stamps of the sensor stream and of the episode log are world time, not wall time.

Note

Unreal Engine gives the step to the game as a single-precision number: 0.0083333338 s. ACRES Core uses the double-precision number 1/120. The relative difference is 5 × 10⁻⁸, that is 0.19 ms in one hour.

Physics Step of the Game#

The engine settings in Acres/Config/DefaultEngine.ini select the asynchronous physics tick with a fixed step. The class AAcresVehiclePawn sets bAsyncPhysicsTickEnabled. The engine then calls AsyncPhysicsTickActor one time for each step on the physics thread.

One frame gives the solver the frame time, with a limit:

\[ F = \min(\Delta t_f,\ \Delta t_{max}) \]

The solver keeps the part of \(F\) that is smaller than one step for the next frame. With the remainder \(\rho\) of the last frame, the frame runs \(N\) steps:

\[ N = \left\lfloor \frac{\rho + F}{h} \right\rfloor, \qquad \rho' = \rho + F - N\,h \]

With \(\Delta t_{max}\) = 0.1 s, one frame runs 12 steps at most. A frame rate of 10 frames for each second or more keeps the simulation at real time. Below this rate the simulation is slower than real time by the factor \(\Delta t_{max} / \Delta t_f\). It does not skip time.

The diagram gives the sequence of one step. The model functions run in the same sequence in ACRES Core.

One physics step of a vehicle: the pawn exchanges the controls, advances the weather and the farm, runs the steering and the drivetrain, examines each wheel contact, solves the driveline, sums the forces, gives them to Chaos and publishes the step. One physics step of a vehicle, 1/120 s INPUTS PHYSICS THREAD, IN THIS SEQUENCE OUTPUTS 1 Exchange the controls controls, bridge command, reset request 2 Examine the body mass properties, energy of the last step, reset 3 Advance the weather and the farm rain, soil water, crops (agent 0 only) 4 Select the control source hand throttle, park brake, automatic drivers 5 Steering and drivetrain StepSteering, StepDrivetrain 6 Each wheel: contact ray cast, surface, soil state, PrepareWheel 7 Driveline solve SolveDriveline wheel speeds, clutch, fuel 8 Each wheel: forces and marks tyre forces, water drag, ruts, crushed crop 9 Air drag and implement implement forces, mass and engine loads 10 Forces to Chaos AddForce, AddTorque 11 Publish the step telemetry, logs, sensor sample The Polaris runs the same step. Its own functions replace the drivetrain parts of steps 5 to 7. Game thread keyboard controls Vehicle bridge command queue Weather and farm models shared by all agents Automatic drivers scripted test, replay, drive script Ground collision mesh, surface map, soil class, soil water, ruts Farm state ruts and tread prints crushed crop field-work marks Chaos rigid body integrates the chassis Telemetry HUD, vehicle bridge Session log, episode log one row for each step Sensor recorder sensor stream, sensor files
One physics step of a vehicle in the game: controls, weather and farm, steering and drivetrain, wheel contacts, driveline solve, forces, Chaos, telemetry. Open the diagram

The sequence has three properties that are important for time.

  • The pawn computes all tyre forces from the body state at the start of the step. Chaos then integrates the chassis.
  • The energy ledger of the chassis closes one step later, after Chaos integrated the step (AccountChassisStep).
  • Agent 0 advances the weather and the farm one time for each step. The other agents share them.

The keyboard ramps of the Polaris also advance on the physics clock (StepPolarisKeys), thus a run without a bridge repeats in lockstep.

Control Step of ACRES Core#

FAcresBatch::Step advances each environment by one control step:

\[ T_c = n_s\,h = 12 \times \tfrac{1}{120}\ \text{s} = 0.1\ \text{s} \]
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

A command row is a set of drive-by-wire messages that arrive at the first physics step of the control step. The batch models a controller that publishes at a fixed rate: it marks a held message as new each \(n_r\) steps. With \(n_r\) = 6 the rate is 20 Hz, the rate of the path follower of the lab. A subsystem that gets no message in a row gets no more messages. The watchdog then disengages it. See Watchdog.

FAcresBatch::StepPhysics advances one physics step with complete commands. core_sim uses it with \(n_s\) = 1. The function FAcresPolaris::Step runs one step in this sequence:

  1. StepDbw, StepUtvSteering, StepUtvPowertrain: the drive-by-wire, the steering and the powertrain.
  2. For each wheel: a ray cast on the terrain, the surface below the contact, then PrepareUtvWheel.
  3. SolveUtvDriveline: the wheel speeds, the tyre forces, the engine speed and the fuel.
  4. The farm stamps of the tyres and of the body.
  5. The sum of the forces, then the step of the rigid body.
  6. AccountChassis: the chassis part of the energy ledger, with the speeds at the middle of the step.

Integration Schemes#

All schemes have the fixed step \(h\). The table gives the scheme of each state.

State Scheme Source
Chassis in the game Chaos rigid body, forces from AddForce and AddTorque AsyncPhysicsTickActor
Chassis in ACRES Core Semi-implicit Euler FAcresPolaris::Step
Wheel spin, clutch, engine speed Backward Euler, solved with bisection SolveDriveline, SolveUtvDriveline, RisingRoot
Tyre shear displacement Backward Euler ShearDisplacement in AcresVehicleModel.cpp
Actuator lags, tyre relaxation, CVT ratio Exact step of a first-order lag AcresSim::Lag
Weather noise Exact step of an Ornstein-Uhlenbeck process Model::OU in AcresEnvironmentModel.cpp
Soil water Explicit routing with substeps that the Courant condition limits AcresFarm::WaterGrid::Step
Implement tines, baler Substeps of the physics step AcresImplementModel.cpp

Chassis of ACRES Core#

FAcresPolaris::Step updates the velocities first and then the pose with the new velocities:

\[ \mathbf{v}_{k+1} = \mathbf{v}_k + \left(\frac{\mathbf{F}_k}{m} + \mathbf{g}\right) h \]
\[ \vec{\omega}_{k+1} = \left(\vec{\omega}_k + \mathbf{R}_k\,\mathbf{I}^{-1}\,\mathbf{R}_k^{\mathsf T}\,\mathbf{M}_k\,h\right)\,\max(0,\ 1 - c_\omega h) \]
\[ \mathbf{x}_{k+1} = \mathbf{x}_k + \mathbf{v}_{k+1}\,h \]
\[ \mathbf{q}_{k+1} = \frac{\mathbf{q}_k + \tfrac{h}{2}\,(0,\ \vec{\omega}_{k+1}) \otimes \mathbf{q}_k}{\left\lVert \mathbf{q}_k + \tfrac{h}{2}\,(0,\ \vec{\omega}_{k+1}) \otimes \mathbf{q}_k \right\rVert} \]

\(\mathbf{R}_k\) is the rotation matrix of \(\mathbf{q}_k\). \(\mathbf{I}\) is the diagonal inertia tensor in the body frame. The forces \(\mathbf{F}_k\) and the moments \(\mathbf{M}_k\) are the tyre forces, the water drag and the air drag at the start of the step. The body-frame acceleration of the state array is \(\mathbf{R}_{k+1}^{\mathsf T}(\mathbf{v}_{k+1} - \mathbf{v}_k)/h\).

The game sets the same properties on the Chaos body: no linear damping and an angular damping of 0.08 1/s. The gravity of the game is 9.80665 m/s². The game also sets the sleep thresholds of the body to zero. A body that sleeps drops small velocities.

Wheels and Driveline#

The tyre force changes fast with the slip. An explicit step of the wheel spin is not stable at 120 Hz. The driveline solve thus uses the speeds at the end of the step. The shaft torque that a wheel needs for the spin rate \(\Omega'\) is:

\[ T(\Omega') = J\,\frac{\Omega' - \Omega}{h} + F_x(\Omega')\,r + T_{res}\,\tanh\!\left(\frac{\Omega'}{0.1}\right) \]

\(T_{res}\) is the sum of the rolling resistance torque and the brake torque. \(T\) increases with \(\Omega'\). RisingRoot finds the carrier speed at which the torque that the wheels need is equal to the torque that the engine side delivers. It brackets the root, then does a fixed number of bisection steps. The Polaris uses 44 to 48 steps. A fixed number of steps gives the same result on each run.

On library soil the tyre force comes from the shear displacement \(j\). Its backward Euler step is:

\[ j' = \operatorname{clamp}\!\left(\frac{j + h\,(\Omega' r - v)}{1 + h\,\max(|\Omega' r|,\ |v|,\ 0.2)/\ell},\ -\ell,\ \ell\right) \]

\(v\) is the forward speed of the contact point. Tyre and Soil gives the force laws.

First-Order Lags#

A lag with the time constant \(\tau\) and the target \(u\) uses the exact solution for a constant target (AcresSim::Lag):

\[ y_{k+1} = u + (y_k - u)\,e^{-h/\tau} \]

This step is stable for each ratio \(h/\tau\).

Weather Noise#

The weather model draws its variation from Ornstein-Uhlenbeck processes with the exact update:

\[ X' = X\,e^{-\Delta t/\tau} + \sigma\,\sqrt{1 - e^{-2\Delta t/\tau}}\ \xi, \qquad \xi \sim \mathcal{N}(0, 1) \]

The standard deviation \(\sigma\) and the correlation time \(\tau\) do not change with the step \(\Delta t\). The runtime steps the weather model when 0.05 s of farm time are due. Weather gives the model.

Sensor Sample Times#

The sensor recorder gets one sample of the vehicle state for each physics step (FAcresSensorRecorder::PhysicsSample). Its clock is the step count times \(h\). Each stream has a due time \(t_{due}\), which starts at 0. The stream takes a sample at step \(k\) when:

\[ t_k + 10^{-8}\ \text{s} \ge t_{due}, \qquad \text{then}\quad t_{due} \leftarrow t_{due} + \frac{1}{f} \]

The function is FAcresSensorRecorder::Due. The rates must be between 1 Hz and 120 Hz. A rate that divides 120 gives samples at equal intervals of \(120/f\) steps. A different rate gives intervals that change between two values, because a sample is always on a step.

Stream Rate Steps between Samples Source of the Rate
Ground truth, actions 120 Hz 1 Each step
IMU 120 Hz 1 sensors.json
INS of the Polaris 100 Hz 1 or 2: five samples in six steps sensors_polaris.json
Drive-by-wire reports 50 Hz 2 or 3 DbwReportPeriodS in AcresPolaris.cpp
CAN signals 20 Hz 6 sensors.json
GNSS, LiDAR, camera 10 Hz 12 sensors.json, sensors_polaris.json
Observation of the vehicle bridge 10 Hz 12 ObservationPeriodS in AcresRlBridge.cpp

The camera is different: the game thread renders it. The camera takes a sample in the first frame at or after its due time. A frame that is later than one period skips samples and the recorder counts them. In lockstep each frame is one physics step, thus each camera sample is on its exact step.

The key delay_s is a modelled transport delay. A sample becomes available delay_s after its sample time. The delay does not change the step of the sample. Camera, LiDAR and INS and GNSS give the sensor models.

core_sim uses the same rule. It sends a clock frame for each step, the INS at 100 Hz, the reports at 50 Hz and the LiDAR at 10 Hz.

Command Timing#

The game reads its sockets one time for each frame. A frame can run 12 physics steps. Without more work, all commands of a frame apply at its first step and the other steps see no command. The header AcresCommandTiming.h gives each command the step that agrees with its arrival time.

Time in ACRES: in a free run the game maps the arrival time of each command onto the solver interval of the frame, and the first physics step at or after that time applies the command. The sensors sample on the physics steps of 1/120 s. In lockstep each frame advances one physics step and the next step waits for the sensors. Physics steps, commands, sensor samples and lockstep FREE RUN: A COMMAND APPLIES AT THE STEP THAT AGREES WITH ITS ARRIVAL TIME Wall clock socket reads, one for each frame Solver time physics steps of 1/120 s read n-1 read n read n+1 slow frame, 50 ms 1 2 3 4 5 6 solver interval of frame n: 6 physics steps a command arrives (50 Hz) a step applies a command The queue keeps the order and the spacing of the commands. Without the queue, the 3 commands reach step 1 together. SENSOR SAMPLES ON THE PHYSICS STEPS, 0.2 S OF SIMULATION TIME step 0 step 12 step 24 physics steps, 120 Hz IMU, 120 Hz INS, 100 Hz DBW reports, 50 Hz CAN signals, 20 Hz LiDAR, camera, GNSS, 10 Hz LOCKSTEP: A REQUEST OF 3 STEPS held step 1 wait for the sensors step 2 wait for the sensors step 3 wait for the sensors barriers, reply held step request Each frame advances the solver by one physics step. The world does not move between two requests.
A command applies at the physics step that agrees with its arrival time. The sensors sample on the steps. In lockstep each frame is one step. Open the diagram

A receiver thread reads the sockets 1000 times for each second and stamps each message with its wall time \(a\). At each frame the game thread starts one read (FCommandClock::BeginRead). It has the wall times \(P\) and \(L\) of the last two reads. It also has the solver time \(S\) that the solver got before this frame and the time \(F\) that this frame adds. The command applies at the solver time (FCommandClock::ApplyAt):

\[ t_a = S + \operatorname{clamp}\!\left(\frac{a - P}{L - P},\ 0,\ 1\right) F \]

The game puts a copy of the full command state into a queue with the time \(t_a\) (TTimedCommands::Push). The physics step that starts at the solver time \(s_k\) takes all entries with:

\[ t_a \le s_k + \varepsilon, \qquad \varepsilon = 0.002\ \text{s} \]

The step uses the last entry that it took (TTimedCommands::Take). The tolerance \(\varepsilon\) is necessary because the engine gives the solver time as a single-precision number.

The mapping has these properties. The Lean proofs in Verification/ show them for exact arithmetic.

  • A command of a read applies in the solver interval \([S,\ S + F]\) of the frame.
  • A command that arrived later does not apply earlier.
  • Two commands that arrived \(\Delta\) apart apply at most \(\Delta\,F/(L - P)\) apart. The queue does not make the gaps longer when the frame keeps real time.
  • The commands of the first read apply at the next step, because no interval exists.
  • During a pause of the world, \(F\) is 0 and the queue gets at most two entries.

Latency. The steps of the interval \([S,\ S + F]\) run after the read. A command thus applies approximately one frame after its arrival, and the commands keep their spacing. In lockstep the mapping is off: each command applies at the next physics step.

core_sim uses the same header. In a free run, one pass of its loop is one "frame" of \(12\) steps at most.

The resets, the worker commands, the farm clock, the implement commands and the drive mode do not use the queue. They apply when the frame reads them. Vehicle Bridge gives the messages.

Watchdog#

The drive-by-wire of the Polaris disengages a subsystem when its controller is silent. StepDbw keeps one age for each subsystem: steering, throttle, brake and ULC.

\[ A_{k+1} = \begin{cases} 0 & \text{a message arrived at step } k \\ A_k + h & \text{otherwise} \end{cases} \]

A subsystem is under drive-by-wire control while the system enable is on, its command has a mode and \(A \le T_w\). With \(T_w\) = 0.1 s the limit is 12 steps of age.

Simulator Step Sum of 12 Steps Disengages
ACRES Core 1/120 as a double 0.09999999999999999 s At the 13th step after the last message, 0.1083 s
The game 1/120 as a float 0.1000000052 s At the 12th step after the last message, 0.1000 s

The game thus disengages one physics step earlier than ACRES Core. The Lean file FloatFacts.lean proves the two sums. The proofs also show a second property. The watchdog does not trip while the messages come due 0.1 s or less apart. This limit includes the jitter of the step times. Verification gives the proofs.

Other timers of the command path:

Timer Value Clock Function
dbw.command_timeout_s 0.1 s Simulation The timeout \(T_w\) of each subsystem.
dbw.ulc_actuator_timeout_s 0.087 s Simulation The throttle and the brake keep the last output of the ULC for 10 steps after the ULC disengages.
ExternalHoldS 0.25 s Simulation A subsystem belongs to the bridge for this time after its last message. Then the driver controls it again.
ActionTimeoutS 1 s Wall A vehicle holds the brake when its agent sent no action for this time. A drive-by-wire client is not such an agent. Not active in lockstep.
CanPresenceS 3 s Wall A CAN controller counts as present while its commands arrive in this time. Not active in lockstep.

The timeout counts simulation time. A controller that sends at 20 Hz of wall time is too slow when the simulation runs at five times real time. Drive-by-Wire and ULC gives the model of the actuators.

Lockstep#

In lockstep the world advances only when a client requests steps. FAcresSimControl::SetLockstepTiming does three things.

  1. It sets a fixed frame time that is equal to the physics step. Each frame then gives the solver one step: \(F = h\), \(N = 1\).
  2. It makes the soil water follow the simulation clock. See Water Clock.
  3. It makes each command apply at the next step.

Between two requests the game holds the world. The game still renders frames, but no physics step runs. For a request of \(n\) steps the game releases \(n\) frames. Before each frame it waits until two conditions are true:

  • the last step is complete;
  • each sensor output that was due in that step is complete: the LiDAR scan and the camera image.

At the end the game sends a barrier on each sensor stream and on each vehicle bridge, then the reply. A client that got the barrier has all data of the steps. Lockstep Stepping gives the procedure. Simulator Control Channel gives the operations.

In core_sim a step request runs the steps at once. A request waits for the LiDAR scans that were due in its steps.

Free Run of core_sim#

core_sim paces the physics clock against the wall clock. With the rate factor \(R\), the number of steps that are due at the wall time \(w\) is:

\[ N = k_0 + \left\lfloor \frac{(w - w_0)\,R}{h} \right\rfloor - k \]

\(k_0\) and \(w_0\) are the step count and the wall time of the anchor. \(k\) is the current step count.

One pass runs at most 12 steps (MaxFrameSteps). When more steps are due, the loop moves the anchor to the current time. The simulation then becomes slower. It does not try to get the time back. With \(R\) = 0 each pass runs 12 steps without a wait.

Water Clock#

The soil water of the game is a grid model that a worker thread steps (FAcresWaterThread).

Mode Who Steps the Grid When the Wheels See New Water
Free run The worker thread, at a priority below normal. The physics thread only sends the farm time that is due to it. When the worker publishes a snapshot. This moment depends on wall time.
Lockstep The physics thread (FAcresFarmRuntime::Advance). At fixed simulation times.

In lockstep the physics thread steps the grid in steps of 10 s of farm time. It publishes a snapshot, with the wheel marks, each 0.5 s of simulation time. The crops advance in fixed steps of 10 s of farm time in the two modes. ACRES Core has no water model. Its soil water is a constant scenario. Soil Water gives the model.

Seeds and Random Streams#

ACRES has no generator that takes its seed from the time or from the hardware.

Stream Generator Seed Source
GNSS noise PCG32 state seed, sequence 1 AcresSensors.cpp
IMU noise and bias PCG32 state seed + 1009, sequence 2 AcresSensors.cpp
LiDAR noise PCG32 state seed + 2018, sequence 3 AcresSensors.cpp
GNSS constellation PCG32 state seed + 3027, sequence 4 AcresGnssModel.h
LiDAR rain clutter PCG32 state seed + 4036, sequence 5 AcresSensors.cpp
INS errors PCG32 state seed + 5045, sequence 6 AcresSensors.cpp
Camera fixed pattern PCG32 state seed + 3027, one sequence for each row AcresCameraModel.h
Camera noise PCG32 state seed + 3027 + (frame + 1) × 1000003, one sequence for each row AcresCameraModel.h
LiDAR ground texture, returns from the body Hash of the place or the beam seed AcresLidarPhysics.h
Weather SplitMix64 with Box-Muller seed of environment.json, -EnvSeed= AcresEnvironmentModel.cpp
Farm vehicles and workers FRandomStream Fixed numbers in the code AcresNpc.cpp
INS of core_sim PCG32 state seed + 5045 + 7919 × agent index, sequence 6 CoreSimIns.cpp
LiDAR of ACRES Core Hash of the beam seed xor (environment × 2³²) xor step AcresCoreBatch.cpp, CoreSimServer.cpp

The seed of the sensors is the key seed of sensors.json. The default is 42. The option -SensorSeed= changes it.

The sensor recorder seeds its streams again at the start of each recording. The IMU bias is the first draw of the IMU stream and stays constant during the episode. The camera seeds one stream for each image row and each frame. The image thus does not change with the number of threads. The episode log stores the seed in the message /sim/episode.

Determinism#

A run is deterministic when the same inputs at the same steps give the same result.

Configuration Repeats? Reason
ACRES Core batch Bit for bit The models have no global state. One thread steps one environment. The fixed bisection counts and the hashed noise do not change.
ACRES Core batch with a different number of threads Bit for bit The environments are independent.
core_sim in lockstep Bit for bit Each command applies at the next step.
The game in lockstep, vehicle state Bit for bit in the measurement below One step for each frame, commands at the next step, soil water on the simulation clock.
The game in lockstep, LiDAR and camera Sample steps repeat The game holds the next step until the GPU work of the step is complete. The pixel values of the camera depend on the frame history of the renderer.
The game in a free run No The frame times set the step of each command. The water worker publishes at wall times.
core_sim in a free run No The pass times set the step of each command.

Sources of a difference between two free runs of the game:

  • Frame timing. The number of steps in a frame and the step of each command depend on the wall time of the frames.
  • The water worker. The step at which the wheels first see a new snapshot of the soil water depends on the thread timing.
  • The GPU. The LiDAR traces the ray-tracing scene of the frame, and the camera uses the frame history. In a free run a scan belongs to the frame in which the GPU did it.
  • Chaos. Chaos integrates only the chassis from the forces of the pawn. Contacts that Chaos resolves itself, for example the chassis box against a building, are not part of the vehicle model.

Measurements#

The values are from the test workstation on 2026-10-02.

Check Result Source
ACRES Core batch: 8 environments, 50 control steps, 1 thread and all threads The state, wheel and energy arrays are identical in all bits. Core/Tests/core_tests.cpp, check "one thread = all threads"
ACRES Core batch: the same run after a reset Identical in all bits. Core/Tests/core_tests.cpp, check "reset repeats"
ACRES Core: Step and StepPhysics with the same commands Identical in all bits. Core/Tests/core_tests.cpp, check "StepPhysics = Step"
core_sim in lockstep: 2 runs of 60 s, 117.69 m Position difference 0.000 m. The two episode logs are equal at all 7320 steps. Tools/SimControl/lockstep_determinism.py --game core, compare_episodes.py
The game in lockstep without rendering (-nullrhi): 2 runs of 60 s, 117.65 m Position difference 0.000 m. The two episode logs are equal at all 7320 steps. Tools/SimControl/lockstep_determinism.py --headless, compare_episodes.py
core_sim through ROS 2 in lockstep: 2 servers, the same commands The ground-truth odometry is identical. ROS/acres_core_sim/test/test_integration.py, test_lockstep_determinism
The game in a free run with rendering: 2 sessions of the log grass_diag, 43 s Position difference 0.008 m at most. Yaw rate difference 0.00005 rad/s RMS. Core/Scripts/unreal_dbw_replay.py, compare_unreal.py --session --session2

The command program of lockstep_determinism.py enables the drive-by-wire and commands a speed of 2 m/s. It turns the steering wheel as a sine with an amplitude of 150° and a period of 20 s. It sends commands each 12 steps. The Polaris drives from the ICSC garage into field 48. The criterion of the script is 1 cm.

The lockstep runs had these speeds: the game without rendering 10 times real time, core_sim 44 to 52 times real time.

The free run of the game does not repeat bit for bit, but the two sessions stayed 8 mm apart. This difference is much smaller than the difference between the game and ACRES Core.

Parity of ACRES Core and the Game#

ACRES Core and the game side by side: the two simulators compile the same engine-free model sources and read the same data. The game adds the Chaos body, the renderer, the GPU sensors, the weather and the Maxxum. ACRES Core adds its own rigid body, terrain, obstacle scene, ray-cast LiDAR and batch. The Python module and core_sim use ACRES Core. Two simulators, one set of model sources THE GAME ADDS SHARED BY THE TWO SIMULATORS ACRES CORE ADDS The game (Unreal Engine) Chaos rigid body chassis integration, collisions Collision mesh ray casts for the wheel contacts Renderer and GPU sensors camera, GPU LiDAR, GNSS, INS Weather and water grid sky, rain, soil-water thread Traffic and workers farm vehicles, persons Maxxum and implements tractor, hitch, PTO Engine-free sources Vehicle and drive-by-wire AcresUtvModel AcresVehicleModel AcresSimModel Soil, tyre and energy AcresSoilModel AcresPowerModel Farm, surface and weather AcresFarmModel AcresSurfaceMap AcresEnvironmentModel Episode log writer Configuration and ACRE map compiles compiles reads reads ACRES Core (libacres_core) Rigid body of the Polaris semi-implicit Euler, 1/120 s Terrain survey triangles, exact ray casts Obstacle scene buildings, bins, trees, collisions Ray-cast LiDAR Helios pattern, planar pattern Farm state crushed crop, ruts, soil-water scenario Batch N environments, thread pool INTERFACES AND USERS Three local sockets bridge, control channel, sensor stream ROS 2 bridge sim_bridge same topics for the two simulators core_sim same sockets acres_core Python module Learners A ROS 2 node sees the same interfaces from the game and from core_sim. A learner steps thousands of Core environments from Python.
ACRES Core and the game compile the same model sources. Each simulator adds its own world layer. Open the diagram

The two simulators compile the same files of Acres/Source/Acres. Core/CMakeLists.txt lists them: AcresUtvModel.cpp, AcresVehicleModel.cpp, AcresSimModel.cpp, AcresSoilModel.cpp, AcresPowerModel.cpp, AcresSurfaceMap.cpp, AcresFarmModel.cpp, AcresEnvironmentModel.cpp and AcresEpisodeLog.cpp. ACRES Core also uses the headers AcresLidarPhysics.h and AcresCommandTiming.h. The two simulators read the same configuration files and the same map products.

The parts that are different:

Part The Game ACRES Core
Chassis integration Chaos Semi-implicit Euler in FAcresPolaris::Step
Wheel contact Ray cast on the collision mesh Ray cast on the same triangles (FAcresTerrain::RayCast)
Step as a number Single precision Double precision
Soil water Water grid with rain and drying Constant scenario
Start of an episode Drop from 0.2 m On the terrain at the static ride height, or the drop
Collisions Chaos stops the chassis A flag. The chassis does not stop.
LiDAR GPU ray tracing of the full scene CPU ray cast on simple shapes
Vehicles Maxxum, Polaris, implements Polaris

Measured Agreement#

The values are from the test workstation on 2026-10-02. Other jobs used a part of the processor during the runs with the game.

Comparison Result Source
Terrain height of ACRES Core against the height formula of the game, 100,000 points Largest difference 1.5 × 10⁻¹³ m Core/Tests/core_tests.cpp, check 2
Surface class, soil template and soil water at 20,000 points 0 differences Core/Tests/core_tests.cpp, check 3
ACRES Core against the Polaris test stand, 3 recorded logs at rest Identical traces Core/Scripts/replay_logs.py
ACRES Core against the Polaris test stand, log grass_diag (30 m on grass) Speed difference 0.012 m/s at most, 0.0015 m/s RMS. Heading 7 × 10⁻⁵ rad. Paths end 3.4 mm apart. Core/Scripts/replay_logs.py
ACRES Core against the game in real time, log grass_diag, 43 s Speed RMSE 0.044 m/s. Mean speed 1.210 m/s against 1.206 m/s. Yaw rate RMSE 0.0010 rad/s. Paths 0.38 m apart at most. Core/Scripts/unreal_dbw_replay.py, compare_unreal.py --session
The same run, windows of 10 s from the pose of the game Position difference 0.20 m (median). Across the track 0.011 m. compare_unreal.py --session
ACRES Core against 15 closed-loop recordings of the game (2026-09-27), 413 windows Position after 2 s, 5 s, 10 s: median 0.17 m, 0.15 m, 0.24 m. 90th percentile 0.44 m, 0.33 m, 0.56 m. compare_unreal.py --follower-all --content-commit 2c2bb5b
The same recordings, across the track Median 0.001 m, 0.006 m, 0.019 m. 90th percentile 0.012 m, 0.050 m, 0.21 m. compare_unreal.py --follower-all
The same recordings, open loop Yaw rate RMSE 0.007 rad/s (median). Mean speed 1.852 m/s against 1.844 m/s. Paths 1 m apart after 56 s (median). compare_unreal.py --follower-all
ACRES Core against itself through the comparison script Speed RMSE 0.0001 m/s. Windows of 10 s: 0.064 m (90th percentile). compare_unreal.py --selftest
Ray-cast LiDAR against the GPU LiDAR, 58 scans, ground beams of the rings at or below -8° Median difference -0.1 mm. Median absolute difference 5.4 mm. 89.9 % of the beams agree to 0.1 m. Core/Scripts/compare_lidar.py
The same scans, share of beams with a return Rings at or below -8°: 0.995 (game), 1.000 (ACRES Core). Rings above: 0.456, 0.353. Core/Scripts/compare_lidar.py

Almost all of the position difference is along the track. Its cause is the speed controller of the drive-by-wire. The ULC makes a small speed oscillation in the two simulators, and the phase of this oscillation is different. The difference across the track stays at centimetres.

The comparison script itself has a floor: ACRES Core against itself gives 0.064 m. A window starts ACRES Core from a pose without the state of the actuators.

The closed-loop recordings and the LiDAR calibration folder are not part of the repository. Headless Core Runs gives the procedure for a comparison with a new session of the game.

Parameters#

Engine settings, Acres/Config/DefaultEngine.ini:

Name Type Unit Default Description
bTickPhysicsAsync bool True Runs the physics on the fixed-step asynchronous tick.
AsyncFixedTimeStepSize number s 0.008333333333333333 The physics step \(h\).
MaxPhysicsDeltaTime number s 0.1 The largest solver time of one frame, \(\Delta t_{max}\).
DefaultGravityZ number cm/s² -980.665 Gravity.
bSmoothFrameRate bool False The engine does not smooth the frame time.
bUseFixedFrameRate bool False The frame time is the measured time. Lockstep sets a fixed frame time at run time.

ACRES Core, FAcresBatchOptions and the arguments of the Python class Batch:

Name Type Unit Default Description
PhysicsDt number s 1. / 120 The physics step \(h\).
SubSteps, substeps integer 12 The number of physics steps in one control step, \(n_s\).
CommandResendSteps, resend_steps integer steps 6 The period \(n_r\) at which the batch sends a held command again. 0: one time only.

core_sim:

Name Type Unit Default Description
--rate number 1.0 The rate factor \(R\) of a free run. 0: no limit.
--lockstep flag off Starts with the world held.
--max-frame-steps integer steps 12 The largest number of steps in one pass of a free run.

Drive-by-wire, Acres/Content/Simulation/polaris.json:

Name Type Unit Default Description
dbw.command_timeout_s number s 0.1 The timeout \(T_w\) of a subsystem without a message.
dbw.ulc_actuator_timeout_s number s 0.087 The time for which the throttle and the brake keep the last output of the ULC.

Sensors and weather:

Name Type Unit Default Description
seed in sensors.json integer 42 The seed of all sensor streams. The option -SensorSeed= changes it.
delay_s in sensors.json number s 0.05 The modelled transport delay between a sample and its delivery.
gnss.hz, lidar.hz, camera.hz in sensors.json number Hz 10 The rates of these streams.
imu.hz in sensors.json number Hz 120 The rate of the IMU.
can.hz in sensors.json number Hz 20 The rate of the CAN signals.
ins.hz in sensors_polaris.json number Hz 100 The rate of the INS of the Polaris.
seed in environment.json integer 42 The seed of the weather. The option -EnvSeed= changes it.
clock.seconds_per_game_minute in environment.json number s 60 The wall time of one minute of farm time. The time scale is 60 divided by this value.

Constants in the code:

Name Type Unit Default Description
CommandDueToleranceS number s 0.002 The tolerance \(\varepsilon\) of the command queue.
ExternalHoldS number s 0.25 The time for which a subsystem belongs to the bridge after a message.
DbwReportPeriodS number s 0.02 The period of the drive-by-wire reports.
ObservationPeriodS number s 0.1 The period of the observation of the vehicle bridge.
ActionTimeoutS number s 1 The wall time after which an agent without actions holds the brake.
CanPresenceS number s 3 The wall time for which a CAN controller counts as present.
AngularDampingPerS number 1/s 0.08 The angular damping \(c_\omega\) of the chassis in ACRES Core. The game sets the same value on the Chaos body.

Code Map#

Item File Function
Physics step of the game Acres/Source/Acres/AcresVehicle.cpp AAcresVehiclePawn::AsyncPhysicsTickActor
Command path of the Polaris Acres/Source/Acres/AcresPolaris.cpp StepPolarisControls
Chassis energy of the last step Acres/Source/Acres/AcresVehicle.cpp AccountChassisStep
Command clock and queue Acres/Source/Acres/AcresCommandTiming.h FCommandClock, TTimedCommands
Read of the sockets Acres/Source/Acres/AcresRlBridge.cpp FAcresRlBridge::BeginCommandRead, Tick
Watchdog Acres/Source/Acres/AcresUtvModel.cpp AcresSim::StepDbw
Lockstep Acres/Source/Acres/AcresSimControl.cpp SetLockstepTiming, OnWorldTickStart, FinishStep
Water clock Acres/Source/Acres/AcresFarmRuntime.cpp FAcresFarmRuntime::Advance, SetDeterministicWater
Weather clock Acres/Source/Acres/AcresEnvironment.cpp FAcresEnvironmentRuntime::Advance
Sensor schedule and seeds Acres/Source/Acres/AcresSensors.cpp FAcresSensorRecorder::Due, PhysicsSample, Start
Driveline solve Acres/Source/Acres/AcresVehicleModel.cpp, AcresUtvModel.cpp SolveDriveline, SolveUtvDriveline, RisingRoot
Control step of ACRES Core Core/Source/AcresCoreBatch.cpp FAcresBatch::Step, StepEnv, StepPhysics
Physics step of ACRES Core Core/Source/AcresCorePolaris.cpp FAcresPolaris::Step
Loop of core_sim ROS/acres_core_sim/src/CoreSimServer.cpp FCoreSimServer::Run, StepOnce, IngestRl
Determinism check Tools/SimControl/lockstep_determinism.py drive, compare
Comparison of two episode logs Tools/SimControl/compare_episodes.py main
Timing proofs Verification/AcresVerification/Proofs/ Clock.lean, Queue.lean, CommandPath.lean, FloatFacts.lean

Limitations#

  • A free run of the game is not deterministic. Use lockstep when a result must repeat.
  • The game disengages a silent drive-by-wire subsystem one physics step earlier than ACRES Core.
  • The step of the chassis in ACRES Core has no gyroscopic term. Chaos integrates the chassis of the game.
  • ACRES Core finds a collision but does not resolve it. After a collision the two simulators give different motion.
  • In a free run a command applies approximately one frame after its arrival. A slow frame makes this delay longer.
  • A sensor rate that does not divide 120 gives samples at intervals that are not equal.
  • The camera of a free run takes its sample on a frame, not on an exact step.
  • The measurements of this page use the Polaris. No measurement exists for the Maxxum in lockstep.
  • The measurement of the game in lockstep is without rendering. This page has no measurement of the pixel values of two runs.

References#

  • O'Neill, M. E. (2014). PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation. Technical Report HMC-CS-2014-0905, Harvey Mudd College.
  • Steele, G. L., Lea, D., and Flood, C. H. (2014). Fast Splittable Pseudorandom Number Generators. Proceedings of the ACM International Conference on Object Oriented Programming Systems Languages and Applications (OOPSLA), 453-472.
  • Box, G. E. P., and Muller, M. E. (1958). A Note on the Generation of Random Normal Deviates. The Annals of Mathematical Statistics, 29(2), 610-611.