Skip to content

Q23: RL vehicle preset input, RL guide, demo videos - #15

Merged
Onlyti merged 12 commits into
mainfrom
feat/rl-vehicle-preset
Sep 22, 2026
Merged

Onlyti merged 12 commits into
mainfrom
feat/rl-vehicle-preset

Conversation

@Onlyti

@Onlyti Onlyti commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Scope

RL environment vehicle preset input, RL usage guide, and demo videos (Q23-a/-b/-c),
plus the follow-up rounds Q23-f (public/private config split), Q23-g (public-tree
hygiene), and Q23-h (G1 reference solution re-integrated at 1 us).

main was merged into this branch (merge commit, no rebase — decision 98), so the
asset caption sha 41f1d99 stays an ancestor of this history and the videos did not
need re-recording.

Q23-a — vehicle/tire preset input

  • EnvConfig.vehicle / EnvConfig.tire: YAML stems, loaded once in the parent
    process
    , zero file writes (the Q10-c shared-resolve-cache race cannot recur here).
  • Lookup order: vehicle_file absolute path -> $VDSIM_PRIVATE_CONFIGS/vehicles/<stem>.yaml
    -> repository configs/vehicles/<stem>.yaml. Relative paths are rejected.
  • vehicle=None keeps the C++ built-in parameters but emits a one-shot UserWarning.
  • Unknown keys / missing required keys raise; there is no silent fallback to C++ defaults.
  • Domain randomisation multiplies the loaded values, not the built-in ones.
  • info (on reset) and env.metadata carry vehicle, tire, param_hash and
    source=public|private — names only, never absolute paths.

Q23-f — public defaults are generic, Ioniq5 is injected privately

  • New configs/vehicles/generic_sedan.yaml and configs/parts/tire/generic_pacejka.yaml
    transcribe the C++ VehicleParams() / TireParams() defaults. A regression pins all
    79 bound fields plus param_hash against the C++ defaults, so the YAML cannot drift.
  • configs/rl/default_env.yaml and fast_env.yaml now name the generic pair.
  • wheels.yml CIBW smoke uses the generic pair.
  • The spec named default_pacejka; that file is a catalogue part and loading it would
    need the catalogue parser this work order forbids, so the same tyre was transcribed
    into flat form as generic_pacejka.

Q23-g — public-tree hygiene

  • git diff origin/main...HEAD | grep '^+' | grep -v '^+++' | grep -ic ioniq = 0.
  • Ioniq5-specific G1 tables, the throughput decomposition and the Ioniq5 preset test
    live outside the repository; the public tests use a synthetic 1750 kg probe car and
    assert that the probe actually discriminates against the generic values (so the
    fixture cannot become vacuous).

Q23-h — G1 reference solution

  • tests/rl/gate_g1_substep_accuracy.py now integrates the reference at 1 us
    (was 0.1 ms), raises max_substeps to 6000, and discards any run where
    solver_substep_clamp_count() != 0.
  • Observed convergence order p = 1.01..1.06, monotone — verdict (a), the reference is
    sound. The old 0.1 ms reference carried 0.059 % (a_y) / 0.117 % (Fz_FL) of its own
    discretisation error, so the earlier tables were optimistic by 3..11 % relative.
  • Both the old and the new reference rows are kept side by side in
    configs/rl/fast_env.yaml and docs/RL_GUIDE.md §6; no threshold was relaxed and
    no metric was dropped.

fast_env vehicle choice (decision 69/83)

fast_env uses the C++ built-in vehicle at L2 with a 2.5 ms max substep: a_y 1.45 %,
yaw 0.53 %, Fz_FL 4.19 % against the 1 us reference — inside the pre-registered
2 % / 5 % gate, at 6.4x the throughput of the alternative. The Ioniq5 combination
fails that gate at 2.5 ms (Fz_FL 7.83 %) and is therefore not used here.

Registration surface

cosim/CMakeLists.txt registers one new ctest target, rl_env (torch/SB3-free checks
only; the PPO smoke stays outside CI, and gymnasium is pinned in the workflow so an
import failure is a FAIL, never a skip).

  • Registration-surface diff vs origin/main: cosim/CMakeLists.txt only, +10 lines.
  • Expected registered count = main canonical + 1 = 507. If CI reports a different
    number, the cause is to be investigated before any document is edited.
  • docs/VALIDATION.md is therefore held at PENDING; it will be promoted only after
    three consecutive identical CI measurements on a fixed head.

Known, out of scope

test_part_store.py can fail under high ctest -j parallelism because the part
catalogue is written to a shared temporary path. The canonical measurement is serial,
so this does not affect the count; it is filed as a separate item.

Onlyti and others added 12 commits September 22, 2026 13:40
EnvConfig gains vehicle/tire preset stems. load_vehicle_preset() reads
configs/vehicles/<stem>.yaml and configs/parts/tire/<stem>.yaml once in
the parent process and writes no file. Keys the C++ parser would ignore,
missing required keys, and catalog-format tyre parts raise ValueError
instead of silently falling back to the C++ generic defaults.
vehicle=None keeps the built-in car and warns. reset() info and env
metadata carry vehicle, tire and a sha256 of the effective parameters.

configs/rl/{default,fast}_env.yaml select ioniq5_awd + ioniq5_pac2002.
tests/rl/test_rl_env.py: Q23-1..6 (plant weight from tyre Fz matches the
YAML mass, DR scales the Ioniq5 baseline, no file writes, strict keys);
paths made repo-relative so the script runs from any worktree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/RL_GUIDE.md: background, concepts (vehicle preset, obs fields with
units/frame, action, reward, termination, reset and domain randomization,
seeds), quick start, three examples (random 16 envs, SB3 PPO, friction
DR), the two videos, and limits (throughput; the Ioniq 5 preset measured
at ~2.7x fewer ticks/s than the generic car; G1 substep gate not repeated
with the preset; preset is a public approximation).

examples/rl/record_vec_demo.py records a VDSimVecEnv rollout (plant state
per control step, terminal state kept before reset) to npz, or one
.vdtrace per env. examples/rl/render_vec_grid.py draws the gym tile grid
or the one-road overlay and encodes with system ffmpeg. Frames are drawn
only at recorded steps. vdsim_render's API is unchanged.

docs/assets/rl: vec_grid.mp4 (16 envs, random policy, 20 s) and
vec_overlay.mp4 (64 envs, shared lane-keep + disturbance, 15 s), with
start / reset / end stills. mkdocs nav gains the guide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…st rl_env (Q23-d, Q23-e)

Q23-d
- tests/rl/gate_g1_substep_accuracy.py takes --vehicle/--tire/--level/--out
  and records the preset param_hash; default (no preset, L3) is unchanged.
- configs/rl/fast_env.yaml: the 2026-09-17 table is labelled as the C++
  built-in car at L3; a second table measures the car this file selects
  (ioniq5_awd + ioniq5_pac2002, param_hash 04652687..., L2).  At 2.5 ms,
  ay 0.27 % and yaw 0.12 % pass, Fz_FL 7.56 % fails the 5 % criterion.
  max_substep_dt is NOT changed (pending PO decision).
- tests/rl/bench_preset_cost.py: one-factor-at-a-time core throughput.
  The whole ~2.7x gap is the MF2002 tyre backend; vehicle params and
  plant_path move throughput by < 6 %.  Measurement only, nothing tuned.
- examples/rl/record_vec_demo.py: per-env disturbance generators
  (SeedSequence(seed).spawn(n)) unless --shared-actions; commit stamp gets
  -dirty when the tree differs from HEAD; caption carries a vehicle note
  (default "public approximation").
- docs/RL_GUIDE.md: video B text/command and Limits updated.

Q23-e
- ctest target rl_env runs tests/rl/test_rl_env.py (gymnasium, no torch).
  The SB3 PPO smoke moved to tests/rl/smoke_sb3_ppo.py, outside ctest.
- build.yml installs gymnasium==0.29.1 in both jobs; a missing gymnasium
  fails rl_env (ModuleNotFoundError), it is not skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The one-line provenance caption overflowed the 1280 px overlay frame and cut
off the commit stamp.  Split after level=.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Recorded and rendered from a clean tree at 8135a76, which contains both
scripts; captions carry commit=8135a76 and "(public approximation)".
Video B now uses per-env disturbance generators: 26 off-track exits split
16 at y=+6 m / 10 at y=-6 m (was one-sided with a shared draw).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Ioniq 5 preset fails the G1 Fz_FL <= 5 % criterion at 2.5 ms (7.56 %).
Of the two passing combinations, take the generic car at 2.5 ms (L2: ay 1.45 %,
yaw 0.53 %, Fz_FL 4.07 %, 326 k steps/s) over Ioniq 5 at 1.0 ms (Fz_FL 2.62 %,
51 k steps/s), which is already default_env.yaml. Criterion unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PO decision 78 / 18_dev_briefing_0903 section 42: the shipped RL declarations
must not name a car whose parameters are not published.

- configs/vehicles/generic_sedan.yaml + configs/parts/tire/generic_pacejka.yaml:
  the values core/src/params.cpp compiles in as VehicleParams()/TireParams(),
  written out as files.  generic_pacejka is a flat TireParams file because
  vdsim_rl refuses catalog parts (schema/body) by design, and
  configs/parts/tire/default_pacejka.yaml is a catalog part.
- configs/rl/{default_env,fast_env}.yaml select that pair.  Same plant as
  before (fast_env ran the unnamed built-in car), so the G1 substep table in
  fast_env.yaml still describes both files; default_env's 1 ms row is
  ay 0.55 %, yaw 0.20 %, Fz_FL 1.22 %.
- load_vehicle_preset resolves a preset as vehicle_file/tire_file (absolute
  path) -> $VDSIM_PRIVATE_CONFIGS/<layout> -> configs/<layout>, so a car that
  cannot be published stays outside the repository.  A relative
  vehicle_file/tire_file is refused (it would depend on the working directory).
