Deployment#
The deployment program runs a driver of the Field Scouting Task on the ROS 2 topics of the
Polaris Ranger. You name a mission. The program plans the route from the position of the vehicle. In each control step
(0.1 s) it builds the observation from the topics and runs the driver. Then it makes two ds_dbw commands: a
SteeringCmd in curvature mode and a UlcCmd in velocity mode.
Status
The program, the drivers reference and ppo and the three modes exist. The driver residual with the gain
\(\alpha\) and the shield of the Framework is specified, not built
(Residual Driver).
Real Vehicle
Only the laboratory runs the program on the vehicle. The safety driver is the safety system.
Modes#
| Mode | Location | Commands Sent | Start |
|---|---|---|---|
| SIM | The simulated Polaris: the game with acres_sim, or ACRES Core with acres_core_sim |
Yes, only after the program detects a simulator | Learning/Scripts/deploy.sh sim |
| SHADOW | The vehicle with a person who drives; or a recorded bag (offline) | None. The node has no publisher | 10_policy_run.py (field-day kit); deploy.sh offline <bag> |
| DRIVE | The vehicle with the safety driver in the seat | Steering (curvature) and ULC (velocity) | 10_policy_run.py --drive |
In DRIVE mode the program stops itself in three cases. It cannot drive (no odometry). The operator stops it (Enter, Ctrl-C).
The drive-by-wire disengages. Each other check is a warning. The program shows and logs each warning. The
option --stop-on makes a check a stop.
Missions#
| Mission Text | Legs | Complete When |
|---|---|---|
F53 |
GoTo, Scout, Return | The vehicle is stopped at H and the field is scouted |
F53,F12 |
GoTo and Scout for each field, then Return | The vehicle is stopped at H and each field is scouted |
home |
Return | The vehicle is stopped at H |
goto:F53 |
GoTo | The vehicle reaches the entry of the field |
Plan. A single field uses its frozen optimal plan. More fields use the exact dynamic programme on the cached cost tables. The route starts at H when the vehicle is within 5 m and 45° of H. From a different pose, the route starts on the lane that the vehicle is on. The vehicle must be within 25 m of a lane.
GoTo mission. goto:F53 is the first leg of the scouting mission of F53: the same entry, direction and access
point. The run ends at the entry with the vehicle stopped. The time \(T_\text{ref}\) is the time of the reference speed
profile over the leg. The GoTo legs from H are long: 307 m (F57) to 2545 m, with a median of 1430 m.
After the time limit. Past \(1.5 \, T_\text{ref}\) the run continues. The mission can still become complete.
Program Structure#
The code is in Learning/acres_learn/deploy/. It needs only numpy. Thus it runs on the vehicle PC (Python 3.10) without
torch. Each mode runs the same step (deploy/loop.py).
| File | Function |
|---|---|
georef.py |
Converts /vehicle/odom (UTM 16N) to the map frame. It agrees with pyproj to \(10^{-5}\) mm |
vehicle.py |
Converts the messages to the quantities of the policy, with the conventions of ACRES Core |
mission.py |
Makes the plan and the reference path. reverse=False plans no three-point turns |
drivers.py |
The drivers reference and ppo:<file.npz or run> behind one interface |
loop.py |
One step: driver command, speed cap, gear logic, checks, one record |
safety.py |
The checks |
report.py |
Writes records.jsonl, summary.json, report.md, report.png |
sim.py, offline.py |
The SIM front end and the offline SHADOW front end |
bundle.py |
Makes the folder for the vehicle PC (acres_policy/) |
Calibration/Polaris/FieldDay/10_policy_run.py |
The front end on the vehicle: SHADOW and DRIVE |
Georeference. The datum chain is NAD83(2011) for an RTK solution, or WGS84 at the epoch for an uncorrected solution. Then follows the NADCON5 shift to NAD83(HARN) and EPSG:2968 (Indiana West) about the site origin. The two transverse Mercator projections use the Krüger series to sixth order (Karney 2011). The heading turns by the difference of the grid convergences: 0.054° at the garage.
Drivers#
| Driver | State | Function |
|---|---|---|
reference |
Exists | The pure-pursuit reference driver. Its cruise speed is the speed cap |
ppo:<file.npz or run> |
Exists | The exported flat policy in numpy. The action is the mean |
residual:<file.npz or run> with --alpha |
Specified, not built | The reference driver plus the residual, then the shield |
A policy file carries its training options. The program uses the same options: launch_hold, path_through and
path_lookahead.
Step#
- The driver gives a curvature and a signed speed.
- The program clips the curvature to ±0.2 m⁻¹.
- The program clips the speed to the speed cap forwards. Backwards, the limit is the minimum of the cap and 1.5 m/s.
- Where gear shifts are permitted (the simulators),
spec.GearManagermakes the stop, theGearCmdand the hold. - Where gear shifts are not permitted, a backward request becomes a stop and the finding
reverse. - For a policy with the option
launch_hold,spec.LaunchHoldholds the speed command while the vehicle stands. - A complete mission holds the speed 0.
- The checks run. The program writes one record.
The launch hold runs after the gear logic in this program. The Framework
specifies a different order for the driver residual: the launch hold runs before the shield.
Vehicle Conventions#
Recorded vehicle data or the simulators confirm each convention.
| Input | Source | Convention |
|---|---|---|
| Position, heading | /vehicle/odom |
The rear-axle ground point (base_footprint) in UTM. The yaw of the forward axis, turned to the map grid |
| Pitch, roll | /vehicle/odom orientation |
Pitch nose up = asin(forward.z). Roll left side up = asin(left.z) |
| Speed, yaw rate | /vehicle/odom twist |
Body frame, signed |
| Lateral acceleration | Speed times yaw rate | /oxts/imu is in the frame of the device, which is 2.7° off the body. Thus the program uses the centripetal value |
| Steering wheel | /vehicle/steering/report |
Degrees, left is positive |
| Wheel speeds | /vehicle/wheel_speeds if published; else the rigid-body speeds of the wheel centres |
m/s |
| Gear | /vehicle/gear/report |
ds_dbw Gear (5 = L) |
| LiDAR | /lidar/points |
Turned by the Helios mount (roll −1.5°, pitch 5.3°). The program drops returns nearer than 1.6 m. Beams within 1° of the horizontal go into 360 bins |
| RTK | /oxts/fix |
Status 2 with σ < 5 cm: fixed. Status 2 otherwise: float. Else: none |
On the RTK bag of 31 July, the twist speed is 1.003 times the speed from the positions. The slope of the yaw rate against the derivative of the heading is 0.96.
SIM Mode#
- Activate the ROS environment:
source ROS/Env/setup_env.sh. - Start a simulator:
ros2 launch acres_core_sim core_sim.launch.py rate:=4 &. The game with the bridge ofacres_simis the alternative. - Run the program:
Learning/Scripts/deploy.sh sim --mission F53 --driver reference --reset-home --out ~/runs/f53_ref
Learning/Scripts/deploy.sh sim --mission F53 --driver ppo:Acres/Saved/Training/scout1/policy.npz \
--reset-home --out ~/runs/f53_ppo
Learning/Scripts/deploy.sh sim --mission goto:F53 --driver reference --reset-home --out ~/runs/goto_f53
Guard. The program makes no publisher until it sees a simulator. A simulator publishes /sim/episode with the
source "unreal" or "core", and a fresh /clock. Each publish checks again that /clock is fresh. Without a simulator
the program prints REFUSED and exits with code 2.
Role of the safety driver. In SIM mode the program does the work of the safety driver. --reset-home puts the
vehicle at H (/sim/reset). The program puts the lever in L and enables the drive-by-wire.
Time. The program runs on simulation time: 10 Hz of /clock. A thread sends the commands again at 50 Hz. Thus the
program follows the rate of a Core that runs faster than real time. The simulator must run free. A lockstep simulator
that waits does not work.
| Option | Default | Function |
|---|---|---|
--mission |
F53 |
The mission text |
--driver |
reference |
The driver |
--out |
Required | The output folder |
--speed-cap |
4.0 m/s | The speed cap (the cruise speed of the task) |
--forward-only |
Off | No gear shifts, as in DRIVE mode |
--reset-home |
Off | Puts the vehicle at H first |
--stop-on |
None | Checks that stop the run |
--max-run-s |
\(3 \, T_\text{ref}\) | The run limit in simulation seconds |
--wait-s |
15 s | The time to look for a simulator |
--datum |
nad83_2011 |
The datum of the odometry |
--odom-max-age-s |
1.0 s | The maximum age of the odometry in simulation seconds |
SHADOW Mode#
On the vehicle PC, in the folder of the kit (~/fieldday_kit):
- Run the check:
python3 10_policy_run.py --check --mission F53. Each line must show GO. - Start the run:
python3 10_policy_run.py --mission F53for the bundled policy. - Or start with the reference driver:
python3 10_policy_run.py --mission F53 --driver reference. - Drive the vehicle by hand, or with the path follower of the laboratory.
- Type
qand Enter (or Ctrl-C) to end the run.
The node subscribes and has no publisher (tested: only /rosout and /parameter_events). The status line shows these
items each 0.5 s:
- The phase and the progress.
- The speed against the command.
- The commanded curvature as a steering-wheel angle, against the measured angle.
- The cross-track error, the RTK state and the warnings.
The report goes to ~/fieldday/<date>/policy_runs/<time>_<mission>_shadow/.
Offline SHADOW on a Recorded Bag#
Learning/Scripts/deploy.sh offline <bag folder or .mcap> --mission home --driver reference --out <dir>
The messages of the bag go through the same code in their recorded order. The step is 0.1 s of bag time. The program copies a sqlite3 bag into the output folder before it opens the bag. It reads an MCAP file in place. The mission starts at the first pose of the bag.
| Option | Default | Function |
|---|---|---|
--mission |
home |
The mission text |
--driver |
reference |
The driver |
--speed-cap |
2.0 m/s | The speed cap |
--stop-on |
None | Checks that count as stops |
--datum |
auto |
auto, nad83_2011 or wgs84 |
--scan-level |
body |
body, or ins to level the scan with gravity |
DRIVE Mode#
python3 10_policy_run.py --mission F53 --drive # the bundled policy, cap 2.0 m/s
python3 10_policy_run.py --mission F53 --drive --driver reference # the reference driver
python3 10_policy_run.py --mission F53 --drive --speed-cap 1.5 --stop-on off_path,obstacle
--drive is the one arming flag. There is no typed confirmation and no countdown. The program plans. Then it waits
until the driver enables the drive-by-wire. It drives from the moment that /vehicle/dbw_enabled is true.
| Event | Reaction of the Program |
|---|---|
/vehicle/dbw_enabled false, or no report for 0.5 s |
One release (enable = false), then no more commands. The driver has the vehicle |
/vehicle/odom older than 0.25 s |
One release, then no more commands. The 0.1 s timeout of the drive-by-wire stops the vehicle |
| Enter or Ctrl-C | The safe stop: ULC 0 m/s until the vehicle is stopped (0.5 s minimum, 8 s maximum), then the release. A second Ctrl-C stops the commands immediately |
| The mission is complete | The safe stop |
A check named in --stop-on |
The safe stop |
| Each other finding | A warning on the status line and in the log |
Commands. SteeringCmd with cmd_type 3 (curvature, \(\lvert \kappa \rvert \le 0.2\) m⁻¹, the rate limits of the
laboratory follower). UlcCmd with cmd_type 1 (velocity, 0 to the speed cap). The program sends them at 50 Hz.
It
never sends a GearCmd, /vehicle/enable, throttle or brake. The mission plan has no three-point turns. The program
holds a backward request of the policy at 0 and reports it. The speed cap is 2.0 m/s by default and 4.0 m/s at most.
10_policy_run.py Option |
Default | Function |
|---|---|---|
--mission |
F53 |
The mission text |
--driver |
ppo |
ppo (the bundled policy.npz or --policy) or reference |
--policy |
acres_policy/policy.npz |
A different policy file |
--drive |
Off | Arms DRIVE mode |
--speed-cap |
2.0 m/s | The speed cap, 4.0 m/s at most |
--stop-on |
None | Checks that stop the run |
--off-path-m |
3.0 m | The distance of the off_path warning |
--max-run-s |
0 (none) | The run time of the time warning |
--datum |
nad83_2011 |
The datum of the odometry |
--no-lidar |
Off | No subscription to /lidar/points |
--check |
Off | The dependency check, then exit |
Checks#
--stop-on accepts each of these names, separated by commas.
| Check | Condition |
|---|---|
rtk |
The solution is not RTK fixed |
off_path |
The rear axle is more than --off-path-m from the reference path of the phase |
crop |
A part of the plan box is on crop outside the edge band |
off_map |
A wheel is off the surveyed tile |
obstacle |
LiDAR returns in the driving corridor within the stop distance |
lidar_stale |
/lidar/points is older than 0.5 s |
safety_stop |
/safety_stop of the laboratory is true |
publisher |
A different node publishes drive-by-wire commands |
overspeed |
The measured speed is 1 m/s above the cap |
reverse |
The driver asked for reverse where gear shifts are not permitted |
time |
The run is longer than --max-run-s |
The obstacle check uses returns 0.35 to 2.3 m above the ground plane of the wheels. It needs at least 8 returns. The corridor is 1.6 m wide plus 0.3 m on each side. The stop distance is \(1.5 \text{ m} + v \cdot 0.5 \text{ s} + v^2 / (2 \cdot 0.8 \text{ m/s}^2)\). Stale odometry always stops the commands.
Checklist before DRIVE#
- Run
python3 10_policy_run.py --check --mission F53. Each line must show GO. - Make sure that the RTK solution is fixed (
01_wait_rtk.py). - Put the vehicle at H with its front to the north. A lane with the front along it is the alternative.
- Do a SHADOW run of the same mission. Drive the first few hundred metres by hand.
- Make sure that the commanded curvature follows the steering wheel.
- Make sure that the cross-track error stays below 1 m and that no
off_pathorcropwarning shows. - Stop the follower and NavRoute. The
publisherwarning shows if they run. - Make sure that the safety driver is in the seat with the belt on.
- Make sure that the hands of the driver are at the wheel and a foot is above the brake.
- Put the gear in L. Make sure that the route is clear.
- Start
python3 10_policy_run.py --mission F53 --drive. - Enable the drive-by-wire when the program tells you.
- To stop: brake or steer, or press Enter on the laptop.
Report#
Each run writes these files.
| File | Content |
|---|---|
records.jsonl |
Each step: inputs, commands, phase, cross-track error, findings |
summary.json, report.md |
The summary |
report.png |
The map with the planned path and the driven track; speed, steering and cross-track error against time |
result.json, events.log |
Vehicle runs only |
commands.csv |
DRIVE only: each command sent |
inputs/ |
Vehicle runs only: a rosbag2 of the inputs (each tenth cloud) and of the records |
| Line of the Report | Meaning |
|---|---|
| Mission | Progress along the reference path (m and share), the last phase, success, distance driven |
| Cross-track | \(\lvert e_y \rvert\) from the reference path of the phase: 50th percentile, 95th percentile, maximum |
| Command against vehicle | Commanded against driven curvature; commanded against reported steering-wheel angle; commanded against measured speed |
| Check | The share of steps on which each check was true, and the first time |
| Stop | The place of the stop, and the place of a stop if each check were a stop |
| Georeference | The ground class under the track, and its distance from the lane centre lines |
Results#
These results are of 1 October 2026. The policy is the best checkpoint of a short test run (14 minutes of training). Its numbers show only that the chain works.
DRIVE stops against the fake vehicle (test_policy_run.py): 17 of 17 cases pass. The cases are:
- The time stop, a driver override, the disable button and a drive-by-wire fault.
- Lost reports and stale odometry.
- The operator key, Ctrl-C, and Ctrl-C two times.
--stop-onwithrtk,obstacleandpublisher.- Warnings that do not stop the run, the speed cap, and a quit before the drive-by-wire is enabled.
In each case the program sent only curvature SteeringCmd and velocity UlcCmd inside their bounds. The release came
last. No node published a gear, enable, throttle or brake command. The 1.0 m/s cap holds.
SIM mode on ACRES Core (policy_core_run.sh, Core at four times real time, cap 4 m/s):
| Mission | Driver | Result | Time | Cross-Track p50 / p95 | Commanded against Driven Curvature, p50 |
|---|---|---|---|---|---|
| F53 (3193 m, with a three-point turn) | Reference | Complete | 892 s (frozen \(T_\text{ref}\) 890 s) | 0.26 / 1.04 m | 0.007 m⁻¹ |
| F33 (2819 m) | Reference | Complete | 778 s (frozen \(T_\text{ref}\) 770 s) | 0.25 / 1.05 m | 0.007 m⁻¹ |
| F53 | PPO (test run) | Not complete: field scouted, then stopped 4.1 m from H | 1500 s (run limit) | 1.54 / 4.33 m | 0.031 m⁻¹ |
| F33 | PPO (test run) | Not complete: field scouted, on the Return at 95 % of the path | 1500 s (run limit) | 3.85 / 20.6 m | 0.022 m⁻¹ |
DRIVE mode on ACRES Core (policy_core_run.sh drive:reference:F53:2.0): the forward-only F53 mission (3220 m) is
complete in 1541 s of simulation. The cross-track error is 0.14 / 0.60 m (p50 / p95). The commanded speed is never above
2.0 m/s. The program ended with the safe stop and the release at H.
SIM mode in the game (policy_game_run.sh <dir> F53 reference 420): in 420 s the reference driver drove 1113 m of
the GoTo of F53. The cross-track error is 0.25 / 0.91 m. The LiDAR of the game sees trees and crop along the lanes. The
obstacle warning was true on 29 % of the steps.
Offline SHADOW on recorded bags (deploy.sh offline, mission home):
| Bag | Result |
|---|---|
human_20260813_172445 (parked, 4.5 s) |
The pose is on the lane west of the bins: 100 % lane, 1.0 m from the centre line |
grass_diag_20260731_174757 (follower, 41.5 s, RTK fixed) |
The track is on lane (57 %) and edge band (42 %), 1.85 m (p50) from the lane centre line. The route of the follower is 3 m east of the centre line of the transit graph |
Georeference (deploy/georef_check.py): the 37 closed-loop runs of the follower have 18,404 logged poses. The numpy
chain puts 95.0 % on lane, 2.6 % on verge, 0.2 % on edge band and 2.1 % on crop. On the bag of 31 July, the heading and
the course over the ground agree to 0.9° (median).
Bundle check. 10_policy_run.py --check on the bundle under Python 3.10 and numpy 1.26 passes each line but ROS.
One step of the flat policy takes 4.5 ms.
Bundle for the Vehicle PC#
Learning/Scripts/ppo.sh export scout1 --checkpoint best --out Acres/Saved/Training/scout1/policy.npz
Learning/Scripts/deploy.sh bundle --policy Acres/Saved/Training/scout1/policy.npz --out ~/kit_out
The bundle is ~/kit_out/acres_policy/ (17 MB) and acres_policy.tar.gz. It holds:
- The sources and configurations of
acres_learn. - The map products and the other files of
Acres/Content/Simulationthat the program reads. - The cached cost tables.
policy.npz.MANIFEST.jsonwith the commit and the SHA-256 of each file.
The bundle goes into ~/fieldday_kit/acres_policy/ with the top-level files of the kit. No installation is necessary.
Demonstration Procedure#
The procedure has a simulator part and a vehicle part. Only the laboratory does the vehicle part. The program never connects to a different computer.
-
Simulator. Make sure that no game and no Core run. Then run one of these commands.
source ROS/Env/setup_env.sh Calibration/Polaris/FieldDay/Test/policy_game_run.sh ~/demo/game F53 reference 420 # the game, 7 minutes CORE_RATE=4 Calibration/Polaris/FieldDay/Test/policy_core_run.sh ~/demo/core sim:reference:F53The Polaris leaves the ICSC garage, drives the lanes to F53, scouts its loop and comes back.
report.pngshows the planned path, the driven track, the speed and the steering. -
Offline SHADOW. Run the program on a recorded drive.
-
SHADOW on the vehicle. Make sure that the RTK solution is fixed and that the vehicle is at H. Run
python3 10_policy_run.py --check --mission F53. Then runpython3 10_policy_run.py --mission F53 --driver reference. The driver drives out of the garage along the planned route by hand. Typeqand Enter. Openreport.png. -
DRIVE on the vehicle. Do this step only if step 3 was correct and the laboratory agrees. Use the reference driver at a low cap for the first few hundred metres.
The driver enables the drive-by-wire when the screen tells him or her. The driver takes control at any time by a brake or steering input, or with Enter on the laptop.
Residual Driver#
State: specified, not built.
| Property | Rule |
|---|---|
| Selection | --driver residual:<file.npz or run> and --alpha <value> |
| Base command | The reference driver on the mission path, with the cruise speed at the speed cap |
| Residual | The exported actor in numpy, with the residual law |
| Order in a control step | Reference driver, residual law, launch hold, shield, gear logic, actuator clip (Order in a Control Step) |
| Shield | The same function as in training (Shield). The program calls shield_curvature, measures the obstacle distance along that arc, then calls shield |
| \(\alpha = 0\) | The run is equal to a run of the driver reference. The exceptions are a shield action for an obstacle and a stop for an input that is not usable |
| Records | Each record has the base command, the residual, the flag intervened, the rule codes violated and \(\alpha\) |
| SHADOW | The log shows the residual command beside the base command. No command goes to the vehicle |
| DRIVE | The status line shows \(\alpha\) and the state of the shield |
The obstacle rule of the shield reads the planar scan. On the vehicle, the scan sees tall crop as a wall. Thus the rule needs an input that does not report crop before it can stop the vehicle beside crop.
The stop of the shield assumes a latency of at most 0.5 s and a deceleration of at least 0.8 m/s². No proof and no measurement gives these values for the vehicle (Verification). The safety driver stays the safety system.
Tests#
| Command | Content |
|---|---|
python Learning/tests/test_deploy.py (in run_all.py) |
Georeference; vehicle conventions; missions from H and from a pose; the GoTo mission; speed cap; gear logic; checks; the SIM guard; the PPO driver |
python3 Calibration/Polaris/FieldDay/Test/test_policy_run.py (ROS 2, DDS fence) |
Each DRIVE stop against the fake vehicle; warnings; no publisher in SHADOW; refusal in SIM without a simulator |
Calibration/Polaris/FieldDay/Test/policy_core_run.sh <dir> [mode:driver:mission[:cap]] |
SIM and DRIVE end to end on ACRES Core |
Limitations#
- The LiDAR of the training has no crop. The scene of ACRES Core has terrain, buildings, bins and trees, but no crop. The real Helios sees corn at the edge of a lane as a wall some metres to the side.
- The steering-wheel comparison uses the fitted steering of
polaris.json(ratio 12.0, centre 12.5°). On the simulators the comparison reads approximately 13° off. - The datum. The program reads
/vehicle/odomas NAD83(2011). Without corrections the OxTS solution drifts towards WGS84, which is 1.3 m away on the tile. Thertkwarning shows this. - Missions start at H or on a lane. From a different place the planner refuses.
- The obstacle check is simple. It uses a straight corridor on a flat ground plane. On a curve or a slope it can miss an obstacle or report a false one.
- Few real recordings fit. The local bags with a converged INS are short. The first SHADOW run on the vehicle is the real test of the conventions.