Skip to content

nav: dyn4_plant, the differentiable two-track vehicle with suspension (P8d) - #731

Merged
transfix merged 8 commits into
masterfrom
feat/dyn-4w
Oct 21, 2026
Merged

transfix merged 8 commits into
masterfrom
feat/dyn-4w

Conversation

@transfix

@transfix transfix commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

P8d from the roadmap's P8 plan (item 5): the differentiable four-wheel vehicle, so the controller can train on a body that rolls, pitches and loads its wheels unevenly, as Jolt's do.

What it is

diff::dyn4_plant (inc/cvc/nav/detail/dyn_4w.h) is P8a's dyn_plant on four contact patches. The state has 14 entries: position, yaw, (vx, vy, r), heave, roll and pitch with their rates, and the steer and drive lags.

  • Wheels. They sit at (lf, ±tf) and (−lr, ±tr). The front pair steers at the Ackermann angles of the single-track model's virtual centre wheel.
  • Ground. A templated ground sampler gives each wheel its friction and terrain height. The scalar type is templated too, so the terrain's gradient flows through the duals. flat_ground is the default.
  • Load. Each corner's spring-damper sets its tire's load, kept positive by a softplus floor.
  • Tires and solve. Each tire follows Pacejka inside the friction ellipse. (vy, r) come from P8a's implicit secant solve over four tires. Heave, roll and pitch take an implicit 3×3 step on the corners' springs and dampers, driven by the ground and the tire forces' moments.
  • Gradients. The VJP uses forward-mode duals (dyn4_plant_vjp). dyn4_bike_step and dyn4_bike_step_bwd put bike_control in front of the plant.

Parameters come from the spec sheet:

  • nav_plant::dyn4_si_of(desc) takes P8a's numbers and adds the half tracks, the chassis box's roll and pitch inertias, and each corner's spring and damper from its axle's suspension frequency and damping ratio.
  • dyn4_params_of and dyn4_params_at scale these to the scene's frame.