- provenance/info/metadata gain source=public|private and still carry names
  only, never a path.
- wheels.yml CIBW smoke builds the generic pair instead of the Ioniq 5 pair.
- tests/rl/test_rl_env.py: Q23-f1 compares all 79 bound fields of the loaded
  params against the C++ defaults and asserts the same param_hash as
  vehicle=None, so a drift between YAML and C++ fails instead of silently
  training a different car; Q23-f2 walks the search order and the refusal of a
  relative path.  No new ctest target (505 registered, unchanged).
- docs/RL_GUIDE.md: generic defaults, the private-root recipe, source in info.
  The Ioniq 5 bullet no longer claims its numbers are a public approximation --
  that was a self-reported provenance, not a measured one.
- docs/VALIDATION.md currency -> PENDING: this tree registers rl_env on top of
  main, so the canonical count is what CI measures three times on the Q23 PR.

Local: validation preset 505/505, 0 failed, twice in a row.
Negative check: mass 1500 -> 1400 in generic_sedan.yaml fails Q23-f1 with the
field named; dropping the private root from the search order fails Q23-f2.
The branch added 37 lines matching /ioniq/i on top of origin/main, which
would enlarge what Q24 has to clean up. Judgement criterion, fixed before
the edit: git diff origin/main...HEAD | grep '^+' | grep -v '^+++' |
grep -ic ioniq == 0.

- configs/rl/fast_env.yaml: drop the Ioniq 5 G1 table (b) and the
  two-candidate narrative. The generic-car tables (a) and the fixed
  criterion stay; a new paragraph says a substep is a property of the car
  and that non-public presets are injected through $VDSIM_PRIVATE_CONFIGS
  with their tables kept alongside them.
- docs/RL_GUIDE.md: §6 now carries the generic 1.0/2.5 ms rows only, plus
  one bullet stating that a preset reading a measured .tir through MF2002
  has measured ~2.7x slower with a larger Fz error (no car named), and one
  bullet documenting the private-injection search order.
- tests/rl/test_rl_env.py: Q23-1/4/6 used ioniq5_awd.yaml as the fixture.
  Every shipped car carries the C++ default values, so the same checks
  against one of them would be vacuous; they now write a synthetic 1750 kg
  probe car into a temporary $VDSIM_PRIVATE_CONFIGS root, the same
  mechanism Q23-f2 already exercises. Q23-1 asserts the fixture actually
  differs from the shipped values, so a future edit cannot make the check
  vacuous silently. The root is scoped to the blocks that use it: left set,
  it also resolves the shipped tyre privately and flips info[source] for
  the public declarations Q23-2 checks.
- tests/rl/gate_g1_substep_accuracy.py: docstring example uses the generic
  stems and points at the private root for anything else.
- tests/rl/bench_preset_cost.py: removed. It exists only to explain this
  one preset's cost; moved to the private path with the table it feeds.
- examples/rl/record_vec_demo.py: --vehicle-note no longer defaults to a
  self-reported provenance string.

Test count unchanged (no ctest target added or removed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shipped mp4s were recorded at 8135a76, a tree that did not contain
examples/rl/record_vec_demo.py or render_vec_grid.py, so their caption
pointed at a commit the commands could not be run from. They also captioned
the car with a self-reported provenance string.

Re-recorded from 41f1d99 with a clean work tree, so the caption reads
commit=41f1d99 with no -dirty suffix and no provenance note, and the two
commands in docs/RL_GUIDE.md section 5 reproduce these files at that commit.

- vec_grid.mp4    20.04 s, 1280x820, 16 envs, random per-env policy, seed 0
- vec_overlay.mp4 15.04 s, 1280x460, 64 envs, scripted lane-keeper, seed 3

PNG frames are taken from the same render pass, not re-rendered separately:
vec_grid_start (t=0), vec_grid_reset (t=1.16 s, env 07 shows
"reset <- off_track"), vec_grid_end (t=20 s), vec_overlay_end (t=15 s).

Per-env disturbance is unchanged and still spreads both ways: of the 47 envs
whose first episode ends, 30 leave the road on the +y side and 17 on -y
(28 off_track, 14 spin_out, 5 stall); final lateral positions across all 64
envs have a 2.28 m standard deviation and the longitudinal spread is 53 m.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gate_g1_substep_accuracy.py: --ref-substep (default 1 us), --max-substeps
(default 6000; refuses to start below DT/min step), clamp count reset and
checked after the run (non-zero => discarded, exit 3), grid 0.01..5 ms,
effective h from ceil(DT/h), adjacent-pair order p, every row also compared
against the 0.1 ms run so the old table is reproduced alongside.

Result (generic car, L2): p = 1.01-1.06 from 1 ms to 0.01 ms, monotone.
0.1 ms reference self-error: ay 0.059 %, Fz_FL 0.117 %. 2.5 ms row moves
1.45 -> 1.51 % (ay) and 4.07 -> 4.19 % (Fz_FL): still passes. Old rows kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Onlyti
Onlyti merged commit e196f1a into main Sep 22, 2026
5 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