Skip to content

physics: log nav_plant runs for fitting a plant model, and dyn_plant's residual against Jolt - #749

Merged
9 commits merged into
masterfrom
feat/plant-log-liesphgp
Oct 21, 2026
Merged

9 commits merged into
masterfrom
feat/plant-log-liesphgp

Conversation

@transfix

@transfix transfix commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

A port-Hamiltonian vehicle model fitted to the physics (a learned plant on SE(2), CVC-Lab/LieSPHGP's method) needs two things this PR adds:

  • the physics' trajectories in a plant-learning format;
  • a baseline to beat. P8a's dynamic model has never been measured against Jolt; this measures it.

What it adds

  • cvc::physics::plant_log (cvc/physics/plant_log.h) samples a sim_world's agents on a nav_plant every tick, as planar rigid bodies in SI. Each sample holds:

    • the centre of mass's pose on SE(2) and its body twist;
    • the steering angle;
    • the command nav held over the tick (a zero-order hold);
    • the measured acceleration, static contacts, and wheels on the ground.
  • one_tick_residuals: the per-tick drift (D14's method) with diff::dyn_plant beside the kinematic bicycle.

    • Each tick, both models step from Jolt's state with the held command and are compared with where Jolt was at the tick's end.
    • Ticks are binned by the lateral g Jolt measured.
    • Ticks with a static contact, a wheel off the ground, an impact (above 1.5 g) or the vehicle at rest are left out and counted.
  • cut_flights / write_npy: equal-length windows of [x, y, psi, vx, vy, r, drive, steer], float64.

  • cvc plant-log (a cvc-cli subcommand over cvc/physics/plant_log_scene.h) drives the Humvee or the RoboRacer on Jolt through nav, in one of three scenes:

    • a slalom on open ground, with swerves past 0.3 g;
    • the coef trainer's city;
    • a cvc::world city bundle: vehicle routes on its rasterized map, vehicles spawned on its terrain.

    It writes flights.npy, meta.json and residual.json. The scenes and the writer are library calls (plant_log_options, drive_plant_log_scene, write_plant_log), so a trainer can drive them in process.

  • src/cvc/tools/liesphgp_pack.py (standard library and numpy only) packs runs into the dataset pickle LieSPHGP's loaders read (t, train_trajectories, test_trajectories, test_trajectories_noisy, settings).

    • Layouts: the 20-wide SE(3)-lifted rows its ROV loader takes with control_dim: 2, or the 8-wide SE(2) rows.
    • Inputs: zero-order hold, with row 0's inputs zero.
    • Noise: observation noise at a stated sigma on the planar state.
    • The split is by seed. --check re-reads the file against that contract.
  • nav_plant_options::ground_height: each start's ground height, for a terrain that is not flat. Empty means ground_z, as before.

  • Roadmap: the drift section now publishes the dynamic model's residual (below).

Measured (local, Linux, Jolt 5.6.0+cvc.6 from cvcpkg; 4 seeds x 60 s, mu 0.9; spec-sheet dyn_bike, before #753; see "After #753" below)

The models' one-tick position residual at nav's 0.06 s tick (6 substeps), in mm, dynamic / kinematic:

lateral g Humvee slalom Humvee city RoboRacer slalom RoboRacer city
0.0-0.1 2.0 / 3.9 2.0 / 3.4 0.9 / 0.7 2.5 / 1.9
0.1-0.2 5.1 / 11.6 4.8 / 11.1 3.3 / 5.6 4.3 / 4.6
0.2-0.3 7.1 / 17.9 7.0 / 18.1 5.0 / 10.7 5.8 / 9.4
0.3-0.5 8.1 / 27.5 8.1 / 26.2 6.9 / 15.8 7.1 / 13.8
>= 0.5 5.2 / 94.0 4.5 / 52.0 6.5 / 25.1 4.8 / 19.0
  • Position and heading: the dynamic model is closer wherever the tires carry load, 4-18x above 0.5 g. At the 0.02 s tick the pattern is the same: Humvee slalom above 0.5 g, 0.9 mm against 31.0 mm.
  • RoboRacer speed: the dynamic model is worse, 0.02-0.12 m/s rms against 0.007-0.06. Its 0.1 s drive lag (dyn_bike_si_of's drive_lag) is slower than the vehicle's driver model.
  • RoboRacer yaw rate: in the city it is off by 0.1-0.3 rad/s rms.

The 50 Hz datasets are 5 s flights from the slalom, the city, and the 3 km Austin south bundle, with seed 101 held out:

vehicle train flights test flights windows (101 points, stride 75)
Humvee 472 166 1,416
RoboRacer 245 111 735
  • Kinematic consistency: in the clean test flights, dx/dt matches R v_b to 0.024 m/s at p99.
  • Austin yield. About half the windows are dropped, and the route planner fails on about 60% of the goals it picks. The Humvee spends 5.6% of its samples with no wheel down; those ticks and windows are left out.
  • RoboRacer in the city. Its chassis touches the ground: 732 static-contact ticks, and 296 of 384 windows dropped.

Tests: physics_plant_log_test 4/4:

  • a run a model made leaves that model no residual (kinematic and dynamic, nsub 1, 2 and 6);
  • the flights, their layout and the .npy;
  • a Humvee slalom on Jolt: row k's command is the one held over the tick, the centre of mass moves with its logged twist to 0.023 m/s, and the peak is 0.87 g;
  • ground_height spawns as ground_z does.

All nav/physics ctest cases pass (402/402). The tool and the packer were run as above; the packer's check passes on all four pickles.

Follow-ups (later commits on this branch)

  • Fitting dyn_bike to Jolt (fit_dyn_bike, dyn_bike_cost, cvc plant-log --fit-dyn): a coordinate descent on the one-tick twist residual over the knobs a spec sheet does not give: drive and steering lags, factors on the tires' B, the yaw inertia, and drag + rolling resistance. It fits on seeds 100-102 and is judged on seed 103.

    run (50 Hz) drive lag tire B front / rear yaw inertia drag + rolling held-out twist residual, (m/s)²
    Humvee slalom 0.15 s x0.40 / x0.30 x1.00 x1 0.00287 -> 0.00045
    Humvee city 0.02 s x0.40 / x0.30 x1.20 x1 0.00280 -> 0.00060
    RoboRacer slalom 0 x0.30 / x0.25 x0.85 x0 0.00035 -> 0.00001
    RoboRacer city 0 x0.30 / x0.30 x1.75 x0 0.00229 -> 0.00018
    • Tires: the spec sheet's Pacejka B is 2.5-4x stiffer than Jolt's tires behave.

    • RoboRacer drive: its driver model tracks the commanded acceleration with an integral trim. So it has no drive lag, and it makes up for drag and rolling resistance.

    • Held out, at nav's 0.06 s tick: the fitted model's position rms against the spec sheet's and the kinematic bicycle's, in mm:

      • Humvee slalom, above 0.5 g: 3.0 / 6.4 / 55.2;
      • RoboRacer city, 0.3-0.5 g: 1.6 / 6.1 / 11.2.

      The RoboRacer's speed residual comes down to the kinematic bicycle's.

    • The Austin fits are not trustworthy: they land on the grids' edges, absorbing what the planar model leaves out on rough terrain.

    • Adopted in physics: fit dyn_bike_si_of to Jolt (tires, direct drive); P8b training now helps on Jolt #753 (merged): dyn_bike_si_of now gives Jolt-fitted values. See "After physics: fit dyn_bike_si_of to Jolt (tires, direct drive); P8b training now helps on Jolt #753" below.

    • Test: FitsTheDynamicModelToRunsOfAKnownVehicle checks that a run made by a vehicle off the spec sheet gives back every knob on its grid.

  • Austin routes. The "planner fails on ~60% of goals" was the goal draw, not the planner: drawing from the whole map lands 150-450 m away only ~6% of the time, and all 8 tries missed.

    • Goals are now drawn from that ring: 0 unplanned requests.
    • The planner keeps 2 m off building meshes.
    • An agent stopped for 5 s gets a new route from where it stands. The Humvees still wedge on bundle geometry the nav grid calls free (a curb or a building base) about every 16 s, so this recovers them rather than preventing it.
    • Moving time rose from ~45% to 54% (Humvee) and 97% (RoboRacer, seed 100).
  • RoboRacer chassis contacts. Its chassis box is centred on the axle line, 2-3 cm above the ground at rest, so at nav's full ±6 m/s² the pitch puts it on the ground. Contact ticks have a median longitudinal acceleration of -6.2 m/s².

    • --accel-margin / --brake-margin (nav_plant's own knobs, recorded in meta.json) at 0.4 take the city's contact ticks from 413 to 12, and the windows kept from 88 of 384 to 352 of 384.
    • The vehicle model itself is unchanged.
  • --verbose prints each agent every 5 s. --samples writes samples.csv. Samples now carry the roll.

  • Datasets regenerated:

    vehicle train flights test flights before the follow-ups
    Humvee 475 182 472 / 166
    RoboRacer 443 167 245 / 111

    Austin gives 83 Humvee and 67 RoboRacer flights.

  • Folded into cvc-cli (2026-10-20). The command line is now cvc plant-log. The standalone cvc-plant-log tool and its CVC_BUILD_PLANT_LOG option are removed, and its scene drivers and directory writer moved into the library as cvc/physics/plant_log_scene.h.

    • The three scenes run through cvc plant-log write byte-identical files to the old tool's (swerve with --fit-dyn --samples, city at 0.06 s with --steer command and the margins, Austin with --verbose --fit-dyn): flights.npy, meta.json, residual.json and samples.csv. Stdout is the same too, apart from the per-tick timing.
    • physics_plant_log_test 7/7, adding the options' defaults and a swerve driven twice to identical runs. cvc_cli_test adds four plant-log cases (56/56 of its help, dispatch and plant-log tests locally).
  • CI fixes.

  • After physics: fit dyn_bike_si_of to Jolt (tires, direct drive); P8b training now helps on Jolt #753 (master merged in; 7d95ba9). Every scene was rerun with dyn_bike_si_of's Jolt-fitted values.

    • Datasets: flights, meta.json and the four LieSPHGP pickles are byte-identical. Jolt drives the vehicles, and dyn_bike_si_of enters only the residuals.

    • Residuals at 0.06 s, position rms in mm, dynamic before physics: fit dyn_bike_si_of to Jolt (tires, direct drive); P8b training now helps on Jolt #753 / dynamic now / kinematic:

      lateral g Humvee slalom Humvee city RoboRacer slalom RoboRacer city
      0.0-0.1 2.0 / 1.2 / 3.9 2.0 / 1.6 / 3.4 0.9 / 0.3 / 0.7 1.9 / 1.3 / 1.9
      0.1-0.2 5.1 / 1.4 / 11.6 4.8 / 1.6 / 11.1 3.3 / 0.4 / 5.6 3.4 / 0.9 / 5.2
      0.2-0.3 7.1 / 1.3 / 17.9 7.0 / 2.0 / 18.1 5.0 / 0.6 / 10.7 4.8 / 1.0 / 8.6
      0.3-0.5 8.1 / 1.2 / 27.5 8.1 / 2.8 / 26.2 6.9 / 0.8 / 15.8 6.5 / 1.8 / 11.7
      >= 0.5 5.2 / 4.2 / 94.0 4.5 / 3.8 / 52.0 6.5 / 0.7 / 25.1 12.6 / 11.5 / 30.4

      Below 0.5 g the dynamic model is 1.3-9x closer than before. The RoboRacer's speed residual now matches the kinematic bicycle's there (0.001-0.035 m/s rms, was 0.018-0.090). What is left is the RoboRacer in the city above 0.5 g, and its yaw rate there.

    • Refits against physics: fit dyn_bike_si_of to Jolt (tires, direct drive); P8b training now helps on Jolt #753's values (50 Hz, fit on seeds 100-102, judged on 103):

      run drive lag tire B front / rear yaw inertia held-out (m/s)²: default -> fit
      Humvee slalom 0.20 s x1.45 / x1.20 x1.00 0.00046 -> 0.00049
      Humvee city 0.04 s x1.00 / x1.00 x1.00 0.00065 -> 0.00061
      RoboRacer slalom 0 x1.00 / x1.00 x0.85 0.00001 -> 0.00001
      RoboRacer city 0 x1.00 / x1.20 x1.75 0.00015 -> 0.00018

      Nothing is left to fit on flat ground: the defaults do as well as a refit on the held-out seed. The Austin fits still land on the grids' edges.

    • Roadmap: both tables are replaced with these. The datasets' rerun is /home/joe/src/cvc/plant-log-runs-2026-10-20/post753/ (local).

    • Tests: physics_plant_log_test 7/7 after the merge.

…s residual against Jolt

A port-Hamiltonian vehicle model fitted to the physics needs the physics' trajectories in a
plant-learning format, and a baseline to beat. This adds both.

- cvc::physics::plant_log (plant_log.h): samples a sim_world's agents on a nav_plant every tick
  as planar rigid bodies in SI: the centre of mass's pose on SE(2), its body twist, the
  steering, the command nav held over the tick (zero-order hold), the measured acceleration,
  contacts and wheels on the ground.
- one_tick_residuals: the plan's per-tick drift (D14) with diff::dyn_plant beside the kinematic
  bicycle. Both step from Jolt's state with the held command, binned by the lateral g Jolt
  measured. Ticks with a static contact, a wheel off the ground, an impact (> 1.5 g) or at
  rest are left out, and counted.
- cut_flights / write_npy: equal-length windows, [x, y, psi, vx, vy, r, drive, steer] float64.
- cvc-plant-log: drives the Humvee or RoboRacer on Jolt through nav in a slalom (swerves past
  0.3 g), the coef trainer's city, or a cvc::world city bundle (vehicle routes on its
  rasterized map, the vehicles spawned on its terrain), and writes flights.npy, meta.json and
  residual.json. src/cvc/tools/liesphgp_pack.py packs runs into the dataset pickle LieSPHGP's
  loaders read (the SE(3)-lifted 20-wide rows of its ROV loader with control_dim 2, or 8-wide
  SE(2)), with observation noise at a stated sigma.
- nav_plant_options::ground_height: a start's ground for a terrain that is not flat (empty:
  ground_z, as before).
- The roadmap's drift section publishes dyn_plant's residual at nav's 0.06 s tick: 4-18x closer
  than the kinematic bicycle above 0.5 g in position; worse in speed for the RoboRacer, whose
  driver model is faster than dyn_bike_si_of's 0.1 s drive lag.

physics_plant_log_test (4): a run a model made leaves that model no residual (kinematic and
dynamic, nsub 1/2/6); flights, their layout and the .npy; a Humvee slalom on Jolt (row k's
command is the one held over the tick, the centre of mass moves with its logged twist to
0.023 m/s, peak 0.87 g); ground_height spawns as ground_z does.
…unding (1e-9)

The run is made in the test and replayed in plant_log.cpp; a compiler may contract the two into
FMAs differently (aarch64), so compare to rounding rather than to 1e-12. Include <iterator>.
…ven; gentler margins

- fit_dyn_bike / dyn_bike_cost: dyn_bike_si_of's knobs a spec sheet does not give, fitted to
  logged runs by coordinate descent on the one-tick twist residual (drive and steering lags,
  factors on the tires' B, the yaw inertia, and drag + rolling resistance). dyn_bike_si_of is
  unchanged; this measures what the plant wants. one_tick_residuals takes a dyn_bike_si too.
  cvc-plant-log --fit-dyn fits on every seed but the last and reports the last.
- Bundle routes: the goal is drawn from the free cells 5-15% of the map away. Drawing from the
  whole map landed in that ring ~6% of the time, which was the "planner fails on ~60% of
  goals". The planner keeps 2 m off building meshes, and an agent stopped for 5 s is replanned
  from where it stands.
- --accel-margin / --brake-margin (nav_plant's), recorded in meta.json: at the full ±6 m/s^2 the
  RoboRacer's low chassis box pitches onto the ground under braking and acceleration.
- --verbose (each agent every 5 s) and --samples (samples.csv); samples carry the roll.

physics_plant_log_test: + FitsTheDynamicModelToRunsOfAKnownVehicle (a run made by a vehicle off
the spec sheet: every knob recovered on its grid, cost 0).
…itted to Jolt

The RoboRacer's city column is from runs planning with 0.4 of its acceleration and braking (its
chassis box scraped the ground at the full 6 m/s^2). The fit (fit_dyn_bike, held-out seed) puts
both vehicles' tires 2.5-4x softer than the spec sheet's B, and the RoboRacer with no drive lag and
no uncompensated resistance. dyn_bike_si_of's defaults are unchanged.
The scene drivers and the directory writer move out of the standalone
cvc-plant-log tool into cvc/physics/plant_log_scene.h (plant_log_options,
drive_plant_log_scene, write_plant_log), so a trainer can drive the scenes in
process. The command line becomes the `cvc plant-log` subcommand of cvc-cli,
and the tool and its CVC_BUILD_PLANT_LOG option are gone.

The three scenes run through `cvc plant-log` write files byte-identical to the
old tool's (flights.npy, meta.json, residual.json, samples.csv), and the same
stdout but for the timing. physics_plant_log_test adds the options' defaults
and a swerve driven twice to identical runs; cvc_cli_test covers the
subcommand.
… dyn_bike_si_of

The 0.06 s residual table now shows the dynamic model before #753 / with its
values now / the kinematic bicycle. Below 0.5 g the fitted values are 1.3-9x
closer, and the RoboRacer's speed residual matches the kinematic bicycle's.
Refitted against #753's values, the flat-ground fits find nothing left: tire
factors back at x1, and the defaults do as well as a fit on the held-out seed.
The datasets are byte-identical (Jolt drives; dyn_bike_si_of only enters the
residuals).
@transfix transfix closed this pull request by merging all changes into master in 17adc93 Oct 21, 2026
@transfix
transfix deleted the feat/plant-log-liesphgp branch October 29, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant