Skip to content

Torch twin of the single-track pH form and its books; dyn_bike's front slip in its wheel's frame - #138

Merged
transfix merged 2 commits into
mainfrom
feat/dyn-bike-ph-twin
Oct 28, 2026
Merged

transfix merged 2 commits into
mainfrom
feat/dyn-bike-ph-twin

Conversation

@transfix

@transfix transfix commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Two commits, for Joe to review before merging:

  1. a torch twin of libcvc's single-track vehicle in port-Hamiltonian form, with its per-step energy books;
  2. dyn_bike's front slip moved into its wheel's frame, following libcvc.

They sit beside dyn_plant in grl_snam/dyn_bike.py. The whitepaper (#137, docs/vehicle-ph-plant.tex §8.2–8.3 and §10) lists the twin as missing.

1. The pH twin (held to libcvc's DynBikePH.Golden, transfix/libcvc#848). New in grl_snam/dyn_bike.py, mirroring libcvc's dyn_ph_* in cvc/nav/detail/dyn_bike.h:

  • dyn_ph_parts_at: everything the form is built from at a state:
    • the axle forces (the port);
    • the tires' lateral forces and secant stiffnesses;
    • u, sg, uf and Dx;
    • the vectors n_f, n_r, t_f, t_r.
  • dyn_ph_rates_at: the power balance dH/dt = supplied − dissipated + floor + grade.
  • dyn_ph_hamiltonian: ½ vᵀMv.
  • dyn_plant_books: dyn_plant's step, unchanged, plus its books:
    • the trapezoid of the rates between the step's start (with the actuators' outputs δ₁, a₁) and the twist the step reached before its limiters;
    • limiter: dH across the speed clamp and the low-speed blend;
    • residual: the rest.
  • dyn_plant(..., with_free=True) also returns the pre-limiter twist (libcvc's dyn_step_free).
  • dyn_plant's step 2 moves into _axles, which the parts share.

tests/test_dyn_bike_ph_parity.py checks the twin against dyn_bike_ph_golden.json, double tier, 1e-9 relative, on eight Humvee cases. Each case checks:

  • the parts and the rates;
  • one step's next state, pre-limiter twist and books;
  • the autograd gradient of a weighting of the books against libcvc's forward-mode duals.

The file also tests that a batch books like its rows. A torch-only gate checks that the books close to second order on a swerve.

2. The wheel-frame front slip (follows transfix/libcvc#852, approved by Joe). The front slip becomes the wheel-frame slip dyn4's tires use:

  • alpha_f = −atan2(w_f, u_f);
  • w_f = cos δ (vy + lf r) − sin δ vx, which is n_f·v;
  • u_f is the floored forward speed in the wheel's frame;
  • the secant is rooted at w_f = 0.

The implicit step's Jacobian takes the front tire's −S_f cos² δ (1, lf) on the body's y. In the pH twin, R's front term becomes S_f n_f n_fᵀ, and f_ε is round-off. Three fixtures are regenerated from libcvc#852:

  • dyn_bike_golden.json;
  • dyn_bike_ph_golden.json;
  • material_ext_golden.json: only its dynamic case changes; the surrogate and bicycle cases are byte-identical.

Measured (torch 2.8, CPU, after both commits):

  • Worst relative difference from libcvc:

    Values Worst relative difference
    Plant next state 0
    Plant gradient 2.8e-16
    pH parts 3.4e-16
    pH rates 3.4e-16
    Books 7.1e-14
    Books gradient 4.4e-14
  • On the golden's cases, the floor power is at most 6e-13 W.

  • On the swerve, the mean |residual| per step falls 3.94× and then 3.97× per halving of the step.

  • test_dyn_bike_parity, test_dyn_bike_ph_parity, test_material_ext_parity, test_dyn4_parity and test_material_bike_parity give 57 passed and 6 skipped. The skips are live-pycvc checks that this machine's pycvc lacks.

  • ruff and black are clean on the changed files.

pycvc has no dynamic binding, so none of this depends on a pycvc release.

…p books

grl_snam/dyn_bike.py gains the twins of libcvc's dyn_ph_* (cvc/nav/detail/
dyn_bike.h), beside dyn_plant:
- dyn_ph_parts_at: everything the form is built from at a state (the axle
  forces, the tires' lateral forces, the secant stiffnesses, u, sg, Dx, and
  the vectors n_f, n_r, t_f, t_r);
- dyn_ph_rates_at: the power balance dH/dt = supplied - dissipated + floor +
  grade, from R (the tires' secants, drag, rolling resistance), G, u and
  f_eps;
- dyn_ph_hamiltonian: 1/2 v^T M v;
- dyn_plant_books: dyn_plant's step, unchanged, and its books: the trapezoid
  of the rates between the step's start (with the actuators' outputs) and
  the twist it reached before its limiters; limiter, dH across the speed
  clamp and the low-speed blend; residual, the rest.
dyn_plant takes with_free=True to also return that pre-limiter twist
(libcvc's dyn_step_free); its step 2 moves into _axles, shared with the
parts, with its arithmetic unchanged.

tests/test_dyn_bike_ph_parity.py holds them to libcvc's
dyn_bike_ph_golden.json (copied to tests/fixtures), double tier, 1e-9
relative: the parts, the rates, one step's next state, pre-limiter twist
and books, and the autograd gradient of a weighting of the books against
libcvc's forward-mode duals, on eight Humvee cases. A batch must match its
rows, and a torch-only gate checks that the books close to second order on
a swerve.

Measured (torch 2.8 CPU): worst relative difference from libcvc 3.4e-16 on
the parts, 5.3e-16 on the rates, 3.5e-14 on the books, 8.5e-14 on the
gradients. On the swerve, the mean |residual| per step falls 3.94x and
3.97x per halving of the step (0.01 -> 0.005 -> 0.0025 s).
Follows transfix/libcvc#852: dyn_plant's front slip becomes the wheel-frame
slip dyn4's tires use, alpha_f = -atan2(w_f, u_f) with w_f = cos(delta)
(vy + lf r) - sin(delta) vx (n_f.v) and u_f the floored forward speed in
the wheel's frame, its secant rooted at w_f = 0. The implicit step's
Jacobian takes the front's -S_f cos^2(delta) (1, lf) on the body's y.

In the pH twin, R's front term is S_f n_f n_f^T, f_eps is round-off, and
DynPHParts gains uf and loses w0f.

The three fixtures are regenerated by libcvc#852: dyn_bike_golden.json,
dyn_bike_ph_golden.json, and material_ext_golden.json (its dynamic case;
the surrogate and bicycle cases are byte-identical).

Measured (torch 2.8 CPU), worst relative difference from libcvc: plant 0
(next state) and 2.8e-16 (gradient); pH parts 3.4e-16, rates 3.4e-16,
books 7.1e-14, books gradient 4.4e-14. The floor power is at most 6e-13 W
on the golden's cases. The swerve's residual still falls 3.94x / 3.97x per
halving of the step.
@transfix transfix changed the title Torch twin of the single-track port-Hamiltonian form and its step books Torch twin of the single-track pH form and its books; dyn_bike's front slip in its wheel's frame Oct 28, 2026
transfix added a commit that referenced this pull request Oct 28, 2026
…852), and the pH twin (#138)

The plant's front slip is now measured in its wheel's frame, as the
four-wheel model's: alpha_w = -atan2(n_w.v, sqrt((t_w.v)^2 + eps^2)), each
rooted at n_w.v = 0 (transfix/libcvc#852). The paper follows:
- Section 3: the directions come first, then the slips in the wheels'
  frames; the old front slip is noted.
- Lemma 1 holds at every steering angle (the secant through w = 0, slope
  P C B u_w / (u_w^2 + w^2) at the root); f_eps is the root branch's
  rounding only.
- Section 4.2 and Proposition 1: R's front term is S_f n_f n_f^T, and the
  vehicle is passive from the drive port alone at every floor.
- Section 4.4: S_f ~ C_f / u_f; the front tire's power is exactly its
  dissipation but for the root branch's rounding.
- Section 4.5: the floor reaches only the slips' forward speeds;
  TheFloorTermIsRoundOff measures at most 2.2e-16 of the flow; the old
  remainder and its eps^2 numbers are kept as history.
- Section 4.7: k_f is the wheel-frame slip's exact sensitivity (eq. kf),
  checked by the new TheTangentIsTheTiresDerivative (1.4e-7).
- Section 5.2: braking limiter -358 J, residual 4e-6.
- Section 10: the four-wheel form now carries the single track's over.
- Tables: the plant table gains the renamed floor test and the tangent
  test, with #852's numbers; the live plant's rows move to their own table
  (the plant table no longer fit a page); the twins table and the GRL-SNAM
  validation rows gain #138's pH twin (test_dyn_bike_ph_parity, 18 passed,
  worst 7.1e-14).
- Section 8.2-8.3, Python access and the equation-to-code map: the pH
  form's parts, rates and books are twinned in GRL-SNAM #138.
- Appendix A: #852's and #138's branch commits, and the printed lines at
  1a0acc77 with the c1f104c0 values they replace.
- Versions: #852.

Built with TeX Live pdflatex, three passes: 27 pages, no warnings.
@transfix
transfix merged commit 2f76ab0 into main Oct 28, 2026
5 checks passed
transfix added a commit that referenced this pull request Oct 28, 2026
The #138 rows, the reproduction appendix and the versions table now cite main at 2f76ab0, where the dynamic twins' parity tests pass again (57 passed, 6 skipped for pycvc). The PDF is rebuilt: 27 pages, no warnings.
transfix added a commit that referenced this pull request Oct 28, 2026
… review fixes, links (#140)

* docs(vehicle-ph-plant): reproduce appendix, steering as a modulation, review fixes, links

- Appendix A: the exact setup behind every measured value (libcvc c1f104c0,
  MSVC 19.44, CMake 3.31.7, Ninja 1.12.1, cvcpkg 2.5.6, torch 2.8.0+cpu);
  the Windows commands that were run, with the extra CMake flags and why;
  the Linux/macOS equivalents (not run verbatim here; libcvc's CI runs the
  suites on every platform); each test's printed lines; GRL-SNAM's pytest
  line and its expected 53 passed, 2 skipped.
- Section 6.6 rewritten for the learned plant's steering: the reference
  form books power through steering (13.4% of |y.u| on the golden fit;
  5.6% on a fit to the analytic plant, which books none). The opt-in
  modulated form (transfix/libcvc#851) has D(v, delta) and G(v, delta), with
  the drive alone as the port. A new validation table holds its tests.
- The learned plant's provenance corrected: public CVC-Lab/LieSPHGP has no
  SE(2) vehicle; the port and its golden follow an SE(2) package built on it
  at 65db994. LieSPHGP's own trainers use the EKF ELBO.
- Review fixes:
  - the dynamic plant is 3.7.0 (#714), the learned plant 3.7.1 (#758);
  - psi for the kinematic heading (theta is the grade);
  - a "mixed" parity tier;
  - the sharp check's wording and its 3.5e-5;
  - the health-check range;
  - test totals, the CTest regex and NavPotential.DriveForce*;
  - the GRL-SNAM paper's full title.
- Links from README.md and the guide's "See also".

pdflatex: 26 pages, three passes, no warnings.

* docs(vehicle-ph-plant): the front slip in its wheel's frame (libcvc #852), and the pH twin (#138)

The plant's front slip is now measured in its wheel's frame, as the
four-wheel model's: alpha_w = -atan2(n_w.v, sqrt((t_w.v)^2 + eps^2)), each
rooted at n_w.v = 0 (transfix/libcvc#852). The paper follows:
- Section 3: the directions come first, then the slips in the wheels'
  frames; the old front slip is noted.
- Lemma 1 holds at every steering angle (the secant through w = 0, slope
  P C B u_w / (u_w^2 + w^2) at the root); f_eps is the root branch's
  rounding only.
- Section 4.2 and Proposition 1: R's front term is S_f n_f n_f^T, and the
  vehicle is passive from the drive port alone at every floor.
- Section 4.4: S_f ~ C_f / u_f; the front tire's power is exactly its
  dissipation but for the root branch's rounding.
- Section 4.5: the floor reaches only the slips' forward speeds;
  TheFloorTermIsRoundOff measures at most 2.2e-16 of the flow; the old
  remainder and its eps^2 numbers are kept as history.
- Section 4.7: k_f is the wheel-frame slip's exact sensitivity (eq. kf),
  checked by the new TheTangentIsTheTiresDerivative (1.4e-7).
- Section 5.2: braking limiter -358 J, residual 4e-6.
- Section 10: the four-wheel form now carries the single track's over.
- Tables: the plant table gains the renamed floor test and the tangent
  test, with #852's numbers; the live plant's rows move to their own table
  (the plant table no longer fit a page); the twins table and the GRL-SNAM
  validation rows gain #138's pH twin (test_dyn_bike_ph_parity, 18 passed,
  worst 7.1e-14).
- Section 8.2-8.3, Python access and the equation-to-code map: the pH
  form's parts, rates and books are twinned in GRL-SNAM #138.
- Appendix A: #852's and #138's branch commits, and the printed lines at
  1a0acc77 with the c1f104c0 values they replace.
- Versions: #852.

Built with TeX Live pdflatex, three passes: 27 pages, no warnings.

* docs(vehicle-ph-plant): cite #138's squash commit 2f76ab0

The #138 rows, the reproduction appendix and the versions table now cite main at 2f76ab0, where the dynamic twins' parity tests pass again (57 passed, 6 skipped for pycvc). The PDF is rebuilt: 27 pages, no warnings.

* docs(vehicle-ph-plant): the point-mass books' twin (#139) and pycvc's tick telemetry (libcvc #857)

- Table 9 (the twins) gains the navigator's point-mass books: book_step,
  energy_residual_bound, material_energy and integrate_surrogate_material's
  books hook, held to libcvc's NavPotential.BooksGolden. book_step on
  libcvc's arguments is held to 1e-12 (double tier); the books are held to
  1e-5 of the step's bound, and 2e-5 on the clamped rollout (float tier).
- 8.3, "What is not twinned yet": the point-mass books are twinned. The tick
  telemetry is a running world's, so it has no twin; libcvc #857 binds it in
  pycvc (nav_sim_world_set_capture_drive_telemetry,
  nav_sim_world_drive_telemetry).
- 8.4 gains "Book the navigator's energy": how to ask for the books, the
  measured parity (1.1e-6 and 8.3e-8 of the bound), and the curved-patch
  caveat. On a patch whose gradient channels are not its values' gradients,
  each step books the force/-grad U gap, which fills 99.65% of the bound at
  h = 0.002. It also says where a live world's context work comes from.
- 10, "Python access", and the equation-to-code map: updated to match.

PDF rebuilt with TeX Live pdflatex, three passes: 28 pages, no warnings.
transfix added a commit to transfix/libcvc that referenced this pull request Oct 28, 2026
… 0) (#852)

**Follows #848** (merged as 9b9270b; master is merged in, so the diff is the switch alone).

The front slip of `dyn_bike` becomes the wheel-frame slip that `dyn_4w` uses (approved by Joe):

    alpha_f = -atan2(w_f, u_f)
    w_f = cos(delta) (vy + lf r) - sin(delta) vx    (= n_fᵀ v, the front axle's sideways velocity in its wheel's frame)
    u_f = sqrt((cos(delta) vx + sin(delta) (vy + lf r))² + eps²)

The front secant is now rooted at w_f = 0. Before, it was rooted at the floored speed's root, u tan(sg δ). The rear slip was already in its wheel's frame.

The implicit step's Jacobian takes the front tire's d(F_yf cos δ)/d(vy, r) = −S_f cos² δ (1, lf), as `dyn_4w`'s does.

**The pH form**
- R's front term is S_f n_f n_fᵀ (it was (S_f / cos δ) n_f n_fᵀ). It is positive semi-definite at any δ.
- f_ε is now round-off. The speed floor enters only the slips' forward speeds. The `floor` line stays in `dyn_ph_rates`, `dyn_ph_books` and `dyn_bike_books`, so their APIs don't change.
- `dyn_ph_parts` gains `uf` and loses `w0f`.
- `dyn_ph_tangent_D` takes the wheel-frame slip. Its front sensitivity is exact in (vy, r), including the forward speed's dependence, which runs along the same (1, lf).

**Tests** (`nav_dyn_bike_test`, 23 tests)
- `DynBikePH.TheFloorTermVanishesWithTheFloor` becomes `TheFloorTermIsRoundOff`. |floor power| / (dissipated + |supplied| + 1 W) is ≤ 2.2e-16 at every floor from 0.5 to 1e-9 m/s. Before, it was 5.35e-4 at 0.5 m/s.
- New: `DynBikePH.TheTangentIsTheTiresDerivative` checks `dyn_ph_tangent_D` against central differences of the tires' lateral force and moment. Worst error: 1.4e-7 of its scale.
- Three goldens are regenerated:
  - `DynBike.Golden`;
  - `DynBikePH.Golden` (the key `w0f` becomes `uf`);
  - `NavMaterialExt.Golden`'s dynamic case. Its surrogate and bicycle cases stay byte-identical.

**Measured, before → after** (MSVC 14.44 Release)

| Measure | Before | After |
|---|---|---|
| Kinematic-limit gap (`DynBike.KinematicLimitFollowsBikeStep`) | 0.01074 | 0.01074 |
| Vector field vs plant, mu 0.9 / 0.6 | 1.62e-4 / 1.06e-4 m g | unchanged |
| States past the peak | 244 of 500 per mu | unchanged |
| Tangent bins | | unchanged to 0.1%: the front passes its peak from about 0.5 mu g; the rear never does |
| Swerve: mean \|residual\| ratio per halving | 3.83 / 3.92 / 3.95 | unchanged |
| Swerve: \|sum\| / flow at hdt 0.01 | 1.21e-3 | unchanged |
| Braking to a stop, hdt 0.01: limiter / \|sum residual\| / flow | −353.5 J / 7.23e-7 | −358.2 J / 4.43e-6 |
| Braking to a stop, hdt 0.005: limiter / \|sum residual\| / flow | −1.722 J / 1.72e-5 | −8.463 J / 1.76e-5 |
| `physics_dyn_bike_plant_test`, Humvee: floor / residual share | 1.14 J / 1.10e-4 | −5.4e-15 J / 1.10e-4 |
| `physics_dyn_bike_plant_test`, RoboRacer: residual share | 8.86e-4 | 8.88e-4 |
| `nav_dyn_4w_test` single-track limit | 0.130 m / 0.016 m/s | unchanged |

Also passing locally:

| Suite | Tests |
|---|---|
| `nav_binding` | 6 |
| `nav_dyn_4w` | 16 |
| `nav_material_bike_rollout` | 12 |
| `nav_material_ext` | 11 (with the regenerated golden) |
| `physics_dyn_bike_plant` | 3 |
| `physics_plant_host` | 5 |
| `nav_sim_world` | 3 |

`nav_plant`, `physics_plant_log`, `nav_coef_train` and `nav_potential` were still running when this was opened.

GRL-SNAM follows in CVC-Lab/GRL-SNAM#138, which brings the torch `dyn_bike.py` and the three fixtures over. It matches the new goldens to 2.8e-16 on the plant and 7.1e-14 on the books.

**Not changed:** `dyn_power.h`'s `dyn_tire_power` still uses the old front slip angle, sg δ − atan2(vy + lf r, u). Its tire-slip power therefore no longer reads the plant's own front slip. It wasn't part of the approved switch, and changing it moves the fitted power model, so it's left for a decision.
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