Gates (nav_dyn_4w_test, the Humvee; with #753's Jolt-fitted tires since 8ef8641)

test result
RestsOnFlatGround settles to the static load split, no drift
SingleTrackLimitIsDynPlant zero track and stiff suspension stay within 0.130 m of dyn_plant over a 93 m drive (0.053 m over 95 m before #753)
SteadyLoadTransferBalances the roll and braking load transfers balance exactly
MuSplitBrakingYawsTowardHighMu the car yaws toward the grippy side
ABumpLoadsItsWheel the bumped wheel's load rises 1.37×, the opposite wheel's 1.11×
PlantGradientMatchesFiniteDifference VJP against central differences: 2.6e-9
StepGradcheckThroughTheController one controller step: 4.0e-5 (starts jittered off the SDF grid nodes)
EnvelopesOnFourWheels the body's lateral acceleration (vy′ + r vx) ≤ 0.94 μg; stopping within 8% of v²/(2μg)
MatchesJoltsHumveeEnvelopes skidpad 6.30 vs Jolt's 6.85 m/s² (−8%; −4% before #753); stopping 24.10 vs 25.90 m (−7%)
TheFrontWheelsFollowAckermann Jolt's P2 gate, cot(right) − cot(left) = track / wheelbase, exact; held at δ at 3 m/s with the low-speed blend off, the rear axle's circle is L / tan δ −0.03 / +0.18 / +0.62% at 0.15 / 0.30 / 0.45 rad
Golden tests/data/dyn4_golden.json: the sprung and rigid Humvee on a μ split with a bump, 11 cases of next state and VJP, reproduced to 1e-9

The controller gradcheck started off the SDF grid nodes because the scene's sampled starts lie on them. Central differences at a node straddle bilinear kinks: unjittered, the same check reads 5e-2. P8b's dynamic gradcheck may have the same artifact. That is a follow-up.

Docs

NAV_TRAINING.md has a new section, "The four-wheel vehicle: dyn4_plant (P8d)".

Verified locally

Windows build from the cvcpkg prefix: all 372 nav, physics and plant ctest cases pass, and the 12 dyn4 tests pass after the follow-up commit. clang-format 18 is clean on the changed files.

The torch twin

CVC-Lab/GRL-SNAM#123 (grl_snam/dyn4.py) is held to the golden. Its worst relative error is 4.2e-17 on the next state and 1.7e-15 on the VJP. CVC-Lab/GRL-SNAM#135 carries the regenerated golden; merge it together with this PR.

Not in this PR

Aligned with #753 (8ef8641)

#753 (merged) fitted dyn_bike_si_of's tires to Jolt: B·C = 1.12 / α_peak. It also made its defaults front_stiffness 1 and drive_lag -1 (by drivetrain).

dyn4_si_of still passed the old 0.85 and 0.1. Merged with master, 4 of 12 dyn4 tests failed: the four-wheel Humvee's front B was 9.10 against the single track's 10.71.

The gates above are the new numbers. The softer tires take the skidpad further from Jolt's (6.30 against 6.85 m/s², still within the 10% gate). Under μ-split braking it yaws more: −0.89 rad in 1.8 s, against −0.16 before.

Master is merged in (362b3b6). Locally (Linux, Jolt 5.6.0+cvc.6): nav_dyn_4w_test 12/12.

… (P8d)

diff::dyn4_plant (inc/cvc/nav/detail/dyn_4w.h) is P8a's vehicle on four contact patches:
- the wheels at (lf, +-tf) and (-lr, +-tr), the front pair at the Ackermann angles of the
  virtual centre wheel;
- each wheel's friction and terrain height from a ground sampler, templated on the scalar so
  the terrain's gradient flows through the duals;
- each corner's spring-damper as its tire's load, softplus-floored;
- Pacejka per tire within the friction ellipse, (vy, r) by P8a's implicit secant solve on four
  tires, and (z, roll, pitch) by an implicit 3x3 step on the corners' springs and dampers,
  driven by the ground and the tire forces' moments.
The VJP is forward-mode duals (dyn4_plant_vjp); dyn4_bike_step / _bwd put bike_control in front.

Parameters from the spec sheet: nav_plant::dyn4_si_of (P8a's numbers, the axles' half tracks,
the chassis box's roll and pitch inertias, each corner's spring and damper from its axle's
suspension frequency and damping ratio), dyn4_params_of and dyn4_params_at (inc/cvc/nav/dyn_4w.h).

nav_dyn_4w_test (the Humvee): rests on flat ground; the single-track limit within 0.053 m of
dyn_plant over 95 m; steady roll and braking load transfer exactly balanced; mu-split braking
yaws toward the high-mu side; a bump loads its own wheel (1.37x against 1.11x); the VJP against
central differences 3.7e-9; one step under the controller 6.2e-5; lateral acceleration <= 0.97
mu g and stopping within 8% of v^2/(2 mu g); within 4% and 7% of Jolt's Humvee skidpad and
stopping. Documented in NAV_TRAINING.md. 372/372 nav/physics/plant ctest cases locally.
dyn4_ackermann(L, y, tan delta) is the front wheel's angle the plant uses. TheFrontWheelsFollowAckermann
is Jolt's P2 gate (cot(right) - cot(left) = track / wheelbase, exact) plus the turn it gives: held at
delta at 3 m/s with the low-speed blend off, the rear axle's circle is L / tan(delta) within
+0.10 / +0.17 / +0.32% at 0.15 / 0.30 / 0.45 rad.

Dyn4.Golden writes tests/data/dyn4_golden.json (CVC_DYN4_GOLDEN_WRITE=1): the sprung and the rigid
Humvee on a mu split with a bump, eleven cases' next states and VJPs, which this build reproduces to
1e-9 and GRL-SNAM's torch twin is held to.
transfix added a commit to CVC-Lab/GRL-SNAM that referenced this pull request Oct 19, 2026
…y against its golden (#123)

grl_snam.dyn4.dyn4_plant is libcvc's diff::dyn4_plant (P8d, transfix/libcvc#731): four
contact patches with Ackermann front wheels, a ground callable (wx, wy) -> (mu, height) per
wheel, spring-damper corners floored by softplus, four tires in the implicit (vy, r) solve, and
heave, roll and pitch by an implicit 3x3 step. The corners' rates follow libcvc's float arithmetic.

tests/test_dyn4_parity.py holds it to libcvc's dyn4_golden.json (Dyn4.Golden, libcvc@368c0e4a):
the sprung and rigid Humvee on a mu split with a bump, eleven cases. Measured worst relative
error: next state 4.2e-17, VJP 1.7e-15 (the gate is 1e-9). Also a batch, the Ackermann relation,
and stopping from 20 m/s within 10% of v^2 / (2 mu g).
The comments and docs describe ext_force as a hook for a caller's own force, without naming a downstream user.
@transfix

Copy link
Copy Markdown
Owner Author

Heads-up: #753 (merged, 2028671) changes what this PR builds on.

dyn4_si_of builds on dyn_bike_si_of(d, 0.9, front_stiffness, drive_lag, g), and #753 changed dyn_bike_si_of in two ways:

  • Tires: B is now fitted to Jolt, B·C = 1.12 / tire_peak_slip_angle.
  • Defaults: front_stiffness 1.0 (was 0.85), and drive_lag -1 meaning by drivetrain: 0 for a direct drive, 0.1 s otherwise.

This branch still merges with master without conflicts, but merged, nav_dyn_4w_test fails 4 of 12 (Linux, Jolt 5.6.0+cvc.6):

  • HumveeParametersFromItsSpecSheet: p4.B_f 9.10 vs p1.B_f 10.71. dyn4_si_of passes 0.85 explicitly, while dyn_bike_si_of now defaults to 1.0.
  • SingleTrackLimitIsDynPlant: positions 5.08 m apart over 93 m (limit 0.93), from the same mismatch.
  • TheFrontWheelsFollowAckermann: at δ 0.45 the circle is 1.14% wide (limit 1%). The softer tires slip more.
  • Golden: dyn4_golden.json was made with the old tires.

Changing dyn4_si_of's defaults to front_stiffness = 1.0, drive_lag = -1.0 (following dyn_bike_si_of) fixed the first three in a local trial merge:

  • single-track limit within 0.130 m;
  • Ackermann +0.62% at δ 0.45.

What remains is to regenerate the golden after merging master (CVC_DYN4_GOLDEN_WRITE=1). The trial merge was local only and nothing was pushed to this branch.

… golden regenerated

#753 fitted dyn_bike_si_of's tires to Jolt and made its defaults front_stiffness
1 and drive_lag by drivetrain. dyn4_si_of still passed the old 0.85 and 0.1, so
the four-wheel Humvee's front B was 0.85x the single track's and four dyn4 tests
failed. Its defaults are now 1.0 and -1.0, and dyn4_golden.json is regenerated
with the new tires.

The envelope test measures the body's lateral acceleration (vy' + r vx), as
DynBike.SteadyLateralNeverExceedsMuG does since #753: on the softer tires r vx
alone read 1.03 mu g at mu 0.4. NAV_TRAINING.md's P8a parameters (stale since
#753) and dyn4's gate numbers are updated. nav_dyn_4w_test 12/12.
# Conflicts:
#	src/cvc/tests/CMakeLists.txt
@transfix
transfix enabled auto-merge (squash) October 21, 2026 18:41
transfix added a commit to CVC-Lab/GRL-SNAM that referenced this pull request Oct 21, 2026
Copies libcvc's regenerated tests/fixtures/dyn4_golden.json (transfix/libcvc#731 at 8ef864171: dyn4_si_of on #753's Jolt-fitted dyn_bike_si_of defaults). grl_snam/dyn4.py reads the vehicle from the file, so no code changes; test_dyn4_parity and test_dyn_bike_parity pass.
# Conflicts:
#	src/cvc/tests/CMakeLists.txt
@transfix
transfix merged commit 2e02c3e into master Oct 21, 2026
24 checks passed
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