Skip to content

nav: dyn_4w is port-Hamiltonian at every slip, with per-step energy books - #850

Merged
transfix merged 4 commits into
masterfrom
feat/dyn-4w-ph
Oct 28, 2026
Merged

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

Conversation

@transfix

@transfix transfix commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

The four-wheel plant (inc/cvc/nav/detail/dyn_4w.h, P8d) now has an explicit port-Hamiltonian form and per-step energy books. It follows the single track's form from #831 (dyn_ph_*, dyn_plant_books).

The form

In x = (q, p), with q = (z, φ, θ) and p = M ξ for the twist ξ = (vx, vy, r, ż, φ̇, θ̇), dyn4_plant's vector field is exactly:

H  = ½ ξᵀ M ξ + Σᵢ ½ kᵢ lᵢ² + m g z          lᵢ = Fz0ᵢ/kᵢ + hgᵢ − cᵢ·q
x' = (J(p) − R) ∂H/∂x + G u + f_lever         u = (F_x1 … F_x4),  y = Gᵀ ∂H/∂x
J  = [[0, 0, I], [0, J_h(p_h), 0], [−I, 0, 0]]
R  = blockdiag(0, c_x e_x e_xᵀ + Σᵢ Sᵢ nᵢ nᵢᵀ, Σᵢ c'ᵢ cᵢ cᵢᵀ)
  • No speed-floor term (f_ε = 0). Each slip is measured in its wheel's frame and rooted at wᵢ = nᵢ·v = 0. The exact secant Sᵢ = −F_yᵢ / wᵢ (dyn4_ph_secant) therefore carries the whole lateral force. Sᵢ ≥ 0 at any slip and any steering angle, so R is positive semi-definite whether the tires slide or not.
  • H holds the corners' springs and gravity on heave. R's vertical block holds the dampers.
  • G's columns are the wheels' drive and brake forces at their contact patches, h below the centre of gravity. So yᵢ includes the roll and pitch rates, and yᵀu is all the power those forces deliver. Steering only turns nᵢ and G's columns, so it does no work.
  • f_lever is the one term that differs from the single track. It is the moment the tires' lateral forces exert on roll and pitch (h·FY). The plant's slips never see the body's roll and pitch rates, so this coupling runs one way only: it can't go in J or in R. It gets its own line in the books, and it is zero when h = 0.
  • Energy rate: dH/dt = −∂Hᵀ R ∂H + yᵀu + ξᵀ f_lever, plus the ground lifting the springs as the wheels roll over it (zero on flat ground).

The grade force (opt-in)

dyn_4w has no gravity along the ground. The terrain only lifts the springs, so a vehicle climbs a hill for free, and the books show that energy as the ground line, coming from outside.

dyn4_params::grade_force (and dyn4_si::grade_force) adds it. It is off by default, so the plant, its goldens and GRL-SNAM's twin are unchanged.

  • dyn4_grade applies each spring's horizontal reaction on the slope under its wheel, −Fᵢ ∇h(Pᵢ), as a force and yaw moment on (vx, vy, r).
  • That is minus the gradient of H's springs in the pose, so it's conservative. With the pose in the state it belongs to J, and the terrain's energy is in H, as the paper describes.
  • The slope comes from central differences of the ground sampler over 1e-3 L, so any sampler works.
  • The pH form gains f_grade (dyn4_ph_grade), and the rates and books gain a grade line. Its power cancels the ground line's.

The books

dyn4_plant takes an optional dyn_step_free*, as dyn_plant does. dyn4_plant_books returns each step's books: dH = supplied − dissipated + lever + grade + ground + limiter + residual.

  • The ground line is exact: the springs' energy at the step's end, on the new ground minus on the ground the step stood on.

The plant's outputs and VJP are unchanged. On the Windows build, dyn4_golden.json and dyn4_ext_rollout_golden.json regenerate byte-for-byte the same from this branch as from master. GRL-SNAM's test_dyn4_parity.py fixtures stay valid.

Gates (nav_dyn_4w_test, Dyn4PH.*)

  • Structure. 500 states on each of: flat ground (μ 0.9 and 0.6) and the golden's split-and-bump ground, sprung and rigid. 267 of the 500 have a tire past its peak.
    • J is skew.
    • R is symmetric, with λ_min ≥ −1e-9‖R‖.
    • ∂H/∂x matches H's central differences to 6e-8.
    • Nothing is supplied when the wheels' forces are zero.
  • Vector field. A 1e-5 s dyn4_plant step against dyn4_ph_rhs, forward and reversing, on flat and split-and-bump ground. Worst error: 2.0e-4 of each row's scale.
  • Step books. 8 s of a 0.2 Hz weave to near full lock (the steering slews at 0.35 rad/s), with 27% of steps having a tire past its peak (slips reach 0.68 rad; the peak is at 0.345).
    • The per-step residual falls 3.85–3.98× per halving of hdt, on flat ground and over hills.
    • At hdt 0.01, |Σ residual| / flow is 5.0e-4 on flat ground and 1.0e-3 over hills.
    • Braking to a stop charges the speed clamp and the low-speed blend.
    • The ground line is zero on flat ground; the lever line is zero with h = 0.
  • The grade force is −∂H/∂pose. Checked against H's central differences in (x, y, ψ): within 3.6e-7 m·g on the golden's bump, and 2.1e-8 m·g over hills. The vector-field gate also passes with it on.
  • Grade force on versus off, over hills, swerving past the peak.
    • Off: the ground line is +2868 J at hdt 0.01.
    • On: ground is +3373 J and grade −3372 J.
    • |ground + grade| per step falls 4.0× per halving of hdt (second order).
  • Coasting up a 5% ramp for 2 s from 10 m/s, with no drag and the dampers nearly off:
    • Off: the speed holds, and H gains m·g·climb, all of it from the ground line.
    • On: the coast loses 0.986 m/s against g·s·t = 0.981, and H holds to within 0.8% of the climb at hdt 0.0025.
    • The remaining error is the step holding the ground at its start. It is first order: 4× smaller than at hdt 0.01.

Two findings about the plant, left for a separate decision (not changed here):

  • The lever term is one-way (above). Making it reciprocal would change the plant and its goldens.
  • The dampers are skyhook dampers. They act on the body's own vertical rate, not on its rate relative to the ground. With them, a steady climb also pays c·ż² per corner (about 11 kJ on that ramp); the books show it as dissipation.

Run locally on Windows (MSVC, Release): nav_dyn_4w_test 21/21 (including Dyn4.Golden and Dyn4ExtRollout.Golden) and nav_dyn_bike_test 21/21.

…ooks

The four-wheel plant (P8d) gets the single track's explicit pH form (#831).
In x = (q, p), with q = (z, phi, th) and p = M xi for the twist
xi = (vx, vy, r, zd, phid, thd), its vector field is exactly

  H  = 1/2 xi^T M xi + sum_i 1/2 k_i l_i^2 + m g z
  x' = (J(p) - R) dH/dx + G u + f_lever,  u = (F_x1..F_x4),  y = G^T dH/dx
  J  = [[0, 0, I], [0, J_h(p_h), 0], [-I, 0, 0]]
  R  = blockdiag(0, c_x e_x e_x^T + sum_i S_i n_i n_i^T, sum_i c'_i c_i c_i^T)

- Each slip is in its wheel's frame, rooted at w_i = n_i . v = 0, so the
  secant S_i = -F_yi / w_i carries the whole lateral force: f_eps = 0.
  S_i >= 0 at any slip and any steering angle, so R is PSD sliding or not.
- H carries the corners' springs and gravity on heave; R_v the dampers.
- G's columns are the wheels' drive and brake forces at their contact
  patches, h below the centre of gravity, so y_i includes the roll and
  pitch rates and y^T u is the forces' whole power. Steering only turns
  n_i and G's columns, so it does no work.
- f_lever: the tires' lateral forces' moments on roll and pitch (h FY).
  The slips do not see the body's roll and pitch rates, so this coupling
  is one-way, in neither J nor R. It is booked on its own line and is zero
  with h = 0.
- dH/dt = -dH/dx^T R dH/dx + y^T u + xi^T f_lever, plus the ground lifting
  the springs as the wheels roll over it (none on flat ground).

dyn4_plant takes an optional dyn_step_free*, as dyn_plant does, and
dyn4_plant_books books each step: dH = supplied - dissipated + lever +
ground + limiter + residual. The plant's outputs and VJP are unchanged: on
the Windows build, dyn4_golden.json and dyn4_ext_rollout_golden.json
regenerate byte-for-byte the same from this branch as from master.

Gates (nav_dyn_4w_test, Dyn4PH):
- the structure at 500 states on each of flat ground (mu 0.9, 0.6) and
  the golden's split-and-bump ground, sprung and rigid, 267 of them with a
  tire past its peak: J skew; R symmetric with lambda_min >= -1e-9 |R|;
  dH/dx against H's central differences (6e-8); nothing supplied with the
  wheels' forces at zero;
- the vector field against a 1e-5 s dyn4_plant step, forward and
  reversing, flat and split-and-bump: worst 2.0e-4 of each row's scale;
- the step books over 8 s of a 0.2 Hz weave to near full lock, 27% of the
  steps with a tire past its peak (slips to 0.68 rad; the peak is at
  0.345): the per-step residual falls 3.85-3.98x per halving of hdt, flat
  and over hills, and |sum residual| / flow at 0.01 s is 5.0e-4 flat and
  1.0e-3 over hills. Braking to a stop charges the clamp and the blend; the
  ground line is zero on flat ground and the lever line zero with h = 0.
dyn_4w has no gravity along the ground: the terrain only lifts the springs,
so a vehicle climbs a hill for free and the books show that energy as the
ground line, from outside. dyn4_params::grade_force (dyn4_si::grade_force),
off by default, adds it:

- dyn4_grade: each spring's horizontal reaction on the slope under its
  wheel, -F_i grad h(P_i), F_i the linear spring force, as a force and yaw
  moment on (vx, vy, r), from the step's start. That is minus the gradient
  of H's springs in the pose, so it is conservative: with the pose in the
  state it belongs to J, and the terrain's energy is in H. The slope is the
  ground's by central differences over 1e-3 L, so any sampler serves.
- The pH form gains f_grade (dyn4_ph_grade), the rates and the books a
  grade line. Its power cancels the ground line's.

Off, nothing changes: dyn4_golden.json and dyn4_ext_rollout_golden.json
still regenerate byte-for-byte the same as master's on the Windows build.

Gates (Dyn4PH):
- the grade force against H's central differences in (x, y, psi): within
  3.6e-7 m g on the golden's bump, 2.1e-8 m g over hills;
- the vector field with it on: worst 2.0e-4 of each row's scale;
- over hills, swerving past the peak: without it the ground line is
  +2868 J at 0.01 s; with it ground +3373 J and grade -3372 J, and
  |ground + grade| per step falls 4.0x per halving of hdt (second order);
- coasting up a 5% ramp for 2 s from 10 m/s (no drag, dampers nearly off):
  without it the speed holds and H gains m g times the climb, all from the
  ground line; with it the coast loses 0.986 m/s against g s t = 0.981 and
  H holds to 0.8% of the climb at 0.0025 s (the step holds the ground at its
  start: first order, 4x smaller than at 0.01 s).

The plant's dampers act on the body's own vertical rate, not on its rate
relative to the ground, so with them a steady climb also pays c zd^2 per
corner (about 11 kJ on that ramp); the books show it as dissipation.
@transfix
transfix merged commit 45dab15 into master Oct 28, 2026
23 of 24 checks passed
transfix added a commit that referenced this pull request Oct 28, 2026
The project version and the cvcpkg recipe family move to 3.7.2. Every recipe is at cvc_revision 1
already, so none restarts.

3.7.2 carries the dynamic plants on the terrain and in a Jolt world, and the four-wheel plant
(#869): --plant dynamic and dynamic4w drive the ground, collide with the
buildings and each other, and book the collisions. It also carries dyn_4w's port-Hamiltonian form
(#850), dyn_bike's wheel-frame front slip (#852), the books goldens for GRL-SNAM's twins (#848,
#849), pycvc's drive telemetry (#857) and keep-alives (#864), the macOS SDK's openssl (#846), and
the state_exec scheduler and evaluator work (#842, #843, #844, #861, #863).
@transfix
transfix deleted the feat/dyn-4w-ph 